diff --git a/src/Analyser/ExprHandler/FuncCallHandler.php b/src/Analyser/ExprHandler/FuncCallHandler.php index 689638715c3..9f77b0c8db0 100644 --- a/src/Analyser/ExprHandler/FuncCallHandler.php +++ b/src/Analyser/ExprHandler/FuncCallHandler.php @@ -22,7 +22,6 @@ use PHPStan\Analyser\ExprHandler\Helper\EarlyTerminatingCallHelper; use PHPStan\Analyser\ExprHandler\Helper\OutputBufferHelper; use PHPStan\Analyser\ExprHandler\Helper\VoidToNullTypeTransformer; -use PHPStan\Analyser\GatheringNodeCallback; use PHPStan\Analyser\ImpurePoint; use PHPStan\Analyser\InternalThrowPoint; use PHPStan\Analyser\MutatingScope; @@ -230,7 +229,7 @@ public function processExpr(NodeScopeResolver $nodeScopeResolver, Stmt $stmt, Ex $arrayWalkArrayArg = null; $arrayWalkOriginalArrayType = null; $arrayWalkOriginalArrayNativeType = null; - $nodeCallbackForArgs = $nodeCallback; + $argsGatherer = null; if ( $functionReflection !== null && $functionReflection->getName() === 'array_walk' @@ -254,7 +253,7 @@ public function processExpr(NodeScopeResolver $nodeScopeResolver, Stmt $stmt, Ex $arrayWalkOriginalArrayType = $scope->getType($arrayWalkArrayArg); $arrayWalkOriginalArrayNativeType = $scope->getNativeType($arrayWalkArrayArg); - $nodeCallbackForArgs = new GatheringNodeCallback(static function (Node $node, Scope $scope) use ($callbackArg, $firstParamName, &$arrayWalkValueTypes): void { + $argsGatherer = static function (Node $node, Scope $scope) use ($callbackArg, $firstParamName, &$arrayWalkValueTypes): void { if (!($node instanceof ClosureReturnStatementsNode) || $node->getClosureExpr() !== $callbackArg) { return; } @@ -286,12 +285,21 @@ public function processExpr(NodeScopeResolver $nodeScopeResolver, Stmt $stmt, Ex TypeCombinator::union(...$types), TypeCombinator::union(...$nativeTypes), ]; - }, $nodeCallback); + }; } } $scopeBeforeArgs = $scope; - $argsResult = $nodeScopeResolver->processArgs($stmt, $functionReflection, null, $variants, $namedArgumentsVariants, $normalizedExpr, $scope, $storage, $nodeCallbackForArgs, $context); + if ($argsGatherer !== null) { + $nodeScopeResolver->pushNodeGatherer($argsGatherer); + } + try { + $argsResult = $nodeScopeResolver->processArgs($stmt, $functionReflection, null, $variants, $namedArgumentsVariants, $normalizedExpr, $scope, $storage, $nodeCallback, $context); + } finally { + if ($argsGatherer !== null) { + $nodeScopeResolver->popNodeGatherer(); + } + } $resolvedParametersAcceptor = $argsResult->getResolvedParametersAcceptor(); $scope = $argsResult->getScope(); $hasYield = $argsResult->hasYield(); diff --git a/src/Analyser/ExprHandler/NewHandler.php b/src/Analyser/ExprHandler/NewHandler.php index dea1f2b9e7d..df56081f6b9 100644 --- a/src/Analyser/ExprHandler/NewHandler.php +++ b/src/Analyser/ExprHandler/NewHandler.php @@ -14,7 +14,6 @@ use PHPStan\Analyser\ExpressionResultFactory; use PHPStan\Analyser\ExpressionResultStorage; use PHPStan\Analyser\ExprHandler; -use PHPStan\Analyser\GatheringNodeCallback; use PHPStan\Analyser\ImpurePoint; use PHPStan\Analyser\InternalThrowPoint; use PHPStan\Analyser\MutatingScope; @@ -130,7 +129,7 @@ public function processExpr(NodeScopeResolver $nodeScopeResolver, Stmt $stmt, Ex if ($constructorReflection->getDeclaringClass()->getName() === $classReflection->getName()) { $constructorResult = null; - $nodeScopeResolver->processStmtNode($expr->class, $scope, $storage, new GatheringNodeCallback(static function (Node $node, Scope $scope) use ($classReflection, &$constructorResult): void { + $nodeScopeResolver->pushNodeGatherer(static function (Node $node, Scope $scope) use ($classReflection, &$constructorResult): void { if (!$node instanceof MethodReturnStatementsNode) { return; } @@ -148,7 +147,13 @@ public function processExpr(NodeScopeResolver $nodeScopeResolver, Stmt $stmt, Ex return; } $constructorResult = $node; - }, $nodeCallback), StatementContext::createTopLevel()); + }); + try { + $nodeScopeResolver->processStmtNode($expr->class, $scope, $storage, $nodeCallback, StatementContext::createTopLevel()); + } finally { + $nodeScopeResolver->popNodeGatherer(); + } + if ($constructorResult !== null) { $throwPoints = array_map(static fn (ThrowPoint $point): InternalThrowPoint => InternalThrowPoint::createFromPublic($point, $scope), $constructorResult->getStatementResult()->getThrowPoints()); $impurePoints = $constructorResult->getImpurePoints(); diff --git a/src/Analyser/GatheringNodeCallback.php b/src/Analyser/GatheringNodeCallback.php deleted file mode 100644 index 6467044d953..00000000000 --- a/src/Analyser/GatheringNodeCallback.php +++ /dev/null @@ -1,50 +0,0 @@ -gatherer = $gatherer; - $this->inner = $inner; - } - - public function __invoke(Node $node, Scope $scope): void - { - ($this->inner)($node, $scope); - ($this->gatherer)($node, $scope); - } - - /** @return callable(Node, Scope): void */ - public function getGatherer(): callable - { - return $this->gatherer; - } - - /** @return callable(Node, Scope): void */ - public function getInner(): callable - { - return $this->inner; - } - -} diff --git a/src/Analyser/NodeScopeResolver.php b/src/Analyser/NodeScopeResolver.php index 95bdb58e739..56dc083900f 100644 --- a/src/Analyser/NodeScopeResolver.php +++ b/src/Analyser/NodeScopeResolver.php @@ -102,6 +102,7 @@ use function array_last; use function array_map; use function array_merge; +use function array_pop; use function array_slice; use function array_values; use function count; @@ -124,6 +125,20 @@ class NodeScopeResolver private ?ExpressionResultStorageStack $expressionResultStorageStack = null; + /** + * Engine-feeding gatherer frames (return statements, execution ends, + * impure points, ...), innermost last. callNodeCallback() feeds every + * frame the raw walk scope at the emission position - gatherers are + * engine code and never ask about types, and their arrays are read as + * soon as the enclosing body walk returns. Frames replace the former + * GatheringNodeCallback wrapper chain: gatherers no longer ride the + * node-callback channel, so callback filters (VirtualAssignNodeCallback) + * cannot accidentally starve them. + * + * @var list + */ + private array $nodeGatherers = []; + /** * @param ExtensionsCollection $functionParameterOutTypeExtensions * @param ExtensionsCollection $methodParameterOutTypeExtensions @@ -217,8 +232,6 @@ public function processNodes( $scope = $scope->toWalkScope(); $expressionResultStorage = new ExpressionResultStorage(); - $alreadyTerminated = false; - $exitPoints = []; $stmts = []; $stmtToNodeIndex = []; @@ -231,6 +244,34 @@ public function processNodes( $stmts[] = $node; } + // a fresh walk an extension starts mid-analysis must not feed the + // interrupted walk's gatherer frames (see processStmtNodes()) + $gatherers = $this->nodeGatherers; + $this->nodeGatherers = []; + try { + $this->processNodesStatements($nodes, $stmts, $stmtToNodeIndex, $scope, $expressionResultStorage, $nodeCallback); + } finally { + $this->nodeGatherers = $gatherers; + } + } + + /** + * @param Node[] $nodes + * @param Node\Stmt[] $stmts + * @param array $stmtToNodeIndex + * @param callable(Node $node, Scope $scope): void $nodeCallback + */ + private function processNodesStatements( + array $nodes, + array $stmts, + array $stmtToNodeIndex, + MutatingScope $scope, + ExpressionResultStorage $expressionResultStorage, + callable $nodeCallback, + ): void + { + $alreadyTerminated = false; + $exitPoints = []; $dummyParent = new Node\Stmt\Nop(); foreach ($stmts as $si => $node) { if ($alreadyTerminated && !($node instanceof Node\Stmt\Function_ || $node instanceof Node\Stmt\ClassLike || $node instanceof Node\Stmt\Label)) { @@ -446,14 +487,23 @@ public function processStmtNodes( // rule-facing ask paths $scope = $scope->toWalkScope(); $storage = new ExpressionResultStorage(); - return $this->processStmtNodesInternal( - $parentNode, - $stmts, - $scope, - $storage, - $nodeCallback, - $context, - )->toPublic(); + // a fresh walk an extension starts mid-analysis must not feed the + // interrupted walk's gatherer frames - they describe the body walk + // that was interrupted, not the nested one + $gatherers = $this->nodeGatherers; + $this->nodeGatherers = []; + try { + return $this->processStmtNodesInternal( + $parentNode, + $stmts, + $scope, + $storage, + $nodeCallback, + $context, + )->toPublic(); + } finally { + $this->nodeGatherers = $gatherers; + } } /** @@ -988,12 +1038,41 @@ private function hasContextSensitiveConstruct(Node $node): bool * * @param callable(Node $node, Scope $scope): void $nodeCallback */ + /** + * Opens an engine-feeding gatherer frame for the duration of a body walk. + * The caller closes it in a finally block via popNodeGatherer(). + * + * @param callable(Node, Scope): void $gatherer + */ + public function pushNodeGatherer(callable $gatherer): void + { + $this->nodeGatherers[] = $gatherer; + } + + public function popNodeGatherer(): void + { + array_pop($this->nodeGatherers); + } + + /** + * @param callable(Node $node, Scope $scope): void $nodeCallback + */ public function replayRecording(RecordingNodeCallback $recording, callable $nodeCallback, ExpressionResultStorage $storage): void { $stack = $this->getExpressionResultStorageStack(); $stack->push($storage); try { - $recording->replayThrough($nodeCallback); + foreach ($recording->getPairs() as [$node, $scope]) { + if (!$scope instanceof MutatingScope) { + throw new ShouldNotHappenException(); + } + // gatherer frames observe replayed emissions exactly like live + // ones - with the raw walk scope + foreach ($this->nodeGatherers as $gatherer) { + $gatherer($node, $scope); + } + $nodeCallback($node, $scope->toNodeCallbackScope()); + } } finally { $stack->pop(); } @@ -1026,15 +1105,14 @@ public function callNodeCallback( ExpressionResultStorage $storage, ): void { - // Engine-feeding gatherers must observe the node at the emission + // Engine-feeding gatherer frames observe the node at the emission // position - their arrays are read as soon as the enclosing body walk // returns. Gatherers are engine code and never ask about types - // handing them the raw scope skips a NodeCallbackScope construction per // emission; the scopes they capture (return statements, impure points) // answer later asks through the storage hub like any MutatingScope. - while ($nodeCallback instanceof GatheringNodeCallback) { - ($nodeCallback->getGatherer())($node, $scope); - $nodeCallback = $nodeCallback->getInner(); + foreach ($this->nodeGatherers as $gatherer) { + $gatherer($node, $scope); } if ($nodeCallback instanceof NoopNodeCallback) { @@ -1155,7 +1233,7 @@ public function processClosureNode( $gatheredYieldStatementsWithScope = []; $closureImpurePoints = []; $invalidateExpressions = []; - $closureStmtsCallback = new GatheringNodeCallback(static function (Node $node, Scope $scope) use (&$executionEnds, &$gatheredReturnStatements, &$gatheredReturnStatementsWithScope, &$gatheredYieldStatements, &$gatheredYieldStatementsWithScope, &$closureScope, &$closureImpurePoints, &$invalidateExpressions): void { + $closureStmtsGatherer = static function (Node $node, Scope $scope) use (&$executionEnds, &$gatheredReturnStatements, &$gatheredReturnStatementsWithScope, &$gatheredYieldStatements, &$gatheredYieldStatementsWithScope, &$closureScope, &$closureImpurePoints, &$invalidateExpressions): void { if ($scope->getAnonymousFunctionReflection() !== $closureScope->getAnonymousFunctionReflection()) { return; } @@ -1188,10 +1266,15 @@ public function processClosureNode( $gatheredReturnStatements[] = new ReturnStatement($scope, $node); $gatheredReturnStatementsWithScope[] = [$node, $scope]; - }, $nodeCallback); + }; if (count($byRefUses) === 0) { - $statementResult = $this->processStmtNodesInternal($expr, $expr->stmts, $closureScope, $storage, $closureStmtsCallback, StatementContext::createTopLevel()); + $this->pushNodeGatherer($closureStmtsGatherer); + try { + $statementResult = $this->processStmtNodesInternal($expr, $expr->stmts, $closureScope, $storage, $nodeCallback, StatementContext::createTopLevel()); + } finally { + $this->popNodeGatherer(); + } $publicStatementResult = $statementResult->toPublic(); $closureReturnStatementsNodeScope = $this->refineClosureNodeScope($closureScope, $scope, $expr, $gatheredReturnStatementsWithScope, $gatheredYieldStatementsWithScope, $executionEnds, $statementResult->getThrowPoints(), array_merge($closureImpurePoints, $statementResult->getImpurePoints()), $invalidateExpressions); $this->callNodeCallback($nodeCallback, new ClosureReturnStatementsNode( @@ -1269,23 +1352,28 @@ public function processClosureNode( } $storage = $originalStorage; - if ( - $replayBodyRecording !== null && $replayPassStorage !== null - && $replayPassResult !== null && $replayEntryScope !== null - && $closureScope->equals($replayEntryScope) - ) { - // the final walk would repeat the recorded fixpoint pass exactly - // (same entry scope, deterministic walk) - adopt the pass's result - // and replay its emissions through the gathering callback instead. - // The pass's own entry scope takes over: the recorded pairs carry - // its anonymous-function reflection, which the gathering filter - // compares by identity (the state is equals-identical anyway). - $closureScope = $replayEntryScope; - $originalStorage->mergeResults($replayPassStorage); - $this->replayRecording($replayBodyRecording, $closureStmtsCallback, $originalStorage); - $statementResult = $replayPassResult; - } else { - $statementResult = $this->processStmtNodesInternal($expr, $expr->stmts, $closureScope, $storage, $closureStmtsCallback, StatementContext::createTopLevel()); + $this->pushNodeGatherer($closureStmtsGatherer); + try { + if ( + $replayBodyRecording !== null && $replayPassStorage !== null + && $replayPassResult !== null && $replayEntryScope !== null + && $closureScope->equals($replayEntryScope) + ) { + // the final walk would repeat the recorded fixpoint pass exactly + // (same entry scope, deterministic walk) - adopt the pass's result + // and replay its emissions through the real callback instead. + // The pass's own entry scope takes over: the recorded pairs carry + // its anonymous-function reflection, which the gatherer's filter + // compares by identity (the state is equals-identical anyway). + $closureScope = $replayEntryScope; + $originalStorage->mergeResults($replayPassStorage); + $this->replayRecording($replayBodyRecording, $nodeCallback, $originalStorage); + $statementResult = $replayPassResult; + } else { + $statementResult = $this->processStmtNodesInternal($expr, $expr->stmts, $closureScope, $storage, $nodeCallback, StatementContext::createTopLevel()); + } + } finally { + $this->popNodeGatherer(); } $publicStatementResult = $statementResult->toPublic(); $closureReturnStatementsNodeScope = $this->refineClosureNodeScope($closureScope, $scope, $expr, $gatheredReturnStatementsWithScope, $gatheredYieldStatementsWithScope, $executionEnds, $statementResult->getThrowPoints(), array_merge($closureImpurePoints, $statementResult->getImpurePoints()), $invalidateExpressions); @@ -1410,7 +1498,7 @@ public function processArrowFunctionNode( // feeds ClosureTypeResolver::buildClosureTypeForArrowFunction(). $arrowFunctionImpurePoints = []; $invalidateExpressions = []; - $arrowFunctionStmtsCallback = new GatheringNodeCallback(static function (Node $node, Scope $innerScope) use ($arrowFunctionScope, &$arrowFunctionImpurePoints, &$invalidateExpressions): void { + $arrowFunctionStmtsGatherer = static function (Node $node, Scope $innerScope) use ($arrowFunctionScope, &$arrowFunctionImpurePoints, &$invalidateExpressions): void { if ($innerScope->getAnonymousFunctionReflection() !== $arrowFunctionScope->getAnonymousFunctionReflection()) { return; } @@ -1432,9 +1520,14 @@ public function processArrowFunctionNode( true, ); $invalidateExpressions[] = new InvalidateExprNode($node->getPropertyFetch()); - }, $nodeCallback); + }; - $exprResult = $this->processExprNode($stmt, $expr->expr, $arrowFunctionScope, $storage, $arrowFunctionStmtsCallback, ExpressionContext::createTopLevel()); + $this->pushNodeGatherer($arrowFunctionStmtsGatherer); + try { + $exprResult = $this->processExprNode($stmt, $expr->expr, $arrowFunctionScope, $storage, $nodeCallback, ExpressionContext::createTopLevel()); + } finally { + $this->popNodeGatherer(); + } $closureTypeThrowPoints = array_map(static fn (InternalThrowPoint $throwPoint) => $throwPoint->toPublic(), $exprResult->getThrowPoints()); $closureTypeImpurePoints = array_merge($arrowFunctionImpurePoints, $exprResult->getImpurePoints()); diff --git a/src/Analyser/NoopNodeCallback.php b/src/Analyser/NoopNodeCallback.php index 61bc0789581..33697cbec55 100644 --- a/src/Analyser/NoopNodeCallback.php +++ b/src/Analyser/NoopNodeCallback.php @@ -4,7 +4,7 @@ use PhpParser\Node; -final class NoopNodeCallback implements ShallowNodeCallback +final class NoopNodeCallback { public function __invoke(Node $node, Scope $scope): void diff --git a/src/Analyser/PropertyHooksProcessor.php b/src/Analyser/PropertyHooksProcessor.php index 5c384291459..3de667461a6 100644 --- a/src/Analyser/PropertyHooksProcessor.php +++ b/src/Analyser/PropertyHooksProcessor.php @@ -113,7 +113,7 @@ public function processPropertyHooks( $gatheredReturnStatements = []; $executionEnds = []; $methodImpurePoints = []; - $statementResult = $nodeScopeResolver->processStmtNodesInternal(new PropertyHookStatementNode($hook), $stmts, $hookScope, $storage, new GatheringNodeCallback(static function (Node $node, Scope $scope) use ($hookScope, &$gatheredReturnStatements, &$executionEnds, &$hookImpurePoints): void { + $nodeScopeResolver->pushNodeGatherer(static function (Node $node, Scope $scope) use ($hookScope, &$gatheredReturnStatements, &$executionEnds, &$hookImpurePoints): void { if ($scope->getFunction() !== $hookScope->getFunction()) { return; } @@ -139,7 +139,12 @@ public function processPropertyHooks( } $gatheredReturnStatements[] = new ReturnStatement($scope, $node); - }, $nodeCallback), StatementContext::createTopLevel())->toPublic(); + }); + try { + $statementResult = $nodeScopeResolver->processStmtNodesInternal(new PropertyHookStatementNode($hook), $stmts, $hookScope, $storage, $nodeCallback, StatementContext::createTopLevel())->toPublic(); + } finally { + $nodeScopeResolver->popNodeGatherer(); + } $nodeScopeResolver->callNodeCallback($nodeCallback, new PropertyHookReturnStatementsNode( $hook, diff --git a/src/Analyser/RecordingNodeCallback.php b/src/Analyser/RecordingNodeCallback.php index be4641fe2d5..37e1b5728ed 100644 --- a/src/Analyser/RecordingNodeCallback.php +++ b/src/Analyser/RecordingNodeCallback.php @@ -3,7 +3,6 @@ namespace PHPStan\Analyser; use PhpParser\Node; -use PHPStan\ShouldNotHappenException; /** * Records every (node, scope) emission of a convergence pass in order. When @@ -14,8 +13,8 @@ * * Recording appends the raw walk scope - nothing asks about types until a * replay happens, so the pass pays no callback-scope construction and no - * storage binding. replayThrough() wraps each pair the way - * NodeScopeResolver::callNodeCallback() would have. + * storage binding. NodeScopeResolver::replayRecording() + * wraps each pair the way callNodeCallback() would have. */ final class RecordingNodeCallback { @@ -29,16 +28,11 @@ public function __invoke(Node $node, Scope $scope): void } /** - * @param callable(Node $node, Scope $scope): void $nodeCallback + * @return list */ - public function replayThrough(callable $nodeCallback): void + public function getPairs(): array { - foreach ($this->pairs as [$node, $scope]) { - if (!$scope instanceof MutatingScope) { - throw new ShouldNotHappenException(); - } - $nodeCallback($node, $scope->toNodeCallbackScope()); - } + return $this->pairs; } } diff --git a/src/Analyser/ShallowNodeCallback.php b/src/Analyser/ShallowNodeCallback.php deleted file mode 100644 index de11ae8a04d..00000000000 --- a/src/Analyser/ShallowNodeCallback.php +++ /dev/null @@ -1,12 +0,0 @@ -processStmtNodesInternal($stmt, $stmt->stmts, $methodScope, $storage, new GatheringNodeCallback(static function (Node $node, Scope $scope) use ($methodScope, &$gatheredReturnStatements, &$gatheredYieldStatements, &$executionEnds, &$methodImpurePoints): void { + $nodeScopeResolver->pushNodeGatherer(static function (Node $node, Scope $scope) use ($methodScope, &$gatheredReturnStatements, &$gatheredYieldStatements, &$executionEnds, &$methodImpurePoints): void { if ($scope->getFunction() !== $methodScope->getFunction()) { return; } @@ -207,7 +206,12 @@ public function processStmt( } $gatheredReturnStatements[] = new ReturnStatement($scope, $node); - }, $nodeCallback), StatementContext::createTopLevel())->toPublic(); + }); + try { + $statementResult = $nodeScopeResolver->processStmtNodesInternal($stmt, $stmt->stmts, $methodScope, $storage, $nodeCallback, StatementContext::createTopLevel())->toPublic(); + } finally { + $nodeScopeResolver->popNodeGatherer(); + } $methodReflection = $methodScope->getFunction(); if (!$methodReflection instanceof PhpMethodFromParserNodeReflection) { diff --git a/src/Analyser/StmtHandler/ExpressionHandler.php b/src/Analyser/StmtHandler/ExpressionHandler.php index 98135ebd86f..9db8b02f087 100644 --- a/src/Analyser/StmtHandler/ExpressionHandler.php +++ b/src/Analyser/StmtHandler/ExpressionHandler.php @@ -8,7 +8,6 @@ use PhpParser\Node\Stmt\Expression; use PHPStan\Analyser\ExpressionContext; use PHPStan\Analyser\ExpressionResultStorage; -use PHPStan\Analyser\GatheringNodeCallback; use PHPStan\Analyser\InternalStatementExitPoint; use PHPStan\Analyser\InternalStatementResult; use PHPStan\Analyser\MutatingScope; @@ -56,7 +55,7 @@ public function processStmt( $entryScope = $scope; $hasAssign = false; $currentScope = $scope; - $result = $nodeScopeResolver->processExprNode($stmt, $stmt->expr, $scope, $storage, new GatheringNodeCallback(static function (Node $node, Scope $scope) use ($currentScope, &$hasAssign): void { + $nodeScopeResolver->pushNodeGatherer(static function (Node $node, Scope $scope) use ($currentScope, &$hasAssign): void { if ( !($node instanceof VariableAssignNode) && !($node instanceof PropertyAssignNode) || $scope->getAnonymousFunctionReflection() !== $currentScope->getAnonymousFunctionReflection() @@ -66,7 +65,13 @@ public function processStmt( } $hasAssign = true; - }, $nodeCallback), ExpressionContext::createTopLevel()); + }); + try { + $result = $nodeScopeResolver->processExprNode($stmt, $stmt->expr, $scope, $storage, $nodeCallback, ExpressionContext::createTopLevel()); + } finally { + $nodeScopeResolver->popNodeGatherer(); + } + $nodeScopeResolver->callNodeCallback($nodeCallback, $stmt, $entryScope, $storage); $throwPoints = array_filter($result->getThrowPoints(), static fn ($throwPoint) => $throwPoint->isExplicit()); if ( diff --git a/src/Analyser/StmtHandler/FunctionHandler.php b/src/Analyser/StmtHandler/FunctionHandler.php index 9f063571615..71d66732769 100644 --- a/src/Analyser/StmtHandler/FunctionHandler.php +++ b/src/Analyser/StmtHandler/FunctionHandler.php @@ -9,7 +9,6 @@ use PhpParser\Node\Stmt\Return_; use PHPStan\Analyser\DeprecatedAttributeResolver; use PHPStan\Analyser\ExpressionResultStorage; -use PHPStan\Analyser\GatheringNodeCallback; use PHPStan\Analyser\ImpurePoint; use PHPStan\Analyser\InternalStatementResult; use PHPStan\Analyser\MutatingScope; @@ -100,7 +99,7 @@ public function processStmt( $gatheredYieldStatements = []; $executionEnds = []; $functionImpurePoints = []; - $statementResult = $nodeScopeResolver->processStmtNodesInternal($stmt, $stmt->stmts, $functionScope, $storage, new GatheringNodeCallback(static function (Node $node, Scope $scope) use ($functionScope, &$gatheredReturnStatements, &$gatheredYieldStatements, &$executionEnds, &$functionImpurePoints): void { + $nodeScopeResolver->pushNodeGatherer(static function (Node $node, Scope $scope) use ($functionScope, &$gatheredReturnStatements, &$gatheredYieldStatements, &$executionEnds, &$functionImpurePoints): void { if ($scope->getFunction() !== $functionScope->getFunction()) { return; } @@ -129,7 +128,12 @@ public function processStmt( } $gatheredReturnStatements[] = new ReturnStatement($scope, $node); - }, $nodeCallback), StatementContext::createTopLevel())->toPublic(); + }); + try { + $statementResult = $nodeScopeResolver->processStmtNodesInternal($stmt, $stmt->stmts, $functionScope, $storage, $nodeCallback, StatementContext::createTopLevel())->toPublic(); + } finally { + $nodeScopeResolver->popNodeGatherer(); + } $nodeScopeResolver->callNodeCallback($nodeCallback, new FunctionReturnStatementsNode( $stmt, diff --git a/src/Analyser/VirtualAssignNodeCallback.php b/src/Analyser/VirtualAssignNodeCallback.php index ae6b9ddd735..376a39e70b7 100644 --- a/src/Analyser/VirtualAssignNodeCallback.php +++ b/src/Analyser/VirtualAssignNodeCallback.php @@ -6,7 +6,7 @@ use PHPStan\Node\PropertyAssignNode; use PHPStan\Node\VariableAssignNode; -final class VirtualAssignNodeCallback implements ShallowNodeCallback +final class VirtualAssignNodeCallback { /** @@ -17,23 +17,15 @@ private function __construct(private mixed $originalNodeCallback) } /** - * Rebuilds the chain instead of wrapping it so that GatheringNodeCallback - * layers stay on the outside. Hiding a gatherer behind this filter would - * keep callNodeCallback() from unwrapping it, and the gatherer would miss - * nodes this filter drops for the rule-facing remainder. + * Filters the rule-facing callback down to assign nodes. Engine-feeding + * gatherer frames live on NodeScopeResolver and observe every emission + * regardless of this filter. * * @param callable(Node $node, Scope $scope): void $nodeCallback * @return callable(Node $node, Scope $scope): void */ public static function create(callable $nodeCallback): callable { - if ($nodeCallback instanceof GatheringNodeCallback) { - return new GatheringNodeCallback( - self::create($nodeCallback->getGatherer()), - self::create($nodeCallback->getInner()), - ); - } - return new self($nodeCallback); }