From 760c3fc019ccc0e3deabb8a7ad301d2aa424fa23 Mon Sep 17 00:00:00 2001 From: Ondrej Mirtes Date: Tue, 28 Jul 2026 20:38:40 +0200 Subject: [PATCH] Run engine-feeding node-callback gatherers synchronously at the emission position Body walks pair an engine-feeding gatherer (impure points, execution ends, return statements, array_walk value types, anonymous-class constructor results, ...) with the rule-facing node callback in one closure, and read the gatherer's by-reference arrays as soon as the walk returns. FiberNodeScopeResolver defers node callbacks to fibers - a rule parking on an unsettled expression would delay the gathering past the read. Pair the two in a GatheringNodeCallback: the fiber engine runs the gatherer synchronously at the emission position and defers only the rule-facing remainder. Co-Authored-By: Claude Fable 5 Claude-Session: https://claude.ai/code/session_019wqGgaD7iqL44t1KgpJS7b --- src/Analyser/ExprHandler/FuncCallHandler.php | 56 ++++++++++--------- src/Analyser/ExprHandler/NewHandler.php | 6 +- src/Analyser/Fiber/FiberNodeScopeResolver.php | 10 ++++ src/Analyser/GatheringNodeCallback.php | 52 +++++++++++++++++ src/Analyser/NodeScopeResolver.php | 35 ++++++------ 5 files changed, 111 insertions(+), 48 deletions(-) create mode 100644 src/Analyser/GatheringNodeCallback.php diff --git a/src/Analyser/ExprHandler/FuncCallHandler.php b/src/Analyser/ExprHandler/FuncCallHandler.php index 3b5d306ffc5..c7d1c46c178 100644 --- a/src/Analyser/ExprHandler/FuncCallHandler.php +++ b/src/Analyser/ExprHandler/FuncCallHandler.php @@ -22,6 +22,7 @@ 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; @@ -253,36 +254,39 @@ public function processExpr(NodeScopeResolver $nodeScopeResolver, Stmt $stmt, Ex $arrayWalkOriginalArrayType = $scope->getType($arrayWalkArrayArg); $arrayWalkOriginalArrayNativeType = $scope->getNativeType($arrayWalkArrayArg); - $nodeCallbackForArgs = static function (Node $node, Scope $scope) use ($nodeCallback, $callbackArg, $firstParamName, &$arrayWalkValueTypes): void { - if ($node instanceof ClosureReturnStatementsNode && $node->getClosureExpr() === $callbackArg) { - $types = []; - $nativeTypes = []; - $stmtResult = $node->getStatementResult(); - foreach ($stmtResult->getExitPoints() as $exitPoint) { - $exitScope = $exitPoint->getScope(); - if (!$exitScope->hasVariableType($firstParamName)->yes()) { - continue; - } + $nodeCallbackForArgs = new GatheringNodeCallback(static function (Node $node, Scope $scope) use ($callbackArg, $firstParamName, &$arrayWalkValueTypes): void { + if (!($node instanceof ClosureReturnStatementsNode) || $node->getClosureExpr() !== $callbackArg) { + return; + } - $types[] = $exitScope->getVariableType($firstParamName); - $nativeTypes[] = $exitScope->getNativeType(new Variable($firstParamName)); - } - if (!$stmtResult->isAlwaysTerminating()) { - $stmtScope = $stmtResult->getScope(); - if ($stmtScope->hasVariableType($firstParamName)->yes()) { - $types[] = $stmtScope->getVariableType($firstParamName); - $nativeTypes[] = $stmtScope->getNativeType(new Variable($firstParamName)); - } + $types = []; + $nativeTypes = []; + $stmtResult = $node->getStatementResult(); + foreach ($stmtResult->getExitPoints() as $exitPoint) { + $exitScope = $exitPoint->getScope(); + if (!$exitScope->hasVariableType($firstParamName)->yes()) { + continue; } - if (count($types) > 0) { - $arrayWalkValueTypes = [ - TypeCombinator::union(...$types), - TypeCombinator::union(...$nativeTypes), - ]; + + $types[] = $exitScope->getVariableType($firstParamName); + $nativeTypes[] = $exitScope->getNativeType(new Variable($firstParamName)); + } + if (!$stmtResult->isAlwaysTerminating()) { + $stmtScope = $stmtResult->getScope(); + if ($stmtScope->hasVariableType($firstParamName)->yes()) { + $types[] = $stmtScope->getVariableType($firstParamName); + $nativeTypes[] = $stmtScope->getNativeType(new Variable($firstParamName)); } } - $nodeCallback($node, $scope); - }; + if (count($types) <= 0) { + return; + } + + $arrayWalkValueTypes = [ + TypeCombinator::union(...$types), + TypeCombinator::union(...$nativeTypes), + ]; + }, $nodeCallback); } } diff --git a/src/Analyser/ExprHandler/NewHandler.php b/src/Analyser/ExprHandler/NewHandler.php index 3598f135a38..6f1f87ba285 100644 --- a/src/Analyser/ExprHandler/NewHandler.php +++ b/src/Analyser/ExprHandler/NewHandler.php @@ -14,6 +14,7 @@ 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,8 +131,7 @@ public function processExpr(NodeScopeResolver $nodeScopeResolver, Stmt $stmt, Ex if ($constructorReflection->getDeclaringClass()->getName() === $classReflection->getName()) { $constructorResult = null; - $nodeScopeResolver->processStmtNode($expr->class, $scope, $storage, static function (Node $node, Scope $scope) use ($nodeCallback, $classReflection, &$constructorResult): void { - $nodeCallback($node, $scope); + $nodeScopeResolver->processStmtNode($expr->class, $scope, $storage, new GatheringNodeCallback(static function (Node $node, Scope $scope) use ($classReflection, &$constructorResult): void { if (!$node instanceof MethodReturnStatementsNode) { return; } @@ -149,7 +149,7 @@ public function processExpr(NodeScopeResolver $nodeScopeResolver, Stmt $stmt, Ex return; } $constructorResult = $node; - }, StatementContext::createTopLevel()); + }, $nodeCallback), StatementContext::createTopLevel()); 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/Fiber/FiberNodeScopeResolver.php b/src/Analyser/Fiber/FiberNodeScopeResolver.php index 45989703ec6..22dc2be0dea 100644 --- a/src/Analyser/Fiber/FiberNodeScopeResolver.php +++ b/src/Analyser/Fiber/FiberNodeScopeResolver.php @@ -7,6 +7,7 @@ use PhpParser\Node\Expr; use PHPStan\Analyser\ExpressionResult; use PHPStan\Analyser\ExpressionResultStorage; +use PHPStan\Analyser\GatheringNodeCallback; use PHPStan\Analyser\MutatingScope; use PHPStan\Analyser\NodeScopeResolver; use PHPStan\Analyser\Scope; @@ -31,6 +32,15 @@ public function callNodeCallback( ExpressionResultStorage $storage, ): void { + // Engine-feeding gatherers must observe the node at the emission + // position - their arrays are read as soon as the enclosing body walk + // returns. Only the rule-facing remainder may be deferred to a fiber; + // a rule parking on an unsettled expression must not delay gathering. + while ($nodeCallback instanceof GatheringNodeCallback) { + ($nodeCallback->getGatherer())($node, $scope); + $nodeCallback = $nodeCallback->getInner(); + } + if (Fiber::getCurrent() !== null) { $nodeCallback($node, $scope->toFiberScope()); return; diff --git a/src/Analyser/GatheringNodeCallback.php b/src/Analyser/GatheringNodeCallback.php new file mode 100644 index 00000000000..6257f53184d --- /dev/null +++ b/src/Analyser/GatheringNodeCallback.php @@ -0,0 +1,52 @@ +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 0bef339bebe..9df08b89aeb 100644 --- a/src/Analyser/NodeScopeResolver.php +++ b/src/Analyser/NodeScopeResolver.php @@ -827,8 +827,7 @@ public function processStmtNode( $gatheredYieldStatements = []; $executionEnds = []; $functionImpurePoints = []; - $statementResult = $this->processStmtNodesInternal($stmt, $stmt->stmts, $functionScope, $storage, static function (Node $node, Scope $scope) use ($nodeCallback, $functionScope, &$gatheredReturnStatements, &$gatheredYieldStatements, &$executionEnds, &$functionImpurePoints): void { - $nodeCallback($node, $scope); + $statementResult = $this->processStmtNodesInternal($stmt, $stmt->stmts, $functionScope, $storage, new GatheringNodeCallback(static function (Node $node, Scope $scope) use ($functionScope, &$gatheredReturnStatements, &$gatheredYieldStatements, &$executionEnds, &$functionImpurePoints): void { if ($scope->getFunction() !== $functionScope->getFunction()) { return; } @@ -857,7 +856,7 @@ public function processStmtNode( } $gatheredReturnStatements[] = new ReturnStatement($scope, $node); - }, StatementContext::createTopLevel())->toPublic(); + }, $nodeCallback), StatementContext::createTopLevel())->toPublic(); $this->callNodeCallback($nodeCallback, new FunctionReturnStatementsNode( $stmt, @@ -982,8 +981,7 @@ public function processStmtNode( $gatheredYieldStatements = []; $executionEnds = []; $methodImpurePoints = []; - $statementResult = $this->processStmtNodesInternal($stmt, $stmt->stmts, $methodScope, $storage, static function (Node $node, Scope $scope) use ($nodeCallback, $methodScope, &$gatheredReturnStatements, &$gatheredYieldStatements, &$executionEnds, &$methodImpurePoints): void { - $nodeCallback($node, $scope); + $statementResult = $this->processStmtNodesInternal($stmt, $stmt->stmts, $methodScope, $storage, new GatheringNodeCallback(static function (Node $node, Scope $scope) use ($methodScope, &$gatheredReturnStatements, &$gatheredYieldStatements, &$executionEnds, &$methodImpurePoints): void { if ($scope->getFunction() !== $methodScope->getFunction()) { return; } @@ -1021,7 +1019,7 @@ public function processStmtNode( } $gatheredReturnStatements[] = new ReturnStatement($scope, $node); - }, StatementContext::createTopLevel())->toPublic(); + }, $nodeCallback), StatementContext::createTopLevel())->toPublic(); $methodReflection = $methodScope->getFunction(); if (!$methodReflection instanceof PhpMethodFromParserNodeReflection) { @@ -1145,16 +1143,17 @@ public function processStmtNode( $earlyTerminationExpr = $this->findEarlyTerminatingExpr($stmt->expr, $scope); $hasAssign = false; $currentScope = $scope; - $result = $this->processExprNode($stmt, $stmt->expr, $scope, $storage, static function (Node $node, Scope $scope) use ($nodeCallback, $currentScope, &$hasAssign): void { + $result = $this->processExprNode($stmt, $stmt->expr, $scope, $storage, new GatheringNodeCallback(static function (Node $node, Scope $scope) use ($currentScope, &$hasAssign): void { if ( - ($node instanceof VariableAssignNode || $node instanceof PropertyAssignNode) - && $scope->getAnonymousFunctionReflection() === $currentScope->getAnonymousFunctionReflection() - && $scope->getFunction() === $currentScope->getFunction() + !($node instanceof VariableAssignNode) && !($node instanceof PropertyAssignNode) + || $scope->getAnonymousFunctionReflection() !== $currentScope->getAnonymousFunctionReflection() + || $scope->getFunction() !== $currentScope->getFunction() ) { - $hasAssign = true; + return; } - $nodeCallback($node, $scope); - }, ExpressionContext::createTopLevel()); + + $hasAssign = true; + }, $nodeCallback), ExpressionContext::createTopLevel()); $throwPoints = array_filter($result->getThrowPoints(), static fn ($throwPoint) => $throwPoint->isExplicit()); if ( count($result->getImpurePoints()) === 0 @@ -3021,8 +3020,7 @@ public function processClosureNode( $gatheredYieldStatements = []; $closureImpurePoints = []; $invalidateExpressions = []; - $closureStmtsCallback = static function (Node $node, Scope $scope) use ($nodeCallback, &$executionEnds, &$gatheredReturnStatements, &$gatheredYieldStatements, &$closureScope, &$closureImpurePoints, &$invalidateExpressions): void { - $nodeCallback($node, $scope); + $closureStmtsCallback = new GatheringNodeCallback(static function (Node $node, Scope $scope) use (&$executionEnds, &$gatheredReturnStatements, &$gatheredYieldStatements, &$closureScope, &$closureImpurePoints, &$invalidateExpressions): void { if ($scope->getAnonymousFunctionReflection() !== $closureScope->getAnonymousFunctionReflection()) { return; } @@ -3053,7 +3051,7 @@ public function processClosureNode( } $gatheredReturnStatements[] = new ReturnStatement($scope, $node); - }; + }, $nodeCallback); if (count($byRefUses) === 0) { $statementResult = $this->processStmtNodesInternalWithoutFlushingPendingFibers($expr, $expr->stmts, $closureScope, $storage, $closureStmtsCallback, StatementContext::createTopLevel()); @@ -3433,8 +3431,7 @@ private function processPropertyHooks( $gatheredReturnStatements = []; $executionEnds = []; $methodImpurePoints = []; - $statementResult = $this->processStmtNodesInternal(new PropertyHookStatementNode($hook), $stmts, $hookScope, $storage, static function (Node $node, Scope $scope) use ($nodeCallback, $hookScope, &$gatheredReturnStatements, &$executionEnds, &$hookImpurePoints): void { - $nodeCallback($node, $scope); + $statementResult = $this->processStmtNodesInternal(new PropertyHookStatementNode($hook), $stmts, $hookScope, $storage, new GatheringNodeCallback(static function (Node $node, Scope $scope) use ($hookScope, &$gatheredReturnStatements, &$executionEnds, &$hookImpurePoints): void { if ($scope->getFunction() !== $hookScope->getFunction()) { return; } @@ -3460,7 +3457,7 @@ private function processPropertyHooks( } $gatheredReturnStatements[] = new ReturnStatement($scope, $node); - }, StatementContext::createTopLevel())->toPublic(); + }, $nodeCallback), StatementContext::createTopLevel())->toPublic(); $this->callNodeCallback($nodeCallback, new PropertyHookReturnStatementsNode( $hook,