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); 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()) { 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, 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(), diff --git a/src/Analyser/NodeScopeResolver.php b/src/Analyser/NodeScopeResolver.php index 56dc083900f..4ed253d91a6 100644 --- a/src/Analyser/NodeScopeResolver.php +++ b/src/Analyser/NodeScopeResolver.php @@ -729,7 +729,10 @@ 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_ + || $stmt instanceof Node\Stmt\Do_; if (!$deferredStmtCallback) { $this->callNodeCallback($nodeCallback, $stmt, $stmtScope, $storage); } @@ -2054,7 +2057,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 +2076,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 +2148,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 +2165,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; 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/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 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/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; 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); } 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);