Skip to content

Commit a205938

Browse files
committed
GNSR - support for NodeCallbackInvoker
1 parent 9ab9647 commit a205938

File tree

6 files changed

+155
-49
lines changed

6 files changed

+155
-49
lines changed

src/Analyser/Generator/GeneratorNodeScopeResolver.php

Lines changed: 70 additions & 47 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,6 @@
1616
use PHPStan\Node\Printer\ExprPrinter;
1717
use PHPStan\ShouldNotHappenException;
1818
use Throwable;
19-
use function array_map;
2019
use function array_merge;
2120
use function array_pop;
2221
use function count;
@@ -162,26 +161,35 @@ private function runTrampoline(
162161
): StmtAnalysisResult
163162
{
164163
while (true) {
165-
$this->processPendingFibers($fibersStorage, $exprAnalysisResultStorage);
164+
$pendingFibersGen = $this->processPendingFibers($fibersStorage, $exprAnalysisResultStorage);
165+
if ($pendingFibersGen->valid()) {
166+
$stack[] = $gen;
167+
$gen = new IdentifiedGeneratorInStack($pendingFibersGen, new Stmt\Expression(new Node\Scalar\String_('pendingFibers')), null, null);
168+
}
166169

167170
if ($gen->generator->valid()) {
168171
$yielded = $gen->generator->current();
169172

170173
if ($yielded instanceof NodeCallbackRequest) {
171174
$alternativeNodeCallback = $yielded->alternativeNodeCallback;
172-
$this->invokeNodeCallback(
173-
$fibersStorage,
174-
$exprAnalysisResultStorage,
175+
$stack[] = $gen;
176+
$gen = new IdentifiedGeneratorInStack(
177+
$this->invokeNodeCallback(
178+
$fibersStorage,
179+
$exprAnalysisResultStorage,
180+
$yielded->node,
181+
$yielded->scope,
182+
$alternativeNodeCallback !== null
183+
? static function (Node $node, Scope $scope) use ($alternativeNodeCallback, $nodeCallback): void {
184+
$alternativeNodeCallback($node, $scope, $nodeCallback);
185+
}
186+
: $nodeCallback,
187+
),
175188
$yielded->node,
176-
$yielded->scope,
177-
$alternativeNodeCallback !== null
178-
? static function (Node $node, Scope $scope) use ($alternativeNodeCallback, $nodeCallback): void {
179-
$alternativeNodeCallback($node, $scope, $nodeCallback);
180-
}
181-
: $nodeCallback,
189+
$yielded->originFile,
190+
$yielded->originLine,
182191
);
183-
184-
$gen->generator->next();
192+
$gen->generator->current();
185193
continue;
186194
} elseif ($yielded instanceof ExprAnalysisRequest) {
187195
$stack[] = $gen;
@@ -272,26 +280,21 @@ private function runTrampoline(
272280
throw new ShouldNotHappenException('Pending fibers with an empty stack should be about synthetic nodes');
273281
}
274282

275-
// todo problem with noop callback
276-
$this->processStmtNodes(
277-
$fibersStorage,
278-
$exprAnalysisResultStorage,
279-
[new Stmt\Expression($request->expr)],
280-
$request->scope,
281-
static function () {
282-
},
283-
StatementContext::createTopLevel(),
283+
$stack[] = $gen;
284+
$gen = new IdentifiedGeneratorInStack(
285+
$this->analyseExpr($exprAnalysisResultStorage, $request->stmt, $request->expr, $request->scope, $request->context, $request->alternativeNodeCallback),
286+
$request->expr,
287+
$request->originFile,
288+
$request->originLine,
284289
);
290+
$gen->generator->current();
291+
continue 2;
285292
}
286293

287-
if (count($fibersStorage->pendingFibers) === 0) {
288-
if (!$result instanceof StmtAnalysisResult) {
289-
throw new ShouldNotHappenException('Top node should be Stmt');
290-
}
291-
return $result;
294+
if (!$result instanceof StmtAnalysisResult) {
295+
throw new ShouldNotHappenException('Top node should be Stmt');
292296
}
293-
294-
throw new ShouldNotHappenException(sprintf('Cannot finish analysis, pending fibers about: %s', implode(', ', array_map(static fn (array $fiber) => get_class($fiber['request']->expr), $fibersStorage->pendingFibers))));
297+
return $result;
295298
}
296299

297300
$gen = array_pop($stack);
@@ -391,8 +394,7 @@ private function runInFiber(callable $callback): Generator
391394

392395
while (!$fiber->isTerminated()) {
393396
if ($request instanceof ExprAnalysisRequest) {
394-
$result = yield $request;
395-
$request = $fiber->resume($result);
397+
$request = $fiber->resume(yield $request);
396398
continue;
397399
}
398400

@@ -463,31 +465,35 @@ private function analyseExprForType(ExprAnalysisResultStorage $storage, Expr $ex
463465

464466
/**
465467
* @param callable(Node, Scope): void $nodeCallback
468+
* @return Generator<int, GeneratorTValueType, GeneratorTSendType, null>
466469
*/
467470
private function invokeNodeCallback(
468471
PendingFibersStorage $fibersStorage,
469472
ExprAnalysisResultStorage $exprAnalysisResultStorage,
470473
Node $node,
471474
Scope $scope,
472475
callable $nodeCallback,
473-
): void
476+
): Generator
474477
{
475478
$fiber = new Fiber(static function () use ($node, $scope, $nodeCallback) {
476479
$nodeCallback($node, $scope);
477480
});
478481
$request = $fiber->start();
479-
$this->runFiberForNodeCallback($fibersStorage, $exprAnalysisResultStorage, $fiber, $request);
482+
yield from $this->runFiberForNodeCallback($fibersStorage, $exprAnalysisResultStorage, $fiber, $request);
483+
484+
return null;
480485
}
481486

482487
/**
483-
* @param Fiber<mixed, ExprAnalysisResult, null, ExprAnalysisRequest> $fiber
488+
* @param Fiber<mixed, ExprAnalysisResult|null, null, ExprAnalysisRequest|NodeCallbackRequest> $fiber
489+
* @return Generator<int, GeneratorTValueType, GeneratorTSendType, void>
484490
*/
485491
private function runFiberForNodeCallback(
486492
PendingFibersStorage $fibersStorage,
487493
ExprAnalysisResultStorage $exprAnalysisResultStorage,
488494
Fiber $fiber,
489-
?ExprAnalysisRequest $request,
490-
): void
495+
ExprAnalysisRequest|NodeCallbackRequest|null $request,
496+
): Generator
491497
{
492498
while (!$fiber->isTerminated()) {
493499
if ($request instanceof ExprAnalysisRequest) {
@@ -506,6 +512,10 @@ private function runFiberForNodeCallback(
506512
];
507513
return;
508514
}
515+
if ($request instanceof NodeCallbackRequest) {
516+
$request = $fiber->resume(yield $request);
517+
continue;
518+
}
509519

510520
throw new ShouldNotHappenException(
511521
'Unknown fiber suspension: ' . get_debug_type($request),
@@ -519,21 +529,34 @@ private function runFiberForNodeCallback(
519529
}
520530
}
521531

522-
private function processPendingFibers(PendingFibersStorage $fibersStorage, ExprAnalysisResultStorage $exprAnalysisResultStorage): void
532+
/**
533+
* @return Generator<int, GeneratorTValueType, GeneratorTSendType, void>
534+
*/
535+
private function processPendingFibers(PendingFibersStorage $fibersStorage, ExprAnalysisResultStorage $exprAnalysisResultStorage): Generator
523536
{
524-
foreach ($fibersStorage->pendingFibers as $key => $pending) {
525-
$request = $pending['request'];
526-
$exprAnalysisResult = $exprAnalysisResultStorage->findExprAnalysisResult($request->expr);
537+
$restartLoop = true;
527538

528-
if ($exprAnalysisResult === null) {
529-
continue;
530-
}
539+
while ($restartLoop) {
540+
$restartLoop = false;
541+
542+
foreach ($fibersStorage->pendingFibers as $key => $pending) {
543+
$request = $pending['request'];
544+
$exprAnalysisResult = $exprAnalysisResultStorage->findExprAnalysisResult($request->expr);
531545

532-
unset($fibersStorage->pendingFibers[$key]);
546+
if ($exprAnalysisResult === null) {
547+
continue;
548+
}
533549

534-
$fiber = $pending['fiber'];
535-
$request = $fiber->resume($exprAnalysisResult);
536-
$this->runFiberForNodeCallback($fibersStorage, $exprAnalysisResultStorage, $fiber, $request);
550+
unset($fibersStorage->pendingFibers[$key]);
551+
$restartLoop = true;
552+
553+
$fiber = $pending['fiber'];
554+
$request = $fiber->resume($exprAnalysisResult);
555+
yield from $this->runFiberForNodeCallback($fibersStorage, $exprAnalysisResultStorage, $fiber, $request);
556+
557+
// Break and restart the loop since the array may have been modified
558+
break;
559+
}
537560
}
538561
}
539562

src/Analyser/Generator/IdentifiedGeneratorInStack.php

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,8 @@ final class IdentifiedGeneratorInStack
2424
* Generator<int, GeneratorTValueType, GeneratorTSendType, ExprAnalysisResult>| // analyseExpr
2525
* Generator<int, GeneratorTValueType, GeneratorTSendType, TypeExprResult>| // analyseExprForType
2626
* Generator<int, GeneratorTValueType, GeneratorTSendType, ExprAnalysisResultStorage>| // persistStorage
27-
* Generator<int, GeneratorTValueType, GeneratorTSendType, RunInFiberResult<mixed>> // runInFiber
27+
* Generator<int, GeneratorTValueType, GeneratorTSendType, RunInFiberResult<mixed>>| // runInFiber
28+
* Generator<int, GeneratorTValueType, GeneratorTSendType, null> // invokeNodeCallback
2829
* ) $generator
2930
* @param Node|Node[] $node
3031
*/

src/Analyser/Generator/NodeCallbackRequest.php

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,10 +4,16 @@
44

55
use PhpParser\Node;
66
use PHPStan\Analyser\Scope;
7+
use function debug_backtrace;
8+
use const DEBUG_BACKTRACE_IGNORE_ARGS;
79

810
final class NodeCallbackRequest
911
{
1012

13+
public ?string $originFile = null;
14+
15+
public ?int $originLine = null;
16+
1117
/**
1218
* @param (callable(Node, Scope, callable(Node, Scope): void): void)|null $alternativeNodeCallback
1319
*/
@@ -17,6 +23,9 @@ public function __construct(
1723
public readonly mixed $alternativeNodeCallback,
1824
)
1925
{
26+
$trace = debug_backtrace(DEBUG_BACKTRACE_IGNORE_ARGS, 1);
27+
$this->originFile = $trace[0]['file'] ?? null;
28+
$this->originLine = $trace[0]['line'] ?? null;
2029
}
2130

2231
}

src/Analyser/Generator/PendingFibersStorage.php

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@
77
final class PendingFibersStorage
88
{
99

10-
/** @var array<array{fiber: Fiber<mixed, ExprAnalysisResult, null, ExprAnalysisRequest>, request: ExprAnalysisRequest}> */
10+
/** @var array<array{fiber: Fiber<mixed, ExprAnalysisResult|null, null, ExprAnalysisRequest|NodeCallbackRequest>, request: ExprAnalysisRequest}> */
1111
public array $pendingFibers = [];
1212

1313
}

tests/PHPStan/Analyser/Generator/GeneratorNodeScopeResolverRuleTest.php

Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
namespace PHPStan\Analyser\Generator;
44

55
use PhpParser\Node;
6+
use PHPStan\Analyser\NodeCallbackInvoker;
67
use PHPStan\Analyser\Scope;
78
use PHPStan\Node\Printer\ExprPrinter;
89
use PHPStan\Rules\IdentifierRuleError;
@@ -12,6 +13,7 @@
1213
use PHPStan\Type\VerbosityLevel;
1314
use PHPUnit\Framework\Attributes\DataProvider;
1415
use PHPUnit\Framework\Attributes\RequiresPhp;
16+
use function sprintf;
1517

1618
/**
1719
* @extends RuleTestCase<Rule<node>>
@@ -93,6 +95,75 @@ static function (Node $node, Scope $scope) {
9395
['\'bar\'', 21],
9496
],
9597
];
98+
yield [
99+
static function (Node $node, Scope $scope) {
100+
/** @var Scope&NodeCallbackInvoker $scope */
101+
if ($node instanceof Node\Stmt\Echo_) {
102+
$echoExprType = $scope->getType($node->exprs[0]);
103+
return [
104+
RuleErrorBuilder::message($echoExprType->describe(VerbosityLevel::precise()))
105+
->identifier('gnsr.rule')
106+
->build(),
107+
];
108+
}
109+
if (!$node instanceof Node\Expr\MethodCall) {
110+
return [];
111+
}
112+
113+
$scope->invokeNodeCallback(new Node\Stmt\Echo_([
114+
$node->getArgs()[0]->value,
115+
], $node->getAttributes()));
116+
117+
return [];
118+
},
119+
[
120+
['1', 21],
121+
['\'foo\'', 23],
122+
],
123+
];
124+
yield [
125+
static function (Node $node, Scope $scope) {
126+
/** @var Scope&NodeCallbackInvoker $scope */
127+
if ($node instanceof Node\Stmt\Echo_) {
128+
$scope->invokeNodeCallback(new Node\Expr\Print_($node->exprs[0], $node->getAttributes()));
129+
return [];
130+
}
131+
132+
if ($node instanceof Node\Expr\Print_) {
133+
$scope->invokeNodeCallback(new Node\Expr\MethodCall(new Node\Expr\Variable('foo'), 'doFoo', [new Node\Arg($node->expr)], $node->getAttributes()));
134+
return [
135+
RuleErrorBuilder::message(sprintf('Virtual node invoked: %s, %s', $scope->getType($node->expr)->describe(VerbosityLevel::value()), $scope->getType(new Node\Expr\BinaryOp\Plus(new Node\Scalar\Int_(1), new Node\Scalar\Int_(2)))->describe(VerbosityLevel::value())))
136+
->identifier('gnsr.rule')
137+
->build(),
138+
];
139+
}
140+
141+
if ($node instanceof Node\Expr\Exit_) {
142+
return [
143+
RuleErrorBuilder::message('exit through')->line(666)->identifier('gnsr.rule')->build(),
144+
];
145+
}
146+
147+
if (!$node instanceof Node\Expr\MethodCall) {
148+
return [];
149+
}
150+
151+
$arg0 = $scope->getType($node->getArgs()[0]->value);
152+
$scope->invokeNodeCallback(new Node\Expr\Exit_());
153+
$arg0 = $scope->getType($node->getArgs()[0]->value);
154+
155+
return [
156+
RuleErrorBuilder::message(sprintf('Called on %s, arg: %s', $scope->getType($node->var)->describe(VerbosityLevel::precise()), $arg0->describe(VerbosityLevel::precise())))->identifier('gnsr.rule')->build(),
157+
];
158+
},
159+
[
160+
['exit through', 666],
161+
['Called on GeneratorNodeScopeResolverRule\\Foo, arg: 1', 21],
162+
['exit through', 666],
163+
['Virtual node invoked: \'foo\', 3', 23],
164+
['Called on GeneratorNodeScopeResolverRule\\Foo, arg: \'foo\'', 23],
165+
],
166+
];
96167
}
97168

98169
/**

tests/PHPStan/Analyser/Generator/data/rule.php

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,4 +19,6 @@ public function doBar(): ?int
1919

2020
function (Foo $foo): void {
2121
$foo->doFoo(1, 2, 3);
22+
23+
echo 'foo';
2224
};

0 commit comments

Comments
 (0)