Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 13 additions & 5 deletions src/Analyser/ExprHandler/FuncCallHandler.php
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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'
Expand All @@ -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;
}
Expand Down Expand Up @@ -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();
Expand Down
11 changes: 8 additions & 3 deletions src/Analyser/ExprHandler/NewHandler.php
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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;
}
Expand All @@ -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();
Expand Down
50 changes: 0 additions & 50 deletions src/Analyser/GatheringNodeCallback.php

This file was deleted.

169 changes: 131 additions & 38 deletions src/Analyser/NodeScopeResolver.php
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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<callable(Node, Scope): void>
*/
private array $nodeGatherers = [];

/**
* @param ExtensionsCollection<FunctionParameterOutTypeExtension> $functionParameterOutTypeExtensions
* @param ExtensionsCollection<MethodParameterOutTypeExtension> $methodParameterOutTypeExtensions
Expand Down Expand Up @@ -217,8 +232,6 @@ public function processNodes(
$scope = $scope->toWalkScope();

$expressionResultStorage = new ExpressionResultStorage();
$alreadyTerminated = false;
$exitPoints = [];

$stmts = [];
$stmtToNodeIndex = [];
Expand All @@ -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<int, int> $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)) {
Expand Down Expand Up @@ -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;
}
}

/**
Expand Down Expand Up @@ -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();
}
Expand Down Expand Up @@ -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) {
Expand Down Expand Up @@ -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;
}
Expand Down Expand Up @@ -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(
Expand Down Expand Up @@ -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);
Expand Down Expand Up @@ -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;
}
Expand All @@ -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());
Expand Down
Loading
Loading