From b8ce16d907c21b0719164b6b2e2b193bc3cde1b8 Mon Sep 17 00:00:00 2001 From: Ondrej Mirtes Date: Wed, 26 Aug 2026 14:19:51 +0200 Subject: [PATCH 1/9] Emit the closure and arrow-function argument callbacks after their results are stored --- src/Analyser/NodeScopeResolver.php | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/src/Analyser/NodeScopeResolver.php b/src/Analyser/NodeScopeResolver.php index 56dc083900f..f4ac0929c9e 100644 --- a/src/Analyser/NodeScopeResolver.php +++ b/src/Analyser/NodeScopeResolver.php @@ -2054,7 +2054,6 @@ public function processArgs( } } - $this->callNodeCallbackWithExpression($nodeCallback, $arg->value, $scopeToPass, $storage, $context); $closureResult = $this->processClosureNode($stmt, $arg->value, $scopeToPass, $storage, $nodeCallback, $context, $parameterType, $parameterNativeType); // the preferred ClosureType read below now answers from this seed // instead of walking the body again (unless a parked fiber may @@ -2074,6 +2073,10 @@ public function processArgs( throwPoints: [], impurePoints: [], )); + // the closure node's own callback fires after its result is + // stored, mirroring processExprNodeInternal() - callback-side + // getType() answers from the stored result + $this->callNodeCallbackWithExpression($nodeCallback, $arg->value, $scopeToPass, $storage, $context); $uses = []; foreach ($arg->value->uses as $use) { @@ -2142,7 +2145,6 @@ public function processArgs( } } - $this->callNodeCallbackWithExpression($nodeCallback, $arg->value, $scopeToPass, $storage, $context); $processArrowFunctionResult = $this->processArrowFunctionNode($stmt, $arg->value, $scopeToPass, $storage, $nodeCallback, $parameterType, $parameterNativeType); // the invalidation read below now answers from this seed instead // of walking the body again (unless a parked fiber may still @@ -2160,6 +2162,10 @@ public function processArgs( } } $this->storeExpressionResult($storage, $arg->value, $arrowFunctionResult); + // the arrow function node's own callback fires after its result + // is stored, mirroring processExprNodeInternal() - callback-side + // getType() answers from the stored result + $this->callNodeCallbackWithExpression($nodeCallback, $arg->value, $scopeToPass, $storage, $context); } else { $exprType = $scope->getType($arg->value); $enterExpressionAssignForByRef = $assignByReference && $arg->value instanceof ArrayDimFetch && $arg->value->dim === null; From 62d74baa9f0ffc67d30f803f187301fb2d56c4ff Mon Sep 17 00:00:00 2001 From: Ondrej Mirtes Date: Tue, 18 Aug 2026 10:41:03 +0200 Subject: [PATCH 2/9] Emit intermediate assign-target links after their dimensions are processed The dimensions loop in doPrepareTarget() fired an intermediate chain link's node callback before evaluating its dimension and before the link's write-flavoured result existed, so callback-side consumers (NonexistentOffsetInArrayDimFetchRule, DependencyResolver) re-walked the yet-unstored sub-expressions. The callback now fires at the end of the iteration, with the link's entry scope, after its result is stored. --- src/Analyser/ExprHandler/AssignHandler.php | 16 +++++++++++----- 1 file changed, 11 insertions(+), 5 deletions(-) diff --git a/src/Analyser/ExprHandler/AssignHandler.php b/src/Analyser/ExprHandler/AssignHandler.php index 4c52ec6abef..b3de7b4b461 100644 --- a/src/Analyser/ExprHandler/AssignHandler.php +++ b/src/Analyser/ExprHandler/AssignHandler.php @@ -578,11 +578,7 @@ private function doPrepareTarget( $lastDimKey = array_key_last($dimFetchStack); foreach ($dimFetchStack as $key => $dimFetch) { $dimExpr = $dimFetch->dim; - - // Callback was already called for last dim at the beginning of the method. - if ($key !== $lastDimKey) { - $nodeScopeResolver->callNodeCallback($nodeCallback, $dimFetch, $enterExpressionAssign ? $scope->enterExpressionAssign($dimFetch) : $scope, $storage); - } + $callbackScope = $scope; if ($dimExpr === null) { $dimResults[$key] = null; @@ -623,6 +619,16 @@ private function doPrepareTarget( $scope = $scope->exitExpressionAssign($dimExpr); } } + + // The whole target's callback fires in prepareTarget() after the + // walk; an intermediate link's fires here, after its dimension was + // processed, so callback-side asks about the dimension answer from + // the storage with the link's entry scope. + if ($key === $lastDimKey) { + continue; + } + + $nodeScopeResolver->callNodeCallback($nodeCallback, $dimFetch, $enterExpressionAssign ? $callbackScope->enterExpressionAssign($dimFetch) : $callbackScope, $storage); } if ($mode->issetSemanticsForRead()) { From dc9397d062c134d1f2dcb8241b623aebfdb20e9a Mon Sep 17 00:00:00 2001 From: Ondrej Mirtes Date: Wed, 26 Aug 2026 14:23:29 +0200 Subject: [PATCH 3/9] Store the AssignOp result before applyWrite() emits assignment nodes --- src/Analyser/ExprHandler/AssignOpHandler.php | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/src/Analyser/ExprHandler/AssignOpHandler.php b/src/Analyser/ExprHandler/AssignOpHandler.php index 03669ecc0cf..186c792b0b8 100644 --- a/src/Analyser/ExprHandler/AssignOpHandler.php +++ b/src/Analyser/ExprHandler/AssignOpHandler.php @@ -100,6 +100,20 @@ public function processExpr(NodeScopeResolver $nodeScopeResolver, Stmt $stmt, Ex ); } + // applyWrite() emits nodes (PropertyAssignNode) whose rules ask about + // this whole `$lvalue OP= value` expression - store a before-scope + // anchored result first so those asks answer from the storage; + // processExprNode() overwrites it with the final result after this + // handler returns + $nodeScopeResolver->storeExpressionResult($storage, $expr, $this->expressionResultFactory->create( + $valueResult->getScope(), + $beforeScope, + $expr, + hasYield: false, + isAlwaysTerminating: false, + throwPoints: [], + impurePoints: [], + )); $assignResult = $this->assignHandler->applyWrite( $nodeScopeResolver, $target, From c1ea5697da3304d25013c457724c7df7dd7aa647 Mon Sep 17 00:00:00 2001 From: Ondrej Mirtes Date: Tue, 18 Aug 2026 10:41:04 +0200 Subject: [PATCH 4/9] Emit array item callbacks after their key and value are processed InvalidKeyInArrayItemRule asks about the item's key at the ArrayItem callback - firing the callback after the key and value walks lets it answer from the storage instead of re-walking them. --- src/Analyser/ExprHandler/ArrayHandler.php | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/src/Analyser/ExprHandler/ArrayHandler.php b/src/Analyser/ExprHandler/ArrayHandler.php index a8e58b932a4..2ac6d9cead4 100644 --- a/src/Analyser/ExprHandler/ArrayHandler.php +++ b/src/Analyser/ExprHandler/ArrayHandler.php @@ -82,7 +82,7 @@ public function processExpr(NodeScopeResolver $nodeScopeResolver, Stmt $stmt, Ex $isAlwaysTerminating = false; foreach ($expr->items as $arrayItem) { $itemNodes[] = new LiteralArrayItem($scope, $arrayItem); - $nodeScopeResolver->callNodeCallback($nodeCallback, $arrayItem, $scope, $storage); + $itemCallbackScope = $scope; if ($arrayItem->key !== null) { $keyResult = $nodeScopeResolver->processExprNode($stmt, $arrayItem->key, $scope, $storage, $nodeCallback, $context->enterDeep()); $hasYield = $hasYield || $keyResult->hasYield(); @@ -98,6 +98,10 @@ public function processExpr(NodeScopeResolver $nodeScopeResolver, Stmt $stmt, Ex $impurePoints = array_merge($impurePoints, $valueResult->getImpurePoints()); $isAlwaysTerminating = $isAlwaysTerminating || $valueResult->isAlwaysTerminating(); $scope = $valueResult->getScope(); + // the item's callback fires after its key and value were processed, + // with the item's entry scope - callback-side asks answer from the + // storage instead of re-walking the yet-unstored sub-expressions + $nodeScopeResolver->callNodeCallback($nodeCallback, $arrayItem, $itemCallbackScope, $storage); } $nodeScopeResolver->callNodeCallback($nodeCallback, new LiteralArrayNode($expr, $itemNodes), $scope, $storage); From ab17cdacec7aacf0054381a25e3176acac57badd Mon Sep 17 00:00:00 2001 From: Ondrej Mirtes Date: Tue, 18 Aug 2026 10:41:04 +0200 Subject: [PATCH 5/9] Defer Unset_, ClassConst, Const_ and While_ statement callbacks past their expressions These statements' rules (UnsetRule, ValueAssignedToClassConstantRule, ValueAssignedToGlobalConstantRule, WhileLoopAlwaysFalseConditionRule) read the statement's child expressions, so the statement callback now fires inside the handler after those expressions are processed - with the entry scope, joining the existing deferred set (Return_, Expression, Echo_, If_, Switch_, Foreach_). The per-constant Const node callbacks move after their value walks for the same reason. --- src/Analyser/NodeScopeResolver.php | 4 +++- src/Analyser/StmtHandler/ClassConstHandler.php | 8 +++++++- src/Analyser/StmtHandler/ConstHandler.php | 8 +++++++- src/Analyser/StmtHandler/UnsetHandler.php | 6 ++++++ src/Analyser/StmtHandler/WhileHandler.php | 6 ++++++ 5 files changed, 29 insertions(+), 3 deletions(-) diff --git a/src/Analyser/NodeScopeResolver.php b/src/Analyser/NodeScopeResolver.php index f4ac0929c9e..516f16fad32 100644 --- a/src/Analyser/NodeScopeResolver.php +++ b/src/Analyser/NodeScopeResolver.php @@ -729,7 +729,9 @@ public function processStmtNode( // PHP < 8.1) then finds the expressions' results in the storage instead // of re-walking them on demand, mirroring processExprNodeInternal(). $deferredStmtCallback = $stmt instanceof Return_ || $stmt instanceof Node\Stmt\Expression || $stmt instanceof Echo_ - || $stmt instanceof If_ || $stmt instanceof Switch_ || $stmt instanceof Foreach_; + || $stmt instanceof If_ || $stmt instanceof Switch_ || $stmt instanceof Foreach_ + || $stmt instanceof Node\Stmt\Unset_ || $stmt instanceof Node\Stmt\ClassConst + || $stmt instanceof Node\Stmt\Const_ || $stmt instanceof Node\Stmt\While_; if (!$deferredStmtCallback) { $this->callNodeCallback($nodeCallback, $stmt, $stmtScope, $storage); } diff --git a/src/Analyser/StmtHandler/ClassConstHandler.php b/src/Analyser/StmtHandler/ClassConstHandler.php index e6f99e33352..fd6f1977582 100644 --- a/src/Analyser/StmtHandler/ClassConstHandler.php +++ b/src/Analyser/StmtHandler/ClassConstHandler.php @@ -38,11 +38,14 @@ public function processStmt( StatementContext $context, ): InternalStatementResult { + $entryScope = $scope; $impurePoints = []; $nodeScopeResolver->processAttributeGroups($stmt, $stmt->attrGroups, $scope, $storage, $nodeCallback); foreach ($stmt->consts as $const) { - $nodeScopeResolver->callNodeCallback($nodeCallback, $const, $scope, $storage); $constResult = $nodeScopeResolver->processExprNode($stmt, $const->value, $scope, $storage, $nodeCallback, ExpressionContext::createDeep()); + // the constant's callback fires after its value was processed, so + // rule-side asks about the value answer from the storage + $nodeScopeResolver->callNodeCallback($nodeCallback, $const, $scope, $storage); $impurePoints = array_merge($impurePoints, $constResult->getImpurePoints()); if ($scope->getClassReflection() === null) { throw new ShouldNotHappenException(); @@ -54,6 +57,9 @@ public function processStmt( ); } + // deferred from processStmtNode() - fires after the values were processed + $nodeScopeResolver->callNodeCallback($nodeCallback, $stmt, $entryScope, $storage); + return new InternalStatementResult($scope, hasYield: false, isAlwaysTerminating: false, exitPoints: [], throwPoints: [], impurePoints: $impurePoints); } diff --git a/src/Analyser/StmtHandler/ConstHandler.php b/src/Analyser/StmtHandler/ConstHandler.php index f7008b80890..9dd7404d9ee 100644 --- a/src/Analyser/StmtHandler/ConstHandler.php +++ b/src/Analyser/StmtHandler/ConstHandler.php @@ -37,10 +37,13 @@ public function processStmt( StatementContext $context, ): InternalStatementResult { + $entryScope = $scope; $impurePoints = []; foreach ($stmt->consts as $const) { - $nodeScopeResolver->callNodeCallback($nodeCallback, $const, $scope, $storage); $constResult = $nodeScopeResolver->processExprNode($stmt, $const->value, $scope, $storage, $nodeCallback, ExpressionContext::createDeep()); + // the constant's callback fires after its value was processed, so + // rule-side asks about the value answer from the storage + $nodeScopeResolver->callNodeCallback($nodeCallback, $const, $scope, $storage); $impurePoints = array_merge($impurePoints, $constResult->getImpurePoints()); if ($const->namespacedName !== null) { $constantName = new Name\FullyQualified($const->namespacedName->toString()); @@ -50,6 +53,9 @@ public function processStmt( $scope = $scope->assignExpression(new ConstFetch($constantName), $constResult->getType(), $constResult->getNativeType()); } + // deferred from processStmtNode() - fires after the values were processed + $nodeScopeResolver->callNodeCallback($nodeCallback, $stmt, $entryScope, $storage); + return new InternalStatementResult($scope, hasYield: false, isAlwaysTerminating: false, exitPoints: [], throwPoints: [], impurePoints: $impurePoints); } diff --git a/src/Analyser/StmtHandler/UnsetHandler.php b/src/Analyser/StmtHandler/UnsetHandler.php index c2bc4f4da1e..af630eba78b 100644 --- a/src/Analyser/StmtHandler/UnsetHandler.php +++ b/src/Analyser/StmtHandler/UnsetHandler.php @@ -52,6 +52,7 @@ public function processStmt( StatementContext $context, ): InternalStatementResult { + $entryScope = $scope; $hasYield = false; $throwPoints = []; $impurePoints = []; @@ -104,6 +105,11 @@ public function processStmt( $scope = $scope->invalidateExpression(new ForeachValueByRefExpr($var)); } + // the Unset_ callback is deferred from processStmtNode(): it fires after + // the unset targets were processed, with the entry scope, so rule-side + // asks about them answer from the storage + $nodeScopeResolver->callNodeCallback($nodeCallback, $stmt, $entryScope, $storage); + return new InternalStatementResult($scope, hasYield: $hasYield, isAlwaysTerminating: false, exitPoints: [], throwPoints: $throwPoints, impurePoints: $impurePoints); } diff --git a/src/Analyser/StmtHandler/WhileHandler.php b/src/Analyser/StmtHandler/WhileHandler.php index 1edd270d700..ec01f387ed8 100644 --- a/src/Analyser/StmtHandler/WhileHandler.php +++ b/src/Analyser/StmtHandler/WhileHandler.php @@ -121,10 +121,16 @@ public function processStmt( // and replay its emissions through the real callback instead $originalStorage->mergeResults($replayPassStorage); $nodeScopeResolver->replayRecording($replayCondRecording, $nodeCallback, $originalStorage); + // the While_ callback is deferred from processStmtNode(): it fires + // after the condition's results are in the storage, with the entry scope + $nodeScopeResolver->callNodeCallback($nodeCallback, $stmt, $scope, $originalStorage); $nodeScopeResolver->replayRecording($replayBodyRecording, $nodeCallback, $originalStorage); $finalScopeResult = $replayPassResult; } else { $bodyScope = $nodeScopeResolver->processExprNode($stmt, $stmt->cond, $bodyScope, $storage, $nodeCallback, ExpressionContext::createDeep())->getTruthyScope(); + // the While_ callback is deferred from processStmtNode(): it fires + // after the condition's real walk stored its result, with the entry scope + $nodeScopeResolver->callNodeCallback($nodeCallback, $stmt, $scope, $storage); $finalScopeResult = $nodeScopeResolver->processStmtNodesInternal($stmt, $stmt->stmts, $bodyScope, $storage, $nodeCallback, $context)->filterOutLoopExitPoints(); } $finalScope = $finalScopeResult->getScope()->filterByFalseyValue($stmt->cond); From 404e7330895fe5e29fb38d509cb69e89e7aec5da Mon Sep 17 00:00:00 2001 From: Ondrej Mirtes Date: Tue, 18 Aug 2026 10:52:47 +0200 Subject: [PATCH 6/9] Process class attributes before emitting InClassNode ClassAttributesRule fires on InClassNode and reads the attribute constructor arguments - walking the attribute groups first lets it answer from the storage. --- src/Analyser/StmtHandler/ClassLikeHandler.php | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/src/Analyser/StmtHandler/ClassLikeHandler.php b/src/Analyser/StmtHandler/ClassLikeHandler.php index f2fbf7dd2e9..2cdbf50e91f 100644 --- a/src/Analyser/StmtHandler/ClassLikeHandler.php +++ b/src/Analyser/StmtHandler/ClassLikeHandler.php @@ -86,7 +86,6 @@ public function processStmt( if (isset($stmt->namespacedName)) { $classReflection = $this->getCurrentClassReflection($nodeScopeResolver, $stmt, $stmt->namespacedName->toString(), $scope); $classScope = $scope->enterClass($classReflection); - $nodeScopeResolver->callNodeCallback($nodeCallback, new InClassNode($stmt, $classReflection), $classScope, $storage); } elseif ($stmt instanceof Class_) { if ($stmt->name === null) { throw new ShouldNotHappenException(); @@ -97,13 +96,16 @@ public function processStmt( $classReflection = $this->reflectionProvider->getAnonymousClassReflection($stmt, $scope); } $classScope = $scope->enterClass($classReflection); - $nodeScopeResolver->callNodeCallback($nodeCallback, new InClassNode($stmt, $classReflection), $classScope, $storage); } else { throw new ShouldNotHappenException(); } $classStatementsGatherer = new ClassStatementsGatherer($classReflection, $nodeCallback); + // the class attributes are processed before the InClassNode emission, so + // rules firing on it (ClassAttributesRule) read the attribute arguments + // from the storage $nodeScopeResolver->processAttributeGroups($stmt, $stmt->attrGroups, $classScope, $storage, $classStatementsGatherer); + $nodeScopeResolver->callNodeCallback($nodeCallback, new InClassNode($stmt, $classReflection), $classScope, $storage); $classLikeStatements = $stmt->stmts; // analyze static methods first; constructor next; instance methods and property hooks last so we can carry over the scope From cc4a87323f379a2d5b884b4fd458983411e64015 Mon Sep 17 00:00:00 2001 From: Ondrej Mirtes Date: Wed, 26 Aug 2026 17:04:38 +0200 Subject: [PATCH 7/9] Defer the Switch_ callback past its case conditions --- src/Analyser/StmtHandler/SwitchHandler.php | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/src/Analyser/StmtHandler/SwitchHandler.php b/src/Analyser/StmtHandler/SwitchHandler.php index 15486de1a14..e5156d02dd8 100644 --- a/src/Analyser/StmtHandler/SwitchHandler.php +++ b/src/Analyser/StmtHandler/SwitchHandler.php @@ -44,7 +44,6 @@ public function processStmt( { $entryScope = $scope; $condResult = $nodeScopeResolver->processExprNode($stmt, $stmt->cond, $scope, $storage, $nodeCallback, ExpressionContext::createDeep()); - $nodeScopeResolver->callNodeCallback($nodeCallback, $stmt, $entryScope, $storage); $scope = $condResult->getScope(); $scopeForBranches = $scope; $finalScope = null; @@ -117,6 +116,11 @@ public function processStmt( } } + // the Switch_ callback is deferred from processStmtNode(): it fires + // after every case condition's walk stored its result, with the entry + // scope, so rules pricing the case conditions answer from the storage + $nodeScopeResolver->callNodeCallback($nodeCallback, $stmt, $entryScope, $storage); + if ($switchConditionArms !== []) { $nodeScopeResolver->callNodeCallback($nodeCallback, new SwitchConditionNode($stmt->cond, $switchConditionArms, $stmt), $scope, $storage); } From ca6954f070a9a1ed65de0fbd344e4164d7c487c4 Mon Sep 17 00:00:00 2001 From: Ondrej Mirtes Date: Wed, 26 Aug 2026 17:04:38 +0200 Subject: [PATCH 8/9] Defer the Do_ callback and emit DoWhileLoopConditionNode after the final condition walk --- src/Analyser/NodeScopeResolver.php | 3 ++- src/Analyser/StmtHandler/DoWhileHandler.php | 8 ++++++-- 2 files changed, 8 insertions(+), 3 deletions(-) diff --git a/src/Analyser/NodeScopeResolver.php b/src/Analyser/NodeScopeResolver.php index 516f16fad32..4ed253d91a6 100644 --- a/src/Analyser/NodeScopeResolver.php +++ b/src/Analyser/NodeScopeResolver.php @@ -731,7 +731,8 @@ public function processStmtNode( $deferredStmtCallback = $stmt instanceof Return_ || $stmt instanceof Node\Stmt\Expression || $stmt instanceof Echo_ || $stmt instanceof If_ || $stmt instanceof Switch_ || $stmt instanceof Foreach_ || $stmt instanceof Node\Stmt\Unset_ || $stmt instanceof Node\Stmt\ClassConst - || $stmt instanceof Node\Stmt\Const_ || $stmt instanceof Node\Stmt\While_; + || $stmt instanceof Node\Stmt\Const_ || $stmt instanceof Node\Stmt\While_ + || $stmt instanceof Node\Stmt\Do_; if (!$deferredStmtCallback) { $this->callNodeCallback($nodeCallback, $stmt, $stmtScope, $storage); } diff --git a/src/Analyser/StmtHandler/DoWhileHandler.php b/src/Analyser/StmtHandler/DoWhileHandler.php index 483ebceb03b..5d069942715 100644 --- a/src/Analyser/StmtHandler/DoWhileHandler.php +++ b/src/Analyser/StmtHandler/DoWhileHandler.php @@ -125,8 +125,6 @@ public function processStmt( $alwaysIterates = $condBooleanType->isTrue()->yes(); } - $nodeScopeResolver->callNodeCallback($nodeCallback, new DoWhileLoopConditionNode($stmt->cond, $bodyScopeResult->toPublic()->getExitPoints(), $bodyScopeResult->hasYield()), $bodyScope, $storage); - if ($alwaysIterates) { $alwaysTerminating = count($bodyScopeResult->getExitPointsByType(Break_::class)) === 0; } else { @@ -146,6 +144,12 @@ public function processStmt( $nodeScopeResolver->processExprNode($stmt, $stmt->cond, $bodyScope, $storage, $nodeCallback, ExpressionContext::createDeep()); } + // both emissions fire after the condition's final walk stored its + // results, so rule-side asks about the condition answer from the + // storage; the Do_ callback is deferred from processStmtNode() + $nodeScopeResolver->callNodeCallback($nodeCallback, new DoWhileLoopConditionNode($stmt->cond, $bodyScopeResult->toPublic()->getExitPoints(), $bodyScopeResult->hasYield()), $bodyScope, $storage); + $nodeScopeResolver->callNodeCallback($nodeCallback, $stmt, $scope, $storage); + $breakExitPoints = $bodyScopeResult->getExitPointsByType(Break_::class); if (count($breakExitPoints) > 0) { $breakScope = $alwaysIterates ? null : $finalScope; From f63f1a1d8a3bd2c47d4d5badd395465713038310 Mon Sep 17 00:00:00 2001 From: Ondrej Mirtes Date: Wed, 26 Aug 2026 17:04:38 +0200 Subject: [PATCH 9/9] Store inc/dec results before the virtual assign emits assignment nodes --- src/Analyser/ExprHandler/PostDecHandler.php | 14 ++++++++++++++ src/Analyser/ExprHandler/PostIncHandler.php | 14 ++++++++++++++ src/Analyser/ExprHandler/PreDecHandler.php | 14 ++++++++++++++ src/Analyser/ExprHandler/PreIncHandler.php | 14 ++++++++++++++ 4 files changed, 56 insertions(+) diff --git a/src/Analyser/ExprHandler/PostDecHandler.php b/src/Analyser/ExprHandler/PostDecHandler.php index ecdf3bd84d8..a4ede3c45b0 100644 --- a/src/Analyser/ExprHandler/PostDecHandler.php +++ b/src/Analyser/ExprHandler/PostDecHandler.php @@ -40,6 +40,20 @@ public function processExpr(NodeScopeResolver $nodeScopeResolver, Stmt $stmt, Ex { $varResult = $nodeScopeResolver->processExprNode($stmt, $expr->var, $scope, $storage, $nodeCallback, $context->enterDeep()); + // processVirtualAssign() emits nodes (PropertyAssignNode) whose rules ask + // about this whole expression - store a before-scope anchored result + // first so those asks answer from the storage; processExprNode() + // overwrites it with the final result after this handler returns + $nodeScopeResolver->storeExpressionResult($storage, $expr, $this->expressionResultFactory->create( + $varResult->getScope(), + $scope, + $expr, + hasYield: false, + isAlwaysTerminating: false, + throwPoints: [], + impurePoints: [], + )); + return $this->expressionResultFactory->create( $nodeScopeResolver->processVirtualAssign( $varResult->getScope(), diff --git a/src/Analyser/ExprHandler/PostIncHandler.php b/src/Analyser/ExprHandler/PostIncHandler.php index 9a68af90336..45feee87c08 100644 --- a/src/Analyser/ExprHandler/PostIncHandler.php +++ b/src/Analyser/ExprHandler/PostIncHandler.php @@ -40,6 +40,20 @@ public function processExpr(NodeScopeResolver $nodeScopeResolver, Stmt $stmt, Ex { $varResult = $nodeScopeResolver->processExprNode($stmt, $expr->var, $scope, $storage, $nodeCallback, $context->enterDeep()); + // processVirtualAssign() emits nodes (PropertyAssignNode) whose rules ask + // about this whole expression - store a before-scope anchored result + // first so those asks answer from the storage; processExprNode() + // overwrites it with the final result after this handler returns + $nodeScopeResolver->storeExpressionResult($storage, $expr, $this->expressionResultFactory->create( + $varResult->getScope(), + $scope, + $expr, + hasYield: false, + isAlwaysTerminating: false, + throwPoints: [], + impurePoints: [], + )); + return $this->expressionResultFactory->create( $nodeScopeResolver->processVirtualAssign( $varResult->getScope(), diff --git a/src/Analyser/ExprHandler/PreDecHandler.php b/src/Analyser/ExprHandler/PreDecHandler.php index 6569fde8c10..542fecd6a28 100644 --- a/src/Analyser/ExprHandler/PreDecHandler.php +++ b/src/Analyser/ExprHandler/PreDecHandler.php @@ -103,6 +103,20 @@ public function processExpr(NodeScopeResolver $nodeScopeResolver, Stmt $stmt, Ex { $varResult = $nodeScopeResolver->processExprNode($stmt, $expr->var, $scope, $storage, $nodeCallback, $context->enterDeep()); + // processVirtualAssign() emits nodes (PropertyAssignNode) whose rules ask + // about this whole expression - store a before-scope anchored result + // first so those asks answer from the storage; processExprNode() + // overwrites it with the final result after this handler returns + $nodeScopeResolver->storeExpressionResult($storage, $expr, $this->expressionResultFactory->create( + $varResult->getScope(), + $scope, + $expr, + hasYield: false, + isAlwaysTerminating: false, + throwPoints: [], + impurePoints: [], + )); + return $this->expressionResultFactory->create( $nodeScopeResolver->processVirtualAssign( $varResult->getScope(), diff --git a/src/Analyser/ExprHandler/PreIncHandler.php b/src/Analyser/ExprHandler/PreIncHandler.php index 7d4be597076..f1cfc99720a 100644 --- a/src/Analyser/ExprHandler/PreIncHandler.php +++ b/src/Analyser/ExprHandler/PreIncHandler.php @@ -104,6 +104,20 @@ public function processExpr(NodeScopeResolver $nodeScopeResolver, Stmt $stmt, Ex { $varResult = $nodeScopeResolver->processExprNode($stmt, $expr->var, $scope, $storage, $nodeCallback, $context->enterDeep()); + // processVirtualAssign() emits nodes (PropertyAssignNode) whose rules ask + // about this whole expression - store a before-scope anchored result + // first so those asks answer from the storage; processExprNode() + // overwrites it with the final result after this handler returns + $nodeScopeResolver->storeExpressionResult($storage, $expr, $this->expressionResultFactory->create( + $varResult->getScope(), + $scope, + $expr, + hasYield: false, + isAlwaysTerminating: false, + throwPoints: [], + impurePoints: [], + )); + return $this->expressionResultFactory->create( $nodeScopeResolver->processVirtualAssign( $varResult->getScope(),