diff --git a/conf/config.neon b/conf/config.neon index af2a15cba5..2ca9629fb9 100644 --- a/conf/config.neon +++ b/conf/config.neon @@ -238,6 +238,7 @@ extensions: autowiredAttributeServices: PHPStan\DependencyInjection\AutowiredAttributeServicesExtension validateServiceTags: PHPStan\DependencyInjection\ValidateServiceTagsExtension gnsr: PHPStan\DependencyInjection\GnsrExtension + fnsr: PHPStan\DependencyInjection\FnsrExtension autowiredAttributeServices: level: null diff --git a/issue-bot/src/Console/RunCommand.php b/issue-bot/src/Console/RunCommand.php index 8027255fe5..f6bc3f1d92 100644 --- a/issue-bot/src/Console/RunCommand.php +++ b/issue-bot/src/Console/RunCommand.php @@ -111,6 +111,7 @@ private function analyseHash(OutputInterface $output, int $phpVersion, Playgroun $output->writeln(sprintf('Starting analysis of %s', $hash)); $startTime = microtime(true); + putenv("PHPSTAN_FNSR=1"); exec(implode(' ', $commandArray), $outputLines, $exitCode); $elapsedTime = microtime(true) - $startTime; $output->writeln(sprintf('Analysis of %s took %.2f s', $hash, $elapsedTime)); diff --git a/src/Analyser/DirectInternalScopeFactory.php b/src/Analyser/DirectInternalScopeFactory.php index 39bdef405e..5fc66a8e41 100644 --- a/src/Analyser/DirectInternalScopeFactory.php +++ b/src/Analyser/DirectInternalScopeFactory.php @@ -3,6 +3,7 @@ namespace PHPStan\Analyser; use PhpParser\Node; +use PHPStan\Analyser\Fiber\FiberScope; use PHPStan\DependencyInjection\Type\DynamicReturnTypeExtensionRegistryProvider; use PHPStan\DependencyInjection\Type\ExpressionTypeResolverExtensionRegistryProvider; use PHPStan\Node\Printer\ExprPrinter; @@ -38,6 +39,7 @@ public function __construct( private int|array|null $configPhpVersion, private $nodeCallback, private ConstantResolver $constantResolver, + private bool $fiber = false, ) { } @@ -61,7 +63,12 @@ public function create( bool $nativeTypesPromoted = false, ): MutatingScope { - return new MutatingScope( + $className = MutatingScope::class; + if ($this->fiber) { + $className = FiberScope::class; + } + + return new $className( $this, $this->reflectionProvider, $this->initializerExprTypeResolver, @@ -97,4 +104,48 @@ public function create( ); } + public function toFiberFactory(): InternalScopeFactory + { + return new self( + $this->reflectionProvider, + $this->initializerExprTypeResolver, + $this->dynamicReturnTypeExtensionRegistryProvider, + $this->expressionTypeResolverExtensionRegistryProvider, + $this->exprPrinter, + $this->typeSpecifier, + $this->propertyReflectionFinder, + $this->parser, + $this->nodeScopeResolver, + $this->richerScopeGetTypeHelper, + $this->phpVersion, + $this->attributeReflectionFactory, + $this->configPhpVersion, + $this->nodeCallback, + $this->constantResolver, + true, + ); + } + + public function toMutatingFactory(): InternalScopeFactory + { + return new self( + $this->reflectionProvider, + $this->initializerExprTypeResolver, + $this->dynamicReturnTypeExtensionRegistryProvider, + $this->expressionTypeResolverExtensionRegistryProvider, + $this->exprPrinter, + $this->typeSpecifier, + $this->propertyReflectionFinder, + $this->parser, + $this->nodeScopeResolver, + $this->richerScopeGetTypeHelper, + $this->phpVersion, + $this->attributeReflectionFactory, + $this->configPhpVersion, + $this->nodeCallback, + $this->constantResolver, + false, + ); + } + } diff --git a/src/Analyser/ExpressionResult.php b/src/Analyser/ExpressionResult.php index 746c518953..46f10f5e55 100644 --- a/src/Analyser/ExpressionResult.php +++ b/src/Analyser/ExpressionResult.php @@ -23,6 +23,7 @@ final class ExpressionResult */ public function __construct( private MutatingScope $scope, + private MutatingScope $beforeScope, private bool $hasYield, private bool $isAlwaysTerminating, private array $throwPoints, @@ -40,6 +41,11 @@ public function getScope(): MutatingScope return $this->scope; } + public function getBeforeScope(): MutatingScope + { + return $this->beforeScope; + } + public function hasYield(): bool { return $this->hasYield; diff --git a/src/Analyser/ExpressionResultStorage.php b/src/Analyser/ExpressionResultStorage.php new file mode 100644 index 0000000000..f40c9eaca5 --- /dev/null +++ b/src/Analyser/ExpressionResultStorage.php @@ -0,0 +1,40 @@ + */ + private SplObjectStorage $results; + + /** @var array, request: ExpressionAnalysisRequest}> */ + public array $pendingFibers = []; + + public function __construct() + { + $this->results = new SplObjectStorage(); + } + + public function duplicate(): self + { + $new = new self(); + $new->results->addAll($this->results); + return $new; + } + + public function storeResult(Expr $expr, ExpressionResult $result): void + { + $this->results[$expr] = $result; + } + + public function findResult(Expr $expr): ?ExpressionResult + { + return $this->results[$expr] ?? null; + } + +} diff --git a/src/Analyser/Fiber/ExpressionAnalysisRequest.php b/src/Analyser/Fiber/ExpressionAnalysisRequest.php new file mode 100644 index 0000000000..0c015a00c6 --- /dev/null +++ b/src/Analyser/Fiber/ExpressionAnalysisRequest.php @@ -0,0 +1,15 @@ +toFiberScope()); + }); + $request = $fiber->start(); + $this->runFiberForNodeCallback($storage, $fiber, $request); + } + + private function runFiberForNodeCallback( + ExpressionResultStorage $storage, + Fiber $fiber, + ?ExpressionAnalysisRequest $request, + ): void + { + while (!$fiber->isTerminated()) { + if ($request instanceof ExpressionAnalysisRequest) { + $result = $storage->findResult($request->expr); + if ($result !== null) { + $request = $fiber->resume($result); + continue; + } + + $storage->pendingFibers[] = [ + 'fiber' => $fiber, + 'request' => $request, + ]; + return; + } + + throw new ShouldNotHappenException( + 'Unknown fiber suspension: ' . get_debug_type($request), + ); + } + + if ($request !== null) { + throw new ShouldNotHappenException( + 'Fiber terminated but we did not handle its request ' . get_debug_type($request), + ); + } + } + + protected function processPendingFibers(ExpressionResultStorage $storage): void + { + foreach ($storage->pendingFibers as $pending) { + $request = $pending['request']; + $result = $storage->findResult($request->expr); + + if ($result !== null) { + throw new ShouldNotHappenException('Pending fibers at the end should be about synthetic nodes'); + } + + $this->processExprNode( + new Node\Stmt\Expression($request->expr), + $request->expr, + $request->scope->toMutatingScope(), + $storage, + new NoopNodeCallback(), + ExpressionContext::createTopLevel(), + ); + if ($storage->findResult($request->expr) === null) { + throw new ShouldNotHappenException(sprintf('processExprNode should have stored the result of %s on line %s', get_class($request->expr), $request->expr->getStartLine())); + } + $this->processPendingFibers($storage); + + // Break and restart the loop since the array may have been modified + return; + } + } + + protected function processPendingFibersForRequestedExpr(ExpressionResultStorage $storage, Expr $expr, ExpressionResult $result): void + { + $restartLoop = true; + + while ($restartLoop) { + $restartLoop = false; + + foreach ($storage->pendingFibers as $key => $pending) { + $request = $pending['request']; + if ($request->expr !== $expr) { + continue; + } + + unset($storage->pendingFibers[$key]); + $restartLoop = true; + + $fiber = $pending['fiber']; + $request = $fiber->resume($result); + $this->runFiberForNodeCallback($storage, $fiber, $request); + + // Break and restart the loop since the array may have been modified + break; + } + } + } + +} diff --git a/src/Analyser/Fiber/FiberScope.php b/src/Analyser/Fiber/FiberScope.php new file mode 100644 index 0000000000..a8e772cba7 --- /dev/null +++ b/src/Analyser/Fiber/FiberScope.php @@ -0,0 +1,46 @@ +getBeforeScope()->toMutatingScope()->getType($node); + } + + /** @api */ + public function getNativeType(Expr $expr): Type + { + /** @var ExpressionResult $exprResult */ + $exprResult = Fiber::suspend( + new ExpressionAnalysisRequest($expr, $this), + ); + + return $exprResult->getBeforeScope()->toMutatingScope()->getNativeType($expr); + } + + public function getKeepVoidType(Expr $node): Type + { + /** @var ExpressionResult $exprResult */ + $exprResult = Fiber::suspend( + new ExpressionAnalysisRequest($node, $this), + ); + + return $exprResult->getBeforeScope()->toMutatingScope()->getKeepVoidType($node); + } + +} diff --git a/src/Analyser/InternalScopeFactory.php b/src/Analyser/InternalScopeFactory.php index 41526310d3..6a4141b251 100644 --- a/src/Analyser/InternalScopeFactory.php +++ b/src/Analyser/InternalScopeFactory.php @@ -39,4 +39,8 @@ public function create( bool $nativeTypesPromoted = false, ): MutatingScope; + public function toFiberFactory(): self; + + public function toMutatingFactory(): self; + } diff --git a/src/Analyser/LazyInternalScopeFactory.php b/src/Analyser/LazyInternalScopeFactory.php index fff31d2350..545a3a14d3 100644 --- a/src/Analyser/LazyInternalScopeFactory.php +++ b/src/Analyser/LazyInternalScopeFactory.php @@ -3,6 +3,7 @@ namespace PHPStan\Analyser; use PhpParser\Node; +use PHPStan\Analyser\Fiber\FiberScope; use PHPStan\DependencyInjection\Container; use PHPStan\DependencyInjection\GenerateFactory; use PHPStan\DependencyInjection\Type\DynamicReturnTypeExtensionRegistryProvider; @@ -29,6 +30,7 @@ final class LazyInternalScopeFactory implements InternalScopeFactory public function __construct( private Container $container, private $nodeCallback, + private bool $fiber = false, ) { $this->phpVersion = $this->container->getParameter('phpVersion'); @@ -53,7 +55,12 @@ public function create( bool $nativeTypesPromoted = false, ): MutatingScope { - return new MutatingScope( + $className = MutatingScope::class; + if ($this->fiber) { + $className = FiberScope::class; + } + + return new $className( $this, $this->container->getByType(ReflectionProvider::class), $this->container->getByType(InitializerExprTypeResolver::class), @@ -89,4 +96,14 @@ public function create( ); } + public function toFiberFactory(): InternalScopeFactory + { + return new self($this->container, $this->nodeCallback, true); + } + + public function toMutatingFactory(): InternalScopeFactory + { + return new self($this->container, $this->nodeCallback, false); + } + } diff --git a/src/Analyser/MutatingScope.php b/src/Analyser/MutatingScope.php index 01d7fe7b0c..80a88875e8 100644 --- a/src/Analyser/MutatingScope.php +++ b/src/Analyser/MutatingScope.php @@ -27,6 +27,7 @@ use PhpParser\Node\Stmt\ClassMethod; use PhpParser\Node\Stmt\Function_; use PhpParser\NodeFinder; +use PHPStan\Analyser\Fiber\FiberScope; use PHPStan\Node\ExecutionEndNode; use PHPStan\Node\Expr\AlwaysRememberedExpr; use PHPStan\Node\Expr\ExistingArrayDimFetch; @@ -167,7 +168,7 @@ use const PHP_INT_MAX; use const PHP_INT_MIN; -final class MutatingScope implements Scope, NodeCallbackInvoker +class MutatingScope implements Scope, NodeCallbackInvoker { private const BOOLEAN_EXPRESSION_MAX_PROCESS_DEPTH = 4; @@ -245,6 +246,58 @@ public function __construct( $this->namespace = $namespace; } + public function toFiberScope(): self + { + if (static::class === FiberScope::class) { + return $this; + } + + return $this->scopeFactory->toFiberFactory()->create( + $this->context, + $this->isDeclareStrictTypes(), + $this->getFunction(), + $this->getNamespace(), + $this->expressionTypes, + $this->nativeExpressionTypes, + $this->conditionalExpressions, + $this->inClosureBindScopeClasses, + $this->anonymousFunctionReflection, + $this->isInFirstLevelStatement(), + $this->currentlyAssignedExpressions, + $this->currentlyAllowedUndefinedExpressions, + $this->inFunctionCallsStack, + $this->afterExtractCall, + $this->parentScope, + $this->nativeTypesPromoted, + ); + } + + public function toMutatingScope(): self + { + if (static::class === self::class) { + return $this; + } + + return $this->scopeFactory->toMutatingFactory()->create( + $this->context, + $this->isDeclareStrictTypes(), + $this->getFunction(), + $this->getNamespace(), + $this->expressionTypes, + $this->nativeExpressionTypes, + $this->conditionalExpressions, + $this->inClosureBindScopeClasses, + $this->anonymousFunctionReflection, + $this->isInFirstLevelStatement(), + $this->currentlyAssignedExpressions, + $this->currentlyAllowedUndefinedExpressions, + $this->inFunctionCallsStack, + $this->afterExtractCall, + $this->parentScope, + $this->nativeTypesPromoted, + ); + } + /** @api */ public function getFile(): string { @@ -982,9 +1035,7 @@ private function resolveType(string $exprString, Expr $node): Type } if ($this->getBooleanExpressionDepth($node->left) <= self::BOOLEAN_EXPRESSION_MAX_PROCESS_DEPTH) { - $noopCallback = static function (): void { - }; - $leftResult = $this->nodeScopeResolver->processExprNode(new Node\Stmt\Expression($node->left), $node->left, $this, $noopCallback, ExpressionContext::createDeep()); + $leftResult = $this->nodeScopeResolver->processExprNode(new Node\Stmt\Expression($node->left), $node->left, $this, new ExpressionResultStorage(), new NoopNodeCallback(), ExpressionContext::createDeep()); $rightBooleanType = $leftResult->getTruthyScope()->getType($node->right)->toBoolean(); } else { $rightBooleanType = $this->filterByTruthyValue($node->left)->getType($node->right)->toBoolean(); @@ -1014,9 +1065,7 @@ private function resolveType(string $exprString, Expr $node): Type } if ($this->getBooleanExpressionDepth($node->left) <= self::BOOLEAN_EXPRESSION_MAX_PROCESS_DEPTH) { - $noopCallback = static function (): void { - }; - $leftResult = $this->nodeScopeResolver->processExprNode(new Node\Stmt\Expression($node->left), $node->left, $this, $noopCallback, ExpressionContext::createDeep()); + $leftResult = $this->nodeScopeResolver->processExprNode(new Node\Stmt\Expression($node->left), $node->left, $this, new ExpressionResultStorage(), new NoopNodeCallback(), ExpressionContext::createDeep()); $rightBooleanType = $leftResult->getFalseyScope()->getType($node->right)->toBoolean(); } else { $rightBooleanType = $this->filterByFalseyValue($node->left)->getType($node->right)->toBoolean(); @@ -1406,6 +1455,7 @@ private function resolveType(string $exprString, Expr $node): Type new Node\Stmt\Expression($node->expr), $node->expr, $arrowScope, + new ExpressionResultStorage(), static function (Node $node, Scope $scope) use ($arrowScope, &$arrowFunctionImpurePoints, &$invalidateExpressions): void { if ($scope->getAnonymousFunctionReflection() !== $arrowScope->getAnonymousFunctionReflection()) { return; @@ -2042,9 +2092,7 @@ static function (Node $node, Scope $scope) use ($arrowScope, &$arrowFunctionImpu } if ($node instanceof Expr\Ternary) { - $noopCallback = static function (): void { - }; - $condResult = $this->nodeScopeResolver->processExprNode(new Node\Stmt\Expression($node->cond), $node->cond, $this, $noopCallback, ExpressionContext::createDeep()); + $condResult = $this->nodeScopeResolver->processExprNode(new Node\Stmt\Expression($node->cond), $node->cond, $this, new ExpressionResultStorage(), new NoopNodeCallback(), ExpressionContext::createDeep()); if ($node->if === null) { $conditionType = $this->getType($node->cond); $booleanConditionType = $conditionType->toBoolean(); @@ -5752,6 +5800,14 @@ public function debug(): array $descriptions[$key] = $nativeTypeHolder->getType()->describe(VerbosityLevel::precise()); } + foreach (array_keys($this->currentlyAssignedExpressions) as $exprString) { + $descriptions[sprintf('currently assigned %s', $exprString)] = 'true'; + } + + foreach (array_keys($this->currentlyAllowedUndefinedExpressions) as $exprString) { + $descriptions[sprintf('currently allowed undefined %s', $exprString)] = 'true'; + } + foreach ($this->conditionalExpressions as $exprString => $holders) { foreach (array_values($holders) as $i => $holder) { $key = sprintf('condition about %s #%d', $exprString, $i + 1); diff --git a/src/Analyser/NodeScopeResolver.php b/src/Analyser/NodeScopeResolver.php index a6cc26018f..d309cff1f3 100644 --- a/src/Analyser/NodeScopeResolver.php +++ b/src/Analyser/NodeScopeResolver.php @@ -214,6 +214,7 @@ use function array_values; use function base64_decode; use function count; +use function get_class; use function in_array; use function is_array; use function is_int; @@ -228,7 +229,7 @@ use const SORT_NUMERIC; #[AutowiredService] -final class NodeScopeResolver +class NodeScopeResolver { private const LOOP_SCOPE_ITERATIONS = 3; @@ -316,6 +317,7 @@ public function processNodes( callable $nodeCallback, ): void { + $expressionResultStorage = new ExpressionResultStorage(); $alreadyTerminated = false; foreach ($nodes as $i => $node) { if ( @@ -325,7 +327,7 @@ public function processNodes( continue; } - $statementResult = $this->processStmtNode($node, $scope, $nodeCallback, StatementContext::createTopLevel()); + $statementResult = $this->processStmtNode($node, $scope, $expressionResultStorage, $nodeCallback, StatementContext::createTopLevel()); $scope = $statementResult->getScope(); if ($alreadyTerminated || !$statementResult->isAlwaysTerminating()) { continue; @@ -333,15 +335,31 @@ public function processNodes( $alreadyTerminated = true; $nextStmts = $this->getNextUnreachableStatements(array_slice($nodes, $i + 1), true); - $this->processUnreachableStatement($nextStmts, $scope, $nodeCallback); + $this->processUnreachableStatement($nextStmts, $scope, $expressionResultStorage, $nodeCallback); } + + $this->processPendingFibers($expressionResultStorage); + } + + private function storeResult(ExpressionResultStorage $storage, Expr $expr, ExpressionResult $result): void + { + $storage->storeResult($expr, $result); + $this->processPendingFibersForRequestedExpr($storage, $expr, $result); + } + + protected function processPendingFibers(ExpressionResultStorage $storage): void + { + } + + protected function processPendingFibersForRequestedExpr(ExpressionResultStorage $storage, Expr $expr, ExpressionResult $result): void + { } /** * @param Node\Stmt[] $nextStmts * @param callable(Node $node, Scope $scope): void $nodeCallback */ - private function processUnreachableStatement(array $nextStmts, MutatingScope $scope, callable $nodeCallback): void + private function processUnreachableStatement(array $nextStmts, MutatingScope $scope, ExpressionResultStorage $storage, callable $nodeCallback): void { if ($nextStmts === []) { return; @@ -363,7 +381,7 @@ private function processUnreachableStatement(array $nextStmts, MutatingScope $sc return; } - $nodeCallback(new UnreachableStatementNode($unreachableStatement, $nextStatements), $scope); + $this->callNodeCallback($nodeCallback, new UnreachableStatementNode($unreachableStatement, $nextStatements), $scope, $storage); } /** @@ -379,13 +397,18 @@ public function processStmtNodes( StatementContext $context, ): StatementResult { - return $this->processStmtNodesInternal( + $storage = new ExpressionResultStorage(); + $result = $this->processStmtNodesInternal( $parentNode, $stmts, $scope, + $storage, $nodeCallback, $context, )->toPublic(); + $this->processPendingFibers($storage); + + return $result; } /** @@ -396,6 +419,7 @@ private function processStmtNodesInternal( Node $parentNode, array $stmts, MutatingScope $scope, + ExpressionResultStorage $storage, callable $nodeCallback, StatementContext $context, ): InternalStatementResult @@ -419,6 +443,7 @@ private function processStmtNodesInternal( $statementResult = $this->processStmtNode( $stmt, $scope, + $storage, $nodeCallback, $context, ); @@ -430,7 +455,7 @@ private function processStmtNodesInternal( if (count($endStatements) > 0) { foreach ($endStatements as $endStatement) { $endStatementResult = $endStatement->getResult(); - $nodeCallback(new ExecutionEndNode( + $this->callNodeCallback($nodeCallback, new ExecutionEndNode( $endStatement->getStatement(), (new InternalStatementResult( $endStatementResult->getScope(), @@ -441,10 +466,10 @@ private function processStmtNodesInternal( $endStatementResult->getImpurePoints(), ))->toPublic(), $parentNode->getReturnType() !== null, - ), $endStatementResult->getScope()); + ), $endStatementResult->getScope(), $storage); } } else { - $nodeCallback(new ExecutionEndNode( + $this->callNodeCallback($nodeCallback, new ExecutionEndNode( $stmt, (new InternalStatementResult( $scope, @@ -455,7 +480,7 @@ private function processStmtNodesInternal( $statementResult->getImpurePoints(), ))->toPublic(), $parentNode->getReturnType() !== null, - ), $scope); + ), $scope, $storage); } } @@ -469,7 +494,7 @@ private function processStmtNodesInternal( $alreadyTerminated = true; $nextStmts = $this->getNextUnreachableStatements(array_slice($stmts, $i + 1), $parentNode instanceof Node\Stmt\Namespace_); - $this->processUnreachableStatement($nextStmts, $scope, $nodeCallback); + $this->processUnreachableStatement($nextStmts, $scope, $storage, $nodeCallback); } $statementResult = new InternalStatementResult($scope, $hasYield, $alreadyTerminated, $exitPoints, $throwPoints, $impurePoints); @@ -478,11 +503,11 @@ private function processStmtNodesInternal( if ($parentNode instanceof Expr\Closure) { $parentNode = new Node\Stmt\Expression($parentNode, $parentNode->getAttributes()); } - $nodeCallback(new ExecutionEndNode( + $this->callNodeCallback($nodeCallback, new ExecutionEndNode( $parentNode, $statementResult->toPublic(), $returnTypeNode !== null, - ), $scope); + ), $scope, $storage); } return $statementResult; @@ -494,6 +519,7 @@ private function processStmtNodesInternal( private function processStmtNode( Node\Stmt $stmt, MutatingScope $scope, + ExpressionResultStorage $storage, callable $nodeCallback, StatementContext $context, ): InternalStatementResult @@ -506,7 +532,7 @@ private function processStmtNode( && !$stmt instanceof Node\Stmt\ClassConst && !$stmt instanceof Node\Stmt\Const_ ) { - $scope = $this->processStmtVarAnnotation($scope, $stmt, null, $nodeCallback); + $scope = $this->processStmtVarAnnotation($scope, $storage, $stmt, null, $nodeCallback); } if ($stmt instanceof Node\Stmt\ClassMethod) { @@ -532,13 +558,13 @@ private function processStmtNode( $stmtScope = $scope; if ($stmt instanceof Node\Stmt\Expression && $stmt->expr instanceof Expr\Throw_) { - $stmtScope = $this->processStmtVarAnnotation($scope, $stmt, $stmt->expr->expr, $nodeCallback); + $stmtScope = $this->processStmtVarAnnotation($scope, $storage, $stmt, $stmt->expr->expr, $nodeCallback); } if ($stmt instanceof Return_) { - $stmtScope = $this->processStmtVarAnnotation($scope, $stmt, $stmt->expr, $nodeCallback); + $stmtScope = $this->processStmtVarAnnotation($scope, $storage, $stmt, $stmt->expr, $nodeCallback); } - $nodeCallback($stmt, $stmtScope); + $this->callNodeCallback($nodeCallback, $stmt, $stmtScope, $storage); $overridingThrowPoints = $this->getOverridingThrowPoints($stmt, $scope); @@ -549,8 +575,8 @@ private function processStmtNode( $alwaysTerminating = false; $exitPoints = []; foreach ($stmt->declares as $declare) { - $nodeCallback($declare, $scope); - $nodeCallback($declare->value, $scope); + $this->callNodeCallback($nodeCallback, $declare, $scope, $storage); + $this->callNodeCallback($nodeCallback, $declare->value, $scope, $storage); if ( $declare->key->name !== 'strict_types' || !($declare->value instanceof Node\Scalar\Int_) @@ -563,7 +589,7 @@ private function processStmtNode( } if ($stmt->stmts !== null) { - $result = $this->processStmtNodesInternal($stmt, $stmt->stmts, $scope, $nodeCallback, $context); + $result = $this->processStmtNodesInternal($stmt, $stmt->stmts, $scope, $storage, $nodeCallback, $context); $scope = $result->getScope(); $hasYield = $result->hasYield(); $throwPoints = $result->getThrowPoints(); @@ -577,15 +603,15 @@ private function processStmtNode( $hasYield = false; $throwPoints = []; $impurePoints = []; - $this->processAttributeGroups($stmt, $stmt->attrGroups, $scope, $nodeCallback); + $this->processAttributeGroups($stmt, $stmt->attrGroups, $scope, $storage, $nodeCallback); [$templateTypeMap, $phpDocParameterTypes, $phpDocImmediatelyInvokedCallableParameters, $phpDocClosureThisTypeParameters, $phpDocReturnType, $phpDocThrowType, $deprecatedDescription, $isDeprecated, $isInternal, , $isPure, $acceptsNamedArguments, , $phpDocComment, $asserts,, $phpDocParameterOutTypes] = $this->getPhpDocs($scope, $stmt); foreach ($stmt->params as $param) { - $this->processParamNode($stmt, $param, $scope, $nodeCallback); + $this->processParamNode($stmt, $param, $scope, $storage, $nodeCallback); } if ($stmt->returnType !== null) { - $nodeCallback($stmt->returnType, $scope); + $this->callNodeCallback($nodeCallback, $stmt->returnType, $scope, $storage); } if (!$isDeprecated) { @@ -614,13 +640,13 @@ private function processStmtNode( throw new ShouldNotHappenException(); } - $nodeCallback(new InFunctionNode($functionReflection, $stmt), $functionScope); + $this->callNodeCallback($nodeCallback, new InFunctionNode($functionReflection, $stmt), $functionScope, $storage); $gatheredReturnStatements = []; $gatheredYieldStatements = []; $executionEnds = []; $functionImpurePoints = []; - $statementResult = $this->processStmtNodesInternal($stmt, $stmt->stmts, $functionScope, static function (Node $node, Scope $scope) use ($nodeCallback, $functionScope, &$gatheredReturnStatements, &$gatheredYieldStatements, &$executionEnds, &$functionImpurePoints): void { + $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); if ($scope->getFunction() !== $functionScope->getFunction()) { return; @@ -652,7 +678,7 @@ private function processStmtNode( $gatheredReturnStatements[] = new ReturnStatement($scope, $node); }, StatementContext::createTopLevel())->toPublic(); - $nodeCallback(new FunctionReturnStatementsNode( + $this->callNodeCallback($nodeCallback, new FunctionReturnStatementsNode( $stmt, $gatheredReturnStatements, $gatheredYieldStatements, @@ -660,20 +686,20 @@ private function processStmtNode( $executionEnds, array_merge($statementResult->getImpurePoints(), $functionImpurePoints), $functionReflection, - ), $functionScope); + ), $functionScope, $storage); } elseif ($stmt instanceof Node\Stmt\ClassMethod) { $hasYield = false; $throwPoints = []; $impurePoints = []; - $this->processAttributeGroups($stmt, $stmt->attrGroups, $scope, $nodeCallback); + $this->processAttributeGroups($stmt, $stmt->attrGroups, $scope, $storage, $nodeCallback); [$templateTypeMap, $phpDocParameterTypes, $phpDocImmediatelyInvokedCallableParameters, $phpDocClosureThisTypeParameters, $phpDocReturnType, $phpDocThrowType, $deprecatedDescription, $isDeprecated, $isInternal, $isFinal, $isPure, $acceptsNamedArguments, $isReadOnly, $phpDocComment, $asserts, $selfOutType, $phpDocParameterOutTypes] = $this->getPhpDocs($scope, $stmt); foreach ($stmt->params as $param) { - $this->processParamNode($stmt, $param, $scope, $nodeCallback); + $this->processParamNode($stmt, $param, $scope, $storage, $nodeCallback); } if ($stmt->returnType !== null) { - $nodeCallback($stmt->returnType, $scope); + $this->callNodeCallback($nodeCallback, $stmt->returnType, $scope, $storage); } if (!$isDeprecated) { @@ -723,7 +749,7 @@ private function processStmtNode( if ($param->getDocComment() !== null) { $phpDoc = $param->getDocComment()->getText(); } - $nodeCallback(new ClassPropertyNode( + $this->callNodeCallback($nodeCallback, new ClassPropertyNode( $param->var->name, $param->flags, $param->type !== null ? ParserNodeTypeToPHPStanType::resolve($param->type, $classReflection) : null, @@ -738,7 +764,7 @@ private function processStmtNode( $classReflection->isReadOnly(), false, $classReflection, - ), $methodScope); + ), $methodScope, $storage); $this->processPropertyHooks( $stmt, $param->type, @@ -746,6 +772,7 @@ private function processStmtNode( $param->var->name, $param->hooks, $scope, + $storage, $nodeCallback, ); $methodScope = $methodScope->assignExpression(new PropertyInitializationExpr($param->var->name), new MixedType(), new MixedType()); @@ -757,7 +784,7 @@ private function processStmtNode( if (!$methodReflection instanceof PhpMethodFromParserNodeReflection) { throw new ShouldNotHappenException(); } - $nodeCallback(new InClassMethodNode($classReflection, $methodReflection, $stmt), $methodScope); + $this->callNodeCallback($nodeCallback, new InClassMethodNode($classReflection, $methodReflection, $stmt), $methodScope, $storage); } if ($stmt->stmts !== null) { @@ -765,7 +792,7 @@ private function processStmtNode( $gatheredYieldStatements = []; $executionEnds = []; $methodImpurePoints = []; - $statementResult = $this->processStmtNodesInternal($stmt, $stmt->stmts, $methodScope, static function (Node $node, Scope $scope) use ($nodeCallback, $methodScope, &$gatheredReturnStatements, &$gatheredYieldStatements, &$executionEnds, &$methodImpurePoints): void { + $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); if ($scope->getFunction() !== $methodScope->getFunction()) { return; @@ -811,7 +838,7 @@ private function processStmtNode( throw new ShouldNotHappenException(); } - $nodeCallback(new MethodReturnStatementsNode( + $this->callNodeCallback($nodeCallback, new MethodReturnStatementsNode( $stmt, $gatheredReturnStatements, $gatheredYieldStatements, @@ -820,7 +847,7 @@ private function processStmtNode( array_merge($statementResult->getImpurePoints(), $methodImpurePoints), $classReflection, $methodReflection, - ), $methodScope); + ), $methodScope, $storage); if ($isConstructor && $this->narrowMethodScopeFromConstructor) { $finalScope = null; @@ -859,7 +886,7 @@ private function processStmtNode( $throwPoints = []; $isAlwaysTerminating = false; foreach ($stmt->exprs as $echoExpr) { - $result = $this->processExprNode($stmt, $echoExpr, $scope, $nodeCallback, ExpressionContext::createDeep()); + $result = $this->processExprNode($stmt, $echoExpr, $scope, $storage, $nodeCallback, ExpressionContext::createDeep()); $throwPoints = array_merge($throwPoints, $result->getThrowPoints()); $scope = $result->getScope(); $hasYield = $hasYield || $result->hasYield(); @@ -873,7 +900,7 @@ private function processStmtNode( return new InternalStatementResult($scope, $hasYield, $isAlwaysTerminating, [], $throwPoints, $impurePoints); } elseif ($stmt instanceof Return_) { if ($stmt->expr !== null) { - $result = $this->processExprNode($stmt, $stmt->expr, $scope, $nodeCallback, ExpressionContext::createDeep()); + $result = $this->processExprNode($stmt, $stmt->expr, $scope, $storage, $nodeCallback, ExpressionContext::createDeep()); $throwPoints = $result->getThrowPoints(); $impurePoints = $result->getImpurePoints(); $scope = $result->getScope(); @@ -889,7 +916,7 @@ private function processStmtNode( ], $overridingThrowPoints ?? $throwPoints, $impurePoints); } elseif ($stmt instanceof Continue_ || $stmt instanceof Break_) { if ($stmt->num !== null) { - $result = $this->processExprNode($stmt, $stmt->num, $scope, $nodeCallback, ExpressionContext::createDeep()); + $result = $this->processExprNode($stmt, $stmt->num, $scope, $storage, $nodeCallback, ExpressionContext::createDeep()); $scope = $result->getScope(); $hasYield = $result->hasYield(); $throwPoints = $result->getThrowPoints(); @@ -910,7 +937,7 @@ private function processStmtNode( $earlyTerminationExpr = $this->findEarlyTerminatingExpr($stmt->expr, $scope); $hasAssign = false; $currentScope = $scope; - $result = $this->processExprNode($stmt, $stmt->expr, $scope, static function (Node $node, Scope $scope) use ($nodeCallback, $currentScope, &$hasAssign): void { + $result = $this->processExprNode($stmt, $stmt->expr, $scope, $storage, static function (Node $node, Scope $scope) use ($nodeCallback, $currentScope, &$hasAssign): void { $nodeCallback($node, $scope); if ($scope->getAnonymousFunctionReflection() !== $currentScope->getAnonymousFunctionReflection()) { return; @@ -933,7 +960,7 @@ private function processStmtNode( && !$stmt->expr instanceof Expr\PostDec && !$stmt->expr instanceof Expr\PreDec ) { - $nodeCallback(new NoopExpressionNode($stmt->expr, $hasAssign), $scope); + $this->callNodeCallback($nodeCallback, new NoopExpressionNode($stmt->expr, $hasAssign), $scope, $storage); } $scope = $result->getScope(); $scope = $scope->filterBySpecifiedTypes($this->typeSpecifier->specifyTypesInCondition( @@ -957,7 +984,7 @@ private function processStmtNode( $scope = $scope->enterNamespace($stmt->name->toString()); } - $scope = $this->processStmtNodesInternal($stmt, $stmt->stmts, $scope, $nodeCallback, $context)->getScope(); + $scope = $this->processStmtNodesInternal($stmt, $stmt->stmts, $scope, $storage, $nodeCallback, $context)->getScope(); $hasYield = false; $throwPoints = []; $impurePoints = []; @@ -973,7 +1000,7 @@ private function processStmtNode( if (isset($stmt->namespacedName)) { $classReflection = $this->getCurrentClassReflection($stmt, $stmt->namespacedName->toString(), $scope); $classScope = $scope->enterClass($classReflection); - $nodeCallback(new InClassNode($stmt, $classReflection), $classScope); + $this->callNodeCallback($nodeCallback, new InClassNode($stmt, $classReflection), $classScope, $storage); } elseif ($stmt instanceof Class_) { if ($stmt->name === null) { throw new ShouldNotHappenException(); @@ -984,7 +1011,7 @@ private function processStmtNode( $classReflection = $this->reflectionProvider->getAnonymousClassReflection($stmt, $scope); } $classScope = $scope->enterClass($classReflection); - $nodeCallback(new InClassNode($stmt, $classReflection), $classScope); + $this->callNodeCallback($nodeCallback, new InClassNode($stmt, $classReflection), $classScope, $storage); } else { throw new ShouldNotHappenException(); } @@ -1008,7 +1035,7 @@ public function __invoke(Node $node, Scope $scope): void } }; - $this->processAttributeGroups($stmt, $stmt->attrGroups, $classScope, $classStatementsGathererCallback); + $this->processAttributeGroups($stmt, $stmt->attrGroups, $classScope, $storage, $classStatementsGathererCallback); $classLikeStatements = $stmt->stmts; if ($this->narrowMethodScopeFromConstructor) { @@ -1029,17 +1056,17 @@ public function __invoke(Node $node, Scope $scope): void }); } - $this->processStmtNodesInternal($stmt, $classLikeStatements, $classScope, $classStatementsGathererCallback, $context); - $nodeCallback(new ClassPropertiesNode($stmt, $this->readWritePropertiesExtensionProvider, $classStatementsGatherer->getProperties(), $classStatementsGatherer->getPropertyUsages(), $classStatementsGatherer->getMethodCalls(), $classStatementsGatherer->getReturnStatementsNodes(), $classStatementsGatherer->getPropertyAssigns(), $classReflection), $classScope); - $nodeCallback(new ClassMethodsNode($stmt, $classStatementsGatherer->getMethods(), $classStatementsGatherer->getMethodCalls(), $classReflection), $classScope); - $nodeCallback(new ClassConstantsNode($stmt, $classStatementsGatherer->getConstants(), $classStatementsGatherer->getConstantFetches(), $classReflection), $classScope); + $this->processStmtNodesInternal($stmt, $classLikeStatements, $classScope, $storage, $classStatementsGathererCallback, $context); + $this->callNodeCallback($nodeCallback, new ClassPropertiesNode($stmt, $this->readWritePropertiesExtensionProvider, $classStatementsGatherer->getProperties(), $classStatementsGatherer->getPropertyUsages(), $classStatementsGatherer->getMethodCalls(), $classStatementsGatherer->getReturnStatementsNodes(), $classStatementsGatherer->getPropertyAssigns(), $classReflection), $classScope, $storage); + $this->callNodeCallback($nodeCallback, new ClassMethodsNode($stmt, $classStatementsGatherer->getMethods(), $classStatementsGatherer->getMethodCalls(), $classReflection), $classScope, $storage); + $this->callNodeCallback($nodeCallback, new ClassConstantsNode($stmt, $classStatementsGatherer->getConstants(), $classStatementsGatherer->getConstantFetches(), $classReflection), $classScope, $storage); $classReflection->evictPrivateSymbols(); $this->calledMethodResults = []; } elseif ($stmt instanceof Node\Stmt\Property) { $hasYield = false; $throwPoints = []; $impurePoints = []; - $this->processAttributeGroups($stmt, $stmt->attrGroups, $scope, $nodeCallback); + $this->processAttributeGroups($stmt, $stmt->attrGroups, $scope, $storage, $nodeCallback); $nativePropertyType = $stmt->type !== null ? ParserNodeTypeToPHPStanType::resolve($stmt->type, $scope->getClassReflection()) : null; @@ -1050,9 +1077,9 @@ public function __invoke(Node $node, Scope $scope): void } foreach ($stmt->props as $prop) { - $nodeCallback($prop, $scope); + $this->callNodeCallback($nodeCallback, $prop, $scope, $storage); if ($prop->default !== null) { - $this->processExprNode($stmt, $prop->default, $scope, $nodeCallback, ExpressionContext::createDeep()); + $this->processExprNode($stmt, $prop->default, $scope, $storage, $nodeCallback, ExpressionContext::createDeep()); } if (!$scope->isInClass()) { @@ -1069,7 +1096,8 @@ public function __invoke(Node $node, Scope $scope): void $propStmt = clone $stmt; $propStmt->setAttributes($prop->getAttributes()); $propStmt->setAttribute('originalPropertyStmt', $stmt); - $nodeCallback( + $this->callNodeCallback( + $nodeCallback, new ClassPropertyNode( $propertyName, $stmt->flags, @@ -1087,6 +1115,7 @@ public function __invoke(Node $node, Scope $scope): void $scope->getClassReflection(), ), $scope, + $storage, ); } @@ -1101,17 +1130,18 @@ public function __invoke(Node $node, Scope $scope): void $propertyName, $stmt->hooks, $scope, + $storage, $nodeCallback, ); } if ($stmt->type !== null) { - $nodeCallback($stmt->type, $scope); + $this->callNodeCallback($nodeCallback, $stmt->type, $scope, $storage); } } elseif ($stmt instanceof If_) { $conditionType = ($this->treatPhpDocTypesAsCertain ? $scope->getType($stmt->cond) : $scope->getNativeType($stmt->cond))->toBoolean(); $ifAlwaysTrue = $conditionType->isTrue()->yes(); - $condResult = $this->processExprNode($stmt, $stmt->cond, $scope, $nodeCallback, ExpressionContext::createDeep()); + $condResult = $this->processExprNode($stmt, $stmt->cond, $scope, $storage, $nodeCallback, ExpressionContext::createDeep()); $exitPoints = []; $throwPoints = $overridingThrowPoints ?? $condResult->getThrowPoints(); $impurePoints = $condResult->getImpurePoints(); @@ -1120,7 +1150,7 @@ public function __invoke(Node $node, Scope $scope): void $alwaysTerminating = true; $hasYield = $condResult->hasYield(); - $branchScopeStatementResult = $this->processStmtNodesInternal($stmt, $stmt->stmts, $condResult->getTruthyScope(), $nodeCallback, $context); + $branchScopeStatementResult = $this->processStmtNodesInternal($stmt, $stmt->stmts, $condResult->getTruthyScope(), $storage, $nodeCallback, $context); if (!$conditionType->isTrue()->no()) { $exitPoints = $branchScopeStatementResult->getExitPoints(); @@ -1144,13 +1174,13 @@ public function __invoke(Node $node, Scope $scope): void $condScope = $scope; foreach ($stmt->elseifs as $elseif) { - $nodeCallback($elseif, $scope); + $this->callNodeCallback($nodeCallback, $elseif, $scope, $storage); $elseIfConditionType = ($this->treatPhpDocTypesAsCertain ? $condScope->getType($elseif->cond) : $scope->getNativeType($elseif->cond))->toBoolean(); - $condResult = $this->processExprNode($stmt, $elseif->cond, $condScope, $nodeCallback, ExpressionContext::createDeep()); + $condResult = $this->processExprNode($stmt, $elseif->cond, $condScope, $storage, $nodeCallback, ExpressionContext::createDeep()); $throwPoints = array_merge($throwPoints, $condResult->getThrowPoints()); $impurePoints = array_merge($impurePoints, $condResult->getImpurePoints()); $condScope = $condResult->getScope(); - $branchScopeStatementResult = $this->processStmtNodesInternal($elseif, $elseif->stmts, $condResult->getTruthyScope(), $nodeCallback, $context); + $branchScopeStatementResult = $this->processStmtNodesInternal($elseif, $elseif->stmts, $condResult->getTruthyScope(), $storage, $nodeCallback, $context); if ( !$ifAlwaysTrue @@ -1189,8 +1219,8 @@ public function __invoke(Node $node, Scope $scope): void $alwaysTerminating = false; } } else { - $nodeCallback($stmt->else, $scope); - $branchScopeStatementResult = $this->processStmtNodesInternal($stmt->else, $stmt->else->stmts, $scope, $nodeCallback, $context); + $this->callNodeCallback($nodeCallback, $stmt->else, $scope, $storage); + $branchScopeStatementResult = $this->processStmtNodesInternal($stmt->else, $stmt->else->stmts, $scope, $storage, $nodeCallback, $context); if (!$ifAlwaysTrue && !$lastElseIfConditionIsTrue) { $exitPoints = array_merge($exitPoints, $branchScopeStatementResult->getExitPoints()); @@ -1223,9 +1253,9 @@ public function __invoke(Node $node, Scope $scope): void $hasYield = false; $throwPoints = []; $impurePoints = []; - $this->processTraitUse($stmt, $scope, $nodeCallback); + $this->processTraitUse($stmt, $scope, $storage->duplicate(), $nodeCallback); } elseif ($stmt instanceof Foreach_) { - $condResult = $this->processExprNode($stmt, $stmt->expr, $scope, $nodeCallback, ExpressionContext::createDeep()); + $condResult = $this->processExprNode($stmt, $stmt->expr, $scope, $storage, $nodeCallback, ExpressionContext::createDeep()); $throwPoints = $overridingThrowPoints ?? $condResult->getThrowPoints(); $impurePoints = $condResult->getImpurePoints(); $scope = $condResult->getScope(); @@ -1236,28 +1266,30 @@ public function __invoke(Node $node, Scope $scope): void if ($stmt->expr instanceof Variable && is_string($stmt->expr->name)) { $scope = $this->processVarAnnotation($scope, [$stmt->expr->name], $stmt); } - $nodeCallback(new InForeachNode($stmt), $scope); + $this->callNodeCallback($nodeCallback, new InForeachNode($stmt), $scope, $storage); $originalScope = $scope; $bodyScope = $scope; if ($stmt->keyVar instanceof Variable) { - $nodeCallback(new VariableAssignNode($stmt->keyVar, new GetIterableKeyTypeExpr($stmt->expr)), $originalScope); + $this->callNodeCallback($nodeCallback, new VariableAssignNode($stmt->keyVar, new GetIterableKeyTypeExpr($stmt->expr)), $originalScope, $storage); } if ($stmt->valueVar instanceof Variable) { - $nodeCallback(new VariableAssignNode($stmt->valueVar, new GetIterableValueTypeExpr($stmt->expr)), $originalScope); + $this->callNodeCallback($nodeCallback, new VariableAssignNode($stmt->valueVar, new GetIterableValueTypeExpr($stmt->expr)), $originalScope, $storage); } + $originalStorage = $storage; + $storage = $originalStorage->duplicate(); if ($context->isTopLevel()) { $originalScope = $this->polluteScopeWithAlwaysIterableForeach ? $scope->filterByTruthyValue($arrayComparisonExpr) : $scope; - $bodyScope = $this->enterForeach($originalScope, $originalScope, $stmt, $nodeCallback); + $bodyScope = $this->enterForeach($originalScope, $storage, $originalScope, $stmt, $nodeCallback); $count = 0; do { $prevScope = $bodyScope; $bodyScope = $bodyScope->mergeWith($this->polluteScopeWithAlwaysIterableForeach ? $scope->filterByTruthyValue($arrayComparisonExpr) : $scope); - $bodyScope = $this->enterForeach($bodyScope, $originalScope, $stmt, $nodeCallback); - $bodyScopeResult = $this->processStmtNodesInternal($stmt, $stmt->stmts, $bodyScope, static function (): void { - }, $context->enterDeep())->filterOutLoopExitPoints(); + $storage = $originalStorage->duplicate(); + $bodyScope = $this->enterForeach($bodyScope, $storage, $originalScope, $stmt, $nodeCallback); + $bodyScopeResult = $this->processStmtNodesInternal($stmt, $stmt->stmts, $bodyScope, $storage, new NoopNodeCallback(), $context->enterDeep())->filterOutLoopExitPoints(); $bodyScope = $bodyScopeResult->getScope(); foreach ($bodyScopeResult->getExitPointsByType(Continue_::class) as $continueExitPoint) { $bodyScope = $bodyScope->mergeWith($continueExitPoint->getScope()); @@ -1274,8 +1306,9 @@ public function __invoke(Node $node, Scope $scope): void } $bodyScope = $bodyScope->mergeWith($this->polluteScopeWithAlwaysIterableForeach ? $scope->filterByTruthyValue($arrayComparisonExpr) : $scope); - $bodyScope = $this->enterForeach($bodyScope, $originalScope, $stmt, $nodeCallback); - $finalScopeResult = $this->processStmtNodesInternal($stmt, $stmt->stmts, $bodyScope, $nodeCallback, $context)->filterOutLoopExitPoints(); + $storage = $originalStorage; + $bodyScope = $this->enterForeach($bodyScope, $storage, $originalScope, $stmt, $nodeCallback); + $finalScopeResult = $this->processStmtNodesInternal($stmt, $stmt->stmts, $bodyScope, $storage, $nodeCallback, $context)->filterOutLoopExitPoints(); $finalScope = $finalScopeResult->getScope(); $scopesWithIterableValueType = []; @@ -1409,8 +1442,9 @@ public function __invoke(Node $node, Scope $scope): void $impurePoints, ); } elseif ($stmt instanceof While_) { - $condResult = $this->processExprNode($stmt, $stmt->cond, $scope, static function (): void { - }, ExpressionContext::createDeep()); + $originalStorage = $storage; + $storage = $originalStorage->duplicate(); + $condResult = $this->processExprNode($stmt, $stmt->cond, $scope, $storage, new NoopNodeCallback(), ExpressionContext::createDeep()); $beforeCondBooleanType = ($this->treatPhpDocTypesAsCertain ? $scope->getType($stmt->cond) : $scope->getNativeType($stmt->cond))->toBoolean(); $condScope = $condResult->getFalseyScope(); if (!$context->isTopLevel() && $beforeCondBooleanType->isFalse()->yes()) { @@ -1434,10 +1468,9 @@ public function __invoke(Node $node, Scope $scope): void do { $prevScope = $bodyScope; $bodyScope = $bodyScope->mergeWith($scope); - $bodyScope = $this->processExprNode($stmt, $stmt->cond, $bodyScope, static function (): void { - }, ExpressionContext::createDeep())->getTruthyScope(); - $bodyScopeResult = $this->processStmtNodesInternal($stmt, $stmt->stmts, $bodyScope, static function (): void { - }, $context->enterDeep())->filterOutLoopExitPoints(); + $storage = $originalStorage->duplicate(); + $bodyScope = $this->processExprNode($stmt, $stmt->cond, $bodyScope, $storage, new NoopNodeCallback(), ExpressionContext::createDeep())->getTruthyScope(); + $bodyScopeResult = $this->processStmtNodesInternal($stmt, $stmt->stmts, $bodyScope, $storage, new NoopNodeCallback(), $context->enterDeep())->filterOutLoopExitPoints(); $bodyScope = $bodyScopeResult->getScope(); foreach ($bodyScopeResult->getExitPointsByType(Continue_::class) as $continueExitPoint) { $bodyScope = $bodyScope->mergeWith($continueExitPoint->getScope()); @@ -1455,8 +1488,9 @@ public function __invoke(Node $node, Scope $scope): void $bodyScope = $bodyScope->mergeWith($scope); $bodyScopeMaybeRan = $bodyScope; - $bodyScope = $this->processExprNode($stmt, $stmt->cond, $bodyScope, $nodeCallback, ExpressionContext::createDeep())->getTruthyScope(); - $finalScopeResult = $this->processStmtNodesInternal($stmt, $stmt->stmts, $bodyScope, $nodeCallback, $context)->filterOutLoopExitPoints(); + $storage = $originalStorage; + $bodyScope = $this->processExprNode($stmt, $stmt->cond, $bodyScope, $storage, $nodeCallback, ExpressionContext::createDeep())->getTruthyScope(); + $finalScopeResult = $this->processStmtNodesInternal($stmt, $stmt->stmts, $bodyScope, $storage, $nodeCallback, $context)->filterOutLoopExitPoints(); $finalScope = $finalScopeResult->getScope()->filterByFalseyValue($stmt->cond); $condBooleanType = ($this->treatPhpDocTypesAsCertain ? $bodyScopeMaybeRan->getType($stmt->cond) : $bodyScopeMaybeRan->getNativeType($stmt->cond))->toBoolean(); @@ -1474,7 +1508,7 @@ public function __invoke(Node $node, Scope $scope): void } $isIterableAtLeastOnce = $beforeCondBooleanType->isTrue()->yes(); - $nodeCallback(new BreaklessWhileLoopNode($stmt, $finalScopeResult->toPublic()->getExitPoints()), $bodyScopeMaybeRan); + $this->callNodeCallback($nodeCallback, new BreaklessWhileLoopNode($stmt, $finalScopeResult->toPublic()->getExitPoints()), $bodyScopeMaybeRan, $storage); if ($alwaysIterates) { $isAlwaysTerminating = count($finalScopeResult->getExitPointsByType(Break_::class)) === 0; @@ -1512,13 +1546,14 @@ public function __invoke(Node $node, Scope $scope): void $hasYield = false; $throwPoints = []; $impurePoints = []; + $originalStorage = $storage; if ($context->isTopLevel()) { do { $prevScope = $bodyScope; $bodyScope = $bodyScope->mergeWith($scope); - $bodyScopeResult = $this->processStmtNodesInternal($stmt, $stmt->stmts, $bodyScope, static function (): void { - }, $context->enterDeep())->filterOutLoopExitPoints(); + $storage = $originalStorage->duplicate(); + $bodyScopeResult = $this->processStmtNodesInternal($stmt, $stmt->stmts, $bodyScope, $storage, new NoopNodeCallback(), $context->enterDeep())->filterOutLoopExitPoints(); $alwaysTerminating = $bodyScopeResult->isAlwaysTerminating(); $bodyScope = $bodyScopeResult->getScope(); foreach ($bodyScopeResult->getExitPointsByType(Continue_::class) as $continueExitPoint) { @@ -1528,8 +1563,7 @@ public function __invoke(Node $node, Scope $scope): void foreach ($bodyScopeResult->getExitPointsByType(Break_::class) as $breakExitPoint) { $finalScope = $breakExitPoint->getScope()->mergeWith($finalScope); } - $bodyScope = $this->processExprNode($stmt, $stmt->cond, $bodyScope, static function (): void { - }, ExpressionContext::createDeep())->getTruthyScope(); + $bodyScope = $this->processExprNode($stmt, $stmt->cond, $bodyScope, $storage, new NoopNodeCallback(), ExpressionContext::createDeep())->getTruthyScope(); if ($bodyScope->equals($prevScope)) { break; } @@ -1543,7 +1577,8 @@ public function __invoke(Node $node, Scope $scope): void $bodyScope = $bodyScope->mergeWith($scope); } - $bodyScopeResult = $this->processStmtNodesInternal($stmt, $stmt->stmts, $bodyScope, $nodeCallback, $context)->filterOutLoopExitPoints(); + $storage = $originalStorage; + $bodyScopeResult = $this->processStmtNodesInternal($stmt, $stmt->stmts, $bodyScope, $storage, $nodeCallback, $context)->filterOutLoopExitPoints(); $bodyScope = $bodyScopeResult->getScope(); foreach ($bodyScopeResult->getExitPointsByType(Continue_::class) as $continueExitPoint) { $bodyScope = $bodyScope->mergeWith($continueExitPoint->getScope()); @@ -1551,7 +1586,7 @@ public function __invoke(Node $node, Scope $scope): void $condBooleanType = ($this->treatPhpDocTypesAsCertain ? $bodyScope->getType($stmt->cond) : $bodyScope->getNativeType($stmt->cond))->toBoolean(); $alwaysIterates = $condBooleanType->isTrue()->yes() && $context->isTopLevel(); - $nodeCallback(new DoWhileLoopConditionNode($stmt->cond, $bodyScopeResult->toPublic()->getExitPoints()), $bodyScope); + $this->callNodeCallback($nodeCallback, new DoWhileLoopConditionNode($stmt->cond, $bodyScopeResult->toPublic()->getExitPoints()), $bodyScope, $storage); if ($alwaysIterates) { $alwaysTerminating = count($bodyScopeResult->getExitPointsByType(Break_::class)) === 0; @@ -1563,13 +1598,13 @@ public function __invoke(Node $node, Scope $scope): void $finalScope = $scope; } if (!$alwaysTerminating) { - $condResult = $this->processExprNode($stmt, $stmt->cond, $bodyScope, $nodeCallback, ExpressionContext::createDeep()); + $condResult = $this->processExprNode($stmt, $stmt->cond, $bodyScope, $storage, $nodeCallback, ExpressionContext::createDeep()); $hasYield = $condResult->hasYield(); $throwPoints = $condResult->getThrowPoints(); $impurePoints = $condResult->getImpurePoints(); $finalScope = $condResult->getFalseyScope(); } else { - $this->processExprNode($stmt, $stmt->cond, $bodyScope, $nodeCallback, ExpressionContext::createDeep()); + $this->processExprNode($stmt, $stmt->cond, $bodyScope, $storage, $nodeCallback, ExpressionContext::createDeep()); } foreach ($bodyScopeResult->getExitPointsByType(Break_::class) as $breakExitPoint) { $finalScope = $breakExitPoint->getScope()->mergeWith($finalScope); @@ -1589,19 +1624,21 @@ public function __invoke(Node $node, Scope $scope): void $throwPoints = []; $impurePoints = []; foreach ($stmt->init as $initExpr) { - $initResult = $this->processExprNode($stmt, $initExpr, $initScope, $nodeCallback, ExpressionContext::createTopLevel()); + $initResult = $this->processExprNode($stmt, $initExpr, $initScope, $storage, $nodeCallback, ExpressionContext::createTopLevel()); $initScope = $initResult->getScope(); $hasYield = $hasYield || $initResult->hasYield(); $throwPoints = array_merge($throwPoints, $initResult->getThrowPoints()); $impurePoints = array_merge($impurePoints, $initResult->getImpurePoints()); } + $originalStorage = $storage; + $storage = $originalStorage->duplicate(); + $bodyScope = $initScope; $isIterableAtLeastOnce = TrinaryLogic::createYes(); $lastCondExpr = array_last($stmt->cond) ?? null; foreach ($stmt->cond as $condExpr) { - $condResult = $this->processExprNode($stmt, $condExpr, $bodyScope, static function (): void { - }, ExpressionContext::createDeep()); + $condResult = $this->processExprNode($stmt, $condExpr, $bodyScope, $storage, new NoopNodeCallback(), ExpressionContext::createDeep()); $initScope = $condResult->getScope(); $condResultScope = $condResult->getScope(); @@ -1622,21 +1659,19 @@ public function __invoke(Node $node, Scope $scope): void $count = 0; do { $prevScope = $bodyScope; + $storage = $originalStorage->duplicate(); $bodyScope = $bodyScope->mergeWith($initScope); if ($lastCondExpr !== null) { - $bodyScope = $this->processExprNode($stmt, $lastCondExpr, $bodyScope, static function (): void { - }, ExpressionContext::createDeep())->getTruthyScope(); + $bodyScope = $this->processExprNode($stmt, $lastCondExpr, $bodyScope, $storage, new NoopNodeCallback(), ExpressionContext::createDeep())->getTruthyScope(); } - $bodyScopeResult = $this->processStmtNodesInternal($stmt, $stmt->stmts, $bodyScope, static function (): void { - }, $context->enterDeep())->filterOutLoopExitPoints(); + $bodyScopeResult = $this->processStmtNodesInternal($stmt, $stmt->stmts, $bodyScope, $storage, new NoopNodeCallback(), $context->enterDeep())->filterOutLoopExitPoints(); $bodyScope = $bodyScopeResult->getScope(); foreach ($bodyScopeResult->getExitPointsByType(Continue_::class) as $continueExitPoint) { $bodyScope = $bodyScope->mergeWith($continueExitPoint->getScope()); } foreach ($stmt->loop as $loopExpr) { - $exprResult = $this->processExprNode($stmt, $loopExpr, $bodyScope, static function (): void { - }, ExpressionContext::createTopLevel()); + $exprResult = $this->processExprNode($stmt, $loopExpr, $bodyScope, $storage, new NoopNodeCallback(), ExpressionContext::createTopLevel()); $bodyScope = $exprResult->getScope(); $hasYield = $hasYield || $exprResult->hasYield(); $throwPoints = array_merge($throwPoints, $exprResult->getThrowPoints()); @@ -1654,16 +1689,17 @@ public function __invoke(Node $node, Scope $scope): void } while ($count < self::LOOP_SCOPE_ITERATIONS); } + $storage = $originalStorage; $bodyScope = $bodyScope->mergeWith($initScope); $alwaysIterates = TrinaryLogic::createFromBoolean($context->isTopLevel()); if ($lastCondExpr !== null) { $alwaysIterates = $alwaysIterates->and($bodyScope->getType($lastCondExpr)->toBoolean()->isTrue()); - $bodyScope = $this->processExprNode($stmt, $lastCondExpr, $bodyScope, $nodeCallback, ExpressionContext::createDeep())->getTruthyScope(); + $bodyScope = $this->processExprNode($stmt, $lastCondExpr, $bodyScope, $storage, $nodeCallback, ExpressionContext::createDeep())->getTruthyScope(); $bodyScope = $this->inferForLoopExpressions($stmt, $lastCondExpr, $bodyScope); } - $finalScopeResult = $this->processStmtNodesInternal($stmt, $stmt->stmts, $bodyScope, $nodeCallback, $context)->filterOutLoopExitPoints(); + $finalScopeResult = $this->processStmtNodesInternal($stmt, $stmt->stmts, $bodyScope, $storage, $nodeCallback, $context)->filterOutLoopExitPoints(); $finalScope = $finalScopeResult->getScope(); foreach ($finalScopeResult->getExitPointsByType(Continue_::class) as $continueExitPoint) { $finalScope = $continueExitPoint->getScope()->mergeWith($finalScope); @@ -1671,7 +1707,7 @@ public function __invoke(Node $node, Scope $scope): void $loopScope = $finalScope; foreach ($stmt->loop as $loopExpr) { - $loopScope = $this->processExprNode($stmt, $loopExpr, $loopScope, $nodeCallback, ExpressionContext::createTopLevel())->getScope(); + $loopScope = $this->processExprNode($stmt, $loopExpr, $loopScope, $storage, $nodeCallback, ExpressionContext::createTopLevel())->getScope(); } $finalScope = $finalScope->generalizeWith($loopScope); @@ -1719,7 +1755,7 @@ public function __invoke(Node $node, Scope $scope): void array_merge($impurePoints, $finalScopeResult->getImpurePoints()), ); } elseif ($stmt instanceof Switch_) { - $condResult = $this->processExprNode($stmt, $stmt->cond, $scope, $nodeCallback, ExpressionContext::createDeep()); + $condResult = $this->processExprNode($stmt, $stmt->cond, $scope, $storage, $nodeCallback, ExpressionContext::createDeep()); $scope = $condResult->getScope(); $scopeForBranches = $scope; $finalScope = null; @@ -1735,7 +1771,7 @@ public function __invoke(Node $node, Scope $scope): void if ($caseNode->cond !== null) { $condExpr = new BinaryOp\Equal($stmt->cond, $caseNode->cond); $fullCondExpr = $fullCondExpr === null ? $condExpr : new BooleanOr($fullCondExpr, $condExpr); - $caseResult = $this->processExprNode($stmt, $caseNode->cond, $scopeForBranches, $nodeCallback, ExpressionContext::createDeep()); + $caseResult = $this->processExprNode($stmt, $caseNode->cond, $scopeForBranches, $storage, $nodeCallback, ExpressionContext::createDeep()); $scopeForBranches = $caseResult->getScope(); $hasYield = $hasYield || $caseResult->hasYield(); $throwPoints = array_merge($throwPoints, $caseResult->getThrowPoints()); @@ -1748,7 +1784,7 @@ public function __invoke(Node $node, Scope $scope): void } $branchScope = $branchScope->mergeWith($prevScope); - $branchScopeResult = $this->processStmtNodesInternal($caseNode, $caseNode->stmts, $branchScope, $nodeCallback, $context); + $branchScopeResult = $this->processStmtNodesInternal($caseNode, $caseNode->stmts, $branchScope, $storage, $nodeCallback, $context); $branchScope = $branchScopeResult->getScope(); $branchFinalScopeResult = $branchScopeResult->filterOutLoopExitPoints(); $hasYield = $hasYield || $branchFinalScopeResult->hasYield(); @@ -1794,7 +1830,7 @@ public function __invoke(Node $node, Scope $scope): void return new InternalStatementResult($finalScope, $hasYield, $alwaysTerminating, $exitPointsForOuterLoop, $throwPoints, $impurePoints); } elseif ($stmt instanceof TryCatch) { - $branchScopeResult = $this->processStmtNodesInternal($stmt, $stmt->stmts, $scope, $nodeCallback, $context); + $branchScopeResult = $this->processStmtNodesInternal($stmt, $stmt->stmts, $scope, $storage, $nodeCallback, $context); $branchScope = $branchScopeResult->getScope(); $finalScope = $branchScopeResult->isAlwaysTerminating() ? null : $branchScope; @@ -1825,7 +1861,7 @@ public function __invoke(Node $node, Scope $scope): void $pastCatchTypes = new NeverType(); foreach ($stmt->catches as $catchNode) { - $nodeCallback($catchNode, $scope); + $this->callNodeCallback($nodeCallback, $catchNode, $scope, $storage); $originalCatchTypes = array_map(static fn (Name $name): Type => new ObjectType($name->toString()), $catchNode->types); $catchTypes = array_map(static fn (Type $type): Type => TypeCombinator::remove($type, $pastCatchTypes), $originalCatchTypes); @@ -1910,7 +1946,7 @@ public function __invoke(Node $node, Scope $scope): void if ($matched) { continue; } - $nodeCallback(new CatchWithUnthrownExceptionNode($catchNode, $catchTypes[$catchTypeIndex], $originalCatchTypes[$catchTypeIndex]), $scope); + $this->callNodeCallback($nodeCallback, new CatchWithUnthrownExceptionNode($catchNode, $catchTypes[$catchTypeIndex], $originalCatchTypes[$catchTypeIndex]), $scope, $storage); } if (count($matchingThrowPoints) === 0) { @@ -1948,7 +1984,7 @@ public function __invoke(Node $node, Scope $scope): void $variableName = $catchNode->var->name; } - $catchScopeResult = $this->processStmtNodesInternal($catchNode, $catchNode->stmts, $catchScope->enterCatchType($catchType, $variableName), $nodeCallback, $context); + $catchScopeResult = $this->processStmtNodesInternal($catchNode, $catchNode->stmts, $catchScope->enterCatchType($catchType, $variableName), $storage, $nodeCallback, $context); $catchScopeForFinally = $catchScopeResult->getScope(); $finalScope = $catchScopeResult->isAlwaysTerminating() ? $finalScope : $catchScopeResult->getScope()->mergeWith($finalScope); @@ -1993,7 +2029,7 @@ public function __invoke(Node $node, Scope $scope): void if ($finallyScope !== null) { $originalFinallyScope = $finallyScope; - $finallyResult = $this->processStmtNodesInternal($stmt->finally, $stmt->finally->stmts, $finallyScope, $nodeCallback, $context); + $finallyResult = $this->processStmtNodesInternal($stmt->finally, $stmt->finally->stmts, $finallyScope, $storage, $nodeCallback, $context); $alwaysTerminating = $alwaysTerminating || $finallyResult->isAlwaysTerminating(); $hasYield = $hasYield || $finallyResult->hasYield(); $throwPointsForLater = array_merge($throwPointsForLater, $finallyResult->getThrowPoints()); @@ -2001,10 +2037,10 @@ public function __invoke(Node $node, Scope $scope): void $finallyScope = $finallyResult->getScope(); $finalScope = $finallyResult->isAlwaysTerminating() ? $finalScope : $finalScope->processFinallyScope($finallyScope, $originalFinallyScope); if (count($finallyResult->getExitPoints()) > 0) { - $nodeCallback(new FinallyExitPointsNode( + $this->callNodeCallback($nodeCallback, new FinallyExitPointsNode( $finallyResult->toPublic()->getExitPoints(), $finallyExitPoints, - ), $scope); + ), $scope, $storage); } $exitPoints = array_merge($exitPoints, $finallyResult->getExitPoints()); } @@ -2016,7 +2052,7 @@ public function __invoke(Node $node, Scope $scope): void $impurePoints = []; foreach ($stmt->vars as $var) { $scope = $this->lookForSetAllowedUndefinedExpressions($scope, $var); - $exprResult = $this->processExprNode($stmt, $var, $scope, $nodeCallback, ExpressionContext::createDeep()); + $exprResult = $this->processExprNode($stmt, $var, $scope, $storage, $nodeCallback, ExpressionContext::createDeep()); $scope = $exprResult->getScope(); $scope = $this->lookForUnsetAllowedUndefinedExpressions($scope, $var); $hasYield = $hasYield || $exprResult->hasYield(); @@ -2046,7 +2082,7 @@ public function leaveNode(Node $node): ?ExistingArrayDimFetch /** @var Expr $clonedVar */ [$clonedVar] = $traverser->traverse([$clonedVar]); - $scope = $this->processVirtualAssign($scope, $stmt, $clonedVar, new UnsetOffsetExpr($var->var, $var->dim), $nodeCallback)->getScope(); + $scope = $this->processVirtualAssign($scope, $storage, $stmt, $clonedVar, new UnsetOffsetExpr($var->var, $var->dim), $nodeCallback)->getScope(); } elseif ($var instanceof PropertyFetch) { $scope = $scope->invalidateExpression($var); $impurePoints[] = new ImpurePoint( @@ -2067,7 +2103,7 @@ public function leaveNode(Node $node): ?ExistingArrayDimFetch $throwPoints = []; $impurePoints = []; foreach ($stmt->uses as $use) { - $nodeCallback($use, $scope); + $this->callNodeCallback($nodeCallback, $use, $scope, $storage); } } elseif ($stmt instanceof Node\Stmt\Global_) { $hasYield = false; @@ -2087,7 +2123,7 @@ public function leaveNode(Node $node): ?ExistingArrayDimFetch throw new ShouldNotHappenException(); } $scope = $this->lookForSetAllowedUndefinedExpressions($scope, $var); - $varResult = $this->processExprNode($stmt, $var, $scope, $nodeCallback, ExpressionContext::createDeep()); + $varResult = $this->processExprNode($stmt, $var, $scope, $storage, $nodeCallback, ExpressionContext::createDeep()); $impurePoints = array_merge($impurePoints, $varResult->getImpurePoints()); $scope = $this->lookForUnsetAllowedUndefinedExpressions($scope, $var); @@ -2119,12 +2155,12 @@ public function leaveNode(Node $node): ?ExistingArrayDimFetch } if ($var->default !== null) { - $defaultExprResult = $this->processExprNode($stmt, $var->default, $scope, $nodeCallback, ExpressionContext::createDeep()); + $defaultExprResult = $this->processExprNode($stmt, $var->default, $scope, $storage, $nodeCallback, ExpressionContext::createDeep()); $impurePoints = array_merge($impurePoints, $defaultExprResult->getImpurePoints()); } $scope = $scope->enterExpressionAssign($var->var); - $varResult = $this->processExprNode($stmt, $var->var, $scope, $nodeCallback, ExpressionContext::createDeep()); + $varResult = $this->processExprNode($stmt, $var->var, $scope, $storage, $nodeCallback, ExpressionContext::createDeep()); $impurePoints = array_merge($impurePoints, $varResult->getImpurePoints()); $scope = $scope->exitExpressionAssign($var->var); @@ -2138,8 +2174,8 @@ public function leaveNode(Node $node): ?ExistingArrayDimFetch $throwPoints = []; $impurePoints = []; foreach ($stmt->consts as $const) { - $nodeCallback($const, $scope); - $constResult = $this->processExprNode($stmt, $const->value, $scope, $nodeCallback, ExpressionContext::createDeep()); + $this->callNodeCallback($nodeCallback, $const, $scope, $storage); + $constResult = $this->processExprNode($stmt, $const->value, $scope, $storage, $nodeCallback, ExpressionContext::createDeep()); $impurePoints = array_merge($impurePoints, $constResult->getImpurePoints()); if ($const->namespacedName !== null) { $constantName = new Name\FullyQualified($const->namespacedName->toString()); @@ -2152,10 +2188,10 @@ public function leaveNode(Node $node): ?ExistingArrayDimFetch $hasYield = false; $throwPoints = []; $impurePoints = []; - $this->processAttributeGroups($stmt, $stmt->attrGroups, $scope, $nodeCallback); + $this->processAttributeGroups($stmt, $stmt->attrGroups, $scope, $storage, $nodeCallback); foreach ($stmt->consts as $const) { - $nodeCallback($const, $scope); - $constResult = $this->processExprNode($stmt, $const->value, $scope, $nodeCallback, ExpressionContext::createDeep()); + $this->callNodeCallback($nodeCallback, $const, $scope, $storage); + $constResult = $this->processExprNode($stmt, $const->value, $scope, $storage, $nodeCallback, ExpressionContext::createDeep()); $impurePoints = array_merge($impurePoints, $constResult->getImpurePoints()); if ($scope->getClassReflection() === null) { throw new ShouldNotHappenException(); @@ -2169,10 +2205,10 @@ public function leaveNode(Node $node): ?ExistingArrayDimFetch } elseif ($stmt instanceof Node\Stmt\EnumCase) { $hasYield = false; $throwPoints = []; - $this->processAttributeGroups($stmt, $stmt->attrGroups, $scope, $nodeCallback); + $this->processAttributeGroups($stmt, $stmt->attrGroups, $scope, $storage, $nodeCallback); $impurePoints = []; if ($stmt->expr !== null) { - $exprResult = $this->processExprNode($stmt, $stmt->expr, $scope, $nodeCallback, ExpressionContext::createDeep()); + $exprResult = $this->processExprNode($stmt, $stmt->expr, $scope, $storage, $nodeCallback, ExpressionContext::createDeep()); $impurePoints = $exprResult->getImpurePoints(); } } elseif ($stmt instanceof InlineHTML) { @@ -2182,7 +2218,7 @@ public function leaveNode(Node $node): ?ExistingArrayDimFetch new ImpurePoint($scope, $stmt, 'betweenPhpTags', 'output between PHP opening and closing tags', true), ]; } elseif ($stmt instanceof Node\Stmt\Block) { - $result = $this->processStmtNodesInternal($stmt, $stmt->stmts, $scope, $nodeCallback, $context); + $result = $this->processStmtNodesInternal($stmt, $stmt->stmts, $scope, $storage, $nodeCallback, $context); if ($this->polluteScopeWithBlock) { return $result; } @@ -2204,7 +2240,7 @@ public function leaveNode(Node $node): ?ExistingArrayDimFetch $hasYield = false; $throwPoints = []; foreach ($stmt->uses as $use) { - $nodeCallback($use, $scope); + $this->callNodeCallback($nodeCallback, $use, $scope, $storage); } $impurePoints = []; } else { @@ -2512,7 +2548,14 @@ private function findEarlyTerminatingExpr(Expr $expr, Scope $scope): ?Expr /** * @param callable(Node $node, Scope $scope): void $nodeCallback */ - public function processExprNode(Node\Stmt $stmt, Expr $expr, MutatingScope $scope, callable $nodeCallback, ExpressionContext $context): ExpressionResult + public function processExprNode( + Node\Stmt $stmt, + Expr $expr, + MutatingScope $scope, + ExpressionResultStorage $storage, + callable $nodeCallback, + ExpressionContext $context, + ): ExpressionResult { if ($expr instanceof Expr\CallLike && $expr->isFirstClassCallable()) { if ($expr instanceof FuncCall) { @@ -2527,10 +2570,21 @@ public function processExprNode(Node\Stmt $stmt, Expr $expr, MutatingScope $scop throw new ShouldNotHappenException(); } - return $this->processExprNode($stmt, $newExpr, $scope, $nodeCallback, $context); + $newExprResult = $this->processExprNode($stmt, $newExpr, $scope, $storage, $nodeCallback, $context); + $storage->storeResult($expr, $newExprResult); + return $newExprResult; } - $this->callNodeCallbackWithExpression($nodeCallback, $expr, $scope, $context); + $existingExprResult = $storage->findResult($expr); + if ($existingExprResult !== null) { + if ($nodeCallback instanceof ShallowNodeCallback) { + return $existingExprResult; + } + throw new ShouldNotHappenException(sprintf('Expr %s on line %d has already been analysed', get_class($expr), $expr->getStartLine())); + } + + $originalScope = $scope; + $this->callNodeCallbackWithExpression($nodeCallback, $expr, $scope, $storage, $context); if ($expr instanceof Variable) { $hasYield = false; @@ -2538,19 +2592,20 @@ public function processExprNode(Node\Stmt $stmt, Expr $expr, MutatingScope $scop $impurePoints = []; $isAlwaysTerminating = false; if ($expr->name instanceof Expr) { - return $this->processExprNode($stmt, $expr->name, $scope, $nodeCallback, $context->enterDeep()); + return $this->processExprNode($stmt, $expr->name, $scope, $storage, $nodeCallback, $context->enterDeep()); } elseif (in_array($expr->name, Scope::SUPERGLOBAL_VARIABLES, true)) { $impurePoints[] = new ImpurePoint($scope, $expr, 'superglobal', 'access to superglobal variable', true); } } elseif ($expr instanceof Assign || $expr instanceof AssignRef) { $result = $this->processAssignVar( $scope, + $storage, $stmt, $expr->var, $expr->expr, $nodeCallback, $context, - function (MutatingScope $scope) use ($stmt, $expr, $nodeCallback, $context): ExpressionResult { + function (MutatingScope $scope) use ($stmt, $expr, $nodeCallback, $context, $storage): ExpressionResult { $impurePoints = []; if ($expr instanceof AssignRef) { $referencedExpr = $expr->expr; @@ -2578,7 +2633,8 @@ function (MutatingScope $scope) use ($stmt, $expr, $nodeCallback, $context): Exp ); } - $result = $this->processExprNode($stmt, $expr->expr, $scope, $nodeCallback, $context->enterDeep()); + $beforeScope = $scope; + $result = $this->processExprNode($stmt, $expr->expr, $scope, $storage, $nodeCallback, $context->enterDeep()); $hasYield = $result->hasYield(); $throwPoints = $result->getThrowPoints(); $impurePoints = array_merge($impurePoints, $result->getImpurePoints()); @@ -2589,11 +2645,15 @@ function (MutatingScope $scope) use ($stmt, $expr, $nodeCallback, $context): Exp $scope = $scope->exitExpressionAssign($expr->expr); } - return new ExpressionResult($scope, $hasYield, $isAlwaysTerminating, $throwPoints, $impurePoints); + $result = new ExpressionResult($scope, $beforeScope, $hasYield, $isAlwaysTerminating, $throwPoints, $impurePoints); + $this->storeResult($storage, $expr, $result); + + return $result; }, true, ); $scope = $result->getScope(); + $this->storeResult($storage, $expr, $result); $hasYield = $result->hasYield(); $throwPoints = $result->getThrowPoints(); $impurePoints = $result->getImpurePoints(); @@ -2603,18 +2663,19 @@ function (MutatingScope $scope) use ($stmt, $expr, $nodeCallback, $context): Exp $varChangedScope = false; $scope = $this->processVarAnnotation($scope, $vars, $stmt, $varChangedScope); if (!$varChangedScope) { - $scope = $this->processStmtVarAnnotation($scope, $stmt, null, $nodeCallback); + $scope = $this->processStmtVarAnnotation($scope, $storage, $stmt, null, $nodeCallback); } } } elseif ($expr instanceof Expr\AssignOp) { $result = $this->processAssignVar( $scope, + $storage, $stmt, $expr->var, $expr, $nodeCallback, $context, - function (MutatingScope $scope) use ($stmt, $expr, $nodeCallback, $context): ExpressionResult { + function (MutatingScope $scope) use ($stmt, $expr, $nodeCallback, $context, $storage): ExpressionResult { $originalScope = $scope; if ($expr instanceof Expr\AssignOp\Coalesce) { $scope = $scope->filterByFalseyValue( @@ -2622,15 +2683,19 @@ function (MutatingScope $scope) use ($stmt, $expr, $nodeCallback, $context): Exp ); } - $result = $this->processExprNode($stmt, $expr->expr, $scope, $nodeCallback, $context->enterDeep()); + $result = $this->processExprNode($stmt, $expr->expr, $scope, $storage, $nodeCallback, $context->enterDeep()); if ($expr instanceof Expr\AssignOp\Coalesce) { - return new ExpressionResult( + $result = new ExpressionResult( $result->getScope()->mergeWith($originalScope), + $originalScope, $result->hasYield(), $result->isAlwaysTerminating(), $result->getThrowPoints(), $result->getImpurePoints(), ); + $this->storeResult($storage, $expr, $result); + + return $result; } return $result; @@ -2638,6 +2703,7 @@ function (MutatingScope $scope) use ($stmt, $expr, $nodeCallback, $context): Exp $expr instanceof Expr\AssignOp\Coalesce, ); $scope = $result->getScope(); + $this->storeResult($storage, $expr, $result); $hasYield = $result->hasYield(); $throwPoints = $result->getThrowPoints(); $impurePoints = $result->getImpurePoints(); @@ -2665,27 +2731,17 @@ function (MutatingScope $scope) use ($stmt, $expr, $nodeCallback, $context): Exp ); } - $nameResult = $this->processExprNode($stmt, $expr->name, $scope, $nodeCallback, $context->enterDeep()); + $nameResult = $this->processExprNode($stmt, $expr->name, $scope, $storage, $nodeCallback, $context->enterDeep()); $scope = $nameResult->getScope(); $throwPoints = $nameResult->getThrowPoints(); $impurePoints = $nameResult->getImpurePoints(); $isAlwaysTerminating = $nameResult->isAlwaysTerminating(); - if ( + if ( // phpcs:ignore $nameType->isObject()->yes() && $nameType->isCallable()->yes() && (new ObjectType(Closure::class))->isSuperTypeOf($nameType)->no() ) { - $invokeResult = $this->processExprNode( - $stmt, - new MethodCall($expr->name, '__invoke', $expr->getArgs(), $expr->getAttributes()), - $scope, - static function (): void { - }, - $context->enterDeep(), - ); - $throwPoints = array_merge($throwPoints, $invokeResult->getThrowPoints()); - $impurePoints = array_merge($impurePoints, $invokeResult->getImpurePoints()); - $isAlwaysTerminating = $invokeResult->isAlwaysTerminating(); + // processed later } elseif ($parametersAcceptor instanceof CallableParametersAcceptor) { $callableThrowPoints = array_map(static fn (SimpleThrowPoint $throwPoint) => $throwPoint->isExplicit() ? InternalThrowPoint::createExplicit($scope, $throwPoint->getType(), $expr, $throwPoint->canContainAnyThrowable()) : InternalThrowPoint::createImplicit($scope, $expr), $parametersAcceptor->getThrowPoints()); if (!$this->implicitThrows) { @@ -2718,29 +2774,31 @@ static function (): void { ); } + $normalizedExpr = $expr; if ($parametersAcceptor !== null) { - $expr = ArgumentsNormalizer::reorderFuncArguments($parametersAcceptor, $expr) ?? $expr; + $normalizedExpr = ArgumentsNormalizer::reorderFuncArguments($parametersAcceptor, $expr) ?? $expr; $returnType = $parametersAcceptor->getReturnType(); $isAlwaysTerminating = $isAlwaysTerminating || $returnType instanceof NeverType && $returnType->isExplicit(); } if ( - $expr->name instanceof Name + $normalizedExpr->name instanceof Name && $functionReflection !== null && $functionReflection->getName() === 'clone' - && count($expr->getArgs()) === 2 + && count($normalizedExpr->getArgs()) === 2 ) { - $clonePropertiesArgType = $scope->getType($expr->getArgs()[1]->value); - $cloneExpr = new TypeExpr($scope->getType(new Expr\Clone_($expr->getArgs()[0]->value))); + $clonePropertiesArgType = $scope->getType($normalizedExpr->getArgs()[1]->value); + $cloneExpr = new TypeExpr($scope->getType(new Expr\Clone_($normalizedExpr->getArgs()[0]->value))); $clonePropertiesArgTypeConstantArrays = $clonePropertiesArgType->getConstantArrays(); foreach ($clonePropertiesArgTypeConstantArrays as $clonePropertiesArgTypeConstantArray) { foreach ($clonePropertiesArgTypeConstantArray->getKeyTypes() as $i => $clonePropertyKeyType) { $clonePropertyKeyTypeScalars = $clonePropertyKeyType->getConstantScalarValues(); - $propertyAttributes = $expr->getAttributes(); + $propertyAttributes = $normalizedExpr->getAttributes(); $propertyAttributes['inCloneWith'] = true; if (count($clonePropertyKeyTypeScalars) === 1) { $this->processVirtualAssign( $scope, + $storage, $stmt, new PropertyFetch($cloneExpr, (string) $clonePropertyKeyTypeScalars[0], $propertyAttributes), new TypeExpr($clonePropertiesArgTypeConstantArray->getValueTypes()[$i]), @@ -2751,6 +2809,7 @@ static function (): void { $this->processVirtualAssign( $scope, + $storage, $stmt, new PropertyFetch($cloneExpr, new TypeExpr($clonePropertyKeyType), $propertyAttributes), new TypeExpr($clonePropertiesArgTypeConstantArray->getValueTypes()[$i]), @@ -2760,15 +2819,36 @@ static function (): void { } } - $result = $this->processArgs($stmt, $functionReflection, null, $parametersAcceptor, $expr, $scope, $nodeCallback, $context); + $result = $this->processArgs($stmt, $functionReflection, null, $parametersAcceptor, $normalizedExpr, $scope, $storage, $nodeCallback, $context); $scope = $result->getScope(); $hasYield = $result->hasYield(); $throwPoints = array_merge($throwPoints, $result->getThrowPoints()); $impurePoints = array_merge($impurePoints, $result->getImpurePoints()); $isAlwaysTerminating = $isAlwaysTerminating || $result->isAlwaysTerminating(); + if ($normalizedExpr->name instanceof Expr) { + $nameType = $scope->getType($normalizedExpr->name); + if ( + $nameType->isObject()->yes() + && $nameType->isCallable()->yes() + && (new ObjectType(Closure::class))->isSuperTypeOf($nameType)->no() + ) { + $invokeResult = $this->processExprNode( + $stmt, + new MethodCall($normalizedExpr->name, '__invoke', $normalizedExpr->getArgs(), $normalizedExpr->getAttributes()), + $scope, + $storage, + new NoopNodeCallback(), + $context->enterDeep(), + ); + $throwPoints = array_merge($throwPoints, $invokeResult->getThrowPoints()); + $impurePoints = array_merge($impurePoints, $invokeResult->getImpurePoints()); + $isAlwaysTerminating = $invokeResult->isAlwaysTerminating(); + } + } + if ($functionReflection !== null) { - $functionThrowPoint = $this->getFunctionThrowPoint($functionReflection, $parametersAcceptor, $expr, $scope); + $functionThrowPoint = $this->getFunctionThrowPoint($functionReflection, $parametersAcceptor, $normalizedExpr, $scope); if ($functionThrowPoint !== null) { $throwPoints[] = $functionThrowPoint; } @@ -2796,18 +2876,18 @@ static function (): void { if ( $functionReflection !== null && $functionReflection->getName() === 'file_put_contents' - && count($expr->getArgs()) > 0 + && count($normalizedExpr->getArgs()) > 0 ) { - $scope = $scope->invalidateExpression(new FuncCall(new Name('file_get_contents'), [$expr->getArgs()[0]])) - ->invalidateExpression(new FuncCall(new Name\FullyQualified('file_get_contents'), [$expr->getArgs()[0]])); + $scope = $scope->invalidateExpression(new FuncCall(new Name('file_get_contents'), [$normalizedExpr->getArgs()[0]])) + ->invalidateExpression(new FuncCall(new Name\FullyQualified('file_get_contents'), [$normalizedExpr->getArgs()[0]])); } if ( $functionReflection !== null && in_array($functionReflection->getName(), ['array_pop', 'array_shift'], true) - && count($expr->getArgs()) >= 1 + && count($normalizedExpr->getArgs()) >= 1 ) { - $arrayArg = $expr->getArgs()[0]->value; + $arrayArg = $normalizedExpr->getArgs()[0]->value; $arrayArgType = $scope->getType($arrayArg); $arrayArgNativeType = $scope->getNativeType($arrayArg); @@ -2815,6 +2895,7 @@ static function (): void { $scope = $this->processVirtualAssign( $scope, + $storage, $stmt, $arrayArg, new NativeTypeExpr( @@ -2828,17 +2909,18 @@ static function (): void { if ( $functionReflection !== null && in_array($functionReflection->getName(), ['array_push', 'array_unshift'], true) - && count($expr->getArgs()) >= 2 + && count($normalizedExpr->getArgs()) >= 2 ) { - $arrayArg = $expr->getArgs()[0]->value; + $arrayArg = $normalizedExpr->getArgs()[0]->value; $scope = $this->processVirtualAssign( $scope, + $storage, $stmt, $arrayArg, new NativeTypeExpr( - $this->getArrayFunctionAppendingType($functionReflection, $scope, $expr), - $this->getArrayFunctionAppendingType($functionReflection, $scope->doNotTreatPhpDocTypesAsCertain(), $expr), + $this->getArrayFunctionAppendingType($functionReflection, $scope, $normalizedExpr), + $this->getArrayFunctionAppendingType($functionReflection, $scope->doNotTreatPhpDocTypesAsCertain(), $normalizedExpr), ), $nodeCallback, )->getScope(); @@ -2855,10 +2937,11 @@ static function (): void { $functionReflection !== null && $functionReflection->getName() === 'shuffle' ) { - $arrayArg = $expr->getArgs()[0]->value; + $arrayArg = $normalizedExpr->getArgs()[0]->value; $scope = $this->processVirtualAssign( $scope, + $storage, $stmt, $arrayArg, new NativeTypeExpr($scope->getType($arrayArg)->shuffleArray(), $scope->getNativeType($arrayArg)->shuffleArray()), @@ -2869,18 +2952,19 @@ static function (): void { if ( $functionReflection !== null && $functionReflection->getName() === 'array_splice' - && count($expr->getArgs()) >= 2 + && count($normalizedExpr->getArgs()) >= 2 ) { - $arrayArg = $expr->getArgs()[0]->value; + $arrayArg = $normalizedExpr->getArgs()[0]->value; $arrayArgType = $scope->getType($arrayArg); $arrayArgNativeType = $scope->getNativeType($arrayArg); - $offsetType = $scope->getType($expr->getArgs()[1]->value); - $lengthType = isset($expr->getArgs()[2]) ? $scope->getType($expr->getArgs()[2]->value) : new NullType(); - $replacementType = isset($expr->getArgs()[3]) ? $scope->getType($expr->getArgs()[3]->value) : new ConstantArrayType([], []); + $offsetType = $scope->getType($normalizedExpr->getArgs()[1]->value); + $lengthType = isset($normalizedExpr->getArgs()[2]) ? $scope->getType($normalizedExpr->getArgs()[2]->value) : new NullType(); + $replacementType = isset($normalizedExpr->getArgs()[3]) ? $scope->getType($normalizedExpr->getArgs()[3]->value) : new ConstantArrayType([], []); $scope = $this->processVirtualAssign( $scope, + $storage, $stmt, $arrayArg, new NativeTypeExpr( @@ -2894,12 +2978,13 @@ static function (): void { if ( $functionReflection !== null && in_array($functionReflection->getName(), ['sort', 'rsort', 'usort'], true) - && count($expr->getArgs()) >= 1 + && count($normalizedExpr->getArgs()) >= 1 ) { - $arrayArg = $expr->getArgs()[0]->value; + $arrayArg = $normalizedExpr->getArgs()[0]->value; $scope = $this->processVirtualAssign( $scope, + $storage, $stmt, $arrayArg, new NativeTypeExpr($this->getArraySortPreserveListFunctionType($scope->getType($arrayArg)), $this->getArraySortPreserveListFunctionType($scope->getNativeType($arrayArg))), @@ -2910,12 +2995,13 @@ static function (): void { if ( $functionReflection !== null && in_array($functionReflection->getName(), ['natcasesort', 'natsort', 'arsort', 'asort', 'ksort', 'krsort', 'uasort', 'uksort'], true) - && count($expr->getArgs()) >= 1 + && count($normalizedExpr->getArgs()) >= 1 ) { - $arrayArg = $expr->getArgs()[0]->value; + $arrayArg = $normalizedExpr->getArgs()[0]->value; $scope = $this->processVirtualAssign( $scope, + $storage, $stmt, $arrayArg, new NativeTypeExpr($this->getArraySortDoNotPreserveListFunctionType($scope->getType($arrayArg)), $this->getArraySortDoNotPreserveListFunctionType($scope->getNativeType($arrayArg))), @@ -2927,7 +3013,7 @@ static function (): void { $functionReflection !== null && $functionReflection->getName() === 'extract' ) { - $extractedArg = $expr->getArgs()[0]->value; + $extractedArg = $normalizedExpr->getArgs()[0]->value; $extractedType = $scope->getType($extractedArg); $constantArrays = $extractedType->getConstantArrays(); if (count($constantArrays) > 0) { @@ -2992,7 +3078,7 @@ static function (): void { ); } - $result = $this->processExprNode($stmt, $expr->var, $closureCallScope ?? $scope, $nodeCallback, $context->enterDeep()); + $result = $this->processExprNode($stmt, $expr->var, $closureCallScope ?? $scope, $storage, $nodeCallback, $context->enterDeep()); $hasYield = $result->hasYield(); $throwPoints = $result->getThrowPoints(); $impurePoints = $result->getImpurePoints(); @@ -3005,7 +3091,7 @@ static function (): void { $methodReflection = null; $calledOnType = $scope->getType($expr->var); if ($expr->name instanceof Expr) { - $methodNameResult = $this->processExprNode($stmt, $expr->name, $scope, $nodeCallback, $context->enterDeep()); + $methodNameResult = $this->processExprNode($stmt, $expr->name, $scope, $storage, $nodeCallback, $context->enterDeep()); $throwPoints = array_merge($throwPoints, $methodNameResult->getThrowPoints()); $scope = $methodNameResult->getScope(); } else { @@ -3041,8 +3127,9 @@ static function (): void { ); } + $normalizedExpr = $expr; if ($parametersAcceptor !== null) { - $expr = ArgumentsNormalizer::reorderMethodArguments($parametersAcceptor, $expr) ?? $expr; + $normalizedExpr = ArgumentsNormalizer::reorderMethodArguments($parametersAcceptor, $expr) ?? $expr; $returnType = $parametersAcceptor->getReturnType(); $isAlwaysTerminating = $returnType instanceof NeverType && $returnType->isExplicit(); } @@ -3052,8 +3139,9 @@ static function (): void { $methodReflection, $methodReflection !== null ? $scope->getNakedMethod($calledOnType, $methodReflection->getName()) : null, $parametersAcceptor, - $expr, + $normalizedExpr, $scope, + $storage, $nodeCallback, $context, ); @@ -3062,21 +3150,21 @@ static function (): void { if ($methodReflection !== null) { $hasSideEffects = $methodReflection->hasSideEffects(); if ($hasSideEffects->yes() || $methodReflection->getName() === '__construct') { - $nodeCallback(new InvalidateExprNode($expr->var), $scope); - $scope = $scope->invalidateExpression($expr->var, true); + $this->callNodeCallback($nodeCallback, new InvalidateExprNode($normalizedExpr->var), $scope, $storage); + $scope = $scope->invalidateExpression($normalizedExpr->var, true); } if ($parametersAcceptor !== null && !$methodReflection->isStatic()) { $selfOutType = $methodReflection->getSelfOutType(); if ($selfOutType !== null) { $scope = $scope->assignExpression( - $expr->var, + $normalizedExpr->var, TemplateTypeHelper::resolveTemplateTypes( $selfOutType, $parametersAcceptor->getResolvedTemplateTypeMap(), $parametersAcceptor instanceof ExtendedParametersAcceptor ? $parametersAcceptor->getCallSiteVarianceMap() : TemplateTypeVarianceMap::createEmpty(), TemplateTypeVariance::createCovariant(), ), - $scope->getNativeType($expr->var), + $scope->getNativeType($normalizedExpr->var), ); } } @@ -3105,12 +3193,14 @@ static function (): void { $impurePoints = array_merge($impurePoints, $result->getImpurePoints()); $isAlwaysTerminating = $isAlwaysTerminating || $result->isAlwaysTerminating(); } elseif ($expr instanceof Expr\NullsafeMethodCall) { + $beforeScope = $scope; $nonNullabilityResult = $this->ensureShallowNonNullability($scope, $scope, $expr->var); - $exprResult = $this->processExprNode($stmt, new MethodCall($expr->var, $expr->name, $expr->args, array_merge($expr->getAttributes(), ['virtualNullsafeMethodCall' => true])), $nonNullabilityResult->getScope(), $nodeCallback, $context); + $exprResult = $this->processExprNode($stmt, new MethodCall($expr->var, $expr->name, $expr->args, array_merge($expr->getAttributes(), ['virtualNullsafeMethodCall' => true])), $nonNullabilityResult->getScope(), $storage, $nodeCallback, $context); $scope = $this->revertNonNullability($exprResult->getScope(), $nonNullabilityResult->getSpecifiedExpressions()); - return new ExpressionResult( + $result = new ExpressionResult( $scope, + $beforeScope, $exprResult->hasYield(), false, $exprResult->getThrowPoints(), @@ -3118,38 +3208,28 @@ static function (): void { static fn (): MutatingScope => $scope->filterByTruthyValue($expr), static fn (): MutatingScope => $scope->filterByFalseyValue($expr), ); + $this->storeResult($storage, $expr, $result); + + return $result; } elseif ($expr instanceof StaticCall) { $hasYield = false; $throwPoints = []; $impurePoints = []; $isAlwaysTerminating = false; if ($expr->class instanceof Expr) { - $objectClasses = $scope->getType($expr->class)->getObjectClassNames(); - if (count($objectClasses) !== 1) { - $objectClasses = $scope->getType(new New_($expr->class))->getObjectClassNames(); - } - if (count($objectClasses) === 1) { - $objectExprResult = $this->processExprNode($stmt, new StaticCall(new Name($objectClasses[0]), $expr->name, []), $scope, static function (): void { - }, $context->enterDeep()); - $additionalThrowPoints = $objectExprResult->getThrowPoints(); - } else { - $additionalThrowPoints = [InternalThrowPoint::createImplicit($scope, $expr)]; - } - $classResult = $this->processExprNode($stmt, $expr->class, $scope, $nodeCallback, $context->enterDeep()); + $classResult = $this->processExprNode($stmt, $expr->class, $scope, $storage, $nodeCallback, $context->enterDeep()); $hasYield = $classResult->hasYield(); $throwPoints = array_merge($throwPoints, $classResult->getThrowPoints()); $impurePoints = array_merge($impurePoints, $classResult->getImpurePoints()); $isAlwaysTerminating = $classResult->isAlwaysTerminating(); - foreach ($additionalThrowPoints as $throwPoint) { - $throwPoints[] = $throwPoint; - } + $scope = $classResult->getScope(); } $parametersAcceptor = null; $methodReflection = null; if ($expr->name instanceof Expr) { - $result = $this->processExprNode($stmt, $expr->name, $scope, $nodeCallback, $context->enterDeep()); + $result = $this->processExprNode($stmt, $expr->name, $scope, $storage, $nodeCallback, $context->enterDeep()); $hasYield = $hasYield || $result->hasYield(); $throwPoints = array_merge($throwPoints, $result->getThrowPoints()); $impurePoints = array_merge($impurePoints, $result->getImpurePoints()); @@ -3218,6 +3298,22 @@ static function (): void { } } + if ($expr->class instanceof Expr) { + $objectClasses = $scope->getType($expr->class)->getObjectClassNames(); + if (count($objectClasses) !== 1) { + $objectClasses = $scope->getType(new New_($expr->class))->getObjectClassNames(); + } + if (count($objectClasses) === 1) { + $objectExprResult = $this->processExprNode($stmt, new StaticCall(new Name($objectClasses[0]), $expr->name, []), $scope, $storage, new NoopNodeCallback(), $context->enterDeep()); + $additionalThrowPoints = $objectExprResult->getThrowPoints(); + } else { + $additionalThrowPoints = [InternalThrowPoint::createImplicit($scope, $expr)]; + } + foreach ($additionalThrowPoints as $throwPoint) { + $throwPoints[] = $throwPoint; + } + } + if ($methodReflection !== null) { $impurePoint = SimpleImpurePoint::createFromVariant($methodReflection, $parametersAcceptor, $scope, $expr->getArgs()); if ($impurePoint !== null) { @@ -3233,12 +3329,13 @@ static function (): void { ); } + $normalizedExpr = $expr; if ($parametersAcceptor !== null) { - $expr = ArgumentsNormalizer::reorderStaticCallArguments($parametersAcceptor, $expr) ?? $expr; + $normalizedExpr = ArgumentsNormalizer::reorderStaticCallArguments($parametersAcceptor, $expr) ?? $expr; $returnType = $parametersAcceptor->getReturnType(); $isAlwaysTerminating = $returnType instanceof NeverType && $returnType->isExplicit(); } - $result = $this->processArgs($stmt, $methodReflection, null, $parametersAcceptor, $expr, $scope, $nodeCallback, $context, $closureBindScope ?? null); + $result = $this->processArgs($stmt, $methodReflection, null, $parametersAcceptor, $normalizedExpr, $scope, $storage, $nodeCallback, $context, $closureBindScope ?? null); $scope = $result->getScope(); $scopeFunction = $scope->getFunction(); @@ -3283,14 +3380,14 @@ static function (): void { $isAlwaysTerminating = $isAlwaysTerminating || $result->isAlwaysTerminating(); } elseif ($expr instanceof PropertyFetch) { $scopeBeforeVar = $scope; - $result = $this->processExprNode($stmt, $expr->var, $scope, $nodeCallback, $context->enterDeep()); + $result = $this->processExprNode($stmt, $expr->var, $scope, $storage, $nodeCallback, $context->enterDeep()); $hasYield = $result->hasYield(); $throwPoints = $result->getThrowPoints(); $impurePoints = $result->getImpurePoints(); $isAlwaysTerminating = $result->isAlwaysTerminating(); $scope = $result->getScope(); if ($expr->name instanceof Expr) { - $result = $this->processExprNode($stmt, $expr->name, $scope, $nodeCallback, $context->enterDeep()); + $result = $this->processExprNode($stmt, $expr->name, $scope, $storage, $nodeCallback, $context->enterDeep()); $hasYield = $hasYield || $result->hasYield(); $throwPoints = array_merge($throwPoints, $result->getThrowPoints()); $impurePoints = array_merge($impurePoints, $result->getImpurePoints()); @@ -3312,12 +3409,14 @@ static function (): void { } } } elseif ($expr instanceof Expr\NullsafePropertyFetch) { + $beforeScope = $scope; $nonNullabilityResult = $this->ensureShallowNonNullability($scope, $scope, $expr->var); - $exprResult = $this->processExprNode($stmt, new PropertyFetch($expr->var, $expr->name, array_merge($expr->getAttributes(), ['virtualNullsafePropertyFetch' => true])), $nonNullabilityResult->getScope(), $nodeCallback, $context); + $exprResult = $this->processExprNode($stmt, new PropertyFetch($expr->var, $expr->name, array_merge($expr->getAttributes(), ['virtualNullsafePropertyFetch' => true])), $nonNullabilityResult->getScope(), $storage, $nodeCallback, $context); $scope = $this->revertNonNullability($exprResult->getScope(), $nonNullabilityResult->getSpecifiedExpressions()); - return new ExpressionResult( + $result = new ExpressionResult( $scope, + $beforeScope, $exprResult->hasYield(), false, $exprResult->getThrowPoints(), @@ -3325,6 +3424,9 @@ static function (): void { static fn (): MutatingScope => $scope->filterByTruthyValue($expr), static fn (): MutatingScope => $scope->filterByFalseyValue($expr), ); + $this->storeResult($storage, $expr, $result); + + return $result; } elseif ($expr instanceof StaticPropertyFetch) { $hasYield = false; $throwPoints = []; @@ -3339,7 +3441,7 @@ static function (): void { ]; $isAlwaysTerminating = false; if ($expr->class instanceof Expr) { - $result = $this->processExprNode($stmt, $expr->class, $scope, $nodeCallback, $context->enterDeep()); + $result = $this->processExprNode($stmt, $expr->class, $scope, $storage, $nodeCallback, $context->enterDeep()); $hasYield = $result->hasYield(); $throwPoints = $result->getThrowPoints(); $impurePoints = $result->getImpurePoints(); @@ -3347,7 +3449,7 @@ static function (): void { $scope = $result->getScope(); } if ($expr->name instanceof Expr) { - $result = $this->processExprNode($stmt, $expr->name, $scope, $nodeCallback, $context->enterDeep()); + $result = $this->processExprNode($stmt, $expr->name, $scope, $storage, $nodeCallback, $context->enterDeep()); $hasYield = $hasYield || $result->hasYield(); $throwPoints = array_merge($throwPoints, $result->getThrowPoints()); $impurePoints = array_merge($impurePoints, $result->getImpurePoints()); @@ -3355,26 +3457,35 @@ static function (): void { $scope = $result->getScope(); } } elseif ($expr instanceof Expr\Closure) { - $processClosureResult = $this->processClosureNode($stmt, $expr, $scope, $nodeCallback, $context, null); + $beforeScope = $scope; + $processClosureResult = $this->processClosureNode($stmt, $expr, $scope, $storage, $nodeCallback, $context, null); + $scope = $processClosureResult->getScope(); - return new ExpressionResult( - $processClosureResult->getScope(), + $result = new ExpressionResult( + $scope, + $beforeScope, false, false, [], [], ); + $this->storeResult($storage, $expr, $result); + + return $result; } elseif ($expr instanceof Expr\ArrowFunction) { - $result = $this->processArrowFunctionNode($stmt, $expr, $scope, $nodeCallback, null); - return new ExpressionResult( + $result = $this->processArrowFunctionNode($stmt, $expr, $scope, $storage, $nodeCallback, null); + $exprResult = new ExpressionResult( $result->getScope(), + $scope, $result->hasYield(), false, [], [], ); + $this->storeResult($storage, $expr, $exprResult); + return $exprResult; } elseif ($expr instanceof ErrorSuppress) { - $result = $this->processExprNode($stmt, $expr->expr, $scope, $nodeCallback, $context); + $result = $this->processExprNode($stmt, $expr->expr, $scope, $storage, $nodeCallback, $context); $hasYield = $result->hasYield(); $throwPoints = $result->getThrowPoints(); $impurePoints = $result->getImpurePoints(); @@ -3390,7 +3501,7 @@ static function (): void { ]; $isAlwaysTerminating = true; if ($expr->expr !== null) { - $result = $this->processExprNode($stmt, $expr->expr, $scope, $nodeCallback, $context->enterDeep()); + $result = $this->processExprNode($stmt, $expr->expr, $scope, $storage, $nodeCallback, $context->enterDeep()); $hasYield = $result->hasYield(); $throwPoints = $result->getThrowPoints(); $impurePoints = array_merge($impurePoints, $result->getImpurePoints()); @@ -3405,7 +3516,7 @@ static function (): void { if (!$part instanceof Expr) { continue; } - $result = $this->processExprNode($stmt, $part, $scope, $nodeCallback, $context->enterDeep()); + $result = $this->processExprNode($stmt, $part, $scope, $storage, $nodeCallback, $context->enterDeep()); $hasYield = $hasYield || $result->hasYield(); $throwPoints = array_merge($throwPoints, $result->getThrowPoints()); $impurePoints = array_merge($impurePoints, $result->getImpurePoints()); @@ -3418,7 +3529,7 @@ static function (): void { $impurePoints = []; $isAlwaysTerminating = false; if ($expr->dim !== null) { - $result = $this->processExprNode($stmt, $expr->dim, $scope, $nodeCallback, $context->enterDeep()); + $result = $this->processExprNode($stmt, $expr->dim, $scope, $storage, $nodeCallback, $context->enterDeep()); $hasYield = $result->hasYield(); $throwPoints = $result->getThrowPoints(); $impurePoints = $result->getImpurePoints(); @@ -3426,7 +3537,7 @@ static function (): void { $scope = $result->getScope(); } - $result = $this->processExprNode($stmt, $expr->var, $scope, $nodeCallback, $context->enterDeep()); + $result = $this->processExprNode($stmt, $expr->var, $scope, $storage, $nodeCallback, $context->enterDeep()); $hasYield = $hasYield || $result->hasYield(); $throwPoints = array_merge($throwPoints, $result->getThrowPoints()); $impurePoints = array_merge($impurePoints, $result->getImpurePoints()); @@ -3440,9 +3551,9 @@ static function (): void { $isAlwaysTerminating = false; foreach ($expr->items as $arrayItem) { $itemNodes[] = new LiteralArrayItem($scope, $arrayItem); - $nodeCallback($arrayItem, $scope); + $this->callNodeCallback($nodeCallback, $arrayItem, $scope, $storage); if ($arrayItem->key !== null) { - $keyResult = $this->processExprNode($stmt, $arrayItem->key, $scope, $nodeCallback, $context->enterDeep()); + $keyResult = $this->processExprNode($stmt, $arrayItem->key, $scope, $storage, $nodeCallback, $context->enterDeep()); $hasYield = $hasYield || $keyResult->hasYield(); $throwPoints = array_merge($throwPoints, $keyResult->getThrowPoints()); $impurePoints = array_merge($impurePoints, $keyResult->getImpurePoints()); @@ -3450,17 +3561,17 @@ static function (): void { $scope = $keyResult->getScope(); } - $valueResult = $this->processExprNode($stmt, $arrayItem->value, $scope, $nodeCallback, $context->enterDeep()); + $valueResult = $this->processExprNode($stmt, $arrayItem->value, $scope, $storage, $nodeCallback, $context->enterDeep()); $hasYield = $hasYield || $valueResult->hasYield(); $throwPoints = array_merge($throwPoints, $valueResult->getThrowPoints()); $impurePoints = array_merge($impurePoints, $valueResult->getImpurePoints()); $isAlwaysTerminating = $isAlwaysTerminating || $valueResult->isAlwaysTerminating(); $scope = $valueResult->getScope(); } - $nodeCallback(new LiteralArrayNode($expr, $itemNodes), $scope); + $this->callNodeCallback($nodeCallback, new LiteralArrayNode($expr, $itemNodes), $scope, $storage); } elseif ($expr instanceof BooleanAnd || $expr instanceof BinaryOp\LogicalAnd) { - $leftResult = $this->processExprNode($stmt, $expr->left, $scope, $nodeCallback, $context->enterDeep()); - $rightResult = $this->processExprNode($stmt, $expr->right, $leftResult->getTruthyScope(), $nodeCallback, $context); + $leftResult = $this->processExprNode($stmt, $expr->left, $scope, $storage, $nodeCallback, $context->enterDeep()); + $rightResult = $this->processExprNode($stmt, $expr->right, $leftResult->getTruthyScope(), $storage, $nodeCallback, $context); $rightExprType = $rightResult->getScope()->getType($expr->right); if ($rightExprType instanceof NeverType && $rightExprType->isExplicit()) { $leftMergedWithRightScope = $leftResult->getFalseyScope(); @@ -3468,10 +3579,11 @@ static function (): void { $leftMergedWithRightScope = $leftResult->getScope()->mergeWith($rightResult->getScope()); } - $this->callNodeCallbackWithExpression($nodeCallback, new BooleanAndNode($expr, $leftResult->getTruthyScope()), $scope, $context); + $this->callNodeCallbackWithExpression($nodeCallback, new BooleanAndNode($expr, $leftResult->getTruthyScope()), $scope, $storage, $context); - return new ExpressionResult( + $result = new ExpressionResult( $leftMergedWithRightScope, + $scope, $leftResult->hasYield() || $rightResult->hasYield(), $leftResult->isAlwaysTerminating(), array_merge($leftResult->getThrowPoints(), $rightResult->getThrowPoints()), @@ -3479,9 +3591,11 @@ static function (): void { static fn (): MutatingScope => $rightResult->getScope()->filterByTruthyValue($expr), static fn (): MutatingScope => $leftMergedWithRightScope->filterByFalseyValue($expr), ); + $this->storeResult($storage, $expr, $result); + return $result; } elseif ($expr instanceof BooleanOr || $expr instanceof BinaryOp\LogicalOr) { - $leftResult = $this->processExprNode($stmt, $expr->left, $scope, $nodeCallback, $context->enterDeep()); - $rightResult = $this->processExprNode($stmt, $expr->right, $leftResult->getFalseyScope(), $nodeCallback, $context); + $leftResult = $this->processExprNode($stmt, $expr->left, $scope, $storage, $nodeCallback, $context->enterDeep()); + $rightResult = $this->processExprNode($stmt, $expr->right, $leftResult->getFalseyScope(), $storage, $nodeCallback, $context); $rightExprType = $rightResult->getScope()->getType($expr->right); if ($rightExprType instanceof NeverType && $rightExprType->isExplicit()) { $leftMergedWithRightScope = $leftResult->getTruthyScope(); @@ -3489,10 +3603,11 @@ static function (): void { $leftMergedWithRightScope = $leftResult->getScope()->mergeWith($rightResult->getScope()); } - $this->callNodeCallbackWithExpression($nodeCallback, new BooleanOrNode($expr, $leftResult->getFalseyScope()), $scope, $context); + $this->callNodeCallbackWithExpression($nodeCallback, new BooleanOrNode($expr, $leftResult->getFalseyScope()), $scope, $storage, $context); - return new ExpressionResult( + $result = new ExpressionResult( $leftMergedWithRightScope, + $scope, $leftResult->hasYield() || $rightResult->hasYield(), $leftResult->isAlwaysTerminating(), array_merge($leftResult->getThrowPoints(), $rightResult->getThrowPoints()), @@ -3500,15 +3615,17 @@ static function (): void { static fn (): MutatingScope => $leftMergedWithRightScope->filterByTruthyValue($expr), static fn (): MutatingScope => $rightResult->getScope()->filterByFalseyValue($expr), ); + $this->storeResult($storage, $expr, $result); + return $result; } elseif ($expr instanceof Coalesce) { $nonNullabilityResult = $this->ensureNonNullability($scope, $expr->left); $condScope = $this->lookForSetAllowedUndefinedExpressions($nonNullabilityResult->getScope(), $expr->left); - $condResult = $this->processExprNode($stmt, $expr->left, $condScope, $nodeCallback, $context->enterDeep()); + $condResult = $this->processExprNode($stmt, $expr->left, $condScope, $storage, $nodeCallback, $context->enterDeep()); $scope = $this->revertNonNullability($condResult->getScope(), $nonNullabilityResult->getSpecifiedExpressions()); $scope = $this->lookForUnsetAllowedUndefinedExpressions($scope, $expr->left); $rightScope = $scope->filterByFalseyValue($expr); - $rightResult = $this->processExprNode($stmt, $expr->right, $rightScope, $nodeCallback, $context->enterDeep()); + $rightResult = $this->processExprNode($stmt, $expr->right, $rightScope, $storage, $nodeCallback, $context->enterDeep()); $rightExprType = $scope->getType($expr->right); if ($rightExprType instanceof NeverType && $rightExprType->isExplicit()) { $scope = $scope->filterByTruthyValue(new Expr\Isset_([$expr->left])); @@ -3524,7 +3641,7 @@ static function (): void { if ($expr->right instanceof FuncCall && $expr->right->isFirstClassCallable()) { $exprResult = $this->processExprNode($stmt, new FuncCall($expr->right->name, [ new Arg($expr->left, attributes: $expr->getAttribute(ReversePipeTransformerVisitor::ARG_ATTRIBUTES_NAME, [])), - ], array_merge($expr->right->getAttributes(), ['virtualPipeOperatorCall' => true])), $scope, $nodeCallback, $context); + ], array_merge($expr->right->getAttributes(), ['virtualPipeOperatorCall' => true])), $scope, $storage, $nodeCallback, $context); $scope = $exprResult->getScope(); $hasYield = $exprResult->hasYield(); $throwPoints = $exprResult->getThrowPoints(); @@ -3533,7 +3650,7 @@ static function (): void { } elseif ($expr->right instanceof MethodCall && $expr->right->isFirstClassCallable()) { $exprResult = $this->processExprNode($stmt, new MethodCall($expr->right->var, $expr->right->name, [ new Arg($expr->left, attributes: $expr->getAttribute(ReversePipeTransformerVisitor::ARG_ATTRIBUTES_NAME, [])), - ], array_merge($expr->right->getAttributes(), ['virtualPipeOperatorCall' => true])), $scope, $nodeCallback, $context); + ], array_merge($expr->right->getAttributes(), ['virtualPipeOperatorCall' => true])), $scope, $storage, $nodeCallback, $context); $scope = $exprResult->getScope(); $hasYield = $exprResult->hasYield(); $throwPoints = $exprResult->getThrowPoints(); @@ -3542,7 +3659,7 @@ static function (): void { } elseif ($expr->right instanceof StaticCall && $expr->right->isFirstClassCallable()) { $exprResult = $this->processExprNode($stmt, new StaticCall($expr->right->class, $expr->right->name, [ new Arg($expr->left, attributes: $expr->getAttribute(ReversePipeTransformerVisitor::ARG_ATTRIBUTES_NAME, [])), - ], array_merge($expr->right->getAttributes(), ['virtualPipeOperatorCall' => true])), $scope, $nodeCallback, $context); + ], array_merge($expr->right->getAttributes(), ['virtualPipeOperatorCall' => true])), $scope, $storage, $nodeCallback, $context); $scope = $exprResult->getScope(); $hasYield = $exprResult->hasYield(); $throwPoints = $exprResult->getThrowPoints(); @@ -3551,7 +3668,7 @@ static function (): void { } else { $exprResult = $this->processExprNode($stmt, new FuncCall($expr->right, [ new Arg($expr->left, attributes: $expr->getAttribute(ReversePipeTransformerVisitor::ARG_ATTRIBUTES_NAME, [])), - ], array_merge($expr->right->getAttributes(), ['virtualPipeOperatorCall' => true])), $scope, $nodeCallback, $context); + ], array_merge($expr->right->getAttributes(), ['virtualPipeOperatorCall' => true])), $scope, $storage, $nodeCallback, $context); $scope = $exprResult->getScope(); $hasYield = $exprResult->hasYield(); $throwPoints = $exprResult->getThrowPoints(); @@ -3559,13 +3676,13 @@ static function (): void { $isAlwaysTerminating = $exprResult->isAlwaysTerminating(); } } elseif ($expr instanceof BinaryOp) { - $result = $this->processExprNode($stmt, $expr->left, $scope, $nodeCallback, $context->enterDeep()); + $result = $this->processExprNode($stmt, $expr->left, $scope, $storage, $nodeCallback, $context->enterDeep()); $scope = $result->getScope(); $hasYield = $result->hasYield(); $throwPoints = $result->getThrowPoints(); $impurePoints = $result->getImpurePoints(); $isAlwaysTerminating = $result->isAlwaysTerminating(); - $result = $this->processExprNode($stmt, $expr->right, $scope, $nodeCallback, $context->enterDeep()); + $result = $this->processExprNode($stmt, $expr->right, $scope, $storage, $nodeCallback, $context->enterDeep()); if ( ($expr instanceof BinaryOp\Div || $expr instanceof BinaryOp\Mod) && !$scope->getType($expr->right)->toNumber()->isSuperTypeOf(new ConstantIntegerType(0))->no() @@ -3578,7 +3695,7 @@ static function (): void { $impurePoints = array_merge($impurePoints, $result->getImpurePoints()); $isAlwaysTerminating = $isAlwaysTerminating || $result->isAlwaysTerminating(); } elseif ($expr instanceof Expr\Include_) { - $result = $this->processExprNode($stmt, $expr->expr, $scope, $nodeCallback, $context->enterDeep()); + $result = $this->processExprNode($stmt, $expr->expr, $scope, $storage, $nodeCallback, $context->enterDeep()); $throwPoints = $result->getThrowPoints(); $throwPoints[] = InternalThrowPoint::createImplicit($scope, $expr); $impurePoints = $result->getImpurePoints(); @@ -3593,7 +3710,7 @@ static function (): void { $isAlwaysTerminating = $result->isAlwaysTerminating(); $scope = $result->getScope()->afterExtractCall(); } elseif ($expr instanceof Expr\Print_) { - $result = $this->processExprNode($stmt, $expr->expr, $scope, $nodeCallback, $context->enterDeep()); + $result = $this->processExprNode($stmt, $expr->expr, $scope, $storage, $nodeCallback, $context->enterDeep()); $throwPoints = $result->getThrowPoints(); $impurePoints = $result->getImpurePoints(); $isAlwaysTerminating = $result->isAlwaysTerminating(); @@ -3602,7 +3719,7 @@ static function (): void { $scope = $result->getScope(); } elseif ($expr instanceof Cast\String_) { - $result = $this->processExprNode($stmt, $expr->expr, $scope, $nodeCallback, $context->enterDeep()); + $result = $this->processExprNode($stmt, $expr->expr, $scope, $storage, $nodeCallback, $context->enterDeep()); $throwPoints = $result->getThrowPoints(); $impurePoints = $result->getImpurePoints(); $isAlwaysTerminating = $result->isAlwaysTerminating(); @@ -3630,7 +3747,7 @@ static function (): void { || $expr instanceof Expr\UnaryMinus || $expr instanceof Expr\UnaryPlus ) { - $result = $this->processExprNode($stmt, $expr->expr, $scope, $nodeCallback, $context->enterDeep()); + $result = $this->processExprNode($stmt, $expr->expr, $scope, $storage, $nodeCallback, $context->enterDeep()); $throwPoints = $result->getThrowPoints(); $impurePoints = $result->getImpurePoints(); $isAlwaysTerminating = $result->isAlwaysTerminating(); @@ -3638,7 +3755,7 @@ static function (): void { $scope = $result->getScope(); } elseif ($expr instanceof Expr\Eval_) { - $result = $this->processExprNode($stmt, $expr->expr, $scope, $nodeCallback, $context->enterDeep()); + $result = $this->processExprNode($stmt, $expr->expr, $scope, $storage, $nodeCallback, $context->enterDeep()); $throwPoints = $result->getThrowPoints(); $throwPoints[] = InternalThrowPoint::createImplicit($scope, $expr); $impurePoints = $result->getImpurePoints(); @@ -3648,7 +3765,7 @@ static function (): void { $scope = $result->getScope(); } elseif ($expr instanceof Expr\YieldFrom) { - $result = $this->processExprNode($stmt, $expr->expr, $scope, $nodeCallback, $context->enterDeep()); + $result = $this->processExprNode($stmt, $expr->expr, $scope, $storage, $nodeCallback, $context->enterDeep()); $throwPoints = $result->getThrowPoints(); $throwPoints[] = InternalThrowPoint::createImplicit($scope, $expr); $impurePoints = $result->getImpurePoints(); @@ -3664,7 +3781,7 @@ static function (): void { $scope = $result->getScope(); } elseif ($expr instanceof BooleanNot) { - $result = $this->processExprNode($stmt, $expr->expr, $scope, $nodeCallback, $context->enterDeep()); + $result = $this->processExprNode($stmt, $expr->expr, $scope, $storage, $nodeCallback, $context->enterDeep()); $scope = $result->getScope(); $hasYield = $result->hasYield(); $throwPoints = $result->getThrowPoints(); @@ -3674,7 +3791,7 @@ static function (): void { $isAlwaysTerminating = false; if ($expr->class instanceof Expr) { - $result = $this->processExprNode($stmt, $expr->class, $scope, $nodeCallback, $context->enterDeep()); + $result = $this->processExprNode($stmt, $expr->class, $scope, $storage, $nodeCallback, $context->enterDeep()); $scope = $result->getScope(); $hasYield = $result->hasYield(); $throwPoints = $result->getThrowPoints(); @@ -3684,23 +3801,23 @@ static function (): void { $hasYield = false; $throwPoints = []; $impurePoints = []; - $nodeCallback($expr->class, $scope); + $this->callNodeCallback($nodeCallback, $expr->class, $scope, $storage); } if ($expr->name instanceof Expr) { - $result = $this->processExprNode($stmt, $expr->name, $scope, $nodeCallback, $context->enterDeep()); + $result = $this->processExprNode($stmt, $expr->name, $scope, $storage, $nodeCallback, $context->enterDeep()); $scope = $result->getScope(); $hasYield = $hasYield || $result->hasYield(); $throwPoints = array_merge($throwPoints, $result->getThrowPoints()); $impurePoints = array_merge($impurePoints, $result->getImpurePoints()); $isAlwaysTerminating = $isAlwaysTerminating || $result->isAlwaysTerminating(); } else { - $nodeCallback($expr->name, $scope); + $this->callNodeCallback($nodeCallback, $expr->name, $scope, $storage); } } elseif ($expr instanceof Expr\Empty_) { $nonNullabilityResult = $this->ensureNonNullability($scope, $expr->expr); $scope = $this->lookForSetAllowedUndefinedExpressions($nonNullabilityResult->getScope(), $expr->expr); - $result = $this->processExprNode($stmt, $expr->expr, $scope, $nodeCallback, $context->enterDeep()); + $result = $this->processExprNode($stmt, $expr->expr, $scope, $storage, $nodeCallback, $context->enterDeep()); $scope = $result->getScope(); $hasYield = $result->hasYield(); $throwPoints = $result->getThrowPoints(); @@ -3717,7 +3834,7 @@ static function (): void { foreach ($expr->vars as $var) { $nonNullabilityResult = $this->ensureNonNullability($scope, $var); $scope = $this->lookForSetAllowedUndefinedExpressions($nonNullabilityResult->getScope(), $var); - $result = $this->processExprNode($stmt, $var, $scope, $nodeCallback, $context->enterDeep()); + $result = $this->processExprNode($stmt, $var, $scope, $storage, $nodeCallback, $context->enterDeep()); $scope = $result->getScope(); $hasYield = $hasYield || $result->hasYield(); $throwPoints = array_merge($throwPoints, $result->getThrowPoints()); @@ -3732,14 +3849,14 @@ static function (): void { $scope = $this->revertNonNullability($scope, $nonNullabilityResult->getSpecifiedExpressions()); } } elseif ($expr instanceof Instanceof_) { - $result = $this->processExprNode($stmt, $expr->expr, $scope, $nodeCallback, $context->enterDeep()); + $result = $this->processExprNode($stmt, $expr->expr, $scope, $storage, $nodeCallback, $context->enterDeep()); $scope = $result->getScope(); $hasYield = $result->hasYield(); $throwPoints = $result->getThrowPoints(); $impurePoints = $result->getImpurePoints(); $isAlwaysTerminating = $result->isAlwaysTerminating(); if ($expr->class instanceof Expr) { - $result = $this->processExprNode($stmt, $expr->class, $scope, $nodeCallback, $context->enterDeep()); + $result = $this->processExprNode($stmt, $expr->class, $scope, $storage, $nodeCallback, $context->enterDeep()); $scope = $result->getScope(); $hasYield = $hasYield || $result->hasYield(); $throwPoints = array_merge($throwPoints, $result->getThrowPoints()); @@ -3748,7 +3865,7 @@ static function (): void { } } elseif ($expr instanceof List_) { // only in assign and foreach, processed elsewhere - return new ExpressionResult($scope, false, false, [], []); + return new ExpressionResult($scope, $scope, false, false, [], []); } elseif ($expr instanceof New_) { $parametersAcceptor = null; $constructorReflection = null; @@ -3757,19 +3874,19 @@ static function (): void { $impurePoints = []; $isAlwaysTerminating = false; $className = null; + $normalizedExpr = $expr; if ($expr->class instanceof Expr || $expr->class instanceof Name) { if ($expr->class instanceof Expr) { $objectClasses = $scope->getType($expr)->getObjectClassNames(); if (count($objectClasses) === 1) { - $objectExprResult = $this->processExprNode($stmt, new New_(new Name($objectClasses[0])), $scope, static function (): void { - }, $context->enterDeep()); + $objectExprResult = $this->processExprNode($stmt, new New_(new Name($objectClasses[0])), $scope, $storage, new NoopNodeCallback(), $context->enterDeep()); $className = $objectClasses[0]; $additionalThrowPoints = $objectExprResult->getThrowPoints(); } else { $additionalThrowPoints = [InternalThrowPoint::createImplicit($scope, $expr)]; } - $result = $this->processExprNode($stmt, $expr->class, $scope, $nodeCallback, $context->enterDeep()); + $result = $this->processExprNode($stmt, $expr->class, $scope, $storage, $nodeCallback, $context->enterDeep()); $scope = $result->getScope(); $hasYield = $result->hasYield(); $throwPoints = $result->getThrowPoints(); @@ -3824,7 +3941,7 @@ static function (): void { } if ($parametersAcceptor !== null) { - $expr = ArgumentsNormalizer::reorderNewArguments($parametersAcceptor, $expr) ?? $expr; + $normalizedExpr = ArgumentsNormalizer::reorderNewArguments($parametersAcceptor, $expr) ?? $expr; } } else { @@ -3840,7 +3957,7 @@ static function (): void { if ($constructorReflection->getDeclaringClass()->getName() === $classReflection->getName()) { $constructorResult = null; - $this->processStmtNode($expr->class, $scope, static function (Node $node, Scope $scope) use ($nodeCallback, $classReflection, &$constructorResult): void { + $this->processStmtNode($expr->class, $scope, $storage, static function (Node $node, Scope $scope) use ($nodeCallback, $classReflection, &$constructorResult): void { $nodeCallback($node, $scope); if (!$node instanceof MethodReturnStatementsNode) { return; @@ -3865,7 +3982,7 @@ static function (): void { $impurePoints = $constructorResult->getImpurePoints(); } } else { - $this->processStmtNode($expr->class, $scope, $nodeCallback, StatementContext::createTopLevel()); + $this->processStmtNode($expr->class, $scope, $storage, $nodeCallback, StatementContext::createTopLevel()); $declaringClass = $constructorReflection->getDeclaringClass(); $constructorThrowPoint = $this->getConstructorThrowPoint($constructorReflection, $parametersAcceptor, $classReflection, $expr, new Name\FullyQualified($declaringClass->getName()), $expr->getArgs(), $scope); if ($constructorThrowPoint !== null) { @@ -3884,11 +4001,11 @@ static function (): void { } } } else { - $this->processStmtNode($expr->class, $scope, $nodeCallback, StatementContext::createTopLevel()); + $this->processStmtNode($expr->class, $scope, $storage, $nodeCallback, StatementContext::createTopLevel()); } } - $result = $this->processArgs($stmt, $constructorReflection, null, $parametersAcceptor, $expr, $scope, $nodeCallback, $context); + $result = $this->processArgs($stmt, $constructorReflection, null, $parametersAcceptor, $normalizedExpr, $scope, $storage, $nodeCallback, $context); $scope = $result->getScope(); $hasYield = $hasYield || $result->hasYield(); $throwPoints = array_merge($throwPoints, $result->getThrowPoints()); @@ -3900,7 +4017,7 @@ static function (): void { || $expr instanceof Expr\PreDec || $expr instanceof Expr\PostDec ) { - $result = $this->processExprNode($stmt, $expr->var, $scope, $nodeCallback, $context->enterDeep()); + $result = $this->processExprNode($stmt, $expr->var, $scope, $storage, $nodeCallback, $context->enterDeep()); $scope = $result->getScope(); $hasYield = $result->hasYield(); $throwPoints = $result->getThrowPoints(); @@ -3916,13 +4033,14 @@ static function (): void { $scope = $this->processVirtualAssign( $scope, + $storage, $stmt, $expr->var, $newExpr, $nodeCallback, )->getScope(); } elseif ($expr instanceof Ternary) { - $ternaryCondResult = $this->processExprNode($stmt, $expr->cond, $scope, $nodeCallback, $context->enterDeep()); + $ternaryCondResult = $this->processExprNode($stmt, $expr->cond, $scope, $storage, $nodeCallback, $context->enterDeep()); $throwPoints = $ternaryCondResult->getThrowPoints(); $impurePoints = $ternaryCondResult->getImpurePoints(); $isAlwaysTerminating = $ternaryCondResult->isAlwaysTerminating(); @@ -3930,14 +4048,14 @@ static function (): void { $ifFalseScope = $ternaryCondResult->getFalseyScope(); $ifTrueType = null; if ($expr->if !== null) { - $ifResult = $this->processExprNode($stmt, $expr->if, $ifTrueScope, $nodeCallback, $context); + $ifResult = $this->processExprNode($stmt, $expr->if, $ifTrueScope, $storage, $nodeCallback, $context); $throwPoints = array_merge($throwPoints, $ifResult->getThrowPoints()); $impurePoints = array_merge($impurePoints, $ifResult->getImpurePoints()); $ifTrueScope = $ifResult->getScope(); $ifTrueType = $ifTrueScope->getType($expr->if); } - $elseResult = $this->processExprNode($stmt, $expr->else, $ifFalseScope, $nodeCallback, $context); + $elseResult = $this->processExprNode($stmt, $expr->else, $ifFalseScope, $storage, $nodeCallback, $context); $throwPoints = array_merge($throwPoints, $elseResult->getThrowPoints()); $impurePoints = array_merge($impurePoints, $elseResult->getImpurePoints()); $ifFalseScope = $elseResult->getScope(); @@ -3961,8 +4079,9 @@ static function (): void { } } - return new ExpressionResult( + $result = new ExpressionResult( $finalScope, + $scope, $ternaryCondResult->hasYield(), $isAlwaysTerminating, $throwPoints, @@ -3970,6 +4089,8 @@ static function (): void { static fn (): MutatingScope => $finalScope->filterByTruthyValue($expr), static fn (): MutatingScope => $finalScope->filterByFalseyValue($expr), ); + $this->storeResult($storage, $expr, $result); + return $result; } elseif ($expr instanceof Expr\Yield_) { $throwPoints = [ @@ -3986,14 +4107,14 @@ static function (): void { ]; $isAlwaysTerminating = false; if ($expr->key !== null) { - $keyResult = $this->processExprNode($stmt, $expr->key, $scope, $nodeCallback, $context->enterDeep()); + $keyResult = $this->processExprNode($stmt, $expr->key, $scope, $storage, $nodeCallback, $context->enterDeep()); $scope = $keyResult->getScope(); $throwPoints = $keyResult->getThrowPoints(); $impurePoints = array_merge($impurePoints, $keyResult->getImpurePoints()); $isAlwaysTerminating = $keyResult->isAlwaysTerminating(); } if ($expr->value !== null) { - $valueResult = $this->processExprNode($stmt, $expr->value, $scope, $nodeCallback, $context->enterDeep()); + $valueResult = $this->processExprNode($stmt, $expr->value, $scope, $storage, $nodeCallback, $context->enterDeep()); $scope = $valueResult->getScope(); $throwPoints = array_merge($throwPoints, $valueResult->getThrowPoints()); $impurePoints = array_merge($impurePoints, $valueResult->getImpurePoints()); @@ -4003,7 +4124,7 @@ static function (): void { } elseif ($expr instanceof Expr\Match_) { $deepContext = $context->enterDeep(); $condType = $scope->getType($expr->cond); - $condResult = $this->processExprNode($stmt, $expr->cond, $scope, $nodeCallback, $deepContext); + $condResult = $this->processExprNode($stmt, $expr->cond, $scope, $storage, $nodeCallback, $deepContext); $scope = $condResult->getScope(); $hasYield = $condResult->hasYield(); $throwPoints = $condResult->getThrowPoints(); @@ -4014,6 +4135,7 @@ static function (): void { $hasDefaultCond = false; $hasAlwaysTrueCond = false; $arms = $expr->arms; + $armCondsToSkip = []; if ($condType->isEnum()->yes()) { // enum match analysis would work even without this if branch // but would be much slower @@ -4034,7 +4156,7 @@ static function (): void { $condNodes = []; $conditionCases = []; $conditionExprs = []; - foreach ($arm->conds as $cond) { + foreach ($arm->conds as $j => $cond) { if (!$cond instanceof Expr\ClassConstFetch) { continue 2; } @@ -4084,7 +4206,7 @@ static function (): void { } } - $this->processExprNode($stmt, $cond, $armConditionScope, $nodeCallback, $deepContext); + $this->processExprNode($stmt, $cond, $armConditionScope, $storage, $nodeCallback, $deepContext); $condNodes[] = new MatchExpressionArmCondition( $cond, @@ -4094,6 +4216,7 @@ static function (): void { $conditionExprs[] = $cond; unset($unusedIndexedEnumCases[$loweredFetchedClassName][$caseName]); + $armCondsToSkip[$i][$j] = true; } $conditionCasesCount = count($conditionCases); @@ -4117,6 +4240,7 @@ static function (): void { $stmt, $arm->body, $matchArmBodyScope, + $storage, $nodeCallback, ExpressionContext::createTopLevel(), ); @@ -4153,7 +4277,7 @@ static function (): void { $hasDefaultCond = true; $matchArmBody = new MatchExpressionArmBody($matchScope, $arm->body); $armNodes[$i] = new MatchExpressionArm($matchArmBody, [], $arm->getStartLine()); - $armResult = $this->processExprNode($stmt, $arm->body, $matchScope, $nodeCallback, ExpressionContext::createTopLevel()); + $armResult = $this->processExprNode($stmt, $arm->body, $matchScope, $storage, $nodeCallback, ExpressionContext::createTopLevel()); $matchScope = $armResult->getScope(); $hasYield = $hasYield || $armResult->hasYield(); $throwPoints = array_merge($throwPoints, $armResult->getThrowPoints()); @@ -4169,9 +4293,12 @@ static function (): void { $filteringExprs = []; $armCondScope = $matchScope; $condNodes = []; - foreach ($arm->conds as $armCond) { + foreach ($arm->conds as $j => $armCond) { + if (isset($armCondsToSkip[$i][$j])) { + continue; + } $condNodes[] = new MatchExpressionArmCondition($armCond, $armCondScope, $armCond->getStartLine()); - $armCondResult = $this->processExprNode($stmt, $armCond, $armCondScope, $nodeCallback, $deepContext); + $armCondResult = $this->processExprNode($stmt, $armCond, $armCondScope, $storage, $nodeCallback, $deepContext); $hasYield = $hasYield || $armCondResult->hasYield(); $throwPoints = array_merge($throwPoints, $armCondResult->getThrowPoints()); $impurePoints = array_merge($impurePoints, $armCondResult->getImpurePoints()); @@ -4186,9 +4313,7 @@ static function (): void { } $filteringExpr = $this->getFilteringExprForMatchArm($expr, $filteringExprs); - - $bodyScope = $this->processExprNode($stmt, $filteringExpr, $matchScope, static function (): void { - }, $deepContext)->getTruthyScope(); + $bodyScope = $matchScope->filterByTruthyValue($filteringExpr); $matchArmBody = new MatchExpressionArmBody($bodyScope, $arm->body); $armNodes[$i] = new MatchExpressionArm($matchArmBody, $condNodes, $arm->getStartLine()); @@ -4196,6 +4321,7 @@ static function (): void { $stmt, $arm->body, $bodyScope, + $storage, $nodeCallback, ExpressionContext::createTopLevel(), ); @@ -4214,9 +4340,9 @@ static function (): void { ksort($armNodes, SORT_NUMERIC); - $nodeCallback(new MatchExpressionNode($expr->cond, array_values($armNodes), $expr, $matchScope), $scope); + $this->callNodeCallback($nodeCallback, new MatchExpressionNode($expr->cond, array_values($armNodes), $expr, $matchScope), $scope, $storage); } elseif ($expr instanceof AlwaysRememberedExpr) { - $result = $this->processExprNode($stmt, $expr->getExpr(), $scope, $nodeCallback, $context); + $result = $this->processExprNode($stmt, $expr->getExpr(), $scope, $storage, $nodeCallback, $context); $hasYield = $result->hasYield(); $throwPoints = $result->getThrowPoints(); $impurePoints = $result->getImpurePoints(); @@ -4224,7 +4350,7 @@ static function (): void { $scope = $result->getScope(); } elseif ($expr instanceof Expr\Throw_) { $hasYield = false; - $result = $this->processExprNode($stmt, $expr->expr, $scope, $nodeCallback, ExpressionContext::createDeep()); + $result = $this->processExprNode($stmt, $expr->expr, $scope, $storage, $nodeCallback, ExpressionContext::createDeep()); $throwPoints = $result->getThrowPoints(); $impurePoints = $result->getImpurePoints(); $isAlwaysTerminating = $result->isAlwaysTerminating(); @@ -4235,7 +4361,7 @@ static function (): void { $hasYield = false; $isAlwaysTerminating = false; if ($expr->getName() instanceof Expr) { - $result = $this->processExprNode($stmt, $expr->getName(), $scope, $nodeCallback, ExpressionContext::createDeep()); + $result = $this->processExprNode($stmt, $expr->getName(), $scope, $storage, $nodeCallback, ExpressionContext::createDeep()); $scope = $result->getScope(); $hasYield = $result->hasYield(); $throwPoints = $result->getThrowPoints(); @@ -4243,14 +4369,14 @@ static function (): void { $isAlwaysTerminating = $result->isAlwaysTerminating(); } } elseif ($expr instanceof MethodCallableNode) { - $result = $this->processExprNode($stmt, $expr->getVar(), $scope, $nodeCallback, ExpressionContext::createDeep()); + $result = $this->processExprNode($stmt, $expr->getVar(), $scope, $storage, $nodeCallback, ExpressionContext::createDeep()); $scope = $result->getScope(); $hasYield = $result->hasYield(); $throwPoints = $result->getThrowPoints(); $impurePoints = $result->getImpurePoints(); $isAlwaysTerminating = false; if ($expr->getName() instanceof Expr) { - $nameResult = $this->processExprNode($stmt, $expr->getVar(), $scope, $nodeCallback, ExpressionContext::createDeep()); + $nameResult = $this->processExprNode($stmt, $expr->getName(), $scope, $storage, $nodeCallback, ExpressionContext::createDeep()); $scope = $nameResult->getScope(); $hasYield = $hasYield || $nameResult->hasYield(); $throwPoints = array_merge($throwPoints, $nameResult->getThrowPoints()); @@ -4263,7 +4389,7 @@ static function (): void { $hasYield = false; $isAlwaysTerminating = false; if ($expr->getClass() instanceof Expr) { - $classResult = $this->processExprNode($stmt, $expr->getClass(), $scope, $nodeCallback, ExpressionContext::createDeep()); + $classResult = $this->processExprNode($stmt, $expr->getClass(), $scope, $storage, $nodeCallback, ExpressionContext::createDeep()); $scope = $classResult->getScope(); $hasYield = $classResult->hasYield(); $throwPoints = $classResult->getThrowPoints(); @@ -4271,7 +4397,7 @@ static function (): void { $isAlwaysTerminating = $classResult->isAlwaysTerminating(); } if ($expr->getName() instanceof Expr) { - $nameResult = $this->processExprNode($stmt, $expr->getName(), $scope, $nodeCallback, ExpressionContext::createDeep()); + $nameResult = $this->processExprNode($stmt, $expr->getName(), $scope, $storage, $nodeCallback, ExpressionContext::createDeep()); $scope = $nameResult->getScope(); $hasYield = $hasYield || $nameResult->hasYield(); $throwPoints = array_merge($throwPoints, $nameResult->getThrowPoints()); @@ -4284,7 +4410,7 @@ static function (): void { $hasYield = false; $isAlwaysTerminating = false; if ($expr->getClass() instanceof Expr) { - $classResult = $this->processExprNode($stmt, $expr->getClass(), $scope, $nodeCallback, ExpressionContext::createDeep()); + $classResult = $this->processExprNode($stmt, $expr->getClass(), $scope, $storage, $nodeCallback, ExpressionContext::createDeep()); $scope = $classResult->getScope(); $hasYield = $classResult->hasYield(); $throwPoints = $classResult->getThrowPoints(); @@ -4301,7 +4427,7 @@ static function (): void { $throwPoints = []; $impurePoints = []; $isAlwaysTerminating = false; - $nodeCallback($expr->name, $scope); + $this->callNodeCallback($nodeCallback, $expr->name, $scope, $storage); } else { $hasYield = false; $throwPoints = []; @@ -4309,8 +4435,9 @@ static function (): void { $isAlwaysTerminating = false; } - return new ExpressionResult( + $result = new ExpressionResult( $scope, + $originalScope, $hasYield, $isAlwaysTerminating, $throwPoints, @@ -4318,6 +4445,8 @@ static function (): void { static fn (): MutatingScope => $scope->filterByTruthyValue($expr), static fn (): MutatingScope => $scope->filterByFalseyValue($expr), ); + $this->storeResult($storage, $expr, $result); + return $result; } private function getArrayFunctionAppendingType(FunctionReflection $functionReflection, Scope $scope, FuncCall $expr): Type @@ -4777,13 +4906,24 @@ private function callNodeCallbackWithExpression( callable $nodeCallback, Expr $expr, MutatingScope $scope, + ExpressionResultStorage $storage, ExpressionContext $context, ): void { if ($context->isDeep()) { $scope = $scope->exitFirstLevelStatements(); } - $nodeCallback($expr, $scope); + $this->callNodeCallback($nodeCallback, $expr, $scope, $storage); + } + + protected function callNodeCallback( + callable $nodeCallback, + Node $node, + MutatingScope $scope, + ExpressionResultStorage $storage, + ): void + { + $nodeCallback($node, $scope); } /** @@ -4793,13 +4933,14 @@ private function processClosureNode( Node\Stmt $stmt, Expr\Closure $expr, MutatingScope $scope, + ExpressionResultStorage $storage, callable $nodeCallback, ExpressionContext $context, ?Type $passedToType, ): ProcessClosureResult { foreach ($expr->params as $param) { - $this->processParamNode($stmt, $param, $scope, $nodeCallback); + $this->processParamNode($stmt, $param, $scope, $storage, $nodeCallback); } $byRefUses = []; @@ -4849,7 +4990,7 @@ private function processClosureNode( $scope = $scope->assignVariable($inAssignRightSideVariableName, $variableType, $variableNativeType, TrinaryLogic::createYes()); } } - $this->processExprNode($stmt, $use->var, $useScope, $nodeCallback, $context); + $this->processExprNode($stmt, $use->var, $useScope, $storage, $nodeCallback, $context); if (!$use->byRef) { continue; } @@ -4858,7 +4999,7 @@ private function processClosureNode( } if ($expr->returnType !== null) { - $nodeCallback($expr->returnType, $scope); + $this->callNodeCallback($nodeCallback, $expr->returnType, $scope, $storage); } $closureScope = $scope->enterAnonymousFunction($expr, $callableParameters); @@ -4871,7 +5012,7 @@ private function processClosureNode( $returnType = $closureType->getReturnType(); $isAlwaysTerminating = ($returnType instanceof NeverType && $returnType->isExplicit()); - $nodeCallback(new InClosureNode($closureType, $expr), $closureScope); + $this->callNodeCallback($nodeCallback, new InClosureNode($closureType, $expr), $closureScope, $storage); $executionEnds = []; $gatheredReturnStatements = []; @@ -4912,27 +5053,29 @@ private function processClosureNode( }; if (count($byRefUses) === 0) { - $statementResult = $this->processStmtNodesInternal($expr, $expr->stmts, $closureScope, $closureStmtsCallback, StatementContext::createTopLevel()); + $statementResult = $this->processStmtNodesInternal($expr, $expr->stmts, $closureScope, $storage, $closureStmtsCallback, StatementContext::createTopLevel()); $publicStatementResult = $statementResult->toPublic(); - $nodeCallback(new ClosureReturnStatementsNode( + $this->callNodeCallback($nodeCallback, new ClosureReturnStatementsNode( $expr, $gatheredReturnStatements, $gatheredYieldStatements, $publicStatementResult, $executionEnds, array_merge($publicStatementResult->getImpurePoints(), $closureImpurePoints), - ), $closureScope); + ), $closureScope, $storage); return new ProcessClosureResult($scope, $statementResult->getThrowPoints(), $statementResult->getImpurePoints(), $invalidateExpressions, $isAlwaysTerminating); } + $originalStorage = $storage; + $count = 0; $closureResultScope = null; do { $prevScope = $closureScope; - $intermediaryClosureScopeResult = $this->processStmtNodesInternal($expr, $expr->stmts, $closureScope, static function (): void { - }, StatementContext::createTopLevel()); + $storage = $originalStorage->duplicate(); + $intermediaryClosureScopeResult = $this->processStmtNodesInternal($expr, $expr->stmts, $closureScope, $storage, new NoopNodeCallback(), StatementContext::createTopLevel()); $intermediaryClosureScope = $intermediaryClosureScopeResult->getScope(); foreach ($intermediaryClosureScopeResult->getExitPoints() as $exitPoint) { $intermediaryClosureScope = $intermediaryClosureScope->mergeWith($exitPoint->getScope()); @@ -4959,16 +5102,17 @@ private function processClosureNode( $closureResultScope = $closureScope; } - $statementResult = $this->processStmtNodesInternal($expr, $expr->stmts, $closureScope, $closureStmtsCallback, StatementContext::createTopLevel()); + $storage = $originalStorage; + $statementResult = $this->processStmtNodesInternal($expr, $expr->stmts, $closureScope, $storage, $closureStmtsCallback, StatementContext::createTopLevel()); $publicStatementResult = $statementResult->toPublic(); - $nodeCallback(new ClosureReturnStatementsNode( + $this->callNodeCallback($nodeCallback, new ClosureReturnStatementsNode( $expr, $gatheredReturnStatements, $gatheredYieldStatements, $publicStatementResult, $executionEnds, array_merge($publicStatementResult->getImpurePoints(), $closureImpurePoints), - ), $closureScope); + ), $closureScope, $storage); return new ProcessClosureResult($scope->processClosureScope($closureResultScope, null, $byRefUses), $statementResult->getThrowPoints(), $statementResult->getImpurePoints(), $invalidateExpressions, $isAlwaysTerminating); } @@ -5013,15 +5157,16 @@ private function processArrowFunctionNode( Node\Stmt $stmt, Expr\ArrowFunction $expr, MutatingScope $scope, + ExpressionResultStorage $storage, callable $nodeCallback, ?Type $passedToType, ): ExpressionResult { foreach ($expr->params as $param) { - $this->processParamNode($stmt, $param, $scope, $nodeCallback); + $this->processParamNode($stmt, $param, $scope, $storage, $nodeCallback); } if ($expr->returnType !== null) { - $nodeCallback($expr->returnType, $scope); + $this->callNodeCallback($nodeCallback, $expr->returnType, $scope, $storage); } $arrowFunctionCallArgs = $expr->getAttribute(ArrowFunctionArgVisitor::ATTRIBUTE_NAME); @@ -5035,10 +5180,10 @@ private function processArrowFunctionNode( if ($arrowFunctionType === null) { throw new ShouldNotHappenException(); } - $nodeCallback(new InArrowFunctionNode($arrowFunctionType, $expr), $arrowFunctionScope); - $exprResult = $this->processExprNode($stmt, $expr->expr, $arrowFunctionScope, $nodeCallback, ExpressionContext::createTopLevel()); + $this->callNodeCallback($nodeCallback, new InArrowFunctionNode($arrowFunctionType, $expr), $arrowFunctionScope, $storage); + $exprResult = $this->processExprNode($stmt, $expr->expr, $arrowFunctionScope, $storage, $nodeCallback, ExpressionContext::createTopLevel()); - return new ExpressionResult($scope, false, $exprResult->isAlwaysTerminating(), $exprResult->getThrowPoints(), $exprResult->getImpurePoints()); + return new ExpressionResult($scope, $scope, false, $exprResult->isAlwaysTerminating(), $exprResult->getThrowPoints(), $exprResult->getImpurePoints()); } /** @@ -5131,19 +5276,20 @@ private function processParamNode( Node\Stmt $stmt, Node\Param $param, MutatingScope $scope, + ExpressionResultStorage $storage, callable $nodeCallback, ): void { - $this->processAttributeGroups($stmt, $param->attrGroups, $scope, $nodeCallback); - $nodeCallback($param, $scope); + $this->processAttributeGroups($stmt, $param->attrGroups, $scope, $storage, $nodeCallback); + $this->callNodeCallback($nodeCallback, $param, $scope, $storage); if ($param->type !== null) { - $nodeCallback($param->type, $scope); + $this->callNodeCallback($nodeCallback, $param->type, $scope, $storage); } if ($param->default === null) { return; } - $this->processExprNode($stmt, $param->default, $scope, $nodeCallback, ExpressionContext::createDeep()); + $this->processExprNode($stmt, $param->default, $scope, $storage, $nodeCallback, ExpressionContext::createDeep()); } /** @@ -5154,6 +5300,7 @@ private function processAttributeGroups( Node\Stmt $stmt, array $attrGroups, MutatingScope $scope, + ExpressionResultStorage $storage, callable $nodeCallback, ): void { @@ -5172,19 +5319,19 @@ private function processAttributeGroups( ); $expr = new New_($attr->name, $attr->args); $expr = ArgumentsNormalizer::reorderNewArguments($parametersAcceptor, $expr) ?? $expr; - $this->processArgs($stmt, $constructorReflection, null, $parametersAcceptor, $expr, $scope, $nodeCallback, ExpressionContext::createDeep()); - $nodeCallback($attr, $scope); + $this->processArgs($stmt, $constructorReflection, null, $parametersAcceptor, $expr, $scope, $storage, $nodeCallback, ExpressionContext::createDeep()); + $this->callNodeCallback($nodeCallback, $attr, $scope, $storage); continue; } } foreach ($attr->args as $arg) { - $this->processExprNode($stmt, $arg->value, $scope, $nodeCallback, ExpressionContext::createDeep()); - $nodeCallback($arg, $scope); + $this->processExprNode($stmt, $arg->value, $scope, $storage, $nodeCallback, ExpressionContext::createDeep()); + $this->callNodeCallback($nodeCallback, $arg, $scope, $storage); } - $nodeCallback($attr, $scope); + $this->callNodeCallback($nodeCallback, $attr, $scope, $storage); } - $nodeCallback($attrGroup, $scope); + $this->callNodeCallback($nodeCallback, $attrGroup, $scope, $storage); } } @@ -5199,6 +5346,7 @@ private function processPropertyHooks( string $propertyName, array $hooks, MutatingScope $scope, + ExpressionResultStorage $storage, callable $nodeCallback, ): void { @@ -5209,13 +5357,13 @@ private function processPropertyHooks( $classReflection = $scope->getClassReflection(); foreach ($hooks as $hook) { - $nodeCallback($hook, $scope); - $this->processAttributeGroups($stmt, $hook->attrGroups, $scope, $nodeCallback); + $this->callNodeCallback($nodeCallback, $hook, $scope, $storage); + $this->processAttributeGroups($stmt, $hook->attrGroups, $scope, $storage, $nodeCallback); [, $phpDocParameterTypes,,,, $phpDocThrowType,,,,,,,, $phpDocComment] = $this->getPhpDocs($scope, $hook); foreach ($hook->params as $param) { - $this->processParamNode($stmt, $param, $scope, $nodeCallback); + $this->processParamNode($stmt, $param, $scope, $storage, $nodeCallback); } [$isDeprecated, $deprecatedDescription] = $this->getDeprecatedAttribute($scope, $hook); @@ -5242,12 +5390,12 @@ private function processPropertyHooks( $propertyReflection = $classReflection->getNativeProperty($propertyName); - $nodeCallback(new InPropertyHookNode( + $this->callNodeCallback($nodeCallback, new InPropertyHookNode( $classReflection, $hookReflection, $propertyReflection, $hook, - ), $hookScope); + ), $hookScope, $storage); $stmts = $hook->getStmts(); if ($stmts === null) { @@ -5265,7 +5413,7 @@ private function processPropertyHooks( $gatheredReturnStatements = []; $executionEnds = []; $methodImpurePoints = []; - $statementResult = $this->processStmtNodesInternal(new PropertyHookStatementNode($hook), $stmts, $hookScope, static function (Node $node, Scope $scope) use ($nodeCallback, $hookScope, &$gatheredReturnStatements, &$executionEnds, &$hookImpurePoints): void { + $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); if ($scope->getFunction() !== $hookScope->getFunction()) { return; @@ -5294,7 +5442,7 @@ private function processPropertyHooks( $gatheredReturnStatements[] = new ReturnStatement($scope, $node); }, StatementContext::createTopLevel())->toPublic(); - $nodeCallback(new PropertyHookReturnStatementsNode( + $this->callNodeCallback($nodeCallback, new PropertyHookReturnStatementsNode( $hook, $gatheredReturnStatements, $statementResult, @@ -5303,7 +5451,7 @@ private function processPropertyHooks( $classReflection, $hookReflection, $propertyReflection, - ), $hookScope); + ), $hookScope, $storage); } } @@ -5367,6 +5515,7 @@ private function processArgs( ?ParametersAcceptor $parametersAcceptor, CallLike $callLike, MutatingScope $scope, + ExpressionResultStorage $storage, callable $nodeCallback, ExpressionContext $context, ?MutatingScope $closureBindScope = null, @@ -5430,7 +5579,7 @@ private function processArgs( } $originalArg = $arg->getAttribute(ArgumentsNormalizer::ORIGINAL_ARG_ATTRIBUTE) ?? $arg; - $nodeCallback($originalArg, $scope); + $this->callNodeCallback($nodeCallback, $originalArg, $scope, $storage); $originalScope = $scope; $scopeToPass = $scope; @@ -5476,14 +5625,16 @@ private function processArgs( } } - $this->callNodeCallbackWithExpression($nodeCallback, $arg->value, $scopeToPass, $context); - $closureResult = $this->processClosureNode($stmt, $arg->value, $scopeToPass, $nodeCallback, $context, $parameterType ?? null); + $this->callNodeCallbackWithExpression($nodeCallback, $arg->value, $scopeToPass, $storage, $context); + $closureResult = $this->processClosureNode($stmt, $arg->value, $scopeToPass, $storage, $nodeCallback, $context, $parameterType ?? null); if ($callCallbackImmediately) { $throwPoints = array_merge($throwPoints, array_map(static fn (InternalThrowPoint $throwPoint) => $throwPoint->isExplicit() ? InternalThrowPoint::createExplicit($scope, $throwPoint->getType(), $arg->value, $throwPoint->canContainAnyThrowable()) : InternalThrowPoint::createImplicit($scope, $arg->value), $closureResult->getThrowPoints())); $impurePoints = array_merge($impurePoints, $closureResult->getImpurePoints()); $isAlwaysTerminating = $isAlwaysTerminating || $closureResult->isAlwaysTerminating(); } + $this->storeResult($storage, $arg->value, new ExpressionResult($closureResult->getScope(), $scopeToPass, false, $isAlwaysTerminating, $throwPoints, $impurePoints)); + $uses = []; foreach ($arg->value->uses as $use) { if (!is_string($use->var->name)) { @@ -5531,16 +5682,17 @@ private function processArgs( } } - $this->callNodeCallbackWithExpression($nodeCallback, $arg->value, $scopeToPass, $context); - $arrowFunctionResult = $this->processArrowFunctionNode($stmt, $arg->value, $scopeToPass, $nodeCallback, $parameterType ?? null); + $this->callNodeCallbackWithExpression($nodeCallback, $arg->value, $scopeToPass, $storage, $context); + $arrowFunctionResult = $this->processArrowFunctionNode($stmt, $arg->value, $scopeToPass, $storage, $nodeCallback, $parameterType ?? null); if ($callCallbackImmediately) { $throwPoints = array_merge($throwPoints, array_map(static fn (InternalThrowPoint $throwPoint) => $throwPoint->isExplicit() ? InternalThrowPoint::createExplicit($scope, $throwPoint->getType(), $arg->value, $throwPoint->canContainAnyThrowable()) : InternalThrowPoint::createImplicit($scope, $arg->value), $arrowFunctionResult->getThrowPoints())); $impurePoints = array_merge($impurePoints, $arrowFunctionResult->getImpurePoints()); $isAlwaysTerminating = $isAlwaysTerminating || $arrowFunctionResult->isAlwaysTerminating(); } + $this->storeResult($storage, $arg->value, new ExpressionResult($arrowFunctionResult->getScope(), $scopeToPass, false, $isAlwaysTerminating, $throwPoints, $impurePoints)); } else { $exprType = $scope->getType($arg->value); - $exprResult = $this->processExprNode($stmt, $arg->value, $scopeToPass, $nodeCallback, $context->enterDeep()); + $exprResult = $this->processExprNode($stmt, $arg->value, $scopeToPass, $storage, $nodeCallback, $context->enterDeep()); $throwPoints = array_merge($throwPoints, $exprResult->getThrowPoints()); $impurePoints = array_merge($impurePoints, $exprResult->getImpurePoints()); $isAlwaysTerminating = $isAlwaysTerminating || $exprResult->isAlwaysTerminating(); @@ -5626,11 +5778,13 @@ private function processArgs( $scope = $this->processVirtualAssign( $scope, + $storage, $stmt, $argValue, new TypeExpr($byRefType), $nodeCallback, )->getScope(); + $scope = $this->lookForUnsetAllowedUndefinedExpressions($scope, $argValue); } } elseif ($calleeReflection !== null && $calleeReflection->hasSideEffects()->yes()) { $argType = $scope->getType($arg->value); @@ -5650,17 +5804,18 @@ private function processArgs( || !(new ThisType($nakedMethodReflection->getDeclaringClass()))->isSuperTypeOf($nakedReturnType)->yes() || $nakedMethodReflection->isPure()->no() ) { - $nodeCallback(new InvalidateExprNode($arg->value), $scope); + $this->callNodeCallback($nodeCallback, new InvalidateExprNode($arg->value), $scope, $storage); $scope = $scope->invalidateExpression($arg->value, true); } } elseif (!(new ResourceType())->isSuperTypeOf($argType)->no()) { - $nodeCallback(new InvalidateExprNode($arg->value), $scope); + $this->callNodeCallback($nodeCallback, new InvalidateExprNode($arg->value), $scope, $storage); $scope = $scope->invalidateExpression($arg->value, true); } } } - return new ExpressionResult($scope, $hasYield, $isAlwaysTerminating, $throwPoints, $impurePoints); + // not storing this, it's scope after processing all args + return new ExpressionResult($scope, $scope, $hasYield, $isAlwaysTerminating, $throwPoints, $impurePoints); } /** @@ -5754,6 +5909,7 @@ private function getParameterOutExtensionsType(CallLike $callLike, $calleeReflec */ private function processAssignVar( MutatingScope $scope, + ExpressionResultStorage $storage, Node\Stmt $stmt, Expr $var, Expr $assignedExpr, @@ -5763,7 +5919,8 @@ private function processAssignVar( bool $enterExpressionAssign, ): ExpressionResult { - $nodeCallback($var, $enterExpressionAssign ? $scope->enterExpressionAssign($var) : $scope); + $originalScope = $scope; + $this->callNodeCallback($nodeCallback, $var, $enterExpressionAssign ? $scope->enterExpressionAssign($var) : $scope, $storage); $hasYield = false; $throwPoints = []; $impurePoints = []; @@ -5787,8 +5944,7 @@ private function processAssignVar( if ($if === null) { $if = $assignedExpr->cond; } - $condScope = $this->processExprNode($stmt, $assignedExpr->cond, $scope, static function (): void { - }, ExpressionContext::createDeep())->getScope(); + $condScope = $this->processExprNode($stmt, $assignedExpr->cond, $scope, $storage, new NoopNodeCallback(), ExpressionContext::createDeep())->getScope(); $truthySpecifiedTypes = $this->typeSpecifier->specifyTypesInCondition($condScope, $assignedExpr->cond, TypeSpecifierContext::createTruthy()); $falseySpecifiedTypes = $this->typeSpecifier->specifyTypesInCondition($condScope, $assignedExpr->cond, TypeSpecifierContext::createFalsey()); $truthyScope = $condScope->filterBySpecifiedTypes($truthySpecifiedTypes); @@ -5820,7 +5976,7 @@ private function processAssignVar( $conditionalExpressions = $this->processSureTypesForConditionalExpressionsAfterAssign($scope, $var->name, $conditionalExpressions, $falseySpecifiedTypes, $falseyType); $conditionalExpressions = $this->processSureNotTypesForConditionalExpressionsAfterAssign($scope, $var->name, $conditionalExpressions, $falseySpecifiedTypes, $falseyType); - $nodeCallback(new VariableAssignNode($var, $assignedExpr), $scopeBeforeAssignEval); + $this->callNodeCallback($nodeCallback, new VariableAssignNode($var, $assignedExpr), $scopeBeforeAssignEval, $storage); $scope = $scope->assignVariable($var->name, $type, $scope->getNativeType($assignedExpr), TrinaryLogic::createYes()); foreach ($conditionalExpressions as $exprString => $holders) { $scope = $scope->addConditionalExpressions($exprString, $holders); @@ -5860,7 +6016,7 @@ private function processAssignVar( if ($enterExpressionAssign) { $scope = $scope->enterExpressionAssign($var); } - $result = $this->processExprNode($stmt, $var, $scope, $nodeCallback, $context->enterDeep()); + $result = $this->processExprNode($stmt, $var, $scope, $storage, $nodeCallback, $context->enterDeep()); $hasYield = $result->hasYield(); $throwPoints = $result->getThrowPoints(); $impurePoints = $result->getImpurePoints(); @@ -5880,7 +6036,7 @@ private function processAssignVar( // Callback was already called for last dim at the beginning of the method. if ($key !== $lastDimKey) { - $nodeCallback($dimFetch, $enterExpressionAssign ? $scope->enterExpressionAssign($dimFetch) : $scope); + $this->callNodeCallback($nodeCallback, $dimFetch, $enterExpressionAssign ? $scope->enterExpressionAssign($dimFetch) : $scope, $storage); } if ($dimExpr === null) { @@ -5894,7 +6050,7 @@ private function processAssignVar( if ($enterExpressionAssign) { $scope->enterExpressionAssign($dimExpr); } - $result = $this->processExprNode($stmt, $dimExpr, $scope, $nodeCallback, $context->enterDeep()); + $result = $this->processExprNode($stmt, $dimExpr, $scope, $storage, $nodeCallback, $context->enterDeep()); $hasYield = $hasYield || $result->hasYield(); $throwPoints = array_merge($throwPoints, $result->getThrowPoints()); $scope = $result->getScope(); @@ -5963,11 +6119,11 @@ private function processAssignVar( if ($varType->isArray()->yes() || !(new ObjectType(ArrayAccess::class))->isSuperTypeOf($varType)->yes()) { if ($var instanceof Variable && is_string($var->name)) { - $nodeCallback(new VariableAssignNode($var, $assignedPropertyExpr), $scopeBeforeAssignEval); + $this->callNodeCallback($nodeCallback, new VariableAssignNode($var, $assignedPropertyExpr), $scopeBeforeAssignEval, $storage); $scope = $scope->assignVariable($var->name, $valueToWrite, $nativeValueToWrite, TrinaryLogic::createYes()); } else { if ($var instanceof PropertyFetch || $var instanceof StaticPropertyFetch) { - $nodeCallback(new PropertyAssignNode($var, $assignedPropertyExpr, $isAssignOp), $scopeBeforeAssignEval); + $this->callNodeCallback($nodeCallback, new PropertyAssignNode($var, $assignedPropertyExpr, $isAssignOp), $scopeBeforeAssignEval, $storage); if ($var instanceof PropertyFetch && $var->name instanceof Node\Identifier && !$isAssignOp) { $scope = $scope->assignInitializedProperty($scope->getType($var->var), $var->name->toString()); } @@ -5980,9 +6136,9 @@ private function processAssignVar( } } else { if ($var instanceof Variable) { - $nodeCallback(new VariableAssignNode($var, $assignedPropertyExpr), $scopeBeforeAssignEval); + $this->callNodeCallback($nodeCallback, new VariableAssignNode($var, $assignedPropertyExpr), $scopeBeforeAssignEval, $storage); } elseif ($var instanceof PropertyFetch || $var instanceof StaticPropertyFetch) { - $nodeCallback(new PropertyAssignNode($var, $assignedPropertyExpr, $isAssignOp), $scopeBeforeAssignEval); + $this->callNodeCallback($nodeCallback, new PropertyAssignNode($var, $assignedPropertyExpr, $isAssignOp), $scopeBeforeAssignEval, $storage); if ($var instanceof PropertyFetch && $var->name instanceof Node\Identifier && !$isAssignOp) { $scope = $scope->assignInitializedProperty($scope->getType($var->var), $var->name->toString()); } @@ -6004,13 +6160,13 @@ private function processAssignVar( $stmt, new MethodCall($var, 'offsetSet'), $scope, - static function (): void { - }, + $storage, + new NoopNodeCallback(), $context, )->getThrowPoints()); } } elseif ($var instanceof PropertyFetch) { - $objectResult = $this->processExprNode($stmt, $var->var, $scope, $nodeCallback, $context); + $objectResult = $this->processExprNode($stmt, $var->var, $scope, $storage, $nodeCallback, $context); $hasYield = $objectResult->hasYield(); $throwPoints = $objectResult->getThrowPoints(); $impurePoints = $objectResult->getImpurePoints(); @@ -6021,7 +6177,7 @@ static function (): void { if ($var->name instanceof Node\Identifier) { $propertyName = $var->name->name; } else { - $propertyNameResult = $this->processExprNode($stmt, $var->name, $scope, $nodeCallback, $context); + $propertyNameResult = $this->processExprNode($stmt, $var->name, $scope, $storage, $nodeCallback, $context); $hasYield = $hasYield || $propertyNameResult->hasYield(); $throwPoints = array_merge($throwPoints, $propertyNameResult->getThrowPoints()); $impurePoints = array_merge($impurePoints, $propertyNameResult->getImpurePoints()); @@ -6045,7 +6201,7 @@ static function (): void { if ($propertyName !== null && $propertyHolderType->hasInstanceProperty($propertyName)->yes()) { $propertyReflection = $propertyHolderType->getInstanceProperty($propertyName, $scope); $assignedExprType = $scope->getType($assignedExpr); - $nodeCallback(new PropertyAssignNode($var, $assignedExpr, $isAssignOp), $scopeBeforeAssignEval); + $this->callNodeCallback($nodeCallback, new PropertyAssignNode($var, $assignedExpr, $isAssignOp), $scopeBeforeAssignEval, $storage); if ($propertyReflection->canChangeTypeAfterAssignment()) { if ($propertyReflection->hasNativeType()) { $propertyNativeType = $propertyReflection->getNativeType(); @@ -6091,7 +6247,7 @@ static function (): void { } else { // fallback $assignedExprType = $scope->getType($assignedExpr); - $nodeCallback(new PropertyAssignNode($var, $assignedExpr, $isAssignOp), $scopeBeforeAssignEval); + $this->callNodeCallback($nodeCallback, new PropertyAssignNode($var, $assignedExpr, $isAssignOp), $scopeBeforeAssignEval, $storage); $scope = $scope->assignExpression($var, $assignedExprType, $scope->getNativeType($assignedExpr)); // simulate dynamic property assign by __set to get throw points if (!$propertyHolderType->hasMethod('__set')->no()) { @@ -6099,8 +6255,8 @@ static function (): void { $stmt, new MethodCall($var->var, '__set'), $scope, - static function (): void { - }, + $storage, + new NoopNodeCallback(), $context, )->getThrowPoints()); } @@ -6110,7 +6266,7 @@ static function (): void { if ($var->class instanceof Node\Name) { $propertyHolderType = $scope->resolveTypeByName($var->class); } else { - $this->processExprNode($stmt, $var->class, $scope, $nodeCallback, $context); + $this->processExprNode($stmt, $var->class, $scope, $storage, $nodeCallback, $context); $propertyHolderType = $scope->getType($var->class); } @@ -6118,7 +6274,7 @@ static function (): void { if ($var->name instanceof Node\Identifier) { $propertyName = $var->name->name; } else { - $propertyNameResult = $this->processExprNode($stmt, $var->name, $scope, $nodeCallback, $context); + $propertyNameResult = $this->processExprNode($stmt, $var->name, $scope, $storage, $nodeCallback, $context); $hasYield = $propertyNameResult->hasYield(); $throwPoints = $propertyNameResult->getThrowPoints(); $impurePoints = $propertyNameResult->getImpurePoints(); @@ -6137,7 +6293,7 @@ static function (): void { if ($propertyName !== null) { $propertyReflection = $scope->getStaticPropertyReflection($propertyHolderType, $propertyName); $assignedExprType = $scope->getType($assignedExpr); - $nodeCallback(new PropertyAssignNode($var, $assignedExpr, $isAssignOp), $scopeBeforeAssignEval); + $this->callNodeCallback($nodeCallback, new PropertyAssignNode($var, $assignedExpr, $isAssignOp), $scopeBeforeAssignEval, $storage); if ($propertyReflection !== null && $propertyReflection->canChangeTypeAfterAssignment()) { if ($propertyReflection->hasNativeType()) { $propertyNativeType = $propertyReflection->getNativeType(); @@ -6168,7 +6324,7 @@ static function (): void { } else { // fallback $assignedExprType = $scope->getType($assignedExpr); - $nodeCallback(new PropertyAssignNode($var, $assignedExpr, $isAssignOp), $scopeBeforeAssignEval); + $this->callNodeCallback($nodeCallback, new PropertyAssignNode($var, $assignedExpr, $isAssignOp), $scopeBeforeAssignEval, $storage); $scope = $scope->assignExpression($var, $assignedExprType, $scope->getNativeType($assignedExpr)); } } elseif ($var instanceof List_) { @@ -6188,9 +6344,9 @@ static function (): void { $itemScope = $itemScope->enterExpressionAssign($arrayItem->value); } $itemScope = $this->lookForSetAllowedUndefinedExpressions($itemScope, $arrayItem->value); - $nodeCallback($arrayItem, $itemScope); + $this->callNodeCallback($nodeCallback, $arrayItem, $itemScope, $storage); if ($arrayItem->key !== null) { - $keyResult = $this->processExprNode($stmt, $arrayItem->key, $itemScope, $nodeCallback, $context->enterDeep()); + $keyResult = $this->processExprNode($stmt, $arrayItem->key, $itemScope, $storage, $nodeCallback, $context->enterDeep()); $hasYield = $hasYield || $keyResult->hasYield(); $throwPoints = array_merge($throwPoints, $keyResult->getThrowPoints()); $impurePoints = array_merge($impurePoints, $keyResult->getImpurePoints()); @@ -6205,12 +6361,13 @@ static function (): void { } $result = $this->processAssignVar( $scope, + $storage, $stmt, $arrayItem->value, new GetOffsetValueTypeExpr($assignedExpr, $dimExpr), $nodeCallback, $context, - static fn (MutatingScope $scope): ExpressionResult => new ExpressionResult($scope, false, false, [], []), + static fn (MutatingScope $scope): ExpressionResult => new ExpressionResult($scope, $scope, false, false, [], []), $enterExpressionAssign, ); $scope = $result->getScope(); @@ -6274,11 +6431,11 @@ static function (): void { } if ($var instanceof Variable && is_string($var->name)) { - $nodeCallback(new VariableAssignNode($var, $assignedPropertyExpr), $scope); + $this->callNodeCallback($nodeCallback, new VariableAssignNode($var, $assignedPropertyExpr), $scope, $storage); $scope = $scope->assignVariable($var->name, $valueToWrite, $nativeValueToWrite, TrinaryLogic::createYes()); } else { if ($var instanceof PropertyFetch || $var instanceof StaticPropertyFetch) { - $nodeCallback(new PropertyAssignNode($var, $assignedPropertyExpr, $isAssignOp), $scope); + $this->callNodeCallback($nodeCallback, new PropertyAssignNode($var, $assignedPropertyExpr, $isAssignOp), $scope, $storage); } $scope = $scope->assignExpression( $var, @@ -6295,28 +6452,24 @@ static function (): void { $scope = $result->getScope(); } - return new ExpressionResult($scope, $hasYield, $isAlwaysTerminating, $throwPoints, $impurePoints); + // stored where processAssignVar is called + return new ExpressionResult($scope, $originalScope, $hasYield, $isAlwaysTerminating, $throwPoints, $impurePoints); } /** * @param callable(Node $node, Scope $scope): void $nodeCallback */ - private function processVirtualAssign(MutatingScope $scope, Node\Stmt $stmt, Expr $var, Expr $assignedExpr, callable $nodeCallback): ExpressionResult + private function processVirtualAssign(MutatingScope $scope, ExpressionResultStorage $storage, Node\Stmt $stmt, Expr $var, Expr $assignedExpr, callable $nodeCallback): ExpressionResult { return $this->processAssignVar( $scope, + $storage, $stmt, $var, $assignedExpr, - static function (Node $node, Scope $scope) use ($nodeCallback): void { - if (!$node instanceof PropertyAssignNode && !$node instanceof VariableAssignNode) { - return; - } - - $nodeCallback($node, $scope); - }, + new VirtualAssignNodeCallback($nodeCallback), ExpressionContext::createDeep(), - static fn (MutatingScope $scope): ExpressionResult => new ExpressionResult($scope, false, false, [], []), + static fn (MutatingScope $scope): ExpressionResult => new ExpressionResult($scope, $scope, false, false, [], []), false, ); } @@ -6549,7 +6702,7 @@ private function processSureNotTypesForConditionalExpressionsAfterAssign(Scope $ /** * @param callable(Node $node, Scope $scope): void $nodeCallback */ - private function processStmtVarAnnotation(MutatingScope $scope, Node\Stmt $stmt, ?Expr $defaultExpr, callable $nodeCallback): MutatingScope + private function processStmtVarAnnotation(MutatingScope $scope, ExpressionResultStorage $storage, Node\Stmt $stmt, ?Expr $defaultExpr, callable $nodeCallback): MutatingScope { $function = $scope->getFunction(); $variableLessTags = []; @@ -6603,7 +6756,7 @@ private function processStmtVarAnnotation(MutatingScope $scope, Node\Stmt $stmt, $variableNode = new Variable($name, $stmt->getAttributes()); $originalType = $scope->getVariableType($name); if (!$originalType->equals($varTag->getType())) { - $nodeCallback(new VarTagChangedExpressionTypeNode($varTag, $variableNode), $scope); + $this->callNodeCallback($nodeCallback, new VarTagChangedExpressionTypeNode($varTag, $variableNode), $scope, $storage); } $scope = $scope->assignVariable( @@ -6619,7 +6772,7 @@ private function processStmtVarAnnotation(MutatingScope $scope, Node\Stmt $stmt, $originalType = $scope->getType($defaultExpr); $varTag = $variableLessTags[0]; if (!$originalType->equals($varTag->getType())) { - $nodeCallback(new VarTagChangedExpressionTypeNode($varTag, $defaultExpr), $scope); + $this->callNodeCallback($nodeCallback, new VarTagChangedExpressionTypeNode($varTag, $defaultExpr), $scope, $storage); } $scope = $scope->assignExpression($defaultExpr, $varTag->getType(), new MixedType()); } @@ -6677,7 +6830,7 @@ private function processVarAnnotation(MutatingScope $scope, array $variableNames /** * @param callable(Node $node, Scope $scope): void $nodeCallback */ - private function enterForeach(MutatingScope $scope, MutatingScope $originalScope, Foreach_ $stmt, callable $nodeCallback): MutatingScope + private function enterForeach(MutatingScope $scope, ExpressionResultStorage $storage, MutatingScope $originalScope, Foreach_ $stmt, callable $nodeCallback): MutatingScope { if ($stmt->expr instanceof Variable && is_string($stmt->expr->name)) { $scope = $this->processVarAnnotation($scope, [$stmt->expr->name], $stmt); @@ -6706,6 +6859,7 @@ private function enterForeach(MutatingScope $scope, MutatingScope $originalScope } else { $scope = $this->processVirtualAssign( $scope, + $storage, $stmt, $stmt->valueVar, new GetIterableValueTypeExpr($stmt->expr), @@ -6720,6 +6874,7 @@ private function enterForeach(MutatingScope $scope, MutatingScope $originalScope } elseif ($stmt->keyVar !== null) { $scope = $this->processVirtualAssign( $scope, + $storage, $stmt, $stmt->keyVar, new GetIterableKeyTypeExpr($stmt->expr), @@ -6786,7 +6941,7 @@ private function enterForeach(MutatingScope $scope, MutatingScope $originalScope /** * @param callable(Node $node, Scope $scope): void $nodeCallback */ - private function processTraitUse(Node\Stmt\TraitUse $node, MutatingScope $classScope, callable $nodeCallback): void + private function processTraitUse(Node\Stmt\TraitUse $node, MutatingScope $classScope, ExpressionResultStorage $storage, callable $nodeCallback): void { $parentTraitNames = []; $parent = $classScope->getParentScope(); @@ -6827,7 +6982,7 @@ private function processTraitUse(Node\Stmt\TraitUse $node, MutatingScope $classS $adaptations[] = $adaptation; } $parserNodes = $this->parser->parseFile($fileName); - $this->processNodesForTraitUse($parserNodes, $traitReflection, $classScope, $adaptations, $nodeCallback); + $this->processNodesForTraitUse($parserNodes, $traitReflection, $classScope, $storage, $adaptations, $nodeCallback); } } @@ -6836,7 +6991,7 @@ private function processTraitUse(Node\Stmt\TraitUse $node, MutatingScope $classS * @param Node\Stmt\TraitUseAdaptation[] $adaptations * @param callable(Node $node, Scope $scope): void $nodeCallback */ - private function processNodesForTraitUse($node, ClassReflection $traitReflection, MutatingScope $scope, array $adaptations, callable $nodeCallback): void + private function processNodesForTraitUse($node, ClassReflection $traitReflection, MutatingScope $scope, ExpressionResultStorage $storage, array $adaptations, callable $nodeCallback): void { if ($node instanceof Node) { if ($node instanceof Node\Stmt\Trait_ && $traitReflection->getName() === (string) $node->namespacedName && $traitReflection->getNativeReflection()->getStartLine() === $node->getStartLine()) { @@ -6883,8 +7038,8 @@ private function processNodesForTraitUse($node, ClassReflection $traitReflection throw new ShouldNotHappenException(); } $traitScope = $scope->enterTrait($traitReflection); - $nodeCallback(new InTraitNode($node, $traitReflection, $scope->getClassReflection()), $traitScope); - $this->processStmtNodesInternal($node, $stmts, $traitScope, $nodeCallback, StatementContext::createTopLevel()); + $this->callNodeCallback($nodeCallback, new InTraitNode($node, $traitReflection, $scope->getClassReflection()), $traitScope, $storage); + $this->processStmtNodesInternal($node, $stmts, $traitScope, $storage, $nodeCallback, StatementContext::createTopLevel()); return; } if ($node instanceof Node\Stmt\ClassLike) { @@ -6895,11 +7050,11 @@ private function processNodesForTraitUse($node, ClassReflection $traitReflection } foreach ($node->getSubNodeNames() as $subNodeName) { $subNode = $node->{$subNodeName}; - $this->processNodesForTraitUse($subNode, $traitReflection, $scope, $adaptations, $nodeCallback); + $this->processNodesForTraitUse($subNode, $traitReflection, $scope, $storage, $adaptations, $nodeCallback); } } elseif (is_array($node)) { foreach ($node as $subNode) { - $this->processNodesForTraitUse($subNode, $traitReflection, $scope, $adaptations, $nodeCallback); + $this->processNodesForTraitUse($subNode, $traitReflection, $scope, $storage, $adaptations, $nodeCallback); } } } @@ -6936,7 +7091,7 @@ private function processCalledMethod(MethodReflection $methodReflection): ?Mutat $parserNodes = $this->parser->parseFile($fileName); $returnStatement = null; - $this->processNodesForCalledMethod($parserNodes, $fileName, $methodReflection, static function (Node $node, Scope $scope) use ($methodReflection, &$returnStatement): void { + $this->processNodesForCalledMethod($parserNodes, new ExpressionResultStorage(), $fileName, $methodReflection, static function (Node $node, Scope $scope) use ($methodReflection, &$returnStatement): void { if (!$node instanceof MethodReturnStatementsNode) { return; } @@ -6991,7 +7146,7 @@ private function processCalledMethod(MethodReflection $methodReflection): ?Mutat * @param Node[]|Node|scalar|null $node * @param callable(Node $node, Scope $scope): void $nodeCallback */ - private function processNodesForCalledMethod($node, string $fileName, MethodReflection $methodReflection, callable $nodeCallback): void + private function processNodesForCalledMethod($node, ExpressionResultStorage $storage, string $fileName, MethodReflection $methodReflection, callable $nodeCallback): void { if ($node instanceof Node) { $declaringClass = $methodReflection->getDeclaringClass(); @@ -7017,7 +7172,7 @@ private function processNodesForCalledMethod($node, string $fileName, MethodRefl } $scope = $this->scopeFactory->create(ScopeContext::create($fileName))->enterClass($declaringClass); - $this->processStmtNode($stmt, $scope, $nodeCallback, StatementContext::createTopLevel()); + $this->processStmtNode($stmt, $scope, $storage, $nodeCallback, StatementContext::createTopLevel()); } return; } @@ -7029,11 +7184,11 @@ private function processNodesForCalledMethod($node, string $fileName, MethodRefl } foreach ($node->getSubNodeNames() as $subNodeName) { $subNode = $node->{$subNodeName}; - $this->processNodesForCalledMethod($subNode, $fileName, $methodReflection, $nodeCallback); + $this->processNodesForCalledMethod($subNode, $storage, $fileName, $methodReflection, $nodeCallback); } } elseif (is_array($node)) { foreach ($node as $subNode) { - $this->processNodesForCalledMethod($subNode, $fileName, $methodReflection, $nodeCallback); + $this->processNodesForCalledMethod($subNode, $storage, $fileName, $methodReflection, $nodeCallback); } } } diff --git a/src/Analyser/NoopNodeCallback.php b/src/Analyser/NoopNodeCallback.php new file mode 100644 index 0000000000..61bc078958 --- /dev/null +++ b/src/Analyser/NoopNodeCallback.php @@ -0,0 +1,15 @@ +originalNodeCallback)($node, $scope); + } + +} diff --git a/src/DependencyInjection/FnsrExtension.php b/src/DependencyInjection/FnsrExtension.php new file mode 100644 index 0000000000..3efff761b4 --- /dev/null +++ b/src/DependencyInjection/FnsrExtension.php @@ -0,0 +1,39 @@ +getContainerBuilder(); + $analyserDef = $builder->getDefinitionByType(Analyser::class); + if (!$analyserDef instanceof ServiceDefinition) { + throw new ShouldNotHappenException(); + } + $analyserDef->setArgument('nodeScopeResolver', '@' . FiberNodeScopeResolver::class); + + $fileAnalyserDef = $builder->getDefinitionByType(FileAnalyser::class); + if (!$fileAnalyserDef instanceof ServiceDefinition) { + throw new ShouldNotHappenException(); + } + $fileAnalyserDef->setArgument('nodeScopeResolver', '@' . FiberNodeScopeResolver::class); + } + +} diff --git a/src/Testing/RuleTestCase.php b/src/Testing/RuleTestCase.php index a00040c931..d34ec23763 100644 --- a/src/Testing/RuleTestCase.php +++ b/src/Testing/RuleTestCase.php @@ -6,6 +6,7 @@ use PHPStan\Analyser\Analyser; use PHPStan\Analyser\AnalyserResultFinalizer; use PHPStan\Analyser\Error; +use PHPStan\Analyser\Fiber\FiberNodeScopeResolver; use PHPStan\Analyser\FileAnalyser; use PHPStan\Analyser\Generator\GeneratorNodeScopeResolver; use PHPStan\Analyser\Generator\GeneratorScopeFactory; @@ -97,7 +98,13 @@ protected function createNodeScopeResolver(): NodeScopeResolver|GeneratorNodeSco $reflectionProvider = $this->createReflectionProvider(); $typeSpecifier = $this->getTypeSpecifier(); - return new NodeScopeResolver( + $enableFnsr = getenv('PHPSTAN_FNSR'); + $className = NodeScopeResolver::class; + if ($enableFnsr === '1') { + $className = FiberNodeScopeResolver::class; + } + + return new $className( $reflectionProvider, self::getContainer()->getByType(InitializerExprTypeResolver::class), self::getReflector(), diff --git a/src/Testing/TypeInferenceTestCase.php b/src/Testing/TypeInferenceTestCase.php index be3471b02d..e040b5a00a 100644 --- a/src/Testing/TypeInferenceTestCase.php +++ b/src/Testing/TypeInferenceTestCase.php @@ -5,6 +5,7 @@ use PhpParser\Node; use PhpParser\Node\Expr\StaticCall; use PhpParser\Node\Name; +use PHPStan\Analyser\Fiber\FiberNodeScopeResolver; use PHPStan\Analyser\Generator\GeneratorNodeScopeResolver; use PHPStan\Analyser\Generator\GeneratorScopeFactory; use PHPStan\Analyser\Generator\NodeHandler\VarAnnotationHelper; @@ -71,7 +72,13 @@ protected static function createNodeScopeResolver(): NodeScopeResolver|Generator $reflectionProvider = self::createReflectionProvider(); $typeSpecifier = $container->getService('typeSpecifier'); - return new NodeScopeResolver( + $enableFnsr = getenv('PHPSTAN_FNSR'); + $className = NodeScopeResolver::class; + if ($enableFnsr === '1') { + $className = FiberNodeScopeResolver::class; + } + + return new $className( $reflectionProvider, $container->getByType(InitializerExprTypeResolver::class), self::getReflector(), diff --git a/tests/PHPStan/Analyser/ExpressionResultTest.php b/tests/PHPStan/Analyser/ExpressionResultTest.php index 56b699f1fc..38b8b5d368 100644 --- a/tests/PHPStan/Analyser/ExpressionResultTest.php +++ b/tests/PHPStan/Analyser/ExpressionResultTest.php @@ -208,6 +208,7 @@ public function testIsAlwaysTerminating( $stmt, $expr, $scope, + new ExpressionResultStorage(), static function (): void { }, ExpressionContext::createTopLevel(), diff --git a/tests/PHPStan/Analyser/Fiber/FiberNodeScopeResolverRuleTest.php b/tests/PHPStan/Analyser/Fiber/FiberNodeScopeResolverRuleTest.php new file mode 100644 index 0000000000..fdfed14cc2 --- /dev/null +++ b/tests/PHPStan/Analyser/Fiber/FiberNodeScopeResolverRuleTest.php @@ -0,0 +1,156 @@ +> + */ +#[RequiresPhp('>= 8.1')] +class FiberNodeScopeResolverRuleTest extends RuleTestCase +{ + + /** @var callable(Node, Scope): list */ + private $ruleCallback; + + protected function getRule(): Rule + { + return new class ($this->ruleCallback) implements Rule { + + /** + * @param callable(Node, Scope): list $ruleCallback + */ + public function __construct(private $ruleCallback) + { + } + + public function getNodeType(): string + { + return Node::class; + } + + public function processNode(Node $node, Scope $scope): array + { + return ($this->ruleCallback)($node, $scope); + } + + }; + } + + public static function dataRule(): iterable + { + yield [ + static fn (Node $node, Scope $scope) => [], + [], + ]; + yield [ + static function (Node $node, Scope $scope) { + if (!$node instanceof Node\Expr\MethodCall) { + return []; + } + + $arg0 = $scope->getType($node->getArgs()[0]->value); + $arg0 = $scope->getType($node->getArgs()[0]->value); // on purpose to hit the cache + + return [ + RuleErrorBuilder::message($arg0->describe(VerbosityLevel::precise()))->identifier('gnsr.rule')->build(), + RuleErrorBuilder::message($scope->getType($node->getArgs()[1]->value)->describe(VerbosityLevel::precise()))->identifier('gnsr.rule')->build(), + RuleErrorBuilder::message($scope->getType($node->getArgs()[2]->value)->describe(VerbosityLevel::precise()))->identifier('gnsr.rule')->build(), + ]; + }, + [ + ['1', 21], + ['2', 21], + ['3', 21], + ], + ]; + yield [ + static function (Node $node, Scope $scope) { + if (!$node instanceof Node\Expr\MethodCall) { + return []; + } + + $synthetic = $scope->getType(new Node\Scalar\String_('foo')); + $synthetic2 = $scope->getType(new Node\Scalar\String_('bar')); + + return [ + RuleErrorBuilder::message($synthetic->describe(VerbosityLevel::precise()))->identifier('gnsr.rule')->build(), + RuleErrorBuilder::message($synthetic2->describe(VerbosityLevel::precise()))->identifier('gnsr.rule')->build(), + ]; + }, + [ + ['\'foo\'', 21], + ['\'bar\'', 21], + ], + ]; + } + + protected function createNodeScopeResolver(): NodeScopeResolver + { + $readWritePropertiesExtensions = $this->getReadWritePropertiesExtensions(); + $reflectionProvider = $this->createReflectionProvider(); + $typeSpecifier = $this->getTypeSpecifier(); + + return new FiberNodeScopeResolver( + $reflectionProvider, + self::getContainer()->getByType(InitializerExprTypeResolver::class), + self::getReflector(), + self::getContainer()->getByType(ClassReflectionFactory::class), + self::getContainer()->getByType(ParameterOutTypeExtensionProvider::class), + $this->getParser(), + self::getContainer()->getByType(FileTypeMapper::class), + self::getContainer()->getByType(PhpVersion::class), + self::getContainer()->getByType(PhpDocInheritanceResolver::class), + self::getContainer()->getByType(FileHelper::class), + $typeSpecifier, + self::getContainer()->getByType(DynamicThrowTypeExtensionProvider::class), + $readWritePropertiesExtensions !== [] ? new DirectReadWritePropertiesExtensionProvider($readWritePropertiesExtensions) : self::getContainer()->getByType(ReadWritePropertiesExtensionProvider::class), + self::getContainer()->getByType(ParameterClosureThisExtensionProvider::class), + self::getContainer()->getByType(ParameterClosureTypeExtensionProvider::class), + self::createScopeFactory($reflectionProvider, $typeSpecifier), + $this->shouldPolluteScopeWithLoopInitialAssignments(), + $this->shouldPolluteScopeWithAlwaysIterableForeach(), + self::getContainer()->getParameter('polluteScopeWithBlock'), + [], + [], + self::getContainer()->getParameter('exceptions')['implicitThrows'], + $this->shouldTreatPhpDocTypesAsCertain(), + self::getContainer()->getParameter('narrowMethodScopeFromConstructor'), + ); + } + + /** + * @param callable(Node, Scope): list $ruleCallback + * @param list $expectedErrors + * @return void + */ + #[DataProvider('dataRule')] + public function testRule(callable $ruleCallback, array $expectedErrors): void + { + $this->ruleCallback = $ruleCallback; + $this->analyse([__DIR__ . '/data/rule.php'], $expectedErrors); + } + +} diff --git a/tests/PHPStan/Analyser/Fiber/FiberNodeScopeResolverTest.php b/tests/PHPStan/Analyser/Fiber/FiberNodeScopeResolverTest.php new file mode 100644 index 0000000000..42869afe3e --- /dev/null +++ b/tests/PHPStan/Analyser/Fiber/FiberNodeScopeResolverTest.php @@ -0,0 +1,84 @@ += 8.1')] +class FiberNodeScopeResolverTest extends TypeInferenceTestCase +{ + + public static function dataFileAsserts(): iterable + { + yield from self::gatherAssertTypes(__DIR__ . '/data/gnsr.php'); + } + + /** + * @param mixed ...$args + */ + #[DataProvider('dataFileAsserts')] + public function testFileAsserts( + string $assertType, + string $file, + ...$args, + ): void + { + $this->assertFileAsserts($assertType, $file, ...$args); + } + + protected static function createNodeScopeResolver(): NodeScopeResolver + { + $container = self::getContainer(); + $reflectionProvider = self::createReflectionProvider(); + $typeSpecifier = $container->getService('typeSpecifier'); + + return new FiberNodeScopeResolver( + $reflectionProvider, + $container->getByType(InitializerExprTypeResolver::class), + self::getReflector(), + $container->getByType(ClassReflectionFactory::class), + $container->getByType(ParameterOutTypeExtensionProvider::class), + self::getParser(), + $container->getByType(FileTypeMapper::class), + $container->getByType(PhpVersion::class), + $container->getByType(PhpDocInheritanceResolver::class), + $container->getByType(FileHelper::class), + $typeSpecifier, + $container->getByType(DynamicThrowTypeExtensionProvider::class), + $container->getByType(ReadWritePropertiesExtensionProvider::class), + $container->getByType(ParameterClosureThisExtensionProvider::class), + $container->getByType(ParameterClosureTypeExtensionProvider::class), + self::createScopeFactory($reflectionProvider, $typeSpecifier), + $container->getParameter('polluteScopeWithLoopInitialAssignments'), + $container->getParameter('polluteScopeWithAlwaysIterableForeach'), + $container->getParameter('polluteScopeWithBlock'), + static::getEarlyTerminatingMethodCalls(), + static::getEarlyTerminatingFunctionCalls(), + $container->getParameter('exceptions')['implicitThrows'], + $container->getParameter('treatPhpDocTypesAsCertain'), + $container->getParameter('narrowMethodScopeFromConstructor'), + ); + } + + public static function getAdditionalConfigFiles(): array + { + return [ + __DIR__ . '/../../../../conf/bleedingEdge.neon', + ]; + } + +} diff --git a/tests/PHPStan/Analyser/Fiber/data/gnsr.php b/tests/PHPStan/Analyser/Fiber/data/gnsr.php new file mode 100644 index 0000000000..a5324af90c --- /dev/null +++ b/tests/PHPStan/Analyser/Fiber/data/gnsr.php @@ -0,0 +1,666 @@ += 8.1 + +declare(strict_types = 1); + +namespace GeneratorNodeScopeResolverTest; + +use Closure; +use DivisionByZeroError; +use function PHPStan\Testing\assertNativeType; +use function PHPStan\Testing\assertType; +use const PHP_VERSION_ID; + +class Foo +{ + + public function doFoo(int $i): ?string + { + return 'foo'; + } + + public function doImplicitArrayCreation(): void + { + $a['bla'] = 1; + assertType('array{bla: 1}', $a); + } + + /** + * @param int $a + * @param int $b + * @return void + */ + public function doPlus($a, $b, int $c, int $d): void + { + assertType('int', $a + $b); + assertNativeType('(array|float|int)', $a + $b); + assertType('2', 1 + 1); + assertNativeType('2', 1 + 1); + assertType('int', $c + $d); + assertNativeType('int', $c + $d); + } + + /** + * @param int $a + * @param int $b + * @return void + */ + public function doDiv($a, $b, int $c, int $d): void + { + assertType('(float|int)', $a / $b); + assertNativeType('(float|int)', $a / $b); + assertType('1', 1 / 1); + assertNativeType('1', 1 / 1); + assertType('(float|int)', $c / $d); + assertNativeType('(float|int)', $c / $d); + + assertType('*ERROR*', $c / 0); // DivisionByZeroError + } + + /** + * @param int $a + * @param int $b + * @return void + */ + public function doMod($a, $b, int $c, int $d): void + { + assertType('int', $a % $b); + assertNativeType('int', $a % $b); + assertType('0', 1 % 1); + assertNativeType('0', 1 % 1); + assertType('int', $c % $d); + assertNativeType('int', $c % $d); + + assertType('*ERROR*', $c % 0); // DivisionByZeroError + } + + /** + * @param int $a + * @param int $b + * @return void + */ + public function doMinus($a, $b, int $c, int $d): void + { + assertType('int', $a - $b); + assertNativeType('(float|int)', $a - $b); + assertType('0', 1 - 1); + assertNativeType('0', 1 - 1); + assertType('int', $c - $d); + assertNativeType('int', $c - $d); + } + + /** + * @param int $a + * @return void + */ + public function doBitwiseNot($a, int $b): void + { + assertType('int', ~$a); + assertNativeType('int', ~$b); + assertType('int', ~1); + assertNativeType('int', ~1); + assertType('int', ~$b); + assertNativeType('int', ~$b); + } + + /** + * @param int $a + * @param int $b + * @return void + */ + public function doBitwiseAnd($a, $b, int $c, int $d): void + { + assertType('int', $a & $b); + assertNativeType('(int|string)', $a & $b); + assertType('1', 1 & 1); + assertNativeType('1', 1 & 1); + assertType('int', $c & $d); + assertNativeType('int', $c & $d); + } + + /** + * @param int $a + * @param int $b + * @return void + */ + public function doBitwiseOr($a, $b, int $c, int $d): void + { + assertType('int', $a | $b); + assertNativeType('(int|string)', $a | $b); + assertType('1', 1 | 1); + assertNativeType('1', 1 | 1); + assertType('int', $c | $d); + assertNativeType('int', $c | $d); + } + + /** + * @param int $a + * @param int $b + * @return void + */ + public function doBitwiseXor($a, $b, int $c, int $d): void + { + assertType('int', $a ^ $b); + assertNativeType('(int|string)', $a ^ $b); + assertType('0', 1 ^ 1); + assertNativeType('0', 1 ^ 1); + assertType('int', $c ^ $d); + assertNativeType('int', $c ^ $d); + } + + /** + * @param int $a + * @param int $b + * @return void + */ + public function doMul($a, $b, int $c, int $d): void + { + assertType('int', $a * $b); + assertNativeType('(float|int)', $a * $b); + assertType('1', 1 * 1); + assertNativeType('1', 1 * 1); + assertType('int', $c * $d); + assertNativeType('int', $c * $d); + } + + /** + * @param int $a + * @param int $b + * @return void + */ + public function doPow($a, $b, int $c, int $d): void + { + assertType('(float|int)', $a ** $b); + assertNativeType('(float|int)', $a ** $b); + assertType('1', 1 ** 1); + assertNativeType('1', 1 ** 1); + assertType('(float|int)', $c ** $d); + assertNativeType('(float|int)', $c ** $d); + } + + /** + * @param string $a + * @param string $b + * @return void + */ + public function doConcat($a, $b, string $c, string $d): void + { + assertType('string', $a . $b); + assertNativeType('string', $a . $b); + assertType("'1a'", '1' . 'a'); + assertNativeType("'1a'", '1' . 'a'); + assertType('string', $c . $d); + assertNativeType('string', $c . $d); + } + + /** + * @param int $ii + */ + function doUnaryPlus(int $i, $ii) + { + $a = '1'; + + assertType('1', +$a); + assertNativeType('1', +$a); + assertType('int', +$i); + assertNativeType('int', +$i); + assertType('int', +$ii); + assertNativeType('float|int', +$ii); + } + + function doUnaryMinus(int $i) { + $a = '1'; + + assertType('-1', -$a); + assertNativeType('-1', -$a); + assertType('int', -$i); + assertNativeType('int', -$i); + } + + /** + * @param int $a + * @param int $b + * @return void + */ + public function doShiftLeft($a, $b, int $c, int $d): void + { + assertType('int', $a << $b); + assertNativeType('(float|int)', $a << $b); + assertType('8', 1 << 3); + assertNativeType('8', 1 << 3); + assertType('int', $c << $d); + assertNativeType('int', $c << $d); + } + + /** + * @param int $a + * @param int $b + * @return void + */ + public function doShiftRight($a, $b, int $c, int $d): void + { + assertType('int', $a >> $b); + assertNativeType('(float|int)', $a >> $b); + assertType('0', 1 >> 3); + assertNativeType('0', 1 >> 3); + assertType('int', $c >> $d); + assertNativeType('int', $c >> $d); + } + + /** + * @param string $a + * @param string $b + * @return void + */ + public function doSpaceship($a, $b, string $c, string $d): void + { + assertType('int<-1, 1>', $a <=> $b); + assertNativeType('int<-1, 1>', $a <=> $b); + assertType('-1', '1' <=> 'a'); + assertNativeType('-1', '1' <=> 'a'); + assertType('int<-1, 1>', $c <=> $d); + assertNativeType('int<-1, 1>', $c <=> $d); + } + + function doCast() { + $a = '1'; + + assertType('1', (int) $a); + assertType("array{'1'}", (array) $a); + assertType('stdClass', (object) $a); + assertType('1.0', (double) $a); + assertType('true', (bool) $a); + assertType("'1'", (string) $a); + + $f = 1.1; + assertType('1', (int) $f); + } + + /** + * @param '1' $b + */ + function doInterpolatedString(string $b) { + $a = '1'; + + assertType("'1'", "$a"); + assertNativeType("'1'", "$a"); + assertType("'1'", "$b"); + assertNativeType("string", "$b"); + } + + +} + +function (): void { + $foo = new Foo(); + assertType(Foo::class, $foo); + assertType('string|null', $foo->doFoo(1)); + assertType($a = '1', (int) $a); +}; + +function (): void { + assertType('array{foo: \'bar\'}', ['foo' => 'bar']); + $a = []; + assertType('array{}', $a); + +}; + +function (): void { + $a['bla'] = 1; + assertType('array{bla: 1}', $a); +}; + +function (): void { + $cb = fn () => 1; + assertType('Closure(): 1', $cb); + + $cb = fn (string $s) => (int) $s; + assertType('Closure(string): int', $cb); + + $cb = function () { + return 1; + }; + assertType('Closure(): 1', $cb); + + $a = 1; + $cb = function () use (&$a) { + return 1; + }; + assertType('Closure(): 1', $cb); + + $cb = function (string $s) { + return $s; + }; + assertType('Closure(string): string', $cb); +}; + +function (): void { + $a = 0; + $cb = function () use (&$a): void { + assertType('0|\'s\'', $a); + $a = 's'; + }; + assertType('0|\'s\'', $a); +}; + +function (): void { + $a = 0; + $b = 0; + $cb = function () use (&$a, $b): void { + assertType('int<0, max>', $a); + assertType('0', $b); + $a = $a + 1; + $b = 1; + }; + assertType('int<0, max>', $a); + assertType('0', $b); +}; + +function (): void { + $a = 0; + $cb = function () use (&$a): void { + assertType('0|1', $a); + $a = 1; + }; + assertType('0|1', $a); +}; + +class FooWithStaticMethods +{ + + public function doFoo(): void + { + assertType('GeneratorNodeScopeResolverTest\\FooWithStaticMethods', self::returnSelf()); + assertNativeType('GeneratorNodeScopeResolverTest\\FooWithStaticMethods', self::returnSelf()); + assertType('GeneratorNodeScopeResolverTest\\FooWithStaticMethods', self::returnPhpDocSelf()); + assertNativeType('mixed', self::returnPhpDocSelf()); + } + + public static function returnSelf(): self + { + + } + + /** + * @return self + */ + public static function returnPhpDocSelf() + { + + } + + /** + * @template T + * @param T $a + * @return T + */ + public static function genericStatic($a) + { + + } + + public function doFoo2(): void + { + assertType('1', self::genericStatic(1)); + + $s = 'GeneratorNodeScopeResolverTest\\FooWithStaticMethods'; + assertType('1', $s::genericStatic(1)); + } + + public function doIf(int $i): void { + if ($i) { + assertType('int|int<1, max>', $i); + } else { + assertType('0', $i); + } + + assertType('int', $i); + } + +} + +class ClosureFromCallableExtension +{ + + /** + * @param callable(string, int=): bool $cb + */ + public function doFoo(callable $cb): void + { + assertType('callable(string, int=): bool', $cb); + assertType('Closure(string, int=): bool', Closure::fromCallable($cb)); + } + +} + +/** + * @template T + */ +class FooGeneric +{ + + /** + * @param T $a + */ + public function __construct($a) + { + + } + +} + +function (): void { + $foo = new FooGeneric(5); + assertType('GeneratorNodeScopeResolverTest\\FooGeneric', $foo); +}; + +function (): void { + $c = new /** @template T of int */ class(1, 2, 3) { + /** + * @param T $i + */ + public function __construct(private int $i, private int $j, private int $k) { + + } + }; +}; + +class MagicConstUser { + function doFoo(): void { + assertType('literal-string&non-falsy-string', __DIR__); + assertType('literal-string&non-falsy-string', __FILE__); + assertType('471', __LINE__); + assertType("'GeneratorNodeScopeResolverTest'", __NAMESPACE__); + assertType("'GeneratorNodeScopeResolverTest\\\\MagicConstUser'", __CLASS__); + assertType("''", __TRAIT__); + assertType("'doFoo'", __FUNCTION__); + assertType("'GeneratorNodeScopeResolverTest\\\\MagicConstUser::doFoo'", __METHOD__); + assertType("''", __PROPERTY__); + } +} + +function (): void { + assertType('int<50207, 80599>', PHP_VERSION_ID); +}; + +function (int $i) { + if ($i == null) { + assertType('0', $i); + } else { + assertType('int|int<1, max>', $i); + } + + assertType('int', $i); +}; + +function (int $i) { + if ($i == false) { + assertType('0', $i); + } else { + assertType('int|int<1, max>', $i); + } + + assertType('int', $i); +}; + +function (int $i) { + if (false == $i) { + assertType('0', $i); + } else { + assertType('int|int<1, max>', $i); + } + + assertType('int', $i); +}; + +function (int $i) { + if ($i == true) { + assertType('int|int<1, max>', $i); + } else { + assertType('0', $i); + } + + assertType('int', $i); +}; + +function (int $i) { + if (true == $i) { + assertType('int|int<1, max>', $i); + } else { + assertType('0', $i); + } + + assertType('int', $i); +}; + +function (mixed $m) { + if ($m == 0) { + assertType('0|0.0|string|false|null', $m); + } else { + assertType("mixed~(0|0.0|'0'|false|null)", $m); + } + + assertType('mixed', $m); +}; + +function (mixed $m) { + if ($m != 0) { + assertType("mixed~(0|0.0|'0'|false|null)", $m); + } else { + assertType('0|0.0|string|false|null', $m); + } + + assertType('mixed', $m); +}; + +function (mixed $m) { + if ($m == '') { + assertType("0|0.0|''|false|null", $m); + } else { + assertType("mixed~(''|false|null)", $m); + } + + assertType('mixed', $m); +}; + +function (array $a): void { + if ($a == []) { + assertType("array{}", $a); + } else { + assertType("non-empty-array", $a); + } + + assertType("array", $a); +}; + +function (array $a): void { + if (!($a == [])) { + assertType("non-empty-array", $a); + } else { + assertType("array{}", $a); + } + + assertType("array", $a); +}; + +function (array $a): void { + if ($a != []) { + assertType("non-empty-array", $a); + } else { + assertType("array{}", $a); + } + + assertType("array", $a); +}; + +function (bool $b): void { + assertType('true', !false); + assertType('false', !true); + assertType('bool', !$b); +}; + +function (mixed $m): void { + if ((bool) $m) { + assertType("mixed~(0|0.0|''|'0'|array{}|false|null)", $m); + } else { + assertType("0|0.0|''|'0'|array{}|false|null", $m); + } + + assertType("mixed", $m); +}; + +function (mixed $m): void { + if ((string) $m) { + //assertType("non-empty-array", $m); + } else { + //assertType("non-empty-array", $m); + } + + assertType("mixed", $m); +}; + +function (mixed $m): void { + if ((int) $m) { + //assertType("non-empty-array", $m); + } else { + //assertType("non-empty-array", $m); + } + + assertType("mixed", $m); +}; + +function (mixed $m): void { + if ((float) $m) { + //assertType("non-empty-array", $m); + } else { + //assertType("non-empty-array", $m); + } + + assertType("mixed", $m); +}; + +function (array $a): void { + if (count($a) === -10) { + assertType('*NEVER*', $a); + } else { + assertType('array', $a); + } + assertType('array', $a); + if (count($a) === 0) { + assertType('array{}', $a); + } else { + assertType('non-empty-array', $a); + } + if (count($a) !== 0) { + assertType('non-empty-array', $a); + } else { + assertType('array{}', $a); + } +}; + +function (string $s): void { + if (strlen($s) === 1) { + assertType('non-empty-string', $s); + } else { + assertType('string', $s); + } +}; diff --git a/tests/PHPStan/Analyser/Fiber/data/rule.php b/tests/PHPStan/Analyser/Fiber/data/rule.php new file mode 100644 index 0000000000..37a3c4413b --- /dev/null +++ b/tests/PHPStan/Analyser/Fiber/data/rule.php @@ -0,0 +1,24 @@ +doFoo($a = 1, $a + 1, 3); + + echo 'foo'; +}; diff --git a/tests/PHPStan/Analyser/LegacyNodeScopeResolverTest.php b/tests/PHPStan/Analyser/LegacyNodeScopeResolverTest.php index 843786a0e5..55b868f5c1 100644 --- a/tests/PHPStan/Analyser/LegacyNodeScopeResolverTest.php +++ b/tests/PHPStan/Analyser/LegacyNodeScopeResolverTest.php @@ -9211,7 +9211,7 @@ static function (Node $node, Scope $scope) use ($file, $evaluatedPointExpression return; } - self::$assertTypesCache[$file][$evaluatedPointExpression] = $scope; + self::$assertTypesCache[$file][$evaluatedPointExpression] = $scope->toMutatingScope(); $assertType($scope); }, diff --git a/tests/PHPStan/Rules/Comparison/MatchExpressionRuleTest.php b/tests/PHPStan/Rules/Comparison/MatchExpressionRuleTest.php index 014b57899c..5f081aee3d 100644 --- a/tests/PHPStan/Rules/Comparison/MatchExpressionRuleTest.php +++ b/tests/PHPStan/Rules/Comparison/MatchExpressionRuleTest.php @@ -148,10 +148,6 @@ public function testEnums(): void 'Match arm comparison between MatchEnums\Foo and MatchEnums\Foo::ONE is always false.', 104, ], - [ - 'Match arm comparison between *NEVER* and MatchEnums\Foo::ONE is always false.', - 113, - ], [ 'Match arm comparison between *NEVER* and MatchEnums\DifferentEnum::ONE is always false.', 113,