From a8db3b0e800b94059a1017cd64223cfcfa897e48 Mon Sep 17 00:00:00 2001 From: Ondrej Mirtes Date: Sun, 13 Sep 2026 10:29:31 +0200 Subject: [PATCH 01/10] Extract statement list processing from NodeScopeResolver into StatementsHandler The statement-list walk (processNodesWithStorage, doProcessStmtNodes, processStatementStep, the two-pass body walk), goto convergence, unreachable statement detection, variable mention collection and the statement-level PHPDoc handling (@var via processStmtVarAnnotation / emitVarTagChangedNode, @throws via getOverridingThrowPoints) now live in the StatementsHandler service. NodeScopeResolver keeps its public entry points and delegates; the handlers that called the moved public methods inject StatementsHandler instead. TemplateArgumentResolver, FileTypeMapper and the unresolvedTemplateArguments toggle move from the NodeScopeResolver constructor to StatementsHandler. Gatherer suspension goes through the new suspendNodeGatherers() / restoreNodeGatherers() pair. Co-Authored-By: Claude Opus 5 (1M context) Claude-Session: https://claude.ai/code/session_014hgcbwfKQnye2coQXtX5XU --- .../Build/NoScopeTypeReadInEngineRule.php | 1 + src/Analyser/ExprHandler/AssignHandler.php | 4 +- src/Analyser/NodeScopeResolver.php | 936 +---------------- src/Analyser/StatementsHandler.php | 945 ++++++++++++++++++ .../StmtHandler/ExpressionHandler.php | 11 +- src/Analyser/StmtHandler/ReturnHandler.php | 11 +- src/Analyser/StmtHandler/TryCatchHandler.php | 9 +- src/Testing/RuleTestCase.php | 7 +- src/Testing/TypeInferenceTestCase.php | 7 +- .../UnresolvedTemplateArgumentType.php | 2 +- tests/PHPStan/Analyser/AnalyserTest.php | 5 +- 11 files changed, 1012 insertions(+), 926 deletions(-) create mode 100644 src/Analyser/StatementsHandler.php diff --git a/build/PHPStan/Build/NoScopeTypeReadInEngineRule.php b/build/PHPStan/Build/NoScopeTypeReadInEngineRule.php index 7c5f267e4a6..d687e3246da 100644 --- a/build/PHPStan/Build/NoScopeTypeReadInEngineRule.php +++ b/build/PHPStan/Build/NoScopeTypeReadInEngineRule.php @@ -27,6 +27,7 @@ final class NoScopeTypeReadInEngineRule implements Rule private const ENGINE_NAMESPACES = [ 'PHPStan\\Analyser\\NodeScopeResolver', + 'PHPStan\\Analyser\\StatementsHandler', 'PHPStan\\Analyser\\StmtHandler\\', 'PHPStan\\Analyser\\ExprHandler\\', ]; diff --git a/src/Analyser/ExprHandler/AssignHandler.php b/src/Analyser/ExprHandler/AssignHandler.php index 7e7759f1c17..b86f94d00ae 100644 --- a/src/Analyser/ExprHandler/AssignHandler.php +++ b/src/Analyser/ExprHandler/AssignHandler.php @@ -46,6 +46,7 @@ use PHPStan\Analyser\PropertyHookThrowPointsResolver; use PHPStan\Analyser\Scope; use PHPStan\Analyser\SpecifiedTypes; +use PHPStan\Analyser\StatementsHandler; use PHPStan\Analyser\TypeSpecifierContext; use PHPStan\Analyser\VarAnnotationProcessor; use PHPStan\Analyser\VariableFlow; @@ -132,6 +133,7 @@ public function __construct( private StaticPropertyFetchHandler $staticPropertyFetchHandler, private MethodThrowPointHelper $methodThrowPointHelper, private PropertyHookThrowPointsResolver $propertyHookThrowPointsResolver, + private StatementsHandler $statementsHandler, ) { } @@ -252,7 +254,7 @@ public function processExpr(NodeScopeResolver $nodeScopeResolver, Stmt $stmt, Ex $varChangedScope = false; $scope = $this->varAnnotationProcessor->processVarAnnotation($scope, $vars, $stmt, $varChangedScope); if (!$varChangedScope) { - $scope = $nodeScopeResolver->processStmtVarAnnotation($scope, $storage, $stmt, null, $nodeCallback); + $scope = $this->statementsHandler->processStmtVarAnnotation($nodeScopeResolver, $scope, $storage, $stmt, null, $nodeCallback); } else { // the @var tag is a declared type the assigned value flows into $templateArgumentFrame = $nodeScopeResolver->observingTemplateArgumentFrame($scope); diff --git a/src/Analyser/NodeScopeResolver.php b/src/Analyser/NodeScopeResolver.php index 2220437ee3f..e1904ebfcf9 100644 --- a/src/Analyser/NodeScopeResolver.php +++ b/src/Analyser/NodeScopeResolver.php @@ -3,13 +3,10 @@ namespace PHPStan\Analyser; use Closure; -use PhpParser\Comment\Doc; use PhpParser\Node; use PhpParser\Node\AttributeGroup; use PhpParser\Node\Expr; use PhpParser\Node\Expr\ArrayDimFetch; -use PhpParser\Node\Expr\Assign; -use PhpParser\Node\Expr\AssignRef; use PhpParser\Node\Expr\CallLike; use PhpParser\Node\Expr\FuncCall; use PhpParser\Node\Expr\List_; @@ -24,7 +21,6 @@ use PhpParser\Node\Stmt\Class_; use PhpParser\Node\Stmt\Echo_; use PhpParser\Node\Stmt\Foreach_; -use PhpParser\Node\Stmt\Goto_; use PhpParser\Node\Stmt\If_; use PhpParser\Node\Stmt\Return_; use PhpParser\Node\Stmt\Static_; @@ -40,7 +36,6 @@ use PHPStan\Analyser\Generics\TemplateArgumentConstraints; use PHPStan\Analyser\Generics\TemplateArgumentFrame; use PHPStan\Analyser\Generics\TemplateArgumentObserver; -use PHPStan\Analyser\Generics\TemplateArgumentResolver; use PHPStan\Analyser\Generics\TemplateArgumentStats; use PHPStan\DependencyInjection\AutowiredExtensions; use PHPStan\DependencyInjection\AutowiredParameter; @@ -61,18 +56,13 @@ use PHPStan\Node\MethodCallableNode; use PHPStan\Node\MethodCallExpressionNode; use PHPStan\Node\PropertyAssignNode; -use PHPStan\Node\PropertyHookStatementNode; use PHPStan\Node\ReturnAfterFinallyNode; use PHPStan\Node\ReturnStatement; use PHPStan\Node\StaticMethodCallableNode; use PHPStan\Node\StaticMethodCallExpressionNode; -use PHPStan\Node\UnreachableStatementNode; -use PHPStan\Node\VarTagChangedExpressionTypeNode; use PHPStan\Parser\ArrowFunctionArgVisitor; use PHPStan\Parser\ClosureArgVisitor; -use PHPStan\Parser\GotoLabelVisitor; use PHPStan\Parser\ImmediatelyInvokedClosureVisitor; -use PHPStan\PhpDoc\Tag\VarTag; use PHPStan\Reflection\Callables\SimpleImpurePoint; use PHPStan\Reflection\Callables\SimpleThrowPoint; use PHPStan\Reflection\ExtendedMethodReflection; @@ -92,7 +82,6 @@ use PHPStan\TrinaryLogic; use PHPStan\Type\ClosureType; use PHPStan\Type\ErrorType; -use PHPStan\Type\FileTypeMapper; use PHPStan\Type\FunctionParameterClosureThisExtension; use PHPStan\Type\FunctionParameterClosureTypeExtension; use PHPStan\Type\FunctionParameterOutTypeExtension; @@ -121,15 +110,12 @@ use function array_map; use function array_merge; use function array_pop; -use function array_slice; use function array_values; use function count; use function get_class; use function getenv; -use function implode; use function in_array; use function is_array; -use function is_int; use function is_string; use function max; use function spl_object_id; @@ -176,9 +162,6 @@ class NodeScopeResolver /** Whether the PHPSTAN_GUARD_NW diagnostic is enabled (cached from the env). */ public static bool $guardNewWorld = false; - /** PHPSTAN_TEMPLATE_ARGUMENTS_DEBUG=1 prints every second-pass re-walk/replay decision. */ - private static bool $debugTemplateArguments = false; - /** * spl_object_id => true of every Expr in the file's parsed AST. Populated * only when the PHPSTAN_GUARD_NW diagnostic is enabled, so the guards can @@ -214,7 +197,6 @@ class NodeScopeResolver public function __construct( private readonly Container $container, private readonly TemplateArgumentObserver $templateArgumentObserver, - private readonly TemplateArgumentResolver $templateArgumentResolver, private readonly ReflectionProvider $reflectionProvider, private readonly FileHelper $fileHelper, #[AutowiredExtensions(of: FunctionParameterOutTypeExtension::class)] @@ -223,7 +205,6 @@ public function __construct( private readonly ExtensionsCollection $methodParameterOutTypeExtensions, #[AutowiredExtensions(of: StaticMethodParameterOutTypeExtension::class)] private readonly ExtensionsCollection $staticMethodParameterOutTypeExtensions, - private readonly FileTypeMapper $fileTypeMapper, #[AutowiredExtensions(of: ReadWritePropertiesExtension::class)] private readonly ExtensionsCollection $readWritePropertiesExtensions, #[AutowiredExtensions(of: FunctionParameterClosureThisExtension::class)] @@ -249,12 +230,10 @@ public function __construct( #[AutowiredParameter] private readonly bool $treatPhpDocTypesAsCertain, private readonly ExpressionResultFactory $expressionResultFactory, - #[AutowiredParameter(ref: '%featureToggles.unresolvedTemplateArguments%')] - private readonly bool $unresolvedTemplateArguments, + private readonly StatementsHandler $statementsHandler, ) { self::$guardNewWorld = getenv('PHPSTAN_GUARD_NW') === '1'; - self::$debugTemplateArguments = getenv('PHPSTAN_TEMPLATE_ARGUMENTS_DEBUG') === '1'; TemplateArgumentStats::enableFromEnvironment(); } @@ -323,102 +302,15 @@ public function processNodes( $scope->pushExpressionResultStorage($expressionResultStorage); // a fresh walk an extension starts mid-analysis must not feed the // interrupted walk's gatherer frames (see processStmtNodes()) - $gatherers = $this->nodeGatherers; - $this->nodeGatherers = []; + $gatherers = $this->suspendNodeGatherers(); try { - $this->processNodesWithStorage($nodes, $scope, $expressionResultStorage, $nodeCallback); + $this->statementsHandler->processNodesWithStorage($this, $nodes, $scope, $expressionResultStorage, $nodeCallback); } finally { - $this->nodeGatherers = $gatherers; + $this->restoreNodeGatherers($gatherers); $scope->popExpressionResultStorage(); } } - /** - * @param Node[] $nodes - * @param callable(Node $node, Scope $scope): void $nodeCallback - */ - private function processNodesWithStorage( - array $nodes, - MutatingScope $scope, - ExpressionResultStorage $expressionResultStorage, - callable $nodeCallback, - ): void - { - $alreadyTerminated = false; - $exitPoints = []; - - $stmts = []; - $stmtToNodeIndex = []; - foreach ($nodes as $i => $node) { - if (!($node instanceof Node\Stmt)) { - continue; - } - - $stmtToNodeIndex[count($stmts)] = $i; - $stmts[] = $node; - } - - $dummyParent = new Node\Stmt\Nop(); - foreach ($stmts as $si => $node) { - if ($alreadyTerminated && !($node instanceof Node\Stmt\Function_ || $node instanceof Node\Stmt\ClassLike || $node instanceof Node\Stmt\Label)) { - continue; - } - - $nestedLabelNames = $node->getAttribute(GotoLabelVisitor::NESTED_BACKWARD_GOTO_LABELS_ATTRIBUTE); - if ($nestedLabelNames !== null) { - $scope = $this->resolveBackwardGotoScope( - $dummyParent, - [$node], - $scope, - $expressionResultStorage, - StatementContext::createDeep(), - static fn (string $name): bool => isset($nestedLabelNames[$name]), - false, - ); - } - - $statementResult = $this->processStmtNode($node, $scope, $expressionResultStorage, $nodeCallback, StatementContext::createTopLevel()); - $scope = $statementResult->getScope(); - - if ($node instanceof Node\Stmt\Label) { - $labelName = $node->name->toString(); - - [$scope, $alreadyTerminated, $exitPoints] = $this->mergeForwardGotoExitPoints( - $labelName, - $scope, - $alreadyTerminated, - $exitPoints, - ); - - if ($alreadyTerminated) { - continue; - } - - if ($node->getAttribute(GotoLabelVisitor::HAS_BACKWARD_GOTO_ATTRIBUTE) === true) { - $scope = $this->resolveBackwardGotoScope( - $dummyParent, - array_slice($stmts, $si + 1), - $scope, - $expressionResultStorage, - StatementContext::createDeep(), - static fn (string $name): bool => $name === $labelName, - true, - ); - } - } - - $exitPoints = array_merge($exitPoints, $statementResult->getExitPoints()); - - if ($alreadyTerminated || !$statementResult->isAlwaysTerminating()) { - continue; - } - - $alreadyTerminated = true; - $nextStmts = $this->getNextUnreachableStatements(array_slice($nodes, $stmtToNodeIndex[$si] + 1), true); - $this->processUnreachableStatement($nextStmts, $scope, $expressionResultStorage, $nodeCallback); - } - } - /** The stored result an outside asker may consume. */ public function findSettledExpressionResult(ExpressionResultStorage $storage, Expr $expr): ?ExpressionResult { @@ -452,10 +344,6 @@ public function storeExpressionResult(ExpressionResultStorage $storage, Expr $ex $storage->storeExpressionResult($expr, $expressionResult); } - /** - * @param Node\Stmt[] $bodyStmts - * @param Closure(string): bool $gotoNameMatcher - */ /** * Narrows a scope by a (often synthetic) control-flow condition the new-world * way: resolve its narrowing through the scope's on-demand dispatcher and apply @@ -468,130 +356,6 @@ public function narrowScopeWithCondition(MutatingScope $scope, Expr $expr, TypeS return $scope->applySpecifiedTypes($specifiedTypes); } - /** - * @param Node\Stmt[] $bodyStmts - * @param Closure(string): bool $gotoNameMatcher - */ - private function resolveBackwardGotoScope( - Node $parentNode, - array $bodyStmts, - MutatingScope $scope, - ExpressionResultStorage $storage, - StatementContext $context, - Closure $gotoNameMatcher, - bool $mergeBodyScopeEachIteration, - ): MutatingScope - { - $bodyScope = $scope; - $count = 0; - $prevEntryScope = null; - do { - $prevScope = $bodyScope; - if ($mergeBodyScopeEachIteration) { - $bodyScope = $bodyScope->mergeWith($scope); - } - if ($prevEntryScope !== null && $bodyScope->equals($prevEntryScope)) { - // walking is deterministic in the entry scope - an unchanged entry - // reproduces the previous pass's exit, so the verification walk is skipped - $bodyScope = $prevScope; - break; - } - $prevEntryScope = $bodyScope; - $tempStorage = $storage->duplicate(); - $bodyScopeResult = $this->processStmtNodesInternal( - $parentNode, - $bodyStmts, - $bodyScope, - $tempStorage, - new NoopNodeCallback(), - $context->withoutTemplateArgumentResolution(), - ); - - $gotoScope = null; - foreach ($bodyScopeResult->getExitPoints() as $ep) { - $epStmt = $ep->getStatement(); - if (!($epStmt instanceof Goto_) || !$gotoNameMatcher($epStmt->name->toString())) { - continue; - } - - $gotoScope = $gotoScope === null ? $ep->getScope() : $gotoScope->mergeWith($ep->getScope()); - } - - if ($gotoScope !== null) { - $bodyScope = $scope->mergeWith($gotoScope); - } - - if ($bodyScope->equals($prevScope)) { - break; - } - - if ($count >= self::GENERALIZE_AFTER_ITERATION) { - $bodyScope = $prevScope->generalizeWith($bodyScope); - } - $count++; - } while ($count < self::LOOP_SCOPE_ITERATIONS); - - return $bodyScope; - } - - /** - * @param InternalStatementExitPoint[] $exitPoints - * @return array{MutatingScope, bool, list} - */ - private function mergeForwardGotoExitPoints( - string $labelName, - MutatingScope $scope, - bool $alreadyTerminated, - array $exitPoints, - ): array - { - $newExitPoints = []; - foreach ($exitPoints as $exitPoint) { - $exitStmt = $exitPoint->getStatement(); - if ($exitStmt instanceof Goto_ && $exitStmt->name->toString() === $labelName) { - if ($alreadyTerminated) { - $scope = $exitPoint->getScope(); - $alreadyTerminated = false; - } else { - $scope = $scope->mergeWith($exitPoint->getScope()); - } - } else { - $newExitPoints[] = $exitPoint; - } - } - - return [$scope, $alreadyTerminated, $newExitPoints]; - } - - /** - * @param Node\Stmt[] $nextStmts - * @param callable(Node $node, Scope $scope): void $nodeCallback - */ - private function processUnreachableStatement(array $nextStmts, MutatingScope $scope, ExpressionResultStorage $storage, callable $nodeCallback): void - { - if ($nextStmts === []) { - return; - } - - $unreachableStatement = null; - $nextStatements = []; - - foreach ($nextStmts as $key => $nextStmt) { - if ($key === 0) { - $unreachableStatement = $nextStmt; - continue; - } - - $nextStatements[] = $nextStmt; - } - - if (!$unreachableStatement instanceof Node\Stmt) { - return; - } - - $this->callNodeCallback($nodeCallback, new UnreachableStatementNode($unreachableStatement, $nextStatements), $scope, $storage); - } - /** * @api * @param Node\Stmt[] $stmts @@ -615,8 +379,7 @@ public function processStmtNodes( // a fresh walk an extension starts mid-analysis must not feed the // interrupted walk's gatherer frames - they describe the body walk // that was interrupted, not the nested one - $gatherers = $this->nodeGatherers; - $this->nodeGatherers = []; + $gatherers = $this->suspendNodeGatherers(); try { return $this->processStmtNodesInternal( $parentNode, @@ -627,7 +390,7 @@ public function processStmtNodes( $context, )->toPublic(); } finally { - $this->nodeGatherers = $gatherers; + $this->restoreNodeGatherers($gatherers); $scope->popExpressionResultStorage(); } } @@ -656,7 +419,7 @@ public function processStmtNodesInternal( $scope->pushExpressionResultStorage($storage); } try { - return $this->doProcessStmtNodes($parentNode, $stmts, $scope, $storage, $nodeCallback, $context); + return $this->statementsHandler->doProcessStmtNodes($this, $parentNode, $stmts, $scope, $storage, $nodeCallback, $context); } finally { if ($pushStorage) { $scope->popExpressionResultStorage(); @@ -664,323 +427,6 @@ public function processStmtNodesInternal( } } - /** - * @param Node\Stmt[] $stmts - * @param callable(Node $node, Scope $scope): void $nodeCallback - */ - private function doProcessStmtNodes( - Node $parentNode, - array $stmts, - MutatingScope $scope, - ExpressionResultStorage $storage, - callable $nodeCallback, - StatementContext $context, - ): InternalStatementResult - { - $stmtCount = count($stmts); - $shouldCheckLastStatement = $parentNode instanceof Node\Stmt\Function_ - || $parentNode instanceof Node\Stmt\ClassMethod - || $parentNode instanceof PropertyHookStatementNode - || $parentNode instanceof Expr\Closure; - - if ( - $shouldCheckLastStatement - && $stmtCount > 0 - && $this->unresolvedTemplateArguments - && $context->shouldResolveTemplateArguments() - ) { - return $this->processBodyStmtNodesTwoPass($parentNode, $stmts, $scope, $storage, $nodeCallback, $context); - } - - $state = new StatementListWalkState($scope); - foreach ($stmts as $i => $stmt) { - $this->processStatementStep($parentNode, $stmts, $i, $stmt, $state, $storage, $nodeCallback, $context, $shouldCheckLastStatement); - } - - $statementResult = $state->toResult(); - if ($stmtCount === 0 && $shouldCheckLastStatement) { - $returnTypeNode = $parentNode->getReturnType(); - if ($parentNode instanceof Expr\Closure) { - $parentNode = new Node\Stmt\Expression($parentNode, $parentNode->getAttributes()); - } - // the body is empty - the statement above is a synthetic wrapper around - // the closure, not an expression statement that was processed - $this->callNodeCallback($nodeCallback, new ExecutionEndNode( - $parentNode, - $statementResult->toPublic(), - $returnTypeNode !== null, - ), $scope, $storage); - } - - return $statementResult; - } - - /** - * One statement of a statement list, advancing $state past it. - * - * @param Node\Stmt[] $stmts - * @param callable(Node $node, Scope $scope): void $nodeCallback - */ - private function processStatementStep( - Node $parentNode, - array $stmts, - int $i, - Node\Stmt $stmt, - StatementListWalkState $state, - ExpressionResultStorage $storage, - callable $nodeCallback, - StatementContext $context, - bool $shouldCheckLastStatement, - ): void - { - if ($state->alreadyTerminated && !($stmt instanceof Node\Stmt\Function_ || $stmt instanceof Node\Stmt\ClassLike || $stmt instanceof Node\Stmt\Label)) { - return; - } - - $isLast = $i === count($stmts) - 1; - - $nestedLabelNames = $stmt->getAttribute(GotoLabelVisitor::NESTED_BACKWARD_GOTO_LABELS_ATTRIBUTE); - if ($nestedLabelNames !== null && $context->isTopLevel()) { - $state->scope = $this->resolveBackwardGotoScope( - $parentNode, - [$stmt], - $state->scope, - $storage, - $context->enterDeep(), - static fn (string $name): bool => isset($nestedLabelNames[$name]), - false, - ); - } - - $statementResult = $this->processStmtNode( - $stmt, - $state->scope, - $storage, - $nodeCallback, - $context, - ); - $state->variableFlows[$i] = $statementResult->getVariableFlow(); - $state->scope = $statementResult->getScope(); - $state->hasYield = $state->hasYield || $statementResult->hasYield(); - - if ($stmt instanceof Node\Stmt\Label) { - $labelName = $stmt->name->toString(); - - [$state->scope, $state->alreadyTerminated, $state->exitPoints] = $this->mergeForwardGotoExitPoints( - $labelName, - $state->scope, - $state->alreadyTerminated, - $state->exitPoints, - ); - - if ($state->alreadyTerminated) { - return; - } - - if ($stmt->getAttribute(GotoLabelVisitor::HAS_BACKWARD_GOTO_ATTRIBUTE) === true && $context->isTopLevel()) { - $state->scope = $this->resolveBackwardGotoScope( - $parentNode, - array_slice($stmts, $i + 1), - $state->scope, - $storage, - $context->enterDeep(), - static fn (string $name): bool => $name === $labelName, - true, - ); - } - } - - if ($shouldCheckLastStatement && $isLast) { - $hasDeclaredReturnType = ($parentNode instanceof Node\FunctionLike || $parentNode instanceof PropertyHookStatementNode) - && $parentNode->getReturnType() !== null; - $endStatements = $statementResult->getEndStatements(); - if (count($endStatements) > 0) { - foreach ($endStatements as $endStatement) { - $endStatementResult = $endStatement->getResult(); - $this->callNodeCallback($nodeCallback, new ExecutionEndNode( - $endStatement->getStatement(), - (new InternalStatementResult( - $endStatementResult->getScope(), - $state->hasYield, - $endStatementResult->isAlwaysTerminating(), - $endStatementResult->getExitPoints(), - $endStatementResult->getThrowPoints(), - $endStatementResult->getImpurePoints(), - ))->toPublic(), - $hasDeclaredReturnType, - $this->readEndStatementExprResult($endStatement->getStatement(), $storage), - ), $endStatementResult->getScope(), $storage); - } - } else { - $this->callNodeCallback($nodeCallback, new ExecutionEndNode( - $stmt, - (new InternalStatementResult( - $state->scope, - $state->hasYield, - $statementResult->isAlwaysTerminating(), - $statementResult->getExitPoints(), - $statementResult->getThrowPoints(), - $statementResult->getImpurePoints(), - ))->toPublic(), - $hasDeclaredReturnType, - $this->readEndStatementExprResult($stmt, $storage), - ), $state->scope, $storage); - } - } - - $state->exitPoints = array_merge($state->exitPoints, $statementResult->getExitPoints()); - $state->throwPoints = array_merge($state->throwPoints, $statementResult->getThrowPoints()); - $state->impurePoints = array_merge($state->impurePoints, $statementResult->getImpurePoints()); - - if ($state->alreadyTerminated || !$statementResult->isAlwaysTerminating()) { - return; - } - - $state->alreadyTerminated = true; - $nextStmts = $this->getNextUnreachableStatements(array_slice($stmts, $i + 1), $parentNode instanceof Node\Stmt\Namespace_); - $this->processUnreachableStatement($nextStmts, $state->scope, $storage, $nodeCallback); - } - - /** - * A function-like body under the unresolvedTemplateArguments toggle is - * walked in two passes. The observation pass walks every statement - * recording rule-facing emissions and threading immutable constraints - * through the scopes returned by expressions and statements. A body that - * created no unresolved template argument simply replays the recording. - * Otherwise TemplateArgumentResolver builds a new resolved frame for the - * second pass, which re-walks only the statements the resolutions can - * influence. Recorded scopes retain their original collection context. - * - * The outer gatherer frames (the method's return statements, execution - * ends, impure points) are suspended during the observation pass and fed - * by the replay and the re-walk, so each emission reaches them once. - * - * @param Node\Stmt[] $stmts - * @param callable(Node $node, Scope $scope): void $nodeCallback - */ - private function processBodyStmtNodesTwoPass( - Node $parentNode, - array $stmts, - MutatingScope $scope, - ExpressionResultStorage $storage, - callable $nodeCallback, - StatementContext $context, - ): InternalStatementResult - { - $statementStartTokenPositions = []; - foreach ($stmts as $stmt) { - $statementStartTokenPositions[] = $stmt->getStartTokenPos(); - } - $parentFrame = $scope->getCurrentTemplateArgumentFrame(); - $parentConstraints = $scope->getTemplateArgumentConstraints(); - $frame = new TemplateArgumentFrame($parentFrame); - if (TemplateArgumentStats::$enabled) { - TemplateArgumentStats::increment('bodiesWalked'); - TemplateArgumentStats::increment('statementsTotal', count($stmts)); - } - $scope = $scope->withTemplateArgumentFrame($frame)->withTemplateArgumentConstraints(null); - $recording = new RecordingNodeCallback(); - $state = new StatementListWalkState($scope); - /** @var list $entries the state and recording offset before each statement, plus the final ones */ - $entries = []; - $observationContext = $context->withoutTemplateArgumentResolution(); - $suspendedGatherers = $this->nodeGatherers; - $this->nodeGatherers = []; - try { - foreach ($stmts as $i => $stmt) { - $entries[$i] = [clone $state, $recording->count()]; - $this->processStatementStep($parentNode, $stmts, $i, $stmt, $state, $storage, $recording, $observationContext, true); - } - } finally { - $this->nodeGatherers = $suspendedGatherers; - } - $frame = $this->templateArgumentResolver->resolve($state->scope->getTemplateArgumentConstraints() ?? TemplateArgumentConstraints::createEmpty(), $parentFrame, $statementStartTokenPositions); - $stmtCount = count($stmts); - $entries[$stmtCount] = [clone $state, $recording->count()]; - - $firstSiteStatementIndex = $frame->firstSiteStatementIndex(); - if ($firstSiteStatementIndex === null) { - $this->replayRecordingRange($recording, 0, $recording->count(), $nodeCallback, $storage, $scope); - - $state->scope = $state->scope->withTemplateArgumentFrame($parentFrame)->withTemplateArgumentConstraints($parentConstraints); - return $state->toResult(); - } - - if (TemplateArgumentStats::$enabled) { - TemplateArgumentStats::increment('bodiesWithSites'); - TemplateArgumentStats::increment('statementsReplayed', $firstSiteStatementIndex); - } - // the second pass: the statements before the first site stand as - // recorded; from there on a statement is re-walked only when it - // created a site or mentions a variable whose tracked state the - // resolutions changed - the rest replay their recording and carry - // their recorded effect onto the re-walked scope - $this->replayRecordingRange($recording, 0, $entries[$firstSiteStatementIndex][1], $nodeCallback, $storage, $scope); - $hasLabels = false; - foreach ($stmts as $stmt) { - if (!$stmt instanceof Node\Stmt\Label && $stmt->getAttribute(GotoLabelVisitor::NESTED_BACKWARD_GOTO_LABELS_ATTRIBUTE) === null) { - continue; - } - $hasLabels = true; - break; - } - $state = clone $entries[$firstSiteStatementIndex][0]; - $state->scope = $state->scope->withTemplateArgumentFrame($frame)->withTemplateArgumentConstraints(null); - for ($i = $firstSiteStatementIndex; $i < $stmtCount; $i++) { - [$recordedEntry, $offset] = $entries[$i]; - [$recordedExit, $nextOffset] = $entries[$i + 1]; - $differingRoots = $state->alreadyTerminated === $recordedEntry->alreadyTerminated - ? $state->scope->getDifferingVariableRoots($recordedEntry->scope) - : null; - if ($differingRoots === [] && !$frame->hasSiteAtOrAfter($i)) { - // converged with the observation pass: the rest of its recording stands - if (TemplateArgumentStats::$enabled) { - TemplateArgumentStats::increment('earlyExits'); - TemplateArgumentStats::increment('statementsReplayed', $stmtCount - $i); - } - $this->replayRecordingRange($recording, $offset, $recording->count(), $nodeCallback, $storage, $scope); - $this->appendRecordedStatementResults($state, $recordedEntry, $entries[$stmtCount][0]); - $state->scope = $entries[$stmtCount][0]->scope; - - $state->scope = $state->scope->withTemplateArgumentFrame($parentFrame)->withTemplateArgumentConstraints($parentConstraints); - return $state->toResult(); - } - - $stmt = $stmts[$i]; - $reWalk = $differingRoots === null - || $hasLabels - || $frame->ownsSiteInStatement($i) - || $this->statementMentionsAnyVariable($stmt, $differingRoots); - if (self::$debugTemplateArguments) { - echo sprintf( - "[template-arguments] %s:%d statement %d: %s (differing: %s)\n", - $scope->getFile(), - $stmt->getStartLine(), - $i, - $reWalk ? 're-walk' : 'replay', - $differingRoots === null ? 'non-variable key' : implode(', ', $differingRoots), - ); - } - if ($reWalk) { - if (TemplateArgumentStats::$enabled) { - TemplateArgumentStats::increment('statementsReWalked'); - } - $this->processStatementStep($parentNode, $stmts, $i, $stmt, $state, $storage, $nodeCallback, $context, true); - continue; - } - - if (TemplateArgumentStats::$enabled) { - TemplateArgumentStats::increment('statementsReplayed'); - } - $this->replayRecordingRange($recording, $offset, $nextOffset, $nodeCallback, $storage, $scope); - $this->appendRecordedStatementResults($state, $recordedEntry, $recordedExit); - $state->scope = $state->scope->withRecordedStatementDelta($recordedEntry->scope, $recordedExit->scope); - } - - $state->scope = $state->scope->withTemplateArgumentFrame($parentFrame)->withTemplateArgumentConstraints($parentConstraints); - return $state->toResult(); - } - /** * The parameter type an argument is observed against: the declared one with * the template types the call already decided substituted - the receiver's @@ -1025,127 +471,6 @@ private function findOriginalParameterType(ParametersAcceptor $acceptor, Paramet return null; } - /** Carries what the recorded walk from $from to $to added onto $state. */ - private function appendRecordedStatementResults(StatementListWalkState $state, StatementListWalkState $from, StatementListWalkState $to): void - { - foreach ($to->variableFlows as $index => $flow) { - if (array_key_exists($index, $from->variableFlows)) { - continue; - } - - $state->variableFlows[$index] = $flow; - } - $state->hasYield = $state->hasYield || ($to->hasYield && !$from->hasYield); - $state->alreadyTerminated = $state->alreadyTerminated || ($to->alreadyTerminated && !$from->alreadyTerminated); - $state->exitPoints = array_merge($state->exitPoints, array_slice($to->exitPoints, count($from->exitPoints))); - $state->throwPoints = array_merge($state->throwPoints, array_slice($to->throwPoints, count($from->throwPoints))); - $state->impurePoints = array_merge($state->impurePoints, array_slice($to->impurePoints, count($from->impurePoints))); - } - - public function getVariableMentionFlow(Node\Stmt $stmt): ?VariableFlow - { - $names = []; - $mentionsEverything = false; - $this->collectMentionedVariables($stmt, $names, $mentionsEverything); - if ($mentionsEverything) { - return VariableFlow::all(VariableFlow::MENTION_ALL); - } - - $flows = []; - foreach (array_keys($names) as $name) { - $flows[] = VariableFlow::mention($name); - } - return VariableFlow::sequence(...$flows); - } - - private const MENTIONED_VARIABLES_ATTRIBUTE = 'templateArgumentMentionedVariables'; - - /** - * @param list $variableNames - */ - private function statementMentionsAnyVariable(Node\Stmt $stmt, array $variableNames): bool - { - /** @var array{array, bool}|null $mentions */ - $mentions = $stmt->getAttribute(self::MENTIONED_VARIABLES_ATTRIBUTE); - if ($mentions === null) { - $names = []; - $mentionsEverything = false; - $this->collectMentionedVariables($stmt, $names, $mentionsEverything); - $mentions = [$names, $mentionsEverything]; - $stmt->setAttribute(self::MENTIONED_VARIABLES_ATTRIBUTE, $mentions); - } - [$names, $mentionsEverything] = $mentions; - if ($mentionsEverything) { - return true; - } - foreach ($variableNames as $variableName) { - if (isset($names[$variableName])) { - return true; - } - } - - return false; - } - - /** - * Every variable the statement can read or write - syntactically, so - * reads and writes alike - with `$this`; a closure body lives in its own - * scope, so only its use() clause (and its bound `$this`) count, an arrow - * function captures implicitly and is traversed. Dynamic access - * (`$$name`, compact(), extract(), get_defined_vars(), eval, include) - * mentions everything. - * - * @param array $names - */ - private function collectMentionedVariables(Node $node, array &$names, bool &$mentionsEverything): void - { - if ($node instanceof Expr\Variable) { - if (!is_string($node->name)) { - $mentionsEverything = true; - return; - } - $names[$node->name] = true; - return; - } - if ($node instanceof Expr\Closure) { - if (!$node->static) { - $names['this'] = true; - } - foreach ($node->uses as $use) { - if (!is_string($use->var->name)) { - $mentionsEverything = true; - continue; - } - $names[$use->var->name] = true; - } - - return; - } - if ($node instanceof Expr\Eval_ || $node instanceof Expr\Include_) { - $mentionsEverything = true; - } elseif ( - $node instanceof Expr\FuncCall - && $node->name instanceof Name - && in_array($node->name->toLowerString(), ['compact', 'extract', 'get_defined_vars'], true) - ) { - $mentionsEverything = true; - } - - foreach ($node->getSubNodeNames() as $subNodeName) { - $subNode = $node->$subNodeName; - if ($subNode instanceof Node) { - $this->collectMentionedVariables($subNode, $names, $mentionsEverything); - } elseif (is_array($subNode)) { - foreach ($subNode as $item) { - if (!$item instanceof Node) { - continue; - } - $this->collectMentionedVariables($item, $names, $mentionsEverything); - } - } - } - } - /** * @param callable(Node $node, Scope $scope): void $nodeCallback */ @@ -1169,9 +494,9 @@ public function processStmtNode( && !$stmt instanceof Node\Stmt\ClassMethod ) { if (!$stmt instanceof Foreach_) { - $scope = $this->processStmtVarAnnotation($scope, $storage, $stmt, null, $nodeCallback); + $scope = $this->statementsHandler->processStmtVarAnnotation($this, $scope, $storage, $stmt, null, $nodeCallback); } - $overridingThrowPoints = $this->getOverridingThrowPoints($stmt, $scope); + $overridingThrowPoints = $this->statementsHandler->getOverridingThrowPoints($stmt, $scope); } if ($stmt instanceof Node\Stmt\ClassMethod) { @@ -1241,39 +566,6 @@ public function processStmtNode( return new InternalStatementResult($scope, hasYield: false, isAlwaysTerminating: false, exitPoints: [], throwPoints: $overridingThrowPoints ?? [], impurePoints: []); } - /** - * @return InternalThrowPoint[]|null - */ - private function getOverridingThrowPoints(Node\Stmt $statement, MutatingScope $scope): ?array - { - foreach ($statement->getComments() as $comment) { - if (!$comment instanceof Doc) { - continue; - } - - $function = $scope->getFunction(); - $resolvedPhpDoc = $this->fileTypeMapper->getResolvedPhpDoc( - $scope->getFile(), - $scope->isInClass() ? $scope->getClassReflection()->getName() : null, - $scope->isInTrait() ? $scope->getTraitReflection()->getName() : null, - $function !== null ? $function->getName() : null, - $comment->getText(), - ); - - $throwsTag = $resolvedPhpDoc->getThrowsTag(); - if ($throwsTag !== null) { - $throwsType = $throwsTag->getType(); - if ($throwsType->isVoid()->yes()) { - return []; - } - - return [InternalThrowPoint::createExplicit($scope, $throwsType, $statement, false)]; - } - } - - return null; - } - public function isAnalysedFile(string $fileName): bool { return isset($this->analysedFiles[$fileName]); @@ -1433,22 +725,6 @@ public function readStoredResult(Expr $expr, ExpressionResultStorage $storage): return $result; } - /** - * The result of the expression an ending statement evaluated, for - * ExecutionEndNode. Null when the statement is not an expression statement, - * and when its expression was not processed into this storage - an end - * statement is collected from wherever execution ended, which can be a - * nested walk whose results never reach the frame the end node is built in. - */ - private function readEndStatementExprResult(Node\Stmt $stmt, ExpressionResultStorage $storage): ?ExpressionResult - { - if (!$stmt instanceof Node\Stmt\Expression) { - return null; - } - - return $storage->findExpressionResult($stmt->expr); - } - /** * The result processArgs() captured for a call argument. Unlike the storage * lookup this reads the argument's authoritative result: a closure/arrow @@ -1848,6 +1124,28 @@ public function popNodeGatherer(): void array_pop($this->nodeGatherers); } + /** + * Detaches all gatherer frames for a walk that must not feed them. The + * caller reattaches them in a finally block via restoreNodeGatherers(). + * + * @return list + */ + public function suspendNodeGatherers(): array + { + $gatherers = $this->nodeGatherers; + $this->nodeGatherers = []; + + return $gatherers; + } + + /** + * @param list $gatherers + */ + public function restoreNodeGatherers(array $gatherers): void + { + $this->nodeGatherers = $gatherers; + } + /** * @param callable(Node $node, Scope $scope): void $nodeCallback */ @@ -3717,147 +3015,6 @@ public function processVirtualAssign(MutatingScope $scope, ExpressionResultStora ); } - /** - * @param callable(Node $node, Scope $scope): void $nodeCallback - */ - public function processStmtVarAnnotation(MutatingScope $scope, ExpressionResultStorage $storage, Node\Stmt $stmt, ?Expr $defaultExpr, callable $nodeCallback): MutatingScope - { - $function = $scope->getFunction(); - $variableLessTags = []; - - foreach ($stmt->getComments() as $comment) { - if (!$comment instanceof Doc) { - continue; - } - - $resolvedPhpDoc = $this->fileTypeMapper->getResolvedPhpDoc( - $scope->getFile(), - $scope->isInClass() ? $scope->getClassReflection()->getName() : null, - $scope->isInTrait() ? $scope->getTraitReflection()->getName() : null, - $function !== null ? $function->getName() : null, - $comment->getText(), - ); - - $assignedVariable = null; - if ( - $stmt instanceof Node\Stmt\Expression - && ($stmt->expr instanceof Assign || $stmt->expr instanceof AssignRef) - && $stmt->expr->var instanceof Variable - && is_string($stmt->expr->var->name) - ) { - $assignedVariable = $stmt->expr->var->name; - } - - foreach ($resolvedPhpDoc->getVarTags() as $name => $varTag) { - if (is_int($name)) { - $variableLessTags[] = $varTag; - continue; - } - - if ($name === $assignedVariable) { - continue; - } - - $certainty = $scope->hasVariableType($name); - if ($certainty->no()) { - continue; - } - - if ($scope->isInClass() && $scope->getFunction() === null) { - continue; - } - - if ($scope->canAnyVariableExist()) { - $certainty = TrinaryLogic::createYes(); - } - - $variableNode = new Variable($name, $stmt->getAttributes()); - $originalType = $scope->getVariableType($name); - if (!$originalType->equals($varTag->getType())) { - $this->callNodeCallback($nodeCallback, new VarTagChangedExpressionTypeNode($varTag, $variableNode), $scope, $storage); - } - $templateArgumentFrame = $this->observingTemplateArgumentFrame($scope); - if ($templateArgumentFrame !== null) { - $scope = $scope->addTemplateArgumentConstraints($this->templateArgumentObserver->collectSend($varTag->getType(), $originalType)); - } - - $nativeScope = $scope->doNotTreatPhpDocTypesAsCertain(); - $scope = $scope->assignVariable( - $name, - $varTag->getType(), - // a plain variable read is scope state - $nativeScope->hasVariableType($name)->no() ? new ErrorType() : $nativeScope->getVariableType($name), - $certainty, - ); - } - } - - if (count($variableLessTags) === 1 && $defaultExpr !== null) { - // only the scope effect here: the changed-type node is emitted by the - // statement handler AFTER it walked the expression (emitVarTagChangedNode), - // so the rule prices the expression from its stored result rather than - // asking the scope about a node not processed yet - $scope = $scope->assignExpression($defaultExpr, $variableLessTags[0]->getType(), new MixedType()); - } - - return $scope; - } - - /** - * The single variable-less @var tag on the statement's doc comment, or null - * when there is none or more than one - the shape processStmtVarAnnotation() - * applies to $defaultExpr and emitVarTagChangedNode() reports on. - */ - private function findSingleVariableLessVarTag(MutatingScope $scope, Node\Stmt $stmt): ?VarTag - { - $function = $scope->getFunction(); - $variableLessTags = []; - foreach ($stmt->getComments() as $comment) { - if (!$comment instanceof Doc) { - continue; - } - - $resolvedPhpDoc = $this->fileTypeMapper->getResolvedPhpDoc( - $scope->getFile(), - $scope->isInClass() ? $scope->getClassReflection()->getName() : null, - $scope->isInTrait() ? $scope->getTraitReflection()->getName() : null, - $function !== null ? $function->getName() : null, - $comment->getText(), - ); - foreach ($resolvedPhpDoc->getVarTags() as $name => $varTag) { - if (!is_int($name)) { - continue; - } - - $variableLessTags[] = $varTag; - } - } - - return count($variableLessTags) === 1 ? $variableLessTags[0] : null; - } - - /** - * Emits the node the @var-changed-type rule listens to, for a statement with - * a single variable-less @var tag over $defaultExpr - called by the handler - * once the expression has been walked (see processStmtVarAnnotation()). - * - * @param callable(Node $node, Scope $scope): void $nodeCallback - */ - public function emitVarTagChangedNode(MutatingScope $scope, ExpressionResultStorage $storage, Node\Stmt $stmt, Expr $defaultExpr, callable $nodeCallback): TemplateArgumentConstraints - { - $varTag = $this->findSingleVariableLessVarTag($scope, $stmt); - if ($varTag === null) { - return TemplateArgumentConstraints::createEmpty(); - } - - $this->callNodeCallback($nodeCallback, new VarTagChangedExpressionTypeNode($varTag, $defaultExpr), $scope, $storage); - $defaultExprResult = $storage->findExpressionResult($defaultExpr); - if ($defaultExprResult === null || $this->observingTemplateArgumentFrame($defaultExprResult->getScope()) === null) { - return TemplateArgumentConstraints::createEmpty(); - } - return $this->templateArgumentObserver->collectSend($varTag->getType(), $defaultExprResult->getType()); - } - /** * The template argument frame of the body being walked while it observes * a body that created unresolved template arguments - null otherwise, so @@ -3922,35 +3079,4 @@ public function collectOffsetSetUsage(MutatingScope $scope, Type $receiverType, return $constraints; } - /** - * @param array $nodes - * @return list - */ - private function getNextUnreachableStatements(array $nodes, bool $earlyBinding): array - { - $stmts = []; - $isPassedUnreachableStatement = false; - foreach ($nodes as $node) { - if ($node instanceof Node\Stmt\Label) { - break; - } - if ($earlyBinding && ($node instanceof Node\Stmt\Function_ || $node instanceof Node\Stmt\ClassLike || $node instanceof Node\Stmt\HaltCompiler)) { - continue; - } - if ($isPassedUnreachableStatement && $node instanceof Node\Stmt) { - $stmts[] = $node; - continue; - } - if ($node instanceof Node\Stmt\Nop || $node instanceof Node\Stmt\InlineHTML) { - continue; - } - if (!$node instanceof Node\Stmt) { - continue; - } - $stmts[] = $node; - $isPassedUnreachableStatement = true; - } - return $stmts; - } - } diff --git a/src/Analyser/StatementsHandler.php b/src/Analyser/StatementsHandler.php new file mode 100644 index 00000000000..c306b69bc72 --- /dev/null +++ b/src/Analyser/StatementsHandler.php @@ -0,0 +1,945 @@ +debugTemplateArguments = getenv('PHPSTAN_TEMPLATE_ARGUMENTS_DEBUG') === '1'; + } + + /** + * @param Node[] $nodes + * @param callable(Node $node, Scope $scope): void $nodeCallback + */ + public function processNodesWithStorage( + NodeScopeResolver $nodeScopeResolver, + array $nodes, + MutatingScope $scope, + ExpressionResultStorage $expressionResultStorage, + callable $nodeCallback, + ): void + { + $alreadyTerminated = false; + $exitPoints = []; + + $stmts = []; + $stmtToNodeIndex = []; + foreach ($nodes as $i => $node) { + if (!($node instanceof Node\Stmt)) { + continue; + } + + $stmtToNodeIndex[count($stmts)] = $i; + $stmts[] = $node; + } + + $dummyParent = new Node\Stmt\Nop(); + foreach ($stmts as $si => $node) { + if ($alreadyTerminated && !($node instanceof Node\Stmt\Function_ || $node instanceof Node\Stmt\ClassLike || $node instanceof Node\Stmt\Label)) { + continue; + } + + $nestedLabelNames = $node->getAttribute(GotoLabelVisitor::NESTED_BACKWARD_GOTO_LABELS_ATTRIBUTE); + if ($nestedLabelNames !== null) { + $scope = $this->resolveBackwardGotoScope( + $nodeScopeResolver, + $dummyParent, + [$node], + $scope, + $expressionResultStorage, + StatementContext::createDeep(), + static fn (string $name): bool => isset($nestedLabelNames[$name]), + false, + ); + } + + $statementResult = $nodeScopeResolver->processStmtNode($node, $scope, $expressionResultStorage, $nodeCallback, StatementContext::createTopLevel()); + $scope = $statementResult->getScope(); + + if ($node instanceof Node\Stmt\Label) { + $labelName = $node->name->toString(); + + [$scope, $alreadyTerminated, $exitPoints] = $this->mergeForwardGotoExitPoints( + $labelName, + $scope, + $alreadyTerminated, + $exitPoints, + ); + + if ($alreadyTerminated) { + continue; + } + + if ($node->getAttribute(GotoLabelVisitor::HAS_BACKWARD_GOTO_ATTRIBUTE) === true) { + $scope = $this->resolveBackwardGotoScope( + $nodeScopeResolver, + $dummyParent, + array_slice($stmts, $si + 1), + $scope, + $expressionResultStorage, + StatementContext::createDeep(), + static fn (string $name): bool => $name === $labelName, + true, + ); + } + } + + $exitPoints = array_merge($exitPoints, $statementResult->getExitPoints()); + + if ($alreadyTerminated || !$statementResult->isAlwaysTerminating()) { + continue; + } + + $alreadyTerminated = true; + $nextStmts = $this->getNextUnreachableStatements(array_slice($nodes, $stmtToNodeIndex[$si] + 1), true); + $this->processUnreachableStatement($nodeScopeResolver, $nextStmts, $scope, $expressionResultStorage, $nodeCallback); + } + } + + /** + * @param Node\Stmt[] $bodyStmts + * @param Closure(string): bool $gotoNameMatcher + */ + private function resolveBackwardGotoScope( + NodeScopeResolver $nodeScopeResolver, + Node $parentNode, + array $bodyStmts, + MutatingScope $scope, + ExpressionResultStorage $storage, + StatementContext $context, + Closure $gotoNameMatcher, + bool $mergeBodyScopeEachIteration, + ): MutatingScope + { + $bodyScope = $scope; + $count = 0; + $prevEntryScope = null; + do { + $prevScope = $bodyScope; + if ($mergeBodyScopeEachIteration) { + $bodyScope = $bodyScope->mergeWith($scope); + } + if ($prevEntryScope !== null && $bodyScope->equals($prevEntryScope)) { + // walking is deterministic in the entry scope - an unchanged entry + // reproduces the previous pass's exit, so the verification walk is skipped + $bodyScope = $prevScope; + break; + } + $prevEntryScope = $bodyScope; + $tempStorage = $storage->duplicate(); + $bodyScopeResult = $nodeScopeResolver->processStmtNodesInternal( + $parentNode, + $bodyStmts, + $bodyScope, + $tempStorage, + new NoopNodeCallback(), + $context->withoutTemplateArgumentResolution(), + ); + + $gotoScope = null; + foreach ($bodyScopeResult->getExitPoints() as $ep) { + $epStmt = $ep->getStatement(); + if (!($epStmt instanceof Goto_) || !$gotoNameMatcher($epStmt->name->toString())) { + continue; + } + + $gotoScope = $gotoScope === null ? $ep->getScope() : $gotoScope->mergeWith($ep->getScope()); + } + + if ($gotoScope !== null) { + $bodyScope = $scope->mergeWith($gotoScope); + } + + if ($bodyScope->equals($prevScope)) { + break; + } + + if ($count >= NodeScopeResolver::GENERALIZE_AFTER_ITERATION) { + $bodyScope = $prevScope->generalizeWith($bodyScope); + } + $count++; + } while ($count < NodeScopeResolver::LOOP_SCOPE_ITERATIONS); + + return $bodyScope; + } + + /** + * @param InternalStatementExitPoint[] $exitPoints + * @return array{MutatingScope, bool, list} + */ + private function mergeForwardGotoExitPoints( + string $labelName, + MutatingScope $scope, + bool $alreadyTerminated, + array $exitPoints, + ): array + { + $newExitPoints = []; + foreach ($exitPoints as $exitPoint) { + $exitStmt = $exitPoint->getStatement(); + if ($exitStmt instanceof Goto_ && $exitStmt->name->toString() === $labelName) { + if ($alreadyTerminated) { + $scope = $exitPoint->getScope(); + $alreadyTerminated = false; + } else { + $scope = $scope->mergeWith($exitPoint->getScope()); + } + } else { + $newExitPoints[] = $exitPoint; + } + } + + return [$scope, $alreadyTerminated, $newExitPoints]; + } + + /** + * @param Node\Stmt[] $nextStmts + * @param callable(Node $node, Scope $scope): void $nodeCallback + */ + private function processUnreachableStatement(NodeScopeResolver $nodeScopeResolver, array $nextStmts, MutatingScope $scope, ExpressionResultStorage $storage, callable $nodeCallback): void + { + if ($nextStmts === []) { + return; + } + + $unreachableStatement = null; + $nextStatements = []; + + foreach ($nextStmts as $key => $nextStmt) { + if ($key === 0) { + $unreachableStatement = $nextStmt; + continue; + } + + $nextStatements[] = $nextStmt; + } + + if (!$unreachableStatement instanceof Node\Stmt) { + return; + } + + $nodeScopeResolver->callNodeCallback($nodeCallback, new UnreachableStatementNode($unreachableStatement, $nextStatements), $scope, $storage); + } + + /** + * @param array $nodes + * @return list + */ + private function getNextUnreachableStatements(array $nodes, bool $earlyBinding): array + { + $stmts = []; + $isPassedUnreachableStatement = false; + foreach ($nodes as $node) { + if ($node instanceof Node\Stmt\Label) { + break; + } + if ($earlyBinding && ($node instanceof Node\Stmt\Function_ || $node instanceof Node\Stmt\ClassLike || $node instanceof Node\Stmt\HaltCompiler)) { + continue; + } + if ($isPassedUnreachableStatement && $node instanceof Node\Stmt) { + $stmts[] = $node; + continue; + } + if ($node instanceof Node\Stmt\Nop || $node instanceof Node\Stmt\InlineHTML) { + continue; + } + if (!$node instanceof Node\Stmt) { + continue; + } + $stmts[] = $node; + $isPassedUnreachableStatement = true; + } + return $stmts; + } + + /** + * @param Node\Stmt[] $stmts + * @param callable(Node $node, Scope $scope): void $nodeCallback + */ + public function doProcessStmtNodes( + NodeScopeResolver $nodeScopeResolver, + Node $parentNode, + array $stmts, + MutatingScope $scope, + ExpressionResultStorage $storage, + callable $nodeCallback, + StatementContext $context, + ): InternalStatementResult + { + $stmtCount = count($stmts); + $shouldCheckLastStatement = $parentNode instanceof Node\Stmt\Function_ + || $parentNode instanceof Node\Stmt\ClassMethod + || $parentNode instanceof PropertyHookStatementNode + || $parentNode instanceof Expr\Closure; + + if ( + $shouldCheckLastStatement + && $stmtCount > 0 + && $this->unresolvedTemplateArguments + && $context->shouldResolveTemplateArguments() + ) { + return $this->processBodyStmtNodesTwoPass($nodeScopeResolver, $parentNode, $stmts, $scope, $storage, $nodeCallback, $context); + } + + $state = new StatementListWalkState($scope); + foreach ($stmts as $i => $stmt) { + $this->processStatementStep($nodeScopeResolver, $parentNode, $stmts, $i, $stmt, $state, $storage, $nodeCallback, $context, $shouldCheckLastStatement); + } + + $statementResult = $state->toResult(); + if ($stmtCount === 0 && $shouldCheckLastStatement) { + $returnTypeNode = $parentNode->getReturnType(); + if ($parentNode instanceof Expr\Closure) { + $parentNode = new Node\Stmt\Expression($parentNode, $parentNode->getAttributes()); + } + // the body is empty - the statement above is a synthetic wrapper around + // the closure, not an expression statement that was processed + $nodeScopeResolver->callNodeCallback($nodeCallback, new ExecutionEndNode( + $parentNode, + $statementResult->toPublic(), + $returnTypeNode !== null, + ), $scope, $storage); + } + + return $statementResult; + } + + /** + * One statement of a statement list, advancing $state past it. + * + * @param Node\Stmt[] $stmts + * @param callable(Node $node, Scope $scope): void $nodeCallback + */ + private function processStatementStep( + NodeScopeResolver $nodeScopeResolver, + Node $parentNode, + array $stmts, + int $i, + Node\Stmt $stmt, + StatementListWalkState $state, + ExpressionResultStorage $storage, + callable $nodeCallback, + StatementContext $context, + bool $shouldCheckLastStatement, + ): void + { + if ($state->alreadyTerminated && !($stmt instanceof Node\Stmt\Function_ || $stmt instanceof Node\Stmt\ClassLike || $stmt instanceof Node\Stmt\Label)) { + return; + } + + $isLast = $i === count($stmts) - 1; + + $nestedLabelNames = $stmt->getAttribute(GotoLabelVisitor::NESTED_BACKWARD_GOTO_LABELS_ATTRIBUTE); + if ($nestedLabelNames !== null && $context->isTopLevel()) { + $state->scope = $this->resolveBackwardGotoScope( + $nodeScopeResolver, + $parentNode, + [$stmt], + $state->scope, + $storage, + $context->enterDeep(), + static fn (string $name): bool => isset($nestedLabelNames[$name]), + false, + ); + } + + $statementResult = $nodeScopeResolver->processStmtNode( + $stmt, + $state->scope, + $storage, + $nodeCallback, + $context, + ); + $state->variableFlows[$i] = $statementResult->getVariableFlow(); + $state->scope = $statementResult->getScope(); + $state->hasYield = $state->hasYield || $statementResult->hasYield(); + + if ($stmt instanceof Node\Stmt\Label) { + $labelName = $stmt->name->toString(); + + [$state->scope, $state->alreadyTerminated, $state->exitPoints] = $this->mergeForwardGotoExitPoints( + $labelName, + $state->scope, + $state->alreadyTerminated, + $state->exitPoints, + ); + + if ($state->alreadyTerminated) { + return; + } + + if ($stmt->getAttribute(GotoLabelVisitor::HAS_BACKWARD_GOTO_ATTRIBUTE) === true && $context->isTopLevel()) { + $state->scope = $this->resolveBackwardGotoScope( + $nodeScopeResolver, + $parentNode, + array_slice($stmts, $i + 1), + $state->scope, + $storage, + $context->enterDeep(), + static fn (string $name): bool => $name === $labelName, + true, + ); + } + } + + if ($shouldCheckLastStatement && $isLast) { + $hasDeclaredReturnType = ($parentNode instanceof Node\FunctionLike || $parentNode instanceof PropertyHookStatementNode) + && $parentNode->getReturnType() !== null; + $endStatements = $statementResult->getEndStatements(); + if (count($endStatements) > 0) { + foreach ($endStatements as $endStatement) { + $endStatementResult = $endStatement->getResult(); + $nodeScopeResolver->callNodeCallback($nodeCallback, new ExecutionEndNode( + $endStatement->getStatement(), + (new InternalStatementResult( + $endStatementResult->getScope(), + $state->hasYield, + $endStatementResult->isAlwaysTerminating(), + $endStatementResult->getExitPoints(), + $endStatementResult->getThrowPoints(), + $endStatementResult->getImpurePoints(), + ))->toPublic(), + $hasDeclaredReturnType, + $this->readEndStatementExprResult($endStatement->getStatement(), $storage), + ), $endStatementResult->getScope(), $storage); + } + } else { + $nodeScopeResolver->callNodeCallback($nodeCallback, new ExecutionEndNode( + $stmt, + (new InternalStatementResult( + $state->scope, + $state->hasYield, + $statementResult->isAlwaysTerminating(), + $statementResult->getExitPoints(), + $statementResult->getThrowPoints(), + $statementResult->getImpurePoints(), + ))->toPublic(), + $hasDeclaredReturnType, + $this->readEndStatementExprResult($stmt, $storage), + ), $state->scope, $storage); + } + } + + $state->exitPoints = array_merge($state->exitPoints, $statementResult->getExitPoints()); + $state->throwPoints = array_merge($state->throwPoints, $statementResult->getThrowPoints()); + $state->impurePoints = array_merge($state->impurePoints, $statementResult->getImpurePoints()); + + if ($state->alreadyTerminated || !$statementResult->isAlwaysTerminating()) { + return; + } + + $state->alreadyTerminated = true; + $nextStmts = $this->getNextUnreachableStatements(array_slice($stmts, $i + 1), $parentNode instanceof Node\Stmt\Namespace_); + $this->processUnreachableStatement($nodeScopeResolver, $nextStmts, $state->scope, $storage, $nodeCallback); + } + + /** + * The result of the expression an ending statement evaluated, for + * ExecutionEndNode. Null when the statement is not an expression statement, + * and when its expression was not processed into this storage - an end + * statement is collected from wherever execution ended, which can be a + * nested walk whose results never reach the frame the end node is built in. + */ + private function readEndStatementExprResult(Node\Stmt $stmt, ExpressionResultStorage $storage): ?ExpressionResult + { + if (!$stmt instanceof Node\Stmt\Expression) { + return null; + } + + return $storage->findExpressionResult($stmt->expr); + } + + /** + * A function-like body under the unresolvedTemplateArguments toggle is + * walked in two passes. The observation pass walks every statement + * recording rule-facing emissions and threading immutable constraints + * through the scopes returned by expressions and statements. A body that + * created no unresolved template argument simply replays the recording. + * Otherwise TemplateArgumentResolver builds a new resolved frame for the + * second pass, which re-walks only the statements the resolutions can + * influence. Recorded scopes retain their original collection context. + * + * The outer gatherer frames (the method's return statements, execution + * ends, impure points) are suspended during the observation pass and fed + * by the replay and the re-walk, so each emission reaches them once. + * + * @param Node\Stmt[] $stmts + * @param callable(Node $node, Scope $scope): void $nodeCallback + */ + private function processBodyStmtNodesTwoPass( + NodeScopeResolver $nodeScopeResolver, + Node $parentNode, + array $stmts, + MutatingScope $scope, + ExpressionResultStorage $storage, + callable $nodeCallback, + StatementContext $context, + ): InternalStatementResult + { + $statementStartTokenPositions = []; + foreach ($stmts as $stmt) { + $statementStartTokenPositions[] = $stmt->getStartTokenPos(); + } + $parentFrame = $scope->getCurrentTemplateArgumentFrame(); + $parentConstraints = $scope->getTemplateArgumentConstraints(); + $frame = new TemplateArgumentFrame($parentFrame); + if (TemplateArgumentStats::$enabled) { + TemplateArgumentStats::increment('bodiesWalked'); + TemplateArgumentStats::increment('statementsTotal', count($stmts)); + } + $scope = $scope->withTemplateArgumentFrame($frame)->withTemplateArgumentConstraints(null); + $recording = new RecordingNodeCallback(); + $state = new StatementListWalkState($scope); + /** @var list $entries the state and recording offset before each statement, plus the final ones */ + $entries = []; + $observationContext = $context->withoutTemplateArgumentResolution(); + $suspendedGatherers = $nodeScopeResolver->suspendNodeGatherers(); + try { + foreach ($stmts as $i => $stmt) { + $entries[$i] = [clone $state, $recording->count()]; + $this->processStatementStep($nodeScopeResolver, $parentNode, $stmts, $i, $stmt, $state, $storage, $recording, $observationContext, true); + } + } finally { + $nodeScopeResolver->restoreNodeGatherers($suspendedGatherers); + } + $frame = $this->templateArgumentResolver->resolve($state->scope->getTemplateArgumentConstraints() ?? TemplateArgumentConstraints::createEmpty(), $parentFrame, $statementStartTokenPositions); + $stmtCount = count($stmts); + $entries[$stmtCount] = [clone $state, $recording->count()]; + + $firstSiteStatementIndex = $frame->firstSiteStatementIndex(); + if ($firstSiteStatementIndex === null) { + $nodeScopeResolver->replayRecordingRange($recording, 0, $recording->count(), $nodeCallback, $storage, $scope); + + $state->scope = $state->scope->withTemplateArgumentFrame($parentFrame)->withTemplateArgumentConstraints($parentConstraints); + return $state->toResult(); + } + + if (TemplateArgumentStats::$enabled) { + TemplateArgumentStats::increment('bodiesWithSites'); + TemplateArgumentStats::increment('statementsReplayed', $firstSiteStatementIndex); + } + // the second pass: the statements before the first site stand as + // recorded; from there on a statement is re-walked only when it + // created a site or mentions a variable whose tracked state the + // resolutions changed - the rest replay their recording and carry + // their recorded effect onto the re-walked scope + $nodeScopeResolver->replayRecordingRange($recording, 0, $entries[$firstSiteStatementIndex][1], $nodeCallback, $storage, $scope); + $hasLabels = false; + foreach ($stmts as $stmt) { + if (!$stmt instanceof Node\Stmt\Label && $stmt->getAttribute(GotoLabelVisitor::NESTED_BACKWARD_GOTO_LABELS_ATTRIBUTE) === null) { + continue; + } + $hasLabels = true; + break; + } + $state = clone $entries[$firstSiteStatementIndex][0]; + $state->scope = $state->scope->withTemplateArgumentFrame($frame)->withTemplateArgumentConstraints(null); + for ($i = $firstSiteStatementIndex; $i < $stmtCount; $i++) { + [$recordedEntry, $offset] = $entries[$i]; + [$recordedExit, $nextOffset] = $entries[$i + 1]; + $differingRoots = $state->alreadyTerminated === $recordedEntry->alreadyTerminated + ? $state->scope->getDifferingVariableRoots($recordedEntry->scope) + : null; + if ($differingRoots === [] && !$frame->hasSiteAtOrAfter($i)) { + // converged with the observation pass: the rest of its recording stands + if (TemplateArgumentStats::$enabled) { + TemplateArgumentStats::increment('earlyExits'); + TemplateArgumentStats::increment('statementsReplayed', $stmtCount - $i); + } + $nodeScopeResolver->replayRecordingRange($recording, $offset, $recording->count(), $nodeCallback, $storage, $scope); + $this->appendRecordedStatementResults($state, $recordedEntry, $entries[$stmtCount][0]); + $state->scope = $entries[$stmtCount][0]->scope; + + $state->scope = $state->scope->withTemplateArgumentFrame($parentFrame)->withTemplateArgumentConstraints($parentConstraints); + return $state->toResult(); + } + + $stmt = $stmts[$i]; + $reWalk = $differingRoots === null + || $hasLabels + || $frame->ownsSiteInStatement($i) + || $this->statementMentionsAnyVariable($stmt, $differingRoots); + if ($this->debugTemplateArguments) { + echo sprintf( + "[template-arguments] %s:%d statement %d: %s (differing: %s)\n", + $scope->getFile(), + $stmt->getStartLine(), + $i, + $reWalk ? 're-walk' : 'replay', + $differingRoots === null ? 'non-variable key' : implode(', ', $differingRoots), + ); + } + if ($reWalk) { + if (TemplateArgumentStats::$enabled) { + TemplateArgumentStats::increment('statementsReWalked'); + } + $this->processStatementStep($nodeScopeResolver, $parentNode, $stmts, $i, $stmt, $state, $storage, $nodeCallback, $context, true); + continue; + } + + if (TemplateArgumentStats::$enabled) { + TemplateArgumentStats::increment('statementsReplayed'); + } + $nodeScopeResolver->replayRecordingRange($recording, $offset, $nextOffset, $nodeCallback, $storage, $scope); + $this->appendRecordedStatementResults($state, $recordedEntry, $recordedExit); + $state->scope = $state->scope->withRecordedStatementDelta($recordedEntry->scope, $recordedExit->scope); + } + + $state->scope = $state->scope->withTemplateArgumentFrame($parentFrame)->withTemplateArgumentConstraints($parentConstraints); + return $state->toResult(); + } + + /** Carries what the recorded walk from $from to $to added onto $state. */ + private function appendRecordedStatementResults(StatementListWalkState $state, StatementListWalkState $from, StatementListWalkState $to): void + { + foreach ($to->variableFlows as $index => $flow) { + if (array_key_exists($index, $from->variableFlows)) { + continue; + } + + $state->variableFlows[$index] = $flow; + } + $state->hasYield = $state->hasYield || ($to->hasYield && !$from->hasYield); + $state->alreadyTerminated = $state->alreadyTerminated || ($to->alreadyTerminated && !$from->alreadyTerminated); + $state->exitPoints = array_merge($state->exitPoints, array_slice($to->exitPoints, count($from->exitPoints))); + $state->throwPoints = array_merge($state->throwPoints, array_slice($to->throwPoints, count($from->throwPoints))); + $state->impurePoints = array_merge($state->impurePoints, array_slice($to->impurePoints, count($from->impurePoints))); + } + + public function getVariableMentionFlow(Node\Stmt $stmt): ?VariableFlow + { + $names = []; + $mentionsEverything = false; + $this->collectMentionedVariables($stmt, $names, $mentionsEverything); + if ($mentionsEverything) { + return VariableFlow::all(VariableFlow::MENTION_ALL); + } + + $flows = []; + foreach (array_keys($names) as $name) { + $flows[] = VariableFlow::mention($name); + } + return VariableFlow::sequence(...$flows); + } + + /** + * @param list $variableNames + */ + private function statementMentionsAnyVariable(Node\Stmt $stmt, array $variableNames): bool + { + /** @var array{array, bool}|null $mentions */ + $mentions = $stmt->getAttribute(self::MENTIONED_VARIABLES_ATTRIBUTE); + if ($mentions === null) { + $names = []; + $mentionsEverything = false; + $this->collectMentionedVariables($stmt, $names, $mentionsEverything); + $mentions = [$names, $mentionsEverything]; + $stmt->setAttribute(self::MENTIONED_VARIABLES_ATTRIBUTE, $mentions); + } + [$names, $mentionsEverything] = $mentions; + if ($mentionsEverything) { + return true; + } + foreach ($variableNames as $variableName) { + if (isset($names[$variableName])) { + return true; + } + } + + return false; + } + + /** + * Every variable the statement can read or write - syntactically, so + * reads and writes alike - with `$this`; a closure body lives in its own + * scope, so only its use() clause (and its bound `$this`) count, an arrow + * function captures implicitly and is traversed. Dynamic access + * (`$$name`, compact(), extract(), get_defined_vars(), eval, include) + * mentions everything. + * + * @param array $names + */ + private function collectMentionedVariables(Node $node, array &$names, bool &$mentionsEverything): void + { + if ($node instanceof Expr\Variable) { + if (!is_string($node->name)) { + $mentionsEverything = true; + return; + } + $names[$node->name] = true; + return; + } + if ($node instanceof Expr\Closure) { + if (!$node->static) { + $names['this'] = true; + } + foreach ($node->uses as $use) { + if (!is_string($use->var->name)) { + $mentionsEverything = true; + continue; + } + $names[$use->var->name] = true; + } + + return; + } + if ($node instanceof Expr\Eval_ || $node instanceof Expr\Include_) { + $mentionsEverything = true; + } elseif ( + $node instanceof Expr\FuncCall + && $node->name instanceof Name + && in_array($node->name->toLowerString(), ['compact', 'extract', 'get_defined_vars'], true) + ) { + $mentionsEverything = true; + } + + foreach ($node->getSubNodeNames() as $subNodeName) { + $subNode = $node->$subNodeName; + if ($subNode instanceof Node) { + $this->collectMentionedVariables($subNode, $names, $mentionsEverything); + } elseif (is_array($subNode)) { + foreach ($subNode as $item) { + if (!$item instanceof Node) { + continue; + } + $this->collectMentionedVariables($item, $names, $mentionsEverything); + } + } + } + } + + /** + * @return InternalThrowPoint[]|null + */ + public function getOverridingThrowPoints(Node\Stmt $statement, MutatingScope $scope): ?array + { + foreach ($statement->getComments() as $comment) { + if (!$comment instanceof Doc) { + continue; + } + + $function = $scope->getFunction(); + $resolvedPhpDoc = $this->fileTypeMapper->getResolvedPhpDoc( + $scope->getFile(), + $scope->isInClass() ? $scope->getClassReflection()->getName() : null, + $scope->isInTrait() ? $scope->getTraitReflection()->getName() : null, + $function !== null ? $function->getName() : null, + $comment->getText(), + ); + + $throwsTag = $resolvedPhpDoc->getThrowsTag(); + if ($throwsTag !== null) { + $throwsType = $throwsTag->getType(); + if ($throwsType->isVoid()->yes()) { + return []; + } + + return [InternalThrowPoint::createExplicit($scope, $throwsType, $statement, false)]; + } + } + + return null; + } + + /** + * @param callable(Node $node, Scope $scope): void $nodeCallback + */ + public function processStmtVarAnnotation(NodeScopeResolver $nodeScopeResolver, MutatingScope $scope, ExpressionResultStorage $storage, Node\Stmt $stmt, ?Expr $defaultExpr, callable $nodeCallback): MutatingScope + { + $function = $scope->getFunction(); + $variableLessTags = []; + + foreach ($stmt->getComments() as $comment) { + if (!$comment instanceof Doc) { + continue; + } + + $resolvedPhpDoc = $this->fileTypeMapper->getResolvedPhpDoc( + $scope->getFile(), + $scope->isInClass() ? $scope->getClassReflection()->getName() : null, + $scope->isInTrait() ? $scope->getTraitReflection()->getName() : null, + $function !== null ? $function->getName() : null, + $comment->getText(), + ); + + $assignedVariable = null; + if ( + $stmt instanceof Node\Stmt\Expression + && ($stmt->expr instanceof Assign || $stmt->expr instanceof AssignRef) + && $stmt->expr->var instanceof Variable + && is_string($stmt->expr->var->name) + ) { + $assignedVariable = $stmt->expr->var->name; + } + + foreach ($resolvedPhpDoc->getVarTags() as $name => $varTag) { + if (is_int($name)) { + $variableLessTags[] = $varTag; + continue; + } + + if ($name === $assignedVariable) { + continue; + } + + $certainty = $scope->hasVariableType($name); + if ($certainty->no()) { + continue; + } + + if ($scope->isInClass() && $scope->getFunction() === null) { + continue; + } + + if ($scope->canAnyVariableExist()) { + $certainty = TrinaryLogic::createYes(); + } + + $variableNode = new Variable($name, $stmt->getAttributes()); + $originalType = $scope->getVariableType($name); + if (!$originalType->equals($varTag->getType())) { + $nodeScopeResolver->callNodeCallback($nodeCallback, new VarTagChangedExpressionTypeNode($varTag, $variableNode), $scope, $storage); + } + $templateArgumentFrame = $nodeScopeResolver->observingTemplateArgumentFrame($scope); + if ($templateArgumentFrame !== null) { + $scope = $scope->addTemplateArgumentConstraints($this->templateArgumentObserver->collectSend($varTag->getType(), $originalType)); + } + + $nativeScope = $scope->doNotTreatPhpDocTypesAsCertain(); + $scope = $scope->assignVariable( + $name, + $varTag->getType(), + // a plain variable read is scope state + $nativeScope->hasVariableType($name)->no() ? new ErrorType() : $nativeScope->getVariableType($name), + $certainty, + ); + } + } + + if (count($variableLessTags) === 1 && $defaultExpr !== null) { + // only the scope effect here: the changed-type node is emitted by the + // statement handler AFTER it walked the expression (emitVarTagChangedNode), + // so the rule prices the expression from its stored result rather than + // asking the scope about a node not processed yet + $scope = $scope->assignExpression($defaultExpr, $variableLessTags[0]->getType(), new MixedType()); + } + + return $scope; + } + + /** + * The single variable-less @var tag on the statement's doc comment, or null + * when there is none or more than one - the shape processStmtVarAnnotation() + * applies to $defaultExpr and emitVarTagChangedNode() reports on. + */ + private function findSingleVariableLessVarTag(MutatingScope $scope, Node\Stmt $stmt): ?VarTag + { + $function = $scope->getFunction(); + $variableLessTags = []; + foreach ($stmt->getComments() as $comment) { + if (!$comment instanceof Doc) { + continue; + } + + $resolvedPhpDoc = $this->fileTypeMapper->getResolvedPhpDoc( + $scope->getFile(), + $scope->isInClass() ? $scope->getClassReflection()->getName() : null, + $scope->isInTrait() ? $scope->getTraitReflection()->getName() : null, + $function !== null ? $function->getName() : null, + $comment->getText(), + ); + foreach ($resolvedPhpDoc->getVarTags() as $name => $varTag) { + if (!is_int($name)) { + continue; + } + + $variableLessTags[] = $varTag; + } + } + + return count($variableLessTags) === 1 ? $variableLessTags[0] : null; + } + + /** + * Emits the node the @var-changed-type rule listens to, for a statement with + * a single variable-less @var tag over $defaultExpr - called by the handler + * once the expression has been walked (see processStmtVarAnnotation()). + * + * @param callable(Node $node, Scope $scope): void $nodeCallback + */ + public function emitVarTagChangedNode(NodeScopeResolver $nodeScopeResolver, MutatingScope $scope, ExpressionResultStorage $storage, Node\Stmt $stmt, Expr $defaultExpr, callable $nodeCallback): TemplateArgumentConstraints + { + $varTag = $this->findSingleVariableLessVarTag($scope, $stmt); + if ($varTag === null) { + return TemplateArgumentConstraints::createEmpty(); + } + + $nodeScopeResolver->callNodeCallback($nodeCallback, new VarTagChangedExpressionTypeNode($varTag, $defaultExpr), $scope, $storage); + $defaultExprResult = $storage->findExpressionResult($defaultExpr); + if ($defaultExprResult === null || $nodeScopeResolver->observingTemplateArgumentFrame($defaultExprResult->getScope()) === null) { + return TemplateArgumentConstraints::createEmpty(); + } + return $this->templateArgumentObserver->collectSend($varTag->getType(), $defaultExprResult->getType()); + } + +} diff --git a/src/Analyser/StmtHandler/ExpressionHandler.php b/src/Analyser/StmtHandler/ExpressionHandler.php index 9386dbb2506..77748ff0a57 100644 --- a/src/Analyser/StmtHandler/ExpressionHandler.php +++ b/src/Analyser/StmtHandler/ExpressionHandler.php @@ -14,6 +14,7 @@ use PHPStan\Analyser\NodeScopeResolver; use PHPStan\Analyser\Scope; use PHPStan\Analyser\StatementContext; +use PHPStan\Analyser\StatementsHandler; use PHPStan\Analyser\StmtHandler; use PHPStan\Analyser\TypeSpecifierContext; use PHPStan\DependencyInjection\AutowiredService; @@ -31,6 +32,12 @@ final class ExpressionHandler implements StmtHandler { + public function __construct( + private StatementsHandler $statementsHandler, + ) + { + } + public function supports(Stmt $stmt): bool { return $stmt instanceof Expression; @@ -48,7 +55,7 @@ public function processStmt( $preAnnotationScope = $scope; $stmtScope = $scope; if ($stmt->expr instanceof Expr\Throw_) { - $stmtScope = $nodeScopeResolver->processStmtVarAnnotation($scope, $storage, $stmt, $stmt->expr->expr, $nodeCallback); + $stmtScope = $this->statementsHandler->processStmtVarAnnotation($nodeScopeResolver, $scope, $storage, $stmt, $stmt->expr->expr, $nodeCallback); $scope = $stmtScope; } $hasAssign = false; @@ -68,7 +75,7 @@ public function processStmt( $result = $nodeScopeResolver->processExprNode($stmt, $stmt->expr, $scope, $storage, $nodeCallback, ExpressionContext::createTopLevel($context->shouldResolveTemplateArguments())); if ($stmt->expr instanceof Expr\Throw_) { // the @var-changed-type node fires now that the thrown expression is stored - $result = $result->withScope($result->getScope()->addTemplateArgumentConstraints($nodeScopeResolver->emitVarTagChangedNode($preAnnotationScope, $storage, $stmt, $stmt->expr->expr, $nodeCallback))); + $result = $result->withScope($result->getScope()->addTemplateArgumentConstraints($this->statementsHandler->emitVarTagChangedNode($nodeScopeResolver, $preAnnotationScope, $storage, $stmt, $stmt->expr->expr, $nodeCallback))); } } finally { $nodeScopeResolver->popNodeGatherer(); diff --git a/src/Analyser/StmtHandler/ReturnHandler.php b/src/Analyser/StmtHandler/ReturnHandler.php index b8eefe161e3..a91be8272c4 100644 --- a/src/Analyser/StmtHandler/ReturnHandler.php +++ b/src/Analyser/StmtHandler/ReturnHandler.php @@ -12,6 +12,7 @@ use PHPStan\Analyser\MutatingScope; use PHPStan\Analyser\NodeScopeResolver; use PHPStan\Analyser\StatementContext; +use PHPStan\Analyser\StatementsHandler; use PHPStan\Analyser\StmtHandler; use PHPStan\Analyser\VariableFlow; use PHPStan\DependencyInjection\AutowiredService; @@ -24,6 +25,12 @@ final class ReturnHandler implements StmtHandler { + public function __construct( + private StatementsHandler $statementsHandler, + ) + { + } + public function supports(Stmt $stmt): bool { return $stmt instanceof Return_; @@ -38,14 +45,14 @@ public function processStmt( StatementContext $context, ): InternalStatementResult { - $stmtScope = $nodeScopeResolver->processStmtVarAnnotation($scope, $storage, $stmt, $stmt->expr, $nodeCallback); + $stmtScope = $this->statementsHandler->processStmtVarAnnotation($nodeScopeResolver, $scope, $storage, $stmt, $stmt->expr, $nodeCallback); if ($stmt->expr !== null) { $result = $nodeScopeResolver->processExprNode($stmt, $stmt->expr, $stmtScope, $storage, $nodeCallback, ExpressionContext::createDeep($context->shouldResolveTemplateArguments())); // the @var-changed-type node fires now that the expression is stored // on the scope BEFORE the @var tag re-typed the expression, so the rule // compares the tag against the expression's walked type - $varConstraints = $nodeScopeResolver->emitVarTagChangedNode($scope, $storage, $stmt, $stmt->expr, $nodeCallback); + $varConstraints = $this->statementsHandler->emitVarTagChangedNode($nodeScopeResolver, $scope, $storage, $stmt, $stmt->expr, $nodeCallback); $throwPoints = $result->getThrowPoints(); $impurePoints = $result->getImpurePoints(); $scope = $result->getScope()->addTemplateArgumentConstraints($varConstraints)->addTemplateArgumentConstraints($nodeScopeResolver->collectReturnSend($stmtScope, $result)); diff --git a/src/Analyser/StmtHandler/TryCatchHandler.php b/src/Analyser/StmtHandler/TryCatchHandler.php index 5f3426d23b8..aedccaccac8 100644 --- a/src/Analyser/StmtHandler/TryCatchHandler.php +++ b/src/Analyser/StmtHandler/TryCatchHandler.php @@ -15,6 +15,7 @@ use PHPStan\Analyser\MutatingScope; use PHPStan\Analyser\NodeScopeResolver; use PHPStan\Analyser\StatementContext; +use PHPStan\Analyser\StatementsHandler; use PHPStan\Analyser\StmtHandler; use PHPStan\Analyser\VariableFlow; use PHPStan\Analyser\VariableFlowBuilder; @@ -43,6 +44,12 @@ final class TryCatchHandler implements StmtHandler { + public function __construct( + private StatementsHandler $statementsHandler, + ) + { + } + public function supports(Stmt $stmt): bool { return $stmt instanceof TryCatch; @@ -194,7 +201,7 @@ public function processStmt( } if (count($matchingThrowPoints) === 0) { - $catchFlows[] = [$originalCatchType, $nodeScopeResolver->getVariableMentionFlow($catchNode)]; + $catchFlows[] = [$originalCatchType, $this->statementsHandler->getVariableMentionFlow($catchNode)]; continue; } diff --git a/src/Testing/RuleTestCase.php b/src/Testing/RuleTestCase.php index 3888ef15c45..6f9cc5cc53a 100644 --- a/src/Testing/RuleTestCase.php +++ b/src/Testing/RuleTestCase.php @@ -9,13 +9,13 @@ use PHPStan\Analyser\ExpressionResultFactory; use PHPStan\Analyser\FileAnalyser; use PHPStan\Analyser\Generics\TemplateArgumentObserver; -use PHPStan\Analyser\Generics\TemplateArgumentResolver; use PHPStan\Analyser\IgnoreErrorExtension; use PHPStan\Analyser\InternalError; use PHPStan\Analyser\LocalIgnoresProcessor; use PHPStan\Analyser\NodeScopeResolver; use PHPStan\Analyser\PerFileAnalysisResettable; use PHPStan\Analyser\RuleErrorTransformer; +use PHPStan\Analyser\StatementsHandler; use PHPStan\Analyser\TypeSpecifier; use PHPStan\Collectors\Collector; use PHPStan\Collectors\Registry as CollectorRegistry; @@ -29,7 +29,6 @@ use PHPStan\Rules\IdentifierRuleError; use PHPStan\Rules\Properties\ReadWritePropertiesExtension; use PHPStan\Rules\Rule; -use PHPStan\Type\FileTypeMapper; use PHPStan\Type\FunctionParameterClosureThisExtension; use PHPStan\Type\FunctionParameterClosureTypeExtension; use PHPStan\Type\FunctionParameterOutTypeExtension; @@ -91,13 +90,11 @@ protected function createNodeScopeResolver(): NodeScopeResolver return new NodeScopeResolver( self::getContainer(), self::getContainer()->getByType(TemplateArgumentObserver::class), - self::getContainer()->getByType(TemplateArgumentResolver::class), $reflectionProvider, self::getContainer()->getByType(FileHelper::class), self::getContainer()->getExtensionsCollection(FunctionParameterOutTypeExtension::class), self::getContainer()->getExtensionsCollection(MethodParameterOutTypeExtension::class), self::getContainer()->getExtensionsCollection(StaticMethodParameterOutTypeExtension::class), - self::getContainer()->getByType(FileTypeMapper::class), $readWritePropertiesExtensions !== [] ? new DirectExtensionsCollection($readWritePropertiesExtensions) : self::getContainer()->getExtensionsCollection(ReadWritePropertiesExtension::class), self::getContainer()->getExtensionsCollection(FunctionParameterClosureThisExtension::class), self::getContainer()->getExtensionsCollection(MethodParameterClosureThisExtension::class), @@ -111,7 +108,7 @@ protected function createNodeScopeResolver(): NodeScopeResolver self::getContainer()->getParameter('exceptions')['implicitThrows'], $this->shouldTreatPhpDocTypesAsCertain(), self::getContainer()->getByType(ExpressionResultFactory::class), - self::getContainer()->getParameter('featureToggles')['unresolvedTemplateArguments'], + self::getContainer()->getByType(StatementsHandler::class), ); } diff --git a/src/Testing/TypeInferenceTestCase.php b/src/Testing/TypeInferenceTestCase.php index 77203859e35..4ea3ab8e7fc 100644 --- a/src/Testing/TypeInferenceTestCase.php +++ b/src/Testing/TypeInferenceTestCase.php @@ -8,12 +8,12 @@ use PhpParser\Node\Name; use PHPStan\Analyser\ExpressionResultFactory; use PHPStan\Analyser\Generics\TemplateArgumentObserver; -use PHPStan\Analyser\Generics\TemplateArgumentResolver; use PHPStan\Analyser\MutatingScope; use PHPStan\Analyser\NodeScopeResolver; use PHPStan\Analyser\PerFileAnalysisResettable; use PHPStan\Analyser\Scope; use PHPStan\Analyser\ScopeContext; +use PHPStan\Analyser\StatementsHandler; use PHPStan\File\FileHelper; use PHPStan\File\SystemAgnosticSimpleRelativePathHelper; use PHPStan\Node\InClassNode; @@ -23,7 +23,6 @@ use PHPStan\ShouldNotHappenException; use PHPStan\TrinaryLogic; use PHPStan\Type\ConstantScalarType; -use PHPStan\Type\FileTypeMapper; use PHPStan\Type\FunctionParameterClosureThisExtension; use PHPStan\Type\FunctionParameterClosureTypeExtension; use PHPStan\Type\FunctionParameterOutTypeExtension; @@ -66,13 +65,11 @@ protected static function createNodeScopeResolver(): NodeScopeResolver return new NodeScopeResolver( $container, $container->getByType(TemplateArgumentObserver::class), - $container->getByType(TemplateArgumentResolver::class), $reflectionProvider, $container->getByType(FileHelper::class), $container->getExtensionsCollection(FunctionParameterOutTypeExtension::class), $container->getExtensionsCollection(MethodParameterOutTypeExtension::class), $container->getExtensionsCollection(StaticMethodParameterOutTypeExtension::class), - $container->getByType(FileTypeMapper::class), $container->getExtensionsCollection(ReadWritePropertiesExtension::class), $container->getExtensionsCollection(FunctionParameterClosureThisExtension::class), $container->getExtensionsCollection(MethodParameterClosureThisExtension::class), @@ -86,7 +83,7 @@ protected static function createNodeScopeResolver(): NodeScopeResolver $container->getParameter('exceptions')['implicitThrows'], $container->getParameter('treatPhpDocTypesAsCertain'), $container->getByType(ExpressionResultFactory::class), - $container->getParameter('featureToggles')['unresolvedTemplateArguments'], + $container->getByType(StatementsHandler::class), ); } diff --git a/src/Type/Generic/UnresolvedTemplateArgumentType.php b/src/Type/Generic/UnresolvedTemplateArgumentType.php index 517741be4fb..f394e9de2b2 100644 --- a/src/Type/Generic/UnresolvedTemplateArgumentType.php +++ b/src/Type/Generic/UnresolvedTemplateArgumentType.php @@ -33,7 +33,7 @@ * generic call, before the surrounding function body decided what it should be. * * Exists only during the observation pass of a function body (see - * NodeScopeResolver::processBodyStmtNodesTwoPass()): every send of the object + * StatementsHandler::processBodyStmtNodesTwoPass()): every send of the object * to a declared type (property, parameter, return, @var) and every method call * on it is recorded against the site (the creating node) and the template name, * and the second pass substitutes the resolved type. Rules never see it. diff --git a/tests/PHPStan/Analyser/AnalyserTest.php b/tests/PHPStan/Analyser/AnalyserTest.php index a9fe9e9b25c..591b777d2b8 100644 --- a/tests/PHPStan/Analyser/AnalyserTest.php +++ b/tests/PHPStan/Analyser/AnalyserTest.php @@ -7,7 +7,6 @@ use PhpParser\NodeVisitor\NameResolver; use PhpParser\Parser\Php7; use PHPStan\Analyser\Generics\TemplateArgumentObserver; -use PHPStan\Analyser\Generics\TemplateArgumentResolver; use PHPStan\Analyser\Ignore\IgnoredErrorHelper; use PHPStan\Analyser\Ignore\IgnoreLexer; use PHPStan\Collectors\Registry as CollectorRegistry; @@ -862,13 +861,11 @@ private function createAnalyser(): Analyser $nodeScopeResolver = new NodeScopeResolver( $container, $container->getByType(TemplateArgumentObserver::class), - $container->getByType(TemplateArgumentResolver::class), $reflectionProvider, $fileHelper, $container->getExtensionsCollection(FunctionParameterOutTypeExtension::class), $container->getExtensionsCollection(MethodParameterOutTypeExtension::class), $container->getExtensionsCollection(StaticMethodParameterOutTypeExtension::class), - $fileTypeMapper, $container->getExtensionsCollection(ReadWritePropertiesExtension::class), $container->getExtensionsCollection(FunctionParameterClosureThisExtension::class), $container->getExtensionsCollection(MethodParameterClosureThisExtension::class), @@ -882,7 +879,7 @@ private function createAnalyser(): Analyser true, $this->shouldTreatPhpDocTypesAsCertain(), $container->getByType(ExpressionResultFactory::class), - $container->getParameter('featureToggles')['unresolvedTemplateArguments'], + $container->getByType(StatementsHandler::class), ); $lexer = new Lexer(); $fileAnalyser = new FileAnalyser( From 7f64cdc9ced66fcf9fbf44c73f251e1833beac03 Mon Sep 17 00:00:00 2001 From: Ondrej Mirtes Date: Sun, 13 Sep 2026 11:02:18 +0200 Subject: [PATCH 02/10] Extract call argument processing from NodeScopeResolver into ArgumentsHandler processArgs(), processDroppedArgs() and their helpers (argument type gathering, acceptor selection, closure parameter types and bound $this, parameter out types, callback invalidation) now live in the ArgumentsHandler service. The call handlers inject it; NodeScopeResolver delegates to it from processAttributeGroups(). The nine parameter out/closure-this/closure-type extension collections and the implicitThrows parameter move from the NodeScopeResolver constructor to ArgumentsHandler. NodeScopeResolver exposes the stored-result walk modes through isReturningStoredExpressionResults() / isConsumingStoredExpressionResults(), and resolveCallableTypeForScope() becomes public. Co-Authored-By: Claude Opus 5 (1M context) Claude-Session: https://claude.ai/code/session_014hgcbwfKQnye2coQXtX5XU --- .../Build/NoScopeTypeReadInEngineRule.php | 1 + src/Analyser/ArgsResult.php | 2 +- src/Analyser/ArgumentsHandler.php | 1259 +++++++++++++++++ src/Analyser/ExprHandler/FuncCallHandler.php | 8 +- .../Helper/FuncCallScopeEffectsHelper.php | 2 +- .../ExprHandler/MethodCallHandler.php | 7 +- src/Analyser/ExprHandler/NewHandler.php | 6 +- .../ExprHandler/StaticCallHandler.php | 6 +- src/Analyser/NodeScopeResolver.php | 1215 +--------------- src/Testing/RuleTestCase.php | 21 +- src/Testing/TypeInferenceTestCase.php | 21 +- tests/PHPStan/Analyser/AnalyserTest.php | 20 +- 12 files changed, 1301 insertions(+), 1267 deletions(-) create mode 100644 src/Analyser/ArgumentsHandler.php diff --git a/build/PHPStan/Build/NoScopeTypeReadInEngineRule.php b/build/PHPStan/Build/NoScopeTypeReadInEngineRule.php index d687e3246da..dd935cef11b 100644 --- a/build/PHPStan/Build/NoScopeTypeReadInEngineRule.php +++ b/build/PHPStan/Build/NoScopeTypeReadInEngineRule.php @@ -27,6 +27,7 @@ final class NoScopeTypeReadInEngineRule implements Rule private const ENGINE_NAMESPACES = [ 'PHPStan\\Analyser\\NodeScopeResolver', + 'PHPStan\\Analyser\\ArgumentsHandler', 'PHPStan\\Analyser\\StatementsHandler', 'PHPStan\\Analyser\\StmtHandler\\', 'PHPStan\\Analyser\\ExprHandler\\', diff --git a/src/Analyser/ArgsResult.php b/src/Analyser/ArgsResult.php index 8a9ce80c900..b08497edaa8 100644 --- a/src/Analyser/ArgsResult.php +++ b/src/Analyser/ArgsResult.php @@ -10,7 +10,7 @@ use function sprintf; /** - * Result of NodeScopeResolver::processArgs(): the scope/throw/impure state after + * Result of ArgumentsHandler::processArgs(): the scope/throw/impure state after * processing all arguments (wrapped ExpressionResult) plus the ParametersAcceptor * resolved from the arg types gathered on the arg-to-arg evolving scope. The * resolved acceptor is type-driven (selectFromTypes) so its generics are resolved diff --git a/src/Analyser/ArgumentsHandler.php b/src/Analyser/ArgumentsHandler.php new file mode 100644 index 00000000000..9600870db18 --- /dev/null +++ b/src/Analyser/ArgumentsHandler.php @@ -0,0 +1,1259 @@ + $functionParameterOutTypeExtensions + * @param ExtensionsCollection $methodParameterOutTypeExtensions + * @param ExtensionsCollection $staticMethodParameterOutTypeExtensions + * @param ExtensionsCollection $functionParameterClosureThisExtensions + * @param ExtensionsCollection $methodParameterClosureThisExtensions + * @param ExtensionsCollection $staticMethodParameterClosureThisExtensions + * @param ExtensionsCollection $functionParameterClosureTypeExtensions + * @param ExtensionsCollection $methodParameterClosureTypeExtensions + * @param ExtensionsCollection $staticMethodParameterClosureTypeExtensions + */ + public function __construct( + private Container $container, + private TemplateArgumentObserver $templateArgumentObserver, + private ExpressionResultFactory $expressionResultFactory, + #[AutowiredExtensions(of: FunctionParameterOutTypeExtension::class)] + private ExtensionsCollection $functionParameterOutTypeExtensions, + #[AutowiredExtensions(of: MethodParameterOutTypeExtension::class)] + private ExtensionsCollection $methodParameterOutTypeExtensions, + #[AutowiredExtensions(of: StaticMethodParameterOutTypeExtension::class)] + private ExtensionsCollection $staticMethodParameterOutTypeExtensions, + #[AutowiredExtensions(of: FunctionParameterClosureThisExtension::class)] + private ExtensionsCollection $functionParameterClosureThisExtensions, + #[AutowiredExtensions(of: MethodParameterClosureThisExtension::class)] + private ExtensionsCollection $methodParameterClosureThisExtensions, + #[AutowiredExtensions(of: StaticMethodParameterClosureThisExtension::class)] + private ExtensionsCollection $staticMethodParameterClosureThisExtensions, + #[AutowiredExtensions(of: FunctionParameterClosureTypeExtension::class)] + private ExtensionsCollection $functionParameterClosureTypeExtensions, + #[AutowiredExtensions(of: MethodParameterClosureTypeExtension::class)] + private ExtensionsCollection $methodParameterClosureTypeExtensions, + #[AutowiredExtensions(of: StaticMethodParameterClosureTypeExtension::class)] + private ExtensionsCollection $staticMethodParameterClosureTypeExtensions, + #[AutowiredParameter(ref: '%exceptions.implicitThrows%')] + private bool $implicitThrows, + ) + { + } + + /** + * @param MethodReflection|FunctionReflection|null $calleeReflection + * @param ParametersAcceptor[] $parametersAcceptors + * @param ParametersAcceptor[]|null $namedArgumentsVariants + * @param callable(Node $node, Scope $scope): void $nodeCallback + * @param (callable(MutatingScope): MutatingScope)|null $closureBindScopeFactory + */ + public function processArgs( + NodeScopeResolver $nodeScopeResolver, + Node\Stmt $stmt, + $calleeReflection, + ?ExtendedMethodReflection $nakedMethodReflection, + array $parametersAcceptors, + ?array $namedArgumentsVariants, + CallLike $callLike, + MutatingScope $scope, + ExpressionResultStorage $storage, + callable $nodeCallback, + ExpressionContext $context, + ?callable $closureBindScopeFactory = null, + ): ArgsResult + { + $args = $callLike->getArgs(); + + // Evolving-scope arg types: gathered as each argument is processed on the + // scope that evolves arg-to-arg. They select the FINAL resolved acceptor + // (the call's return type, by-ref OUT types), which type-resolves generics + // from the actual argument types. + $gatheredTypes = []; + $gatheredUnpack = false; + $gatheredHasName = false; + $gatheredArgTypeByIndex = []; + + // Metadata acceptor base - NO forward read. The per-argument resolution below picks the + // count-correct variant (the by-ref/variadic STRUCTURE is variant-stable except where it is + // keyed off the argument count, e.g. sscanf - and the count is known structurally) and + // resolves generic parameter types from the args gathered so far; the call's return type + // comes from the post-loop resolved acceptor. + $metadataAcceptor = $parametersAcceptors[0] ?? null; + + // Whether selecting an acceptor is type-driven at all: multiple variants to + // choose between, templates or conditionals to resolve from the arg types, + // or named-argument variants. When it is not, the gathered arg types can + // never influence the selected acceptor, so the faithful-return gather walk + // of a closure/arrow argument (gatherClosureArgType()) would be pure waste - + // its signature-only shallow type keeps the count/name bookkeeping correct. + $typeDrivenAcceptorSelection = count($parametersAcceptors) > 1 + || $namedArgumentsVariants !== null + || ($metadataAcceptor !== null && ParametersAcceptorSelector::hasAcceptorTemplateOrLateResolvableType($metadataAcceptor)); + + // Both predicates are hoisted out of the per-argument loop - they traverse + // the acceptor's parameter types. + $hasTemplateParameterType = $metadataAcceptor !== null + && ParametersAcceptorSelector::hasAcceptorTemplateOrLateResolvableParameterType($metadataAcceptor); + $argMetadataIsTypeDriven = count($parametersAcceptors) > 1 || $hasTemplateParameterType; + + $hasYield = false; + $throwPoints = []; + $impurePoints = []; + $isAlwaysTerminating = false; + /** @var list $deferredInvalidateExpressions */ + $deferredInvalidateExpressions = []; + /** @var ProcessClosureResult[] $deferredByRefClosureResults */ + $deferredByRefClosureResults = []; + + $processingOrder = array_keys($args); + usort($processingOrder, static function (int $a, int $b) use ($args): int { + $aOriginalArg = $args[$a]->getAttribute(ArgumentsNormalizer::ORIGINAL_ARG_ATTRIBUTE); + $bOriginalArg = $args[$b]->getAttribute(ArgumentsNormalizer::ORIGINAL_ARG_ATTRIBUTE); + $aValue = $aOriginalArg !== null ? $aOriginalArg->value : $args[$a]->value; + $bValue = $bOriginalArg !== null ? $bOriginalArg->value : $args[$b]->value; + $aIsClosure = $aValue instanceof Expr\Closure || $aValue instanceof Expr\ArrowFunction; + $bIsClosure = $bValue instanceof Expr\Closure || $bValue instanceof Expr\ArrowFunction; + if ($aIsClosure !== $bIsClosure) { + // closures sort after non-closures so every sibling feeding an + // intrinsic override / generic callable(T) is in scope first + return $aIsClosure ? 1 : -1; + } + + $aOriginal = $args[$a]->getAttribute(ArgumentsNormalizer::ORIGINAL_ARG_ATTRIBUTE); + $bOriginal = $args[$b]->getAttribute(ArgumentsNormalizer::ORIGINAL_ARG_ATTRIBUTE); + if ($aOriginal === null && $bOriginal === null) { + return $a <=> $b; + } + if ($aOriginal === null) { + return 1; + } + if ($bOriginal === null) { + return -1; + } + + return $aOriginal->getStartTokenPos() <=> $bOriginal->getStartTokenPos(); + }); + + $argResults = []; + $byRefArguments = []; + $countStableMetadataAcceptor = null; + foreach ($processingOrder as $i) { + $arg = $args[$i]; + + if ($arg->value instanceof Expr\Closure || $arg->value instanceof Expr\ArrowFunction) { + // Gather the closure/arrow type for the FINAL resolved acceptor on + // the evolving scope, BEFORE the body is processed with a possibly + // generic-resolved parameter injected, so the inferred return type + // stays faithful to the closure's own declaration and its own + // contribution (a TValue from its return) participates in the final + // resolution (see gatherClosureArgType()). + $originalArgForGather = $arg->getAttribute(ArgumentsNormalizer::ORIGINAL_ARG_ATTRIBUTE) ?? $arg; + $gatheredArgTypeByIndex[$i] = $typeDrivenAcceptorSelection + ? $this->gatherClosureArgType($nodeScopeResolver, $parametersAcceptors, $i, $arg->value, $scope) + : $this->container->getByType(ClosureTypeResolver::class)->getClosureType($scope, $arg->value, true, $storage); + $this->addGatheredArgType($gatheredTypes, $gatheredUnpack, $gatheredHasName, $originalArgForGather, $i, $gatheredArgTypeByIndex[$i]); + } + + $argMetadataAcceptor = $metadataAcceptor; + if ($metadataAcceptor !== null && $argMetadataIsTypeDriven) { + if ($this->argConsumesResolvedParameterType($arg->value)) { + // Resolve the acceptor for this argument from the args gathered SO FAR, padded to the + // full argument count with mixed. Closures sort last and by-ref out-params follow the + // args that pin them, so determining siblings are already processed; the mixed pad keeps + // the argument COUNT correct so the by-ref/variadic variant stays stable (e.g. sscanf), + // while processed siblings resolve a generic callable(T) parameter. No forward read. + $paddedTypes = []; + $paddedUnpack = false; + $paddedHasName = false; + foreach ($args as $j => $paddedArg) { + $paddedOriginalArg = $paddedArg->getAttribute(ArgumentsNormalizer::ORIGINAL_ARG_ATTRIBUTE) ?? $paddedArg; + $this->addGatheredArgType($paddedTypes, $paddedUnpack, $paddedHasName, $paddedOriginalArg, $j, $gatheredArgTypeByIndex[$j] ?? new MixedType()); + } + $argMetadataAcceptor = $this->selectArgsMetadataAcceptor($nodeScopeResolver, $args, $paddedTypes, $parametersAcceptors, $namedArgumentsVariants, $paddedHasName, $paddedUnpack, $scope); + } else { + // Only a closure/arrow function consumes the generic-RESOLVED + // parameter type: its body is inferred from the resolved + // callable(T) - directly, or through the in-function-call stack + // when nested anywhere inside the argument. Every other argument + // reads variant-stable facts off its parameter (by-ref flag, + // callable bookkeeping), so one all-mixed count-stable selection + // serves them all instead of a full template inference per argument. + if ($countStableMetadataAcceptor === null) { + $paddedTypes = []; + $paddedUnpack = false; + $paddedHasName = false; + foreach ($args as $j => $paddedArg) { + $paddedOriginalArg = $paddedArg->getAttribute(ArgumentsNormalizer::ORIGINAL_ARG_ATTRIBUTE) ?? $paddedArg; + $this->addGatheredArgType($paddedTypes, $paddedUnpack, $paddedHasName, $paddedOriginalArg, $j, new MixedType()); + } + $countStableMetadataAcceptor = $this->selectArgsMetadataAcceptor($nodeScopeResolver, $args, $paddedTypes, $parametersAcceptors, $namedArgumentsVariants, $paddedHasName, $paddedUnpack, $scope); + } + $argMetadataAcceptor = $countStableMetadataAcceptor; + } + } + $parameters = $argMetadataAcceptor !== null ? $argMetadataAcceptor->getParameters() : null; + + $assignByReference = false; + $parameter = null; + $parameterType = null; + $parameterNativeType = null; + if ($parameters !== null) { + $matchedParameter = null; + if ($arg->name !== null) { + foreach ($parameters as $p) { + if ($p->getName() === $arg->name->toString()) { + $matchedParameter = $p; + break; + } + } + } elseif (isset($parameters[$i])) { + $matchedParameter = $parameters[$i]; + } + + if ($matchedParameter !== null) { + $assignByReference = $matchedParameter->passedByReference()->createsNewVariable(); + $parameterType = $matchedParameter->getType(); + + if ($matchedParameter instanceof ExtendedParameterReflection) { + $parameterNativeType = $matchedParameter->getNativeType(); + } + $parameter = $matchedParameter; + } elseif (count($parameters) > 0 && $argMetadataAcceptor->isVariadic()) { + $lastParameter = array_last($parameters); + $assignByReference = $lastParameter->passedByReference()->createsNewVariable(); + $parameterType = $lastParameter->getType(); + + if ($lastParameter instanceof ExtendedParameterReflection) { + $parameterNativeType = $lastParameter->getNativeType(); + } + $parameter = $lastParameter; + } + } + + if ($parameter !== null && !$parameter->passedByReference()->no()) { + $byRefArguments[spl_object_id($arg->value)] = true; + } + $lookForUnset = false; + if ($assignByReference) { + $isBuiltin = false; + if ($calleeReflection instanceof FunctionReflection && $calleeReflection->isBuiltin()) { + $isBuiltin = true; + } elseif ($calleeReflection instanceof ExtendedMethodReflection && $calleeReflection->getDeclaringClass()->isBuiltin()) { + $isBuiltin = true; + } + if ( + $isBuiltin + || ($parameterNativeType === null || !$parameterNativeType->isNull()->no()) + ) { + $scope = $nodeScopeResolver->lookForSetAllowedUndefinedExpressions($scope, $arg->value); + $lookForUnset = true; + } + } + + $originalArg = $arg->getAttribute(ArgumentsNormalizer::ORIGINAL_ARG_ATTRIBUTE) ?? $arg; + if ($calleeReflection !== null) { + $rememberTypes = !$originalArg->value instanceof Expr\Closure && !$originalArg->value instanceof Expr\ArrowFunction; + $scope = $scope->pushInFunctionCall($calleeReflection, $parameter, $rememberTypes); + } + + $nodeScopeResolver->callNodeCallback($nodeCallback, $originalArg, $scope, $storage); + + $originalScope = $scope; + $scopeToPass = $scope; + if ($i === 0 && $closureBindScopeFactory !== null && ($arg->value instanceof Expr\Closure || $arg->value instanceof Expr\ArrowFunction)) { + $scopeToPass = $closureBindScopeFactory($scope); + } + + if ($arg->value instanceof Expr\Closure) { + + $storedClosureArgResult = null; + if ($nodeScopeResolver->isReturningStoredExpressionResults() || $nodeScopeResolver->isConsumingStoredExpressionResults()) { + // an on-demand re-walk of the enclosing call must not re-run the + // closure's whole by-ref convergence: consume the main walk's + // stored result, or (when the body release already dropped it) + // price the closure through getClosureType's per-node cache - + // a single body walk on miss, none on repeat asks + $storedClosureArgResult = $storage->findExpressionResult($arg->value); + // consume mode alone (the nullsafe call's plain-twin walk) is the + // FIRST and only walk of these arguments - nothing stored them, so + // a miss means "walk it", not "price it silently": pricing skips + // the body walk and never fires the closure's node callbacks + if ($storedClosureArgResult === null && $nodeScopeResolver->isReturningStoredExpressionResults()) { + $closureTypeResolver = $this->container->getByType(ClosureTypeResolver::class); + $storedClosureArgResult = $this->expressionResultFactory->create( + $scopeToPass, + beforeScope: $scopeToPass, + expr: $arg->value, + hasYield: false, + isAlwaysTerminating: false, + throwPoints: [], + impurePoints: [], + type: $closureTypeResolver->getClosureType($scopeToPass, $arg->value, false, $storage), + nativeType: $closureTypeResolver->getClosureType($scopeToPass->doNotTreatPhpDocTypesAsCertain(), $arg->value, false, $storage), + typeCallback: null, + specifyTypesCallback: SpecifiedTypes::emptySpecifyCallback(), + ); + $nodeScopeResolver->storeExpressionResult($storage, $arg->value, $storedClosureArgResult); + } + } + if ($storedClosureArgResult !== null) { + $argResults[spl_object_id($arg->value)] = $storedClosureArgResult; + } else { + $restoreThisScope = null; + if ( + $closureBindScopeFactory === null + && $parameter instanceof ExtendedParameterReflection + && !$arg->value->static + ) { + $closureThisType = $this->resolveClosureThisType($callLike, $calleeReflection, $parameter, $scopeToPass); + if ($closureThisType !== null) { + $restoreThisScope = $scopeToPass; + $scopeToPass = $scopeToPass->assignVariable('this', $closureThisType, new ObjectWithoutClassType(), TrinaryLogic::createYes()) + ->withClosureBindScopeClasses($closureThisType->getObjectClassNames()); + } + } + + if ($parameter !== null) { + $overwritingParameterType = $this->getParameterTypeFromParameterClosureTypeExtension($callLike, $calleeReflection, $parameter, $scopeToPass); + + if ($overwritingParameterType !== null) { + $parameterType = $overwritingParameterType; + + // resolve the native flavour through the same extension on the + // natively-promoted scope, so the closure parameters keep + // their native precision too + $overwritingParameterNativeType = $this->getParameterTypeFromParameterClosureTypeExtension($callLike, $calleeReflection, $parameter, $scopeToPass->doNotTreatPhpDocTypesAsCertain()); + if ($overwritingParameterNativeType !== null) { + $parameterNativeType = $overwritingParameterNativeType; + } + } + } + + $closureResult = $nodeScopeResolver->processClosureNode($stmt, $arg->value, $scopeToPass, $storage, $nodeCallback, $context, $parameterType, $parameterNativeType); + if ($this->callCallbackImmediately($parameter, $parameterType, $calleeReflection)) { + $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()); + } + + $closureTypeResolver = $this->container->getByType(ClosureTypeResolver::class); + $storedClosureResult = $this->expressionResultFactory->create( + $closureResult->getScope(), + $scopeToPass, + $arg->value, + variableFlow: ClosureHandler::getVariableFlow($arg->value), + hasYield: false, + isAlwaysTerminating: false, + throwPoints: [], + impurePoints: [], + type: $closureTypeResolver->buildClosureTypeForClosure( + $scopeToPass, + $arg->value, + $closureResult->getGatheredReturnStatements(), + $closureResult->getGatheredYieldStatements(), + $closureResult->getExecutionEnds(), + $closureResult->getThrowPoints(), + $closureResult->getClosureTypeImpurePoints(), + $closureResult->getInvalidateExpressions(), + false, + $storage, + ), + // the native flavour reads the stored native types off the same + // single body walk - no second walk on the promoted scope + nativeType: $closureTypeResolver->buildClosureTypeForClosure( + $scopeToPass, + $arg->value, + $closureResult->getGatheredReturnStatements(), + $closureResult->getGatheredYieldStatements(), + $closureResult->getExecutionEnds(), + $closureResult->getThrowPoints(), + $closureResult->getClosureTypeImpurePoints(), + $closureResult->getInvalidateExpressions(), + true, + $storage, + ), + typeCallback: null, + specifyTypesCallback: SpecifiedTypes::emptySpecifyCallback(), + ); + $nodeScopeResolver->storeExpressionResult($storage, $arg->value, $storedClosureResult); + // the closure node's own callback fires after its result is + // stored, mirroring processExprNodeInternal() - callback-side + // getType() answers from the stored result + $nodeScopeResolver->callNodeCallbackWithExpression($nodeCallback, $arg->value, $scopeToPass, $storage, $context); + // the arg result must be the properly-typed stored result - + // ArgsResult readers price array_push() & co. from it + $argResults[spl_object_id($arg->value)] = $storedClosureResult; + + $uses = []; + foreach ($arg->value->uses as $use) { + if (!is_string($use->var->name)) { + continue; + } + + $uses[] = $use->var->name; + } + + $scope = $closureResult->getScope(); + $deferredByRefClosureResults[] = $closureResult; + // Prefer the invalidate expressions collected on the ClosureType - + // they also cover writes the closure's own body walk observed, + // unlike $closureResult->getInvalidateExpressions(). + $closureExprType = $storedClosureResult->getType(); + $invalidateExpressions = $closureExprType instanceof ClosureType + ? $closureExprType->getInvalidateExpressions() + : $closureResult->getInvalidateExpressions(); + if ($restoreThisScope !== null) { + $nodeFinder = new NodeFinder(); + $cb = static fn ($expr) => $expr instanceof Variable && $expr->name === 'this'; + foreach ($invalidateExpressions as $j => $invalidateExprNode) { + $foundThis = $nodeFinder->findFirst([$invalidateExprNode->getExpr()], $cb); + if ($foundThis === null) { + continue; + } + + unset($invalidateExpressions[$j]); + } + $invalidateExpressions = array_values($invalidateExpressions); + $scope = $scope->restoreThis($restoreThisScope); + } + + if ($this->shouldInvalidateCallbackExpressions($parameter)) { + $deferredInvalidateExpressions[] = [$invalidateExpressions, $uses]; + } + } + } elseif ($arg->value instanceof Expr\ArrowFunction) { + + $storedClosureArgResult = null; + if ($nodeScopeResolver->isReturningStoredExpressionResults() || $nodeScopeResolver->isConsumingStoredExpressionResults()) { + // see the Closure branch above - consume or price via the cache + $storedClosureArgResult = $storage->findExpressionResult($arg->value); + // consume mode alone (the nullsafe call's plain-twin walk) is the + // first and only walk of the argument - a miss means walk it + if ($storedClosureArgResult === null && $nodeScopeResolver->isReturningStoredExpressionResults()) { + $closureTypeResolver = $this->container->getByType(ClosureTypeResolver::class); + $storedClosureArgResult = $this->expressionResultFactory->create( + $scopeToPass, + beforeScope: $scopeToPass, + expr: $arg->value, + hasYield: false, + isAlwaysTerminating: false, + throwPoints: [], + impurePoints: [], + type: $closureTypeResolver->getClosureType($scopeToPass, $arg->value, false, $storage), + nativeType: $closureTypeResolver->getClosureType($scopeToPass->doNotTreatPhpDocTypesAsCertain(), $arg->value, false, $storage), + typeCallback: null, + specifyTypesCallback: SpecifiedTypes::emptySpecifyCallback(), + ); + $nodeScopeResolver->storeExpressionResult($storage, $arg->value, $storedClosureArgResult); + } + } + if ($storedClosureArgResult !== null) { + $argResults[spl_object_id($arg->value)] = $storedClosureArgResult; + } else { + if ( + $closureBindScopeFactory === null + && $parameter instanceof ExtendedParameterReflection + && !$arg->value->static + ) { + $closureThisType = $this->resolveClosureThisType($callLike, $calleeReflection, $parameter, $scopeToPass); + if ($closureThisType !== null) { + $scopeToPass = $scopeToPass->assignVariable('this', $closureThisType, new ObjectWithoutClassType(), TrinaryLogic::createYes()) + ->withClosureBindScopeClasses($closureThisType->getObjectClassNames()); + } + } + + if ($parameter !== null) { + $overwritingParameterType = $this->getParameterTypeFromParameterClosureTypeExtension($callLike, $calleeReflection, $parameter, $scopeToPass); + + if ($overwritingParameterType !== null) { + $parameterType = $overwritingParameterType; + + // resolve the native flavour through the same extension on the + // natively-promoted scope, so the closure parameters keep + // their native precision too + $overwritingParameterNativeType = $this->getParameterTypeFromParameterClosureTypeExtension($callLike, $calleeReflection, $parameter, $scopeToPass->doNotTreatPhpDocTypesAsCertain()); + if ($overwritingParameterNativeType !== null) { + $parameterNativeType = $overwritingParameterNativeType; + } + } + } + + $arrowFunctionResult = $nodeScopeResolver->processArrowFunctionNode($stmt, $arg->value, $scopeToPass, $storage, $nodeCallback, $parameterType, $parameterNativeType, $context); + $arrowFunctionExprResult = $arrowFunctionResult->getExpressionResult(); + if ($this->callCallbackImmediately($parameter, $parameterType, $calleeReflection)) { + $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), $arrowFunctionExprResult->getThrowPoints())); + $impurePoints = array_merge($impurePoints, $arrowFunctionExprResult->getImpurePoints()); + } + $arrowFunctionClosureTypeResolver = $this->container->getByType(ClosureTypeResolver::class); + $arrowFunctionScope = $arrowFunctionResult->getArrowFunctionScope(); + // both flavours are built from the single body walk (see + // ArrowFunctionHandler); the built type also answers the + // invalidate-expressions read below without re-walking the + // still-unstored node through Scope::getType() + $arrowFunctionType = $arrowFunctionClosureTypeResolver->buildClosureTypeForArrowFunction( + $scopeToPass, + $arg->value, + $arrowFunctionScope, + $arrowFunctionResult->getClosureTypeThrowPoints(), + $arrowFunctionResult->getClosureTypeImpurePoints(), + $arrowFunctionResult->getInvalidateExpressions(), + false, + $storage, + ); + $storedArrowResult = $this->expressionResultFactory->create( + $arrowFunctionExprResult->getScope(), + variableFlow: $arrowFunctionExprResult->getVariableFlow(), + beforeScope: $scopeToPass, + expr: $arg->value, + hasYield: $arrowFunctionExprResult->hasYield(), + isAlwaysTerminating: $arrowFunctionExprResult->isAlwaysTerminating(), + throwPoints: $arrowFunctionExprResult->getThrowPoints(), + impurePoints: $arrowFunctionExprResult->getImpurePoints(), + type: $arrowFunctionType, + nativeType: $arrowFunctionClosureTypeResolver->buildClosureTypeForArrowFunction( + $scopeToPass, + $arg->value, + $arrowFunctionScope, + $arrowFunctionResult->getClosureTypeThrowPoints(), + $arrowFunctionResult->getClosureTypeImpurePoints(), + $arrowFunctionResult->getInvalidateExpressions(), + true, + $storage, + ), + typeCallback: null, + specifyTypesCallback: SpecifiedTypes::emptySpecifyCallback(), + ); + $nodeScopeResolver->storeExpressionResult($storage, $arg->value, $storedArrowResult); + // the arrow function node's own callback fires after its result + // is stored, mirroring processExprNodeInternal() - callback-side + // getType() answers from the stored result + $nodeScopeResolver->callNodeCallbackWithExpression($nodeCallback, $arg->value, $scopeToPass, $storage, $context); + // the arg result must be the properly-typed stored result, not + // the body walk's placeholder (whose typeCallback answers mixed) - + // ArgsResult readers price array_push() & co. from it + $argResults[spl_object_id($arg->value)] = $storedArrowResult; + if ($this->shouldInvalidateCallbackExpressions($parameter)) { + $deferredInvalidateExpressions[] = [$arrowFunctionType->getInvalidateExpressions(), $arrowFunctionType->getUsedVariables()]; + } + } + $scope = $scope->addTemplateArgumentConstraints($argResults[spl_object_id($arg->value)]->getScope()->getTemplateArgumentConstraints()); + } else { + $enterExpressionAssignForByRef = $assignByReference && $arg->value instanceof ArrayDimFetch && $arg->value->dim === null; + if ($enterExpressionAssignForByRef) { + $scopeToPass = $scopeToPass->enterExpressionAssign($arg->value); + } + $argContext = $context->enterDeep(); + if (!$arg->unpack && $arg->value instanceof Expr\Array_) { + $argContext = $argContext->enterPassedToType($parameterType, $parameterNativeType); + } + $exprResult = $nodeScopeResolver->processExprNode($stmt, $arg->value, $scopeToPass, $storage, $nodeCallback, $argContext); + $argResults[spl_object_id($arg->value)] = $exprResult; + $exprType = $exprResult->getType(); + $throwPoints = array_merge($throwPoints, $exprResult->getThrowPoints()); + $impurePoints = array_merge($impurePoints, $exprResult->getImpurePoints()); + $isAlwaysTerminating = $isAlwaysTerminating || $exprResult->isAlwaysTerminating(); + $scope = $exprResult->getScope(); + if ($enterExpressionAssignForByRef) { + $scope = $scope->exitExpressionAssign($arg->value); + } + $hasYield = $hasYield || $exprResult->hasYield(); + + if ($exprType->isCallable()->yes()) { + $acceptors = $exprType->getCallableParametersAcceptors($scope); + if (count($acceptors) === 1) { + if ($this->shouldInvalidateCallbackExpressions($parameter)) { + $deferredInvalidateExpressions[] = [$acceptors[0]->getInvalidateExpressions(), $acceptors[0]->getUsedVariables()]; + } + if ($this->callCallbackImmediately($parameter, $parameterType, $calleeReflection)) { + $callableThrowPoints = array_map(static fn (SimpleThrowPoint $throwPoint) => $throwPoint->isExplicit() ? InternalThrowPoint::createExplicit($scope, $throwPoint->getType(), $arg->value, $throwPoint->canContainAnyThrowable()) : InternalThrowPoint::createImplicit($scope, $arg->value), $acceptors[0]->getThrowPoints()); + if (!$this->implicitThrows) { + $callableThrowPoints = array_values(array_filter($callableThrowPoints, static fn (InternalThrowPoint $throwPoint) => $throwPoint->isExplicit())); + } + $throwPoints = array_merge($throwPoints, $callableThrowPoints); + $impurePoints = array_merge($impurePoints, array_map(static fn (SimpleImpurePoint $impurePoint) => new ImpurePoint($scope, $arg->value, $impurePoint->getIdentifier(), $impurePoint->getDescription(), $impurePoint->isCertain()), $acceptors[0]->getImpurePoints())); + } + } + } + + $gatheredArgTypeByIndex[$i] = $exprResult->getType(); + $this->addGatheredArgType($gatheredTypes, $gatheredUnpack, $gatheredHasName, $originalArg, $i, $gatheredArgTypeByIndex[$i]); + $templateArgumentFrame = $nodeScopeResolver->observingTemplateArgumentFrame($scope); + if ($templateArgumentFrame !== null && $parameter !== null) { + // the metadata acceptor is resolved against the arguments gathered + // before this one, so a template this argument itself decides is + // still its bound there - observe the declared parameter type, + // where such a template is uninformative and the receiver's + // class-level arguments are already in place + $scope = $scope->addTemplateArgumentConstraints($this->templateArgumentObserver->collectArgument( + $this->findOriginalParameterType($argMetadataAcceptor, $parameter) ?? $parameter->getType(), + $gatheredArgTypeByIndex[$i], + ($calleeReflection instanceof FunctionReflection || $calleeReflection instanceof ExtendedMethodReflection) && $calleeReflection->isPure()->yes(), + )); + } + } + + if ($assignByReference && $lookForUnset) { + $scope = $nodeScopeResolver->lookForUnsetAllowedUndefinedExpressions($scope, $arg->value); + } + + if ($calleeReflection !== null) { + $scope = $scope->popInFunctionCall(); + } + + if ($i !== 0 || $closureBindScopeFactory === null) { + continue; + } + + $scope = $scope->restoreOriginalScopeAfterClosureBind($originalScope); + } + + foreach ($deferredInvalidateExpressions as [$invalidateExpressions, $uses]) { + $scope = $nodeScopeResolver->processImmediatelyCalledCallable($scope, $invalidateExpressions, $uses); + } + + foreach ($deferredByRefClosureResults as $deferredClosureResult) { + $scope = $deferredClosureResult->applyByRefUseScope($scope); + } + + // Type-driven resolved acceptor: the arg types gathered on the evolving + // scope select (and generic-resolve) the acceptor that drives the call's + // return type. Intrinsic overrides are applied on the final scope, + // mirroring the original selectFromArgs(). + // When the selection is not type-driven, the single acceptor IS the + // resolved acceptor - the fast path selectFromArgs() used to take. + $resolvedAcceptor = null; + if ($parametersAcceptors !== []) { + $resolvedAcceptor = $typeDrivenAcceptorSelection + ? $this->selectArgsMetadataAcceptor($nodeScopeResolver, $args, $gatheredTypes, $parametersAcceptors, $namedArgumentsVariants, $gatheredHasName, $gatheredUnpack, $scope) + : $metadataAcceptor; + } + + if ($resolvedAcceptor !== null && $nodeScopeResolver->observingTemplateArgumentFrame($scope) !== null) { + $scope = $scope->addTemplateArgumentConstraints($this->templateArgumentObserver->collectCall( + $callLike, + $resolvedAcceptor, + $gatheredTypes, + $callLike instanceof New_ && $calleeReflection instanceof MethodReflection ? $calleeReflection->getDeclaringClass()->getTemplateTypeMap() : null, + )); + } + + // The by-ref OUT writeback reads the metadata acceptor: it is selected from + // the full argument count (stable variant). When that single acceptor still + // carries templates (fast path), its OUT types need generic-resolving from the + // now-complete gathered arg types - the post-loop $resolvedAcceptor is exactly + // that (same variant, resolved); otherwise the metadata acceptor is already resolved. + $writebackAcceptor = $metadataAcceptor; + if ( + $metadataAcceptor !== null + && $argMetadataIsTypeDriven + ) { + $writebackAcceptor = $resolvedAcceptor; + } + $writebackParameters = $writebackAcceptor !== null ? $writebackAcceptor->getParameters() : null; + if ($writebackParameters !== null) { + foreach ($args as $i => $arg) { + $assignByReference = false; + $currentParameter = null; + if (isset($writebackParameters[$i])) { + $currentParameter = $writebackParameters[$i]; + } elseif (count($writebackParameters) > 0 && $writebackAcceptor->isVariadic()) { + $currentParameter = array_last($writebackParameters); + } + + if ($currentParameter !== null) { + $assignByReference = $currentParameter->passedByReference()->createsNewVariable(); + } + + if ($assignByReference) { + $argValue = $arg->value; + if (!$argValue instanceof Variable || $argValue->name !== 'this') { + $paramOutType = $this->getParameterOutExtensionsType($callLike, $calleeReflection, $currentParameter, $scope); + if ($paramOutType !== null) { + $byRefType = $paramOutType; + } elseif ( + $currentParameter instanceof ExtendedParameterReflection + && $currentParameter->getOutType() !== null + ) { + $byRefType = $currentParameter->getOutType(); + } elseif ( + $calleeReflection instanceof MethodReflection + && !$calleeReflection->getDeclaringClass()->isBuiltin() + ) { + $byRefType = $currentParameter->getType(); + } elseif ( + $calleeReflection instanceof FunctionReflection + && !$calleeReflection->isBuiltin() + ) { + $byRefType = $currentParameter->getType(); + } else { + $byRefType = new MixedType(); + } + + $scope = $nodeScopeResolver->processVirtualAssign( + $scope, + $storage, + $stmt, + $argValue, + new TypeExpr($byRefType), + $nodeCallback, + )->getScope(); + $scope = $nodeScopeResolver->lookForUnsetAllowedUndefinedExpressions($scope, $argValue); + } + } elseif ($calleeReflection !== null && $calleeReflection->hasSideEffects()->yes()) { + $argType = $this->readArgResult($argResults, $arg->value)->getTypeOnScope($scope, false); + if (!$argType->isObject()->no()) { + $nakedReturnType = null; + if ($nakedMethodReflection !== null) { + $nakedParametersAcceptor = $this->selectArgsAcceptor( + $gatheredTypes, + $nakedMethodReflection->getVariants(), + $nakedMethodReflection->getNamedArgumentsVariants(), + $gatheredHasName, + $gatheredUnpack, + ); + $nakedReturnType = $nakedParametersAcceptor->getReturnType(); + } + if ( + $nakedReturnType === null + || !(new ThisType($nakedMethodReflection->getDeclaringClass()))->isSuperTypeOf($nakedReturnType)->yes() + || $nakedMethodReflection->isPure()->no() + ) { + $nodeScopeResolver->callNodeCallback($nodeCallback, new InvalidateExprNode($arg->value), $scope, $storage); + $scope = $scope->invalidateExpression($arg->value, true); + } + } elseif (!(new ResourceType())->isSuperTypeOf($argType)->no()) { + $nodeScopeResolver->callNodeCallback($nodeCallback, new InvalidateExprNode($arg->value), $scope, $storage); + $scope = $scope->invalidateExpression($arg->value, true); + } + } + } + } + + // not storing this, it's scope after processing all args + return new ArgsResult( + $this->expressionResultFactory->create( + $scope, + $scope, + $callLike, + $hasYield, + $isAlwaysTerminating, + $throwPoints, + $impurePoints, + typeCallback: static fn () => new MixedType(), + specifyTypesCallback: SpecifiedTypes::emptySpecifyCallback(), + ), + $resolvedAcceptor, + $argResults, + $byRefArguments, + ); + } + + /** + * Ports the gather-keying of ParametersAcceptorSelector::selectFromArgs(): + * indexes the gathered arg type by name (sets $hasName) vs position, and + * expands unpacked constant arrays / falls back to the iterable value type + * (sets $unpack), so selectFromTypes() picks the matching variant. + * + * @param array $types + */ + private function addGatheredArgType(array &$types, bool &$unpack, bool &$hasName, Node\Arg $originalArg, int $i, Type $type): void + { + if ($originalArg->name !== null) { + $index = $originalArg->name->toString(); + $hasName = true; + } else { + $index = $i; + } + + if ($originalArg->unpack) { + $unpack = true; + $constantArrays = $type->getConstantArrays(); + if (count($constantArrays) > 0) { + foreach ($constantArrays as $constantArray) { + $values = $constantArray->getValueTypes(); + foreach ($constantArray->getKeyTypes() as $j => $keyType) { + $valueType = $values[$j]; + $valueIndex = $keyType->getValue(); + if (is_string($valueIndex)) { + $hasName = true; + } else { + $valueIndex = $i + $j; + } + + $types[$valueIndex] = isset($types[$valueIndex]) + ? TypeCombinator::union($types[$valueIndex], $valueType) + : $valueType; + } + } + } else { + $types[$index] = $type->getIterableValueType(); + } + } else { + $types[$index] = $type; + } + } + + /** + * Whether processing this argument consumes the generic-RESOLVED parameter + * type: a closure/arrow function does - its parameters and body scope are + * typed from the resolved callable(T) - whether it IS the argument or is + * nested anywhere inside it (the enclosing parameter is pushed on the + * in-function-call stack and the nested closure types itself from there). + * Every other argument only reads variant-stable facts off its parameter. + */ + private function argConsumesResolvedParameterType(Expr $value): bool + { + if ($value instanceof Expr\Closure || $value instanceof Expr\ArrowFunction) { + return true; + } + + // cached on the node - args are re-processed across convergence passes + $cached = $value->getAttribute('phpstanArgContainsClosure'); + if ($cached !== null) { + return $cached; + } + + $contains = (new NodeFinder())->findFirst( + [$value], + static fn (Node $node): bool => $node instanceof Expr\Closure || $node instanceof Expr\ArrowFunction, + ) !== null; + $value->setAttribute('phpstanArgContainsClosure', $contains); + + return $contains; + } + + /** + * Resolves the type of a closure/arrow function argument for the generic + * gather, mirroring ParametersAcceptorSelector::selectFromArgs(): the closure + * type is read with the RAW (un-generic-resolved) acceptor parameter pushed + * onto the in-function-call stack, so its body sees the template parameter + * (effectively mixed for an untyped param) rather than a parameter already + * resolved from sibling args. That keeps the inferred return type (the U in + * callable(T): U) faithful to the closure's own declaration. + * + * @param ParametersAcceptor[] $parametersAcceptors + */ + private function gatherClosureArgType(NodeScopeResolver $nodeScopeResolver, array $parametersAcceptors, int $i, Expr $closureExpr, MutatingScope $scope): Type + { + $rawParameter = null; + if (count($parametersAcceptors) === 1) { + $rawParameters = $parametersAcceptors[0]->getParameters(); + if (isset($rawParameters[$i])) { + $rawParameter = $rawParameters[$i]; + } elseif (count($rawParameters) > 0 && $parametersAcceptors[0]->isVariadic()) { + $rawParameter = array_last($rawParameters); + } + } + + if ($rawParameter !== null) { + $scope = $scope->pushInFunctionCall(null, $rawParameter, false); + } + + return $nodeScopeResolver->resolveCallableTypeForScope($closureExpr, $scope); + } + + /** + * @param array $types + * @param ParametersAcceptor[] $parametersAcceptors + * @param ParametersAcceptor[]|null $namedArgumentsVariants + */ + private function selectArgsAcceptor(array $types, array $parametersAcceptors, ?array $namedArgumentsVariants, bool $hasName, bool $unpack): ParametersAcceptor + { + return $hasName && $namedArgumentsVariants !== null + ? ParametersAcceptorSelector::selectFromTypes($types, $namedArgumentsVariants, $unpack) + : ParametersAcceptorSelector::selectFromTypes($types, $parametersAcceptors, $unpack); + } + + /** + * Applies the intrinsic argument overrides (array_map/filter/walk/find, + * curl_setopt, implode, Closure::bind) on the arg-to-arg evolved scope via + * the non-reprocessing readers, then type-selects the metadata acceptor over + * the arg types gathered so far. The overrides read sibling arg types - which + * closures-last ordering keeps in scope/$gatheredTypes before any closure. + * + * @param Node\Arg[] $args + * @param array $gatheredTypes + * @param ParametersAcceptor[] $parametersAcceptors + * @param ParametersAcceptor[]|null $namedArgumentsVariants + */ + private function selectArgsMetadataAcceptor(NodeScopeResolver $nodeScopeResolver, array $args, array $gatheredTypes, array $parametersAcceptors, ?array $namedArgumentsVariants, bool $hasName, bool $unpack, MutatingScope $scope): ParametersAcceptor + { + $overridden = ParametersAcceptorSelector::applyIntrinsicArgOverrides( + $args, + $parametersAcceptors, + $namedArgumentsVariants, + $scope, + static fn (Expr $e): Type => $nodeScopeResolver->readTypeOfMaybeStored($e, $scope), + static fn (Expr $e): Type => $nodeScopeResolver->readTypeOfMaybeStored($e, $scope->doNotTreatPhpDocTypesAsCertain()), + static fn (Type $t): Type => $scope->getIterableValueType($t), + static fn (Type $t): Type => $scope->getIterableKeyType($t), + ); + + return $this->selectArgsAcceptor($gatheredTypes, $overridden, $namedArgumentsVariants, $hasName, $unpack); + } + + /** + * Arguments normalization (reordering, default-filling) can drop an original + * argument from the call processArgs() iterates - duplicate, unknown-named or + * extra arguments in an invalid call. The parameters check still asks their + * types to report the error, so process them too (their result is stored). + * A NoopNodeCallback keeps the dropped arguments out of rule processing, + * matching the behaviour when this guard is off. + */ + public function processDroppedArgs( + NodeScopeResolver $nodeScopeResolver, + Node\Stmt $stmt, + CallLike $originalCall, + CallLike $normalizedCall, + MutatingScope $scope, + ExpressionResultStorage $storage, + ExpressionContext $context, + ): void + { + if ($originalCall === $normalizedCall) { + return; + } + + $keptValueIds = []; + foreach ($normalizedCall->getArgs() as $normalizedArg) { + $keptValueIds[spl_object_id($normalizedArg->value)] = true; + } + + foreach ($originalCall->getArgs() as $originalArg) { + if (isset($keptValueIds[spl_object_id($originalArg->value)])) { + continue; + } + + $nodeScopeResolver->processExprNode($stmt, $originalArg->value, $scope, $storage, new NoopNodeCallback(), $context->enterDeep()->withoutTemplateArgumentResolution()); + } + } + + /** + * @param MethodReflection|FunctionReflection|null $calleeReflection + */ + private function callCallbackImmediately(?ParameterReflection $parameter, ?Type $parameterType, $calleeReflection): bool + { + $parameterCallableType = null; + if ($parameterType !== null && $calleeReflection instanceof FunctionReflection) { + $parameterCallableType = TypeUtils::findCallableType($parameterType); + } + + if ($parameter instanceof ExtendedParameterReflection) { + $parameterCallImmediately = $parameter->isImmediatelyInvokedCallable(); + if ($parameterCallImmediately->maybe()) { + $callCallbackImmediately = $parameterCallableType !== null; + } else { + $callCallbackImmediately = $parameterCallImmediately->yes(); + } + } else { + $callCallbackImmediately = $parameterCallableType !== null; + } + + return $callCallbackImmediately; + } + + /** + * A callback passed as an argument escapes the current scope and may be invoked, + * so its mutations have to invalidate the outer scope - unless the parameter is + * explicitly marked as later-invoked, in which case the callback only runs after + * the current function returns and its mutations are not visible here yet. + */ + private function shouldInvalidateCallbackExpressions(?ParameterReflection $parameter): bool + { + if ($parameter instanceof ExtendedParameterReflection) { + return !$parameter->isImmediatelyInvokedCallable()->no(); + } + + return true; + } + + /** + * @param MethodReflection|FunctionReflection|null $calleeReflection + */ + private function getParameterTypeFromParameterClosureTypeExtension(CallLike $callLike, $calleeReflection, ParameterReflection $parameter, MutatingScope $scope): ?Type + { + if ($callLike instanceof FuncCall && $calleeReflection instanceof FunctionReflection) { + foreach ($this->functionParameterClosureTypeExtensions->getAll() as $functionParameterClosureTypeExtension) { + if ($functionParameterClosureTypeExtension->isFunctionSupported($calleeReflection, $parameter)) { + return $functionParameterClosureTypeExtension->getTypeFromFunctionCall($calleeReflection, $callLike, $parameter, $scope); + } + } + } elseif ($calleeReflection instanceof MethodReflection) { + if ($callLike instanceof StaticCall) { + foreach ($this->staticMethodParameterClosureTypeExtensions->getAll() as $staticMethodParameterClosureTypeExtension) { + if ($staticMethodParameterClosureTypeExtension->isStaticMethodSupported($calleeReflection, $parameter)) { + return $staticMethodParameterClosureTypeExtension->getTypeFromStaticMethodCall($calleeReflection, $callLike, $parameter, $scope); + } + } + } elseif ($callLike instanceof New_ && $callLike->class instanceof Name) { + $staticCall = new StaticCall( + $callLike->class, + new Identifier('__construct'), + $callLike->getArgs(), + ); + foreach ($this->staticMethodParameterClosureTypeExtensions->getAll() as $staticMethodParameterClosureTypeExtension) { + if ($staticMethodParameterClosureTypeExtension->isStaticMethodSupported($calleeReflection, $parameter)) { + return $staticMethodParameterClosureTypeExtension->getTypeFromStaticMethodCall($calleeReflection, $staticCall, $parameter, $scope); + } + } + } elseif ($callLike instanceof MethodCall) { + foreach ($this->methodParameterClosureTypeExtensions->getAll() as $methodParameterClosureTypeExtension) { + if ($methodParameterClosureTypeExtension->isMethodSupported($calleeReflection, $parameter)) { + return $methodParameterClosureTypeExtension->getTypeFromMethodCall($calleeReflection, $callLike, $parameter, $scope); + } + } + } + } + + return null; + } + + /** + * @param MethodReflection|FunctionReflection|null $calleeReflection + */ + private function getParameterOutExtensionsType(CallLike $callLike, $calleeReflection, ParameterReflection $currentParameter, MutatingScope $scope): ?Type + { + $paramOutTypes = []; + if ($callLike instanceof FuncCall && $calleeReflection instanceof FunctionReflection) { + foreach ($this->functionParameterOutTypeExtensions->getAll() as $functionParameterOutTypeExtension) { + if (!$functionParameterOutTypeExtension->isFunctionSupported($calleeReflection, $currentParameter)) { + continue; + } + + $resolvedType = $functionParameterOutTypeExtension->getParameterOutTypeFromFunctionCall($calleeReflection, $callLike, $currentParameter, $scope); + if ($resolvedType === null) { + continue; + } + $paramOutTypes[] = $resolvedType; + } + } elseif ($callLike instanceof MethodCall && $calleeReflection instanceof MethodReflection) { + foreach ($this->methodParameterOutTypeExtensions->getAll() as $methodParameterOutTypeExtension) { + if (!$methodParameterOutTypeExtension->isMethodSupported($calleeReflection, $currentParameter)) { + continue; + } + + $resolvedType = $methodParameterOutTypeExtension->getParameterOutTypeFromMethodCall($calleeReflection, $callLike, $currentParameter, $scope); + if ($resolvedType === null) { + continue; + } + $paramOutTypes[] = $resolvedType; + } + } elseif ($callLike instanceof StaticCall && $calleeReflection instanceof MethodReflection) { + foreach ($this->staticMethodParameterOutTypeExtensions->getAll() as $staticMethodParameterOutTypeExtension) { + if (!$staticMethodParameterOutTypeExtension->isStaticMethodSupported($calleeReflection, $currentParameter)) { + continue; + } + + $resolvedType = $staticMethodParameterOutTypeExtension->getParameterOutTypeFromStaticMethodCall($calleeReflection, $callLike, $currentParameter, $scope); + if ($resolvedType === null) { + continue; + } + $paramOutTypes[] = $resolvedType; + } + } + + if (count($paramOutTypes) === 1) { + return $paramOutTypes[0]; + } + + if (count($paramOutTypes) > 1) { + return TypeCombinator::union(...$paramOutTypes); + } + + return null; + } + + /** + * @param FunctionReflection|MethodReflection|null $calleeReflection + */ + private function resolveClosureThisType( + ?CallLike $call, + $calleeReflection, + ParameterReflection $parameter, + MutatingScope $scope, + ): ?Type + { + if ($call instanceof FuncCall && $calleeReflection instanceof FunctionReflection) { + foreach ($this->functionParameterClosureThisExtensions->getAll() as $extension) { + if (! $extension->isFunctionSupported($calleeReflection, $parameter)) { + continue; + } + $type = $extension->getClosureThisTypeFromFunctionCall($calleeReflection, $call, $parameter, $scope); + if ($type !== null) { + return $type; + } + } + } elseif ($call instanceof StaticCall && $calleeReflection instanceof MethodReflection) { + foreach ($this->staticMethodParameterClosureThisExtensions->getAll() as $extension) { + if (! $extension->isStaticMethodSupported($calleeReflection, $parameter)) { + continue; + } + $type = $extension->getClosureThisTypeFromStaticMethodCall($calleeReflection, $call, $parameter, $scope); + if ($type !== null) { + return $type; + } + } + } elseif ($call instanceof MethodCall && $calleeReflection instanceof MethodReflection) { + foreach ($this->methodParameterClosureThisExtensions->getAll() as $extension) { + if (! $extension->isMethodSupported($calleeReflection, $parameter)) { + continue; + } + $type = $extension->getClosureThisTypeFromMethodCall($calleeReflection, $call, $parameter, $scope); + if ($type !== null) { + return $type; + } + } + } + + if ($parameter instanceof ExtendedParameterReflection) { + return $parameter->getClosureThisType(); + } + + return null; + } + + /** + * The parameter type an argument is observed against: the declared one with + * the template types the call already decided substituted - the receiver's + * class-level arguments, a template an earlier argument inferred - while a + * template still open (ErrorType in the resolved map, typically the one this + * very argument decides) stays a TemplateType, which the observer ignores. + * The resolved parameter type would carry such a template's bound instead. + */ + private function findOriginalParameterType(ParametersAcceptor $acceptor, ParameterReflection $parameter): ?Type + { + if (!$acceptor instanceof ResolvedFunctionVariant) { + return $parameter->getType(); + } + $originalParameters = $acceptor->getOriginalParametersAcceptor()->getParameters(); + foreach ($acceptor->getParameters() as $index => $resolvedParameter) { + if ($resolvedParameter !== $parameter) { + continue; + } + if (!isset($originalParameters[$index])) { + return null; + } + $originalType = $originalParameters[$index]->getType(); + if (!$originalType->hasTemplateOrLateResolvableType()) { + return $originalType; + } + $decided = []; + foreach ($acceptor->getResolvedTemplateTypeMap()->getTypes() as $name => $type) { + if ($type instanceof ErrorType) { + continue; + } + $decided[$name] = $type; + } + + return TemplateTypeHelper::resolveTemplateTypes( + $originalType, + new TemplateTypeMap($decided), + $acceptor->getCallSiteVarianceMap(), + TemplateTypeVariance::createContravariant(), + ); + } + + return null; + } + + /** + * The result processArgs() captured for a call argument. Unlike the storage + * lookup this reads the argument's authoritative result: a closure/arrow + * function argument's captured result is the properly-typed one, not the + * placeholder its body walk stored. + * + * @param array $argResults + */ + private function readArgResult(array $argResults, Expr $argValue): ExpressionResult + { + $result = $argResults[spl_object_id($argValue)] ?? null; + if ($result === null) { + throw new ShouldNotHappenException(sprintf( + '%s on line %d has no captured ExpressionResult - it was not processed as an argument by processArgs().', + get_class($argValue), + $argValue->getStartLine(), + )); + } + + return $result; + } + +} diff --git a/src/Analyser/ExprHandler/FuncCallHandler.php b/src/Analyser/ExprHandler/FuncCallHandler.php index 78ad61113b9..3682474ee5a 100644 --- a/src/Analyser/ExprHandler/FuncCallHandler.php +++ b/src/Analyser/ExprHandler/FuncCallHandler.php @@ -13,6 +13,7 @@ use PhpParser\Node\Scalar\String_; use PhpParser\Node\Stmt; use PHPStan\Analyser\ArgsResult; +use PHPStan\Analyser\ArgumentsHandler; use PHPStan\Analyser\ArgumentsNormalizer; use PHPStan\Analyser\ExpressionContext; use PHPStan\Analyser\ExpressionResult; @@ -103,6 +104,7 @@ public function __construct( private DynamicReturnTypeStoragePrimer $storagePrimer, private ImpossibleCheckTypeHelper $impossibleCheckTypeHelper, private ClosureTypeResolver $closureTypeResolver, + private ArgumentsHandler $argumentsHandler, ) { } @@ -140,7 +142,7 @@ public function processExpr(NodeScopeResolver $nodeScopeResolver, Stmt $stmt, Ex // their turn. The closure is then walked on the post-argument scope. $shallowVariants = $this->closureTypeResolver->getDeclaredClosureType($scope, $expr->name)->getCallableParametersAcceptors($scope); $shallowAcceptor = ParametersAcceptorSelector::combineVariantsForNormalization($expr->getArgs(), $shallowVariants, null); - $argumentsWalkedAhead = $nodeScopeResolver->processArgs($stmt, null, null, $shallowVariants, null, ArgumentsNormalizer::reorderFuncArguments($shallowAcceptor, $expr) ?? $expr, $scope, $storage, $nodeCallback, $context); + $argumentsWalkedAhead = $this->argumentsHandler->processArgs($nodeScopeResolver, $stmt, null, null, $shallowVariants, null, ArgumentsNormalizer::reorderFuncArguments($shallowAcceptor, $expr) ?? $expr, $scope, $storage, $nodeCallback, $context); $scope = $argumentsWalkedAhead->getScope(); } if ($expr->name instanceof Expr) { @@ -320,7 +322,7 @@ public function processExpr(NodeScopeResolver $nodeScopeResolver, Stmt $stmt, Ex // the arguments are processed; the call resolves from the walked // closure's acceptor, which is the sole variant of its type ? $argumentsWalkedAhead->withResolvedParametersAcceptor($parametersAcceptor) - : $nodeScopeResolver->processArgs($stmt, $functionReflection, null, $variants, $namedArgumentsVariants, $normalizedExpr, $scope, $storage, $nodeCallback, $context); + : $this->argumentsHandler->processArgs($nodeScopeResolver, $stmt, $functionReflection, null, $variants, $namedArgumentsVariants, $normalizedExpr, $scope, $storage, $nodeCallback, $context); } finally { if ($argsGatherer !== null) { $nodeScopeResolver->popNodeGatherer(); @@ -332,7 +334,7 @@ public function processExpr(NodeScopeResolver $nodeScopeResolver, Stmt $stmt, Ex if ($argumentsWalkedAhead === null) { $scope = $argsResult->getScope(); } - $nodeScopeResolver->processDroppedArgs($stmt, $expr, $normalizedExpr, $scope, $storage, $context); + $this->argumentsHandler->processDroppedArgs($nodeScopeResolver, $stmt, $expr, $normalizedExpr, $scope, $storage, $context); $hasYield = $argsResult->hasYield(); $throwPoints = array_merge($throwPoints, $argsResult->getThrowPoints()); $impurePoints = array_merge($impurePoints, $argsResult->getImpurePoints()); diff --git a/src/Analyser/ExprHandler/Helper/FuncCallScopeEffectsHelper.php b/src/Analyser/ExprHandler/Helper/FuncCallScopeEffectsHelper.php index 255400602cd..379dc823178 100644 --- a/src/Analyser/ExprHandler/Helper/FuncCallScopeEffectsHelper.php +++ b/src/Analyser/ExprHandler/Helper/FuncCallScopeEffectsHelper.php @@ -385,7 +385,7 @@ public function applyCallScopeEffects(NodeScopeResolver $nodeScopeResolver, Stmt /** * Invalidates the arguments a callee could write through, mirroring what - * NodeScopeResolver::processArgs() does for a callee with side effects. A + * ArgumentsHandler::processArgs() does for a callee with side effects. A * closure has no FunctionReflection, so processArgs() skips it. * * @param callable(Node $node, Scope $scope): void $nodeCallback diff --git a/src/Analyser/ExprHandler/MethodCallHandler.php b/src/Analyser/ExprHandler/MethodCallHandler.php index 718bbeeba81..ab75278cd1f 100644 --- a/src/Analyser/ExprHandler/MethodCallHandler.php +++ b/src/Analyser/ExprHandler/MethodCallHandler.php @@ -7,6 +7,7 @@ use PhpParser\Node\Identifier; use PhpParser\Node\Stmt; use PHPStan\Analyser\ArgsResult; +use PHPStan\Analyser\ArgumentsHandler; use PHPStan\Analyser\ArgumentsNormalizer; use PHPStan\Analyser\CalledMethodProcessor; use PHPStan\Analyser\ExpressionContext; @@ -75,6 +76,7 @@ public function __construct( private DefaultNarrowingHelper $defaultNarrowingHelper, private DynamicReturnTypeStoragePrimer $storagePrimer, private EarlyTerminatingCallHelper $earlyTerminatingHelper, + private ArgumentsHandler $argumentsHandler, ) { } @@ -156,7 +158,8 @@ public function processExpr(NodeScopeResolver $nodeScopeResolver, Stmt $stmt, Ex if ($parametersAcceptor !== null && $context->getInAssignRightSideExpr() === $expr) { $context = $context->enterAssignRightSideCallArgs($parametersAcceptor); } - $argsResult = $nodeScopeResolver->processArgs( + $argsResult = $this->argumentsHandler->processArgs( + $nodeScopeResolver, $stmt, $methodReflection, $methodReflection !== null ? $scope->getNakedMethod($calledOnType, $methodReflection->getName()) : null, @@ -170,7 +173,7 @@ public function processExpr(NodeScopeResolver $nodeScopeResolver, Stmt $stmt, Ex ); $resolvedParametersAcceptor = $argsResult->getResolvedParametersAcceptor(); $scope = $argsResult->getScope(); - $nodeScopeResolver->processDroppedArgs($stmt, $expr, $normalizedExpr, $scope, $storage, $context); + $this->argumentsHandler->processDroppedArgs($nodeScopeResolver, $stmt, $expr, $normalizedExpr, $scope, $storage, $context); if ($methodReflection !== null) { // created after the args were processed - the pure-unless-callable- diff --git a/src/Analyser/ExprHandler/NewHandler.php b/src/Analyser/ExprHandler/NewHandler.php index efa729771f4..0d25d3b6469 100644 --- a/src/Analyser/ExprHandler/NewHandler.php +++ b/src/Analyser/ExprHandler/NewHandler.php @@ -9,6 +9,7 @@ use PhpParser\Node\Name; use PhpParser\Node\Stmt; use PHPStan\Analyser\ArgsResult; +use PHPStan\Analyser\ArgumentsHandler; use PHPStan\Analyser\ArgumentsNormalizer; use PHPStan\Analyser\ExpressionContext; use PHPStan\Analyser\ExpressionResult; @@ -97,6 +98,7 @@ public function __construct( private DefaultNarrowingHelper $defaultNarrowingHelper, private DynamicReturnTypeStoragePrimer $storagePrimer, private Container $container, + private ArgumentsHandler $argumentsHandler, ) { } @@ -236,10 +238,10 @@ public function processExpr(NodeScopeResolver $nodeScopeResolver, Stmt $stmt, Ex $variants = $constructorReflection !== null ? $constructorReflection->getVariants() : []; $namedArgumentsVariants = $constructorReflection !== null ? $constructorReflection->getNamedArgumentsVariants() : null; $scopeBeforeArgs = $scope; - $argsResult = $nodeScopeResolver->processArgs($stmt, $constructorReflection, null, $variants, $namedArgumentsVariants, $normalizedExpr, $scope, $storage, $nodeCallback, $context); + $argsResult = $this->argumentsHandler->processArgs($nodeScopeResolver, $stmt, $constructorReflection, null, $variants, $namedArgumentsVariants, $normalizedExpr, $scope, $storage, $nodeCallback, $context); $resolvedParametersAcceptor = $argsResult->getResolvedParametersAcceptor(); $scope = $argsResult->getScope(); - $nodeScopeResolver->processDroppedArgs($stmt, $expr, $normalizedExpr, $scope, $storage, $context); + $this->argumentsHandler->processDroppedArgs($nodeScopeResolver, $stmt, $expr, $normalizedExpr, $scope, $storage, $context); $hasYield = $hasYield || $argsResult->hasYield(); $throwPoints = array_merge($throwPoints, $argsResult->getThrowPoints()); $impurePoints = array_merge($impurePoints, $argsResult->getImpurePoints()); diff --git a/src/Analyser/ExprHandler/StaticCallHandler.php b/src/Analyser/ExprHandler/StaticCallHandler.php index d8816c219fc..9a453b2ea95 100644 --- a/src/Analyser/ExprHandler/StaticCallHandler.php +++ b/src/Analyser/ExprHandler/StaticCallHandler.php @@ -9,6 +9,7 @@ use PhpParser\Node\Name; use PhpParser\Node\Stmt; use PHPStan\Analyser\ArgsResult; +use PHPStan\Analyser\ArgumentsHandler; use PHPStan\Analyser\ArgumentsNormalizer; use PHPStan\Analyser\ExpressionContext; use PHPStan\Analyser\ExpressionResult; @@ -78,6 +79,7 @@ public function __construct( private DefaultNarrowingHelper $defaultNarrowingHelper, private DynamicReturnTypeStoragePrimer $storagePrimer, private EarlyTerminatingCallHelper $earlyTerminatingHelper, + private ArgumentsHandler $argumentsHandler, ) { } @@ -250,10 +252,10 @@ public function processExpr(NodeScopeResolver $nodeScopeResolver, Stmt $stmt, Ex if ($parametersAcceptor !== null && $context->getInAssignRightSideExpr() === $expr) { $context = $context->enterAssignRightSideCallArgs($parametersAcceptor); } - $argsResult = $nodeScopeResolver->processArgs($stmt, $methodReflection, null, $variants, $namedArgumentsVariants, $normalizedExpr, $scope, $storage, $nodeCallback, $context, $closureBindScopeFactory); + $argsResult = $this->argumentsHandler->processArgs($nodeScopeResolver, $stmt, $methodReflection, null, $variants, $namedArgumentsVariants, $normalizedExpr, $scope, $storage, $nodeCallback, $context, $closureBindScopeFactory); $resolvedParametersAcceptor = $argsResult->getResolvedParametersAcceptor(); $scope = $argsResult->getScope(); - $nodeScopeResolver->processDroppedArgs($stmt, $expr, $normalizedExpr, $scope, $storage, $context); + $this->argumentsHandler->processDroppedArgs($nodeScopeResolver, $stmt, $expr, $normalizedExpr, $scope, $storage, $context); if ($methodReflection !== null) { // created after the args were processed - the pure-unless-callable- diff --git a/src/Analyser/NodeScopeResolver.php b/src/Analyser/NodeScopeResolver.php index e1904ebfcf9..c380c39030b 100644 --- a/src/Analyser/NodeScopeResolver.php +++ b/src/Analyser/NodeScopeResolver.php @@ -7,7 +7,6 @@ use PhpParser\Node\AttributeGroup; use PhpParser\Node\Expr; use PhpParser\Node\Expr\ArrayDimFetch; -use PhpParser\Node\Expr\CallLike; use PhpParser\Node\Expr\FuncCall; use PhpParser\Node\Expr\List_; use PhpParser\Node\Expr\MethodCall; @@ -16,8 +15,6 @@ use PhpParser\Node\Expr\StaticCall; use PhpParser\Node\Expr\StaticPropertyFetch; use PhpParser\Node\Expr\Variable; -use PhpParser\Node\Identifier; -use PhpParser\Node\Name; use PhpParser\Node\Stmt\Class_; use PhpParser\Node\Stmt\Echo_; use PhpParser\Node\Stmt\Foreach_; @@ -28,7 +25,6 @@ use PhpParser\NodeFinder; use PHPStan\Analyser\ExprHandler\ArrowFunctionHandler; use PHPStan\Analyser\ExprHandler\AssignHandler; -use PHPStan\Analyser\ExprHandler\ClosureHandler; use PHPStan\Analyser\ExprHandler\Helper\ClosureParameterResolver; use PHPStan\Analyser\ExprHandler\Helper\ClosureTypeResolver; use PHPStan\Analyser\ExprHandler\Helper\NonNullabilityHelper; @@ -63,54 +59,27 @@ use PHPStan\Parser\ArrowFunctionArgVisitor; use PHPStan\Parser\ClosureArgVisitor; use PHPStan\Parser\ImmediatelyInvokedClosureVisitor; -use PHPStan\Reflection\Callables\SimpleImpurePoint; -use PHPStan\Reflection\Callables\SimpleThrowPoint; -use PHPStan\Reflection\ExtendedMethodReflection; -use PHPStan\Reflection\ExtendedParameterReflection; -use PHPStan\Reflection\FunctionReflection; -use PHPStan\Reflection\MethodReflection; use PHPStan\Reflection\Native\NativeMethodReflection; use PHPStan\Reflection\Native\NativeParameterReflection; use PHPStan\Reflection\ParameterReflection; -use PHPStan\Reflection\ParametersAcceptor; use PHPStan\Reflection\ParametersAcceptorSelector; use PHPStan\Reflection\Php\PhpMethodReflection; use PHPStan\Reflection\ReflectionProvider; -use PHPStan\Reflection\ResolvedFunctionVariant; use PHPStan\Rules\Properties\ReadWritePropertiesExtension; use PHPStan\ShouldNotHappenException; use PHPStan\TrinaryLogic; use PHPStan\Type\ClosureType; use PHPStan\Type\ErrorType; -use PHPStan\Type\FunctionParameterClosureThisExtension; -use PHPStan\Type\FunctionParameterClosureTypeExtension; -use PHPStan\Type\FunctionParameterOutTypeExtension; -use PHPStan\Type\Generic\TemplateTypeHelper; -use PHPStan\Type\Generic\TemplateTypeMap; -use PHPStan\Type\Generic\TemplateTypeVariance; -use PHPStan\Type\MethodParameterClosureThisExtension; -use PHPStan\Type\MethodParameterClosureTypeExtension; -use PHPStan\Type\MethodParameterOutTypeExtension; use PHPStan\Type\MixedType; use PHPStan\Type\NullType; -use PHPStan\Type\ObjectWithoutClassType; -use PHPStan\Type\ResourceType; -use PHPStan\Type\StaticMethodParameterClosureThisExtension; -use PHPStan\Type\StaticMethodParameterClosureTypeExtension; -use PHPStan\Type\StaticMethodParameterOutTypeExtension; -use PHPStan\Type\ThisType; use PHPStan\Type\Type; use PHPStan\Type\TypeCombinator; use PHPStan\Type\TypeUtils; use PHPStan\Type\UnionType; -use function array_filter; use function array_key_exists; -use function array_keys; -use function array_last; use function array_map; use function array_merge; use function array_pop; -use function array_values; use function count; use function get_class; use function getenv; @@ -120,7 +89,6 @@ use function max; use function spl_object_id; use function sprintf; -use function usort; #[AutowiredService] class NodeScopeResolver @@ -182,16 +150,7 @@ class NodeScopeResolver public static array $guardProcessedExprIds = []; /** - * @param ExtensionsCollection $functionParameterOutTypeExtensions - * @param ExtensionsCollection $methodParameterOutTypeExtensions - * @param ExtensionsCollection $staticMethodParameterOutTypeExtensions * @param ExtensionsCollection $readWritePropertiesExtensions - * @param ExtensionsCollection $functionParameterClosureThisExtensions - * @param ExtensionsCollection $methodParameterClosureThisExtensions - * @param ExtensionsCollection $staticMethodParameterClosureThisExtensions - * @param ExtensionsCollection $functionParameterClosureTypeExtensions - * @param ExtensionsCollection $methodParameterClosureTypeExtensions - * @param ExtensionsCollection $staticMethodParameterClosureTypeExtensions * @param ExtensionsCollection $perFileAnalysisResettables */ public function __construct( @@ -199,38 +158,19 @@ public function __construct( private readonly TemplateArgumentObserver $templateArgumentObserver, private readonly ReflectionProvider $reflectionProvider, private readonly FileHelper $fileHelper, - #[AutowiredExtensions(of: FunctionParameterOutTypeExtension::class)] - private readonly ExtensionsCollection $functionParameterOutTypeExtensions, - #[AutowiredExtensions(of: MethodParameterOutTypeExtension::class)] - private readonly ExtensionsCollection $methodParameterOutTypeExtensions, - #[AutowiredExtensions(of: StaticMethodParameterOutTypeExtension::class)] - private readonly ExtensionsCollection $staticMethodParameterOutTypeExtensions, #[AutowiredExtensions(of: ReadWritePropertiesExtension::class)] private readonly ExtensionsCollection $readWritePropertiesExtensions, - #[AutowiredExtensions(of: FunctionParameterClosureThisExtension::class)] - private readonly ExtensionsCollection $functionParameterClosureThisExtensions, - #[AutowiredExtensions(of: MethodParameterClosureThisExtension::class)] - private readonly ExtensionsCollection $methodParameterClosureThisExtensions, - #[AutowiredExtensions(of: StaticMethodParameterClosureThisExtension::class)] - private readonly ExtensionsCollection $staticMethodParameterClosureThisExtensions, - #[AutowiredExtensions(of: FunctionParameterClosureTypeExtension::class)] - private readonly ExtensionsCollection $functionParameterClosureTypeExtensions, - #[AutowiredExtensions(of: MethodParameterClosureTypeExtension::class)] - private readonly ExtensionsCollection $methodParameterClosureTypeExtensions, - #[AutowiredExtensions(of: StaticMethodParameterClosureTypeExtension::class)] - private readonly ExtensionsCollection $staticMethodParameterClosureTypeExtensions, #[AutowiredExtensions(of: PerFileAnalysisResettable::class)] private readonly ExtensionsCollection $perFileAnalysisResettables, #[AutowiredParameter] private readonly bool $polluteScopeWithLoopInitialAssignments, #[AutowiredParameter] private readonly bool $polluteScopeWithAlwaysIterableForeach, - #[AutowiredParameter(ref: '%exceptions.implicitThrows%')] - private readonly bool $implicitThrows, #[AutowiredParameter] private readonly bool $treatPhpDocTypesAsCertain, private readonly ExpressionResultFactory $expressionResultFactory, private readonly StatementsHandler $statementsHandler, + private readonly ArgumentsHandler $argumentsHandler, ) { self::$guardNewWorld = getenv('PHPSTAN_GUARD_NW') === '1'; @@ -427,50 +367,6 @@ public function processStmtNodesInternal( } } - /** - * The parameter type an argument is observed against: the declared one with - * the template types the call already decided substituted - the receiver's - * class-level arguments, a template an earlier argument inferred - while a - * template still open (ErrorType in the resolved map, typically the one this - * very argument decides) stays a TemplateType, which the observer ignores. - * The resolved parameter type would carry such a template's bound instead. - */ - private function findOriginalParameterType(ParametersAcceptor $acceptor, ParameterReflection $parameter): ?Type - { - if (!$acceptor instanceof ResolvedFunctionVariant) { - return $parameter->getType(); - } - $originalParameters = $acceptor->getOriginalParametersAcceptor()->getParameters(); - foreach ($acceptor->getParameters() as $index => $resolvedParameter) { - if ($resolvedParameter !== $parameter) { - continue; - } - if (!isset($originalParameters[$index])) { - return null; - } - $originalType = $originalParameters[$index]->getType(); - if (!$originalType->hasTemplateOrLateResolvableType()) { - return $originalType; - } - $decided = []; - foreach ($acceptor->getResolvedTemplateTypeMap()->getTypes() as $name => $type) { - if ($type instanceof ErrorType) { - continue; - } - $decided[$name] = $type; - } - - return TemplateTypeHelper::resolveTemplateTypes( - $originalType, - new TemplateTypeMap($decided), - $acceptor->getCallSiteVarianceMap(), - TemplateTypeVariance::createContravariant(), - ); - } - - return null; - } - /** * @param callable(Node $node, Scope $scope): void $nodeCallback */ @@ -592,6 +488,18 @@ public function getReadWritePropertiesExtensions(): ExtensionsCollection return $this->readWritePropertiesExtensions; } + /** Whether an on-demand walk answers already processed real nodes from their stored results (see processExprOnDemand()). */ + public function isReturningStoredExpressionResults(): bool + { + return $this->returnStoredExpressionResults; + } + + /** Whether the walk consumes stored results unconditionally (see processExprNodeConsumingStored()). */ + public function isConsumingStoredExpressionResults(): bool + { + return $this->consumeStoredExpressionResults; + } + public function lookForSetAllowedUndefinedExpressions(MutatingScope $scope, Expr $expr): MutatingScope { return $this->lookForExpressionCallback($scope, $expr, static fn (MutatingScope $scope, Expr $expr): MutatingScope => $scope->setAllowedUndefinedExpression($expr)); @@ -673,7 +581,7 @@ public function processExprOnDemand(Expr $expr, MutatingScope $scope, Expression // evaluated mid-walk prices another synthetic node) and a hard reset // would turn stored-result consumption off for the rest of the outer // walk - re-processing every remaining subtree and bypassing the - // closure-argument consume guards in processArgs() + // closure-argument consume guards in ArgumentsHandler::processArgs() $previous = $this->returnStoredExpressionResults; $this->returnStoredExpressionResults = true; $scope->pushExpressionResultStorage($storage); @@ -725,28 +633,6 @@ public function readStoredResult(Expr $expr, ExpressionResultStorage $storage): return $result; } - /** - * The result processArgs() captured for a call argument. Unlike the storage - * lookup this reads the argument's authoritative result: a closure/arrow - * function argument's captured result is the properly-typed one, not the - * placeholder its body walk stored. - * - * @param array $argResults - */ - private function readArgResult(array $argResults, Expr $argValue): ExpressionResult - { - $result = $argResults[spl_object_id($argValue)] ?? null; - if ($result === null) { - throw new ShouldNotHappenException(sprintf( - '%s on line %d has no captured ExpressionResult - it was not processed as an argument by processArgs().', - get_class($argValue), - $argValue->getStartLine(), - )); - } - - return $result; - } - /** * The type, on the given scope, of a node that may or may not have a stored * ExpressionResult. Every call site of this method is UNDECIDED about whether @@ -1726,7 +1612,7 @@ public function createNativeCallableParameters(MutatingScope $scope, Expr $closu * closure is being processed, so on-demand processing would re-enter * processClosureNodeInternal() endlessly. */ - private function resolveCallableTypeForScope(Expr $expr, MutatingScope $scope): Type + public function resolveCallableTypeForScope(Expr $expr, MutatingScope $scope): Type { if ($expr instanceof Expr\Closure || $expr instanceof Expr\ArrowFunction) { return $this->container->getByType(ClosureTypeResolver::class)->getClosureType($scope, $expr, false, $scope->getCurrentExpressionResultStorage()); @@ -1876,7 +1762,7 @@ public function processAttributeGroups( ); $expr = new New_($attr->name, $attr->args); $expr = ArgumentsNormalizer::reorderNewArguments($parametersAcceptor, $expr) ?? $expr; - $this->processArgs($stmt, $constructorReflection, null, $constructorReflection->getVariants(), $constructorReflection->getNamedArgumentsVariants(), $expr, $scope, $storage, $nodeCallback, ExpressionContext::createDeep()); + $this->argumentsHandler->processArgs($this, $stmt, $constructorReflection, null, $constructorReflection->getVariants(), $constructorReflection->getNamedArgumentsVariants(), $expr, $scope, $storage, $nodeCallback, ExpressionContext::createDeep()); $this->callNodeCallback($nodeCallback, $attr, $scope, $storage); continue; } @@ -1892,1075 +1778,6 @@ public function processAttributeGroups( } } - /** - * @param FunctionReflection|MethodReflection|null $calleeReflection - */ - private function resolveClosureThisType( - ?CallLike $call, - $calleeReflection, - ParameterReflection $parameter, - MutatingScope $scope, - ): ?Type - { - if ($call instanceof FuncCall && $calleeReflection instanceof FunctionReflection) { - foreach ($this->functionParameterClosureThisExtensions->getAll() as $extension) { - if (! $extension->isFunctionSupported($calleeReflection, $parameter)) { - continue; - } - $type = $extension->getClosureThisTypeFromFunctionCall($calleeReflection, $call, $parameter, $scope); - if ($type !== null) { - return $type; - } - } - } elseif ($call instanceof StaticCall && $calleeReflection instanceof MethodReflection) { - foreach ($this->staticMethodParameterClosureThisExtensions->getAll() as $extension) { - if (! $extension->isStaticMethodSupported($calleeReflection, $parameter)) { - continue; - } - $type = $extension->getClosureThisTypeFromStaticMethodCall($calleeReflection, $call, $parameter, $scope); - if ($type !== null) { - return $type; - } - } - } elseif ($call instanceof MethodCall && $calleeReflection instanceof MethodReflection) { - foreach ($this->methodParameterClosureThisExtensions->getAll() as $extension) { - if (! $extension->isMethodSupported($calleeReflection, $parameter)) { - continue; - } - $type = $extension->getClosureThisTypeFromMethodCall($calleeReflection, $call, $parameter, $scope); - if ($type !== null) { - return $type; - } - } - } - - if ($parameter instanceof ExtendedParameterReflection) { - return $parameter->getClosureThisType(); - } - - return null; - } - - /** - * @param MethodReflection|FunctionReflection|null $calleeReflection - * @param ParametersAcceptor[] $parametersAcceptors - * @param ParametersAcceptor[]|null $namedArgumentsVariants - * @param callable(Node $node, Scope $scope): void $nodeCallback - * @param (callable(MutatingScope): MutatingScope)|null $closureBindScopeFactory - */ - public function processArgs( - Node\Stmt $stmt, - $calleeReflection, - ?ExtendedMethodReflection $nakedMethodReflection, - array $parametersAcceptors, - ?array $namedArgumentsVariants, - CallLike $callLike, - MutatingScope $scope, - ExpressionResultStorage $storage, - callable $nodeCallback, - ExpressionContext $context, - ?callable $closureBindScopeFactory = null, - ): ArgsResult - { - $args = $callLike->getArgs(); - - // Evolving-scope arg types: gathered as each argument is processed on the - // scope that evolves arg-to-arg. They select the FINAL resolved acceptor - // (the call's return type, by-ref OUT types), which type-resolves generics - // from the actual argument types. - $gatheredTypes = []; - $gatheredUnpack = false; - $gatheredHasName = false; - $gatheredArgTypeByIndex = []; - - // Metadata acceptor base - NO forward read. The per-argument resolution below picks the - // count-correct variant (the by-ref/variadic STRUCTURE is variant-stable except where it is - // keyed off the argument count, e.g. sscanf - and the count is known structurally) and - // resolves generic parameter types from the args gathered so far; the call's return type - // comes from the post-loop resolved acceptor. - $metadataAcceptor = $parametersAcceptors[0] ?? null; - - // Whether selecting an acceptor is type-driven at all: multiple variants to - // choose between, templates or conditionals to resolve from the arg types, - // or named-argument variants. When it is not, the gathered arg types can - // never influence the selected acceptor, so the faithful-return gather walk - // of a closure/arrow argument (gatherClosureArgType()) would be pure waste - - // its signature-only shallow type keeps the count/name bookkeeping correct. - $typeDrivenAcceptorSelection = count($parametersAcceptors) > 1 - || $namedArgumentsVariants !== null - || ($metadataAcceptor !== null && ParametersAcceptorSelector::hasAcceptorTemplateOrLateResolvableType($metadataAcceptor)); - - // Both predicates are hoisted out of the per-argument loop - they traverse - // the acceptor's parameter types. - $hasTemplateParameterType = $metadataAcceptor !== null - && ParametersAcceptorSelector::hasAcceptorTemplateOrLateResolvableParameterType($metadataAcceptor); - $argMetadataIsTypeDriven = count($parametersAcceptors) > 1 || $hasTemplateParameterType; - - $hasYield = false; - $throwPoints = []; - $impurePoints = []; - $isAlwaysTerminating = false; - /** @var list $deferredInvalidateExpressions */ - $deferredInvalidateExpressions = []; - /** @var ProcessClosureResult[] $deferredByRefClosureResults */ - $deferredByRefClosureResults = []; - - $processingOrder = array_keys($args); - usort($processingOrder, static function (int $a, int $b) use ($args): int { - $aOriginalArg = $args[$a]->getAttribute(ArgumentsNormalizer::ORIGINAL_ARG_ATTRIBUTE); - $bOriginalArg = $args[$b]->getAttribute(ArgumentsNormalizer::ORIGINAL_ARG_ATTRIBUTE); - $aValue = $aOriginalArg !== null ? $aOriginalArg->value : $args[$a]->value; - $bValue = $bOriginalArg !== null ? $bOriginalArg->value : $args[$b]->value; - $aIsClosure = $aValue instanceof Expr\Closure || $aValue instanceof Expr\ArrowFunction; - $bIsClosure = $bValue instanceof Expr\Closure || $bValue instanceof Expr\ArrowFunction; - if ($aIsClosure !== $bIsClosure) { - // closures sort after non-closures so every sibling feeding an - // intrinsic override / generic callable(T) is in scope first - return $aIsClosure ? 1 : -1; - } - - $aOriginal = $args[$a]->getAttribute(ArgumentsNormalizer::ORIGINAL_ARG_ATTRIBUTE); - $bOriginal = $args[$b]->getAttribute(ArgumentsNormalizer::ORIGINAL_ARG_ATTRIBUTE); - if ($aOriginal === null && $bOriginal === null) { - return $a <=> $b; - } - if ($aOriginal === null) { - return 1; - } - if ($bOriginal === null) { - return -1; - } - - return $aOriginal->getStartTokenPos() <=> $bOriginal->getStartTokenPos(); - }); - - $argResults = []; - $byRefArguments = []; - $countStableMetadataAcceptor = null; - foreach ($processingOrder as $i) { - $arg = $args[$i]; - - if ($arg->value instanceof Expr\Closure || $arg->value instanceof Expr\ArrowFunction) { - // Gather the closure/arrow type for the FINAL resolved acceptor on - // the evolving scope, BEFORE the body is processed with a possibly - // generic-resolved parameter injected, so the inferred return type - // stays faithful to the closure's own declaration and its own - // contribution (a TValue from its return) participates in the final - // resolution (see gatherClosureArgType()). - $originalArgForGather = $arg->getAttribute(ArgumentsNormalizer::ORIGINAL_ARG_ATTRIBUTE) ?? $arg; - $gatheredArgTypeByIndex[$i] = $typeDrivenAcceptorSelection - ? $this->gatherClosureArgType($parametersAcceptors, $i, $arg->value, $scope) - : $this->container->getByType(ClosureTypeResolver::class)->getClosureType($scope, $arg->value, true, $storage); - $this->addGatheredArgType($gatheredTypes, $gatheredUnpack, $gatheredHasName, $originalArgForGather, $i, $gatheredArgTypeByIndex[$i]); - } - - $argMetadataAcceptor = $metadataAcceptor; - if ($metadataAcceptor !== null && $argMetadataIsTypeDriven) { - if ($this->argConsumesResolvedParameterType($arg->value)) { - // Resolve the acceptor for this argument from the args gathered SO FAR, padded to the - // full argument count with mixed. Closures sort last and by-ref out-params follow the - // args that pin them, so determining siblings are already processed; the mixed pad keeps - // the argument COUNT correct so the by-ref/variadic variant stays stable (e.g. sscanf), - // while processed siblings resolve a generic callable(T) parameter. No forward read. - $paddedTypes = []; - $paddedUnpack = false; - $paddedHasName = false; - foreach ($args as $j => $paddedArg) { - $paddedOriginalArg = $paddedArg->getAttribute(ArgumentsNormalizer::ORIGINAL_ARG_ATTRIBUTE) ?? $paddedArg; - $this->addGatheredArgType($paddedTypes, $paddedUnpack, $paddedHasName, $paddedOriginalArg, $j, $gatheredArgTypeByIndex[$j] ?? new MixedType()); - } - $argMetadataAcceptor = $this->selectArgsMetadataAcceptor($args, $paddedTypes, $parametersAcceptors, $namedArgumentsVariants, $paddedHasName, $paddedUnpack, $scope); - } else { - // Only a closure/arrow function consumes the generic-RESOLVED - // parameter type: its body is inferred from the resolved - // callable(T) - directly, or through the in-function-call stack - // when nested anywhere inside the argument. Every other argument - // reads variant-stable facts off its parameter (by-ref flag, - // callable bookkeeping), so one all-mixed count-stable selection - // serves them all instead of a full template inference per argument. - if ($countStableMetadataAcceptor === null) { - $paddedTypes = []; - $paddedUnpack = false; - $paddedHasName = false; - foreach ($args as $j => $paddedArg) { - $paddedOriginalArg = $paddedArg->getAttribute(ArgumentsNormalizer::ORIGINAL_ARG_ATTRIBUTE) ?? $paddedArg; - $this->addGatheredArgType($paddedTypes, $paddedUnpack, $paddedHasName, $paddedOriginalArg, $j, new MixedType()); - } - $countStableMetadataAcceptor = $this->selectArgsMetadataAcceptor($args, $paddedTypes, $parametersAcceptors, $namedArgumentsVariants, $paddedHasName, $paddedUnpack, $scope); - } - $argMetadataAcceptor = $countStableMetadataAcceptor; - } - } - $parameters = $argMetadataAcceptor !== null ? $argMetadataAcceptor->getParameters() : null; - - $assignByReference = false; - $parameter = null; - $parameterType = null; - $parameterNativeType = null; - if ($parameters !== null) { - $matchedParameter = null; - if ($arg->name !== null) { - foreach ($parameters as $p) { - if ($p->getName() === $arg->name->toString()) { - $matchedParameter = $p; - break; - } - } - } elseif (isset($parameters[$i])) { - $matchedParameter = $parameters[$i]; - } - - if ($matchedParameter !== null) { - $assignByReference = $matchedParameter->passedByReference()->createsNewVariable(); - $parameterType = $matchedParameter->getType(); - - if ($matchedParameter instanceof ExtendedParameterReflection) { - $parameterNativeType = $matchedParameter->getNativeType(); - } - $parameter = $matchedParameter; - } elseif (count($parameters) > 0 && $argMetadataAcceptor->isVariadic()) { - $lastParameter = array_last($parameters); - $assignByReference = $lastParameter->passedByReference()->createsNewVariable(); - $parameterType = $lastParameter->getType(); - - if ($lastParameter instanceof ExtendedParameterReflection) { - $parameterNativeType = $lastParameter->getNativeType(); - } - $parameter = $lastParameter; - } - } - - if ($parameter !== null && !$parameter->passedByReference()->no()) { - $byRefArguments[spl_object_id($arg->value)] = true; - } - $lookForUnset = false; - if ($assignByReference) { - $isBuiltin = false; - if ($calleeReflection instanceof FunctionReflection && $calleeReflection->isBuiltin()) { - $isBuiltin = true; - } elseif ($calleeReflection instanceof ExtendedMethodReflection && $calleeReflection->getDeclaringClass()->isBuiltin()) { - $isBuiltin = true; - } - if ( - $isBuiltin - || ($parameterNativeType === null || !$parameterNativeType->isNull()->no()) - ) { - $scope = $this->lookForSetAllowedUndefinedExpressions($scope, $arg->value); - $lookForUnset = true; - } - } - - $originalArg = $arg->getAttribute(ArgumentsNormalizer::ORIGINAL_ARG_ATTRIBUTE) ?? $arg; - if ($calleeReflection !== null) { - $rememberTypes = !$originalArg->value instanceof Expr\Closure && !$originalArg->value instanceof Expr\ArrowFunction; - $scope = $scope->pushInFunctionCall($calleeReflection, $parameter, $rememberTypes); - } - - $this->callNodeCallback($nodeCallback, $originalArg, $scope, $storage); - - $originalScope = $scope; - $scopeToPass = $scope; - if ($i === 0 && $closureBindScopeFactory !== null && ($arg->value instanceof Expr\Closure || $arg->value instanceof Expr\ArrowFunction)) { - $scopeToPass = $closureBindScopeFactory($scope); - } - - if ($arg->value instanceof Expr\Closure) { - - $storedClosureArgResult = null; - if ($this->returnStoredExpressionResults || $this->consumeStoredExpressionResults) { - // an on-demand re-walk of the enclosing call must not re-run the - // closure's whole by-ref convergence: consume the main walk's - // stored result, or (when the body release already dropped it) - // price the closure through getClosureType's per-node cache - - // a single body walk on miss, none on repeat asks - $storedClosureArgResult = $storage->findExpressionResult($arg->value); - // consume mode alone (the nullsafe call's plain-twin walk) is the - // FIRST and only walk of these arguments - nothing stored them, so - // a miss means "walk it", not "price it silently": pricing skips - // the body walk and never fires the closure's node callbacks - if ($storedClosureArgResult === null && $this->returnStoredExpressionResults) { - $closureTypeResolver = $this->container->getByType(ClosureTypeResolver::class); - $storedClosureArgResult = $this->expressionResultFactory->create( - $scopeToPass, - beforeScope: $scopeToPass, - expr: $arg->value, - hasYield: false, - isAlwaysTerminating: false, - throwPoints: [], - impurePoints: [], - type: $closureTypeResolver->getClosureType($scopeToPass, $arg->value, false, $storage), - nativeType: $closureTypeResolver->getClosureType($scopeToPass->doNotTreatPhpDocTypesAsCertain(), $arg->value, false, $storage), - typeCallback: null, - specifyTypesCallback: SpecifiedTypes::emptySpecifyCallback(), - ); - $this->storeExpressionResult($storage, $arg->value, $storedClosureArgResult); - } - } - if ($storedClosureArgResult !== null) { - $argResults[spl_object_id($arg->value)] = $storedClosureArgResult; - } else { - $restoreThisScope = null; - if ( - $closureBindScopeFactory === null - && $parameter instanceof ExtendedParameterReflection - && !$arg->value->static - ) { - $closureThisType = $this->resolveClosureThisType($callLike, $calleeReflection, $parameter, $scopeToPass); - if ($closureThisType !== null) { - $restoreThisScope = $scopeToPass; - $scopeToPass = $scopeToPass->assignVariable('this', $closureThisType, new ObjectWithoutClassType(), TrinaryLogic::createYes()) - ->withClosureBindScopeClasses($closureThisType->getObjectClassNames()); - } - } - - if ($parameter !== null) { - $overwritingParameterType = $this->getParameterTypeFromParameterClosureTypeExtension($callLike, $calleeReflection, $parameter, $scopeToPass); - - if ($overwritingParameterType !== null) { - $parameterType = $overwritingParameterType; - - // resolve the native flavour through the same extension on the - // natively-promoted scope, so the closure parameters keep - // their native precision too - $overwritingParameterNativeType = $this->getParameterTypeFromParameterClosureTypeExtension($callLike, $calleeReflection, $parameter, $scopeToPass->doNotTreatPhpDocTypesAsCertain()); - if ($overwritingParameterNativeType !== null) { - $parameterNativeType = $overwritingParameterNativeType; - } - } - } - - $closureResult = $this->processClosureNode($stmt, $arg->value, $scopeToPass, $storage, $nodeCallback, $context, $parameterType, $parameterNativeType); - if ($this->callCallbackImmediately($parameter, $parameterType, $calleeReflection)) { - $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()); - } - - $closureTypeResolver = $this->container->getByType(ClosureTypeResolver::class); - $storedClosureResult = $this->expressionResultFactory->create( - $closureResult->getScope(), - $scopeToPass, - $arg->value, - variableFlow: ClosureHandler::getVariableFlow($arg->value), - hasYield: false, - isAlwaysTerminating: false, - throwPoints: [], - impurePoints: [], - type: $closureTypeResolver->buildClosureTypeForClosure( - $scopeToPass, - $arg->value, - $closureResult->getGatheredReturnStatements(), - $closureResult->getGatheredYieldStatements(), - $closureResult->getExecutionEnds(), - $closureResult->getThrowPoints(), - $closureResult->getClosureTypeImpurePoints(), - $closureResult->getInvalidateExpressions(), - false, - $storage, - ), - // the native flavour reads the stored native types off the same - // single body walk - no second walk on the promoted scope - nativeType: $closureTypeResolver->buildClosureTypeForClosure( - $scopeToPass, - $arg->value, - $closureResult->getGatheredReturnStatements(), - $closureResult->getGatheredYieldStatements(), - $closureResult->getExecutionEnds(), - $closureResult->getThrowPoints(), - $closureResult->getClosureTypeImpurePoints(), - $closureResult->getInvalidateExpressions(), - true, - $storage, - ), - typeCallback: null, - specifyTypesCallback: SpecifiedTypes::emptySpecifyCallback(), - ); - $this->storeExpressionResult($storage, $arg->value, $storedClosureResult); - // the closure node's own callback fires after its result is - // stored, mirroring processExprNodeInternal() - callback-side - // getType() answers from the stored result - $this->callNodeCallbackWithExpression($nodeCallback, $arg->value, $scopeToPass, $storage, $context); - // the arg result must be the properly-typed stored result - - // ArgsResult readers price array_push() & co. from it - $argResults[spl_object_id($arg->value)] = $storedClosureResult; - - $uses = []; - foreach ($arg->value->uses as $use) { - if (!is_string($use->var->name)) { - continue; - } - - $uses[] = $use->var->name; - } - - $scope = $closureResult->getScope(); - $deferredByRefClosureResults[] = $closureResult; - // Prefer the invalidate expressions collected on the ClosureType - - // they also cover writes the closure's own body walk observed, - // unlike $closureResult->getInvalidateExpressions(). - $closureExprType = $storedClosureResult->getType(); - $invalidateExpressions = $closureExprType instanceof ClosureType - ? $closureExprType->getInvalidateExpressions() - : $closureResult->getInvalidateExpressions(); - if ($restoreThisScope !== null) { - $nodeFinder = new NodeFinder(); - $cb = static fn ($expr) => $expr instanceof Variable && $expr->name === 'this'; - foreach ($invalidateExpressions as $j => $invalidateExprNode) { - $foundThis = $nodeFinder->findFirst([$invalidateExprNode->getExpr()], $cb); - if ($foundThis === null) { - continue; - } - - unset($invalidateExpressions[$j]); - } - $invalidateExpressions = array_values($invalidateExpressions); - $scope = $scope->restoreThis($restoreThisScope); - } - - if ($this->shouldInvalidateCallbackExpressions($parameter)) { - $deferredInvalidateExpressions[] = [$invalidateExpressions, $uses]; - } - } - } elseif ($arg->value instanceof Expr\ArrowFunction) { - - $storedClosureArgResult = null; - if ($this->returnStoredExpressionResults || $this->consumeStoredExpressionResults) { - // see the Closure branch above - consume or price via the cache - $storedClosureArgResult = $storage->findExpressionResult($arg->value); - // consume mode alone (the nullsafe call's plain-twin walk) is the - // first and only walk of the argument - a miss means walk it - if ($storedClosureArgResult === null && $this->returnStoredExpressionResults) { - $closureTypeResolver = $this->container->getByType(ClosureTypeResolver::class); - $storedClosureArgResult = $this->expressionResultFactory->create( - $scopeToPass, - beforeScope: $scopeToPass, - expr: $arg->value, - hasYield: false, - isAlwaysTerminating: false, - throwPoints: [], - impurePoints: [], - type: $closureTypeResolver->getClosureType($scopeToPass, $arg->value, false, $storage), - nativeType: $closureTypeResolver->getClosureType($scopeToPass->doNotTreatPhpDocTypesAsCertain(), $arg->value, false, $storage), - typeCallback: null, - specifyTypesCallback: SpecifiedTypes::emptySpecifyCallback(), - ); - $this->storeExpressionResult($storage, $arg->value, $storedClosureArgResult); - } - } - if ($storedClosureArgResult !== null) { - $argResults[spl_object_id($arg->value)] = $storedClosureArgResult; - } else { - if ( - $closureBindScopeFactory === null - && $parameter instanceof ExtendedParameterReflection - && !$arg->value->static - ) { - $closureThisType = $this->resolveClosureThisType($callLike, $calleeReflection, $parameter, $scopeToPass); - if ($closureThisType !== null) { - $scopeToPass = $scopeToPass->assignVariable('this', $closureThisType, new ObjectWithoutClassType(), TrinaryLogic::createYes()) - ->withClosureBindScopeClasses($closureThisType->getObjectClassNames()); - } - } - - if ($parameter !== null) { - $overwritingParameterType = $this->getParameterTypeFromParameterClosureTypeExtension($callLike, $calleeReflection, $parameter, $scopeToPass); - - if ($overwritingParameterType !== null) { - $parameterType = $overwritingParameterType; - - // resolve the native flavour through the same extension on the - // natively-promoted scope, so the closure parameters keep - // their native precision too - $overwritingParameterNativeType = $this->getParameterTypeFromParameterClosureTypeExtension($callLike, $calleeReflection, $parameter, $scopeToPass->doNotTreatPhpDocTypesAsCertain()); - if ($overwritingParameterNativeType !== null) { - $parameterNativeType = $overwritingParameterNativeType; - } - } - } - - $arrowFunctionResult = $this->processArrowFunctionNode($stmt, $arg->value, $scopeToPass, $storage, $nodeCallback, $parameterType, $parameterNativeType, $context); - $arrowFunctionExprResult = $arrowFunctionResult->getExpressionResult(); - if ($this->callCallbackImmediately($parameter, $parameterType, $calleeReflection)) { - $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), $arrowFunctionExprResult->getThrowPoints())); - $impurePoints = array_merge($impurePoints, $arrowFunctionExprResult->getImpurePoints()); - } - $arrowFunctionClosureTypeResolver = $this->container->getByType(ClosureTypeResolver::class); - $arrowFunctionScope = $arrowFunctionResult->getArrowFunctionScope(); - // both flavours are built from the single body walk (see - // ArrowFunctionHandler); the built type also answers the - // invalidate-expressions read below without re-walking the - // still-unstored node through Scope::getType() - $arrowFunctionType = $arrowFunctionClosureTypeResolver->buildClosureTypeForArrowFunction( - $scopeToPass, - $arg->value, - $arrowFunctionScope, - $arrowFunctionResult->getClosureTypeThrowPoints(), - $arrowFunctionResult->getClosureTypeImpurePoints(), - $arrowFunctionResult->getInvalidateExpressions(), - false, - $storage, - ); - $storedArrowResult = $this->expressionResultFactory->create( - $arrowFunctionExprResult->getScope(), - variableFlow: $arrowFunctionExprResult->getVariableFlow(), - beforeScope: $scopeToPass, - expr: $arg->value, - hasYield: $arrowFunctionExprResult->hasYield(), - isAlwaysTerminating: $arrowFunctionExprResult->isAlwaysTerminating(), - throwPoints: $arrowFunctionExprResult->getThrowPoints(), - impurePoints: $arrowFunctionExprResult->getImpurePoints(), - type: $arrowFunctionType, - nativeType: $arrowFunctionClosureTypeResolver->buildClosureTypeForArrowFunction( - $scopeToPass, - $arg->value, - $arrowFunctionScope, - $arrowFunctionResult->getClosureTypeThrowPoints(), - $arrowFunctionResult->getClosureTypeImpurePoints(), - $arrowFunctionResult->getInvalidateExpressions(), - true, - $storage, - ), - typeCallback: null, - specifyTypesCallback: SpecifiedTypes::emptySpecifyCallback(), - ); - $this->storeExpressionResult($storage, $arg->value, $storedArrowResult); - // the arrow function node's own callback fires after its result - // is stored, mirroring processExprNodeInternal() - callback-side - // getType() answers from the stored result - $this->callNodeCallbackWithExpression($nodeCallback, $arg->value, $scopeToPass, $storage, $context); - // the arg result must be the properly-typed stored result, not - // the body walk's placeholder (whose typeCallback answers mixed) - - // ArgsResult readers price array_push() & co. from it - $argResults[spl_object_id($arg->value)] = $storedArrowResult; - if ($this->shouldInvalidateCallbackExpressions($parameter)) { - $deferredInvalidateExpressions[] = [$arrowFunctionType->getInvalidateExpressions(), $arrowFunctionType->getUsedVariables()]; - } - } - $scope = $scope->addTemplateArgumentConstraints($argResults[spl_object_id($arg->value)]->getScope()->getTemplateArgumentConstraints()); - } else { - $enterExpressionAssignForByRef = $assignByReference && $arg->value instanceof ArrayDimFetch && $arg->value->dim === null; - if ($enterExpressionAssignForByRef) { - $scopeToPass = $scopeToPass->enterExpressionAssign($arg->value); - } - $argContext = $context->enterDeep(); - if (!$arg->unpack && $arg->value instanceof Expr\Array_) { - $argContext = $argContext->enterPassedToType($parameterType, $parameterNativeType); - } - $exprResult = $this->processExprNode($stmt, $arg->value, $scopeToPass, $storage, $nodeCallback, $argContext); - $argResults[spl_object_id($arg->value)] = $exprResult; - $exprType = $exprResult->getType(); - $throwPoints = array_merge($throwPoints, $exprResult->getThrowPoints()); - $impurePoints = array_merge($impurePoints, $exprResult->getImpurePoints()); - $isAlwaysTerminating = $isAlwaysTerminating || $exprResult->isAlwaysTerminating(); - $scope = $exprResult->getScope(); - if ($enterExpressionAssignForByRef) { - $scope = $scope->exitExpressionAssign($arg->value); - } - $hasYield = $hasYield || $exprResult->hasYield(); - - if ($exprType->isCallable()->yes()) { - $acceptors = $exprType->getCallableParametersAcceptors($scope); - if (count($acceptors) === 1) { - if ($this->shouldInvalidateCallbackExpressions($parameter)) { - $deferredInvalidateExpressions[] = [$acceptors[0]->getInvalidateExpressions(), $acceptors[0]->getUsedVariables()]; - } - if ($this->callCallbackImmediately($parameter, $parameterType, $calleeReflection)) { - $callableThrowPoints = array_map(static fn (SimpleThrowPoint $throwPoint) => $throwPoint->isExplicit() ? InternalThrowPoint::createExplicit($scope, $throwPoint->getType(), $arg->value, $throwPoint->canContainAnyThrowable()) : InternalThrowPoint::createImplicit($scope, $arg->value), $acceptors[0]->getThrowPoints()); - if (!$this->implicitThrows) { - $callableThrowPoints = array_values(array_filter($callableThrowPoints, static fn (InternalThrowPoint $throwPoint) => $throwPoint->isExplicit())); - } - $throwPoints = array_merge($throwPoints, $callableThrowPoints); - $impurePoints = array_merge($impurePoints, array_map(static fn (SimpleImpurePoint $impurePoint) => new ImpurePoint($scope, $arg->value, $impurePoint->getIdentifier(), $impurePoint->getDescription(), $impurePoint->isCertain()), $acceptors[0]->getImpurePoints())); - } - } - } - - $gatheredArgTypeByIndex[$i] = $exprResult->getType(); - $this->addGatheredArgType($gatheredTypes, $gatheredUnpack, $gatheredHasName, $originalArg, $i, $gatheredArgTypeByIndex[$i]); - $templateArgumentFrame = $this->observingTemplateArgumentFrame($scope); - if ($templateArgumentFrame !== null && $parameter !== null) { - // the metadata acceptor is resolved against the arguments gathered - // before this one, so a template this argument itself decides is - // still its bound there - observe the declared parameter type, - // where such a template is uninformative and the receiver's - // class-level arguments are already in place - $scope = $scope->addTemplateArgumentConstraints($this->templateArgumentObserver->collectArgument( - $this->findOriginalParameterType($argMetadataAcceptor, $parameter) ?? $parameter->getType(), - $gatheredArgTypeByIndex[$i], - ($calleeReflection instanceof FunctionReflection || $calleeReflection instanceof ExtendedMethodReflection) && $calleeReflection->isPure()->yes(), - )); - } - } - - if ($assignByReference && $lookForUnset) { - $scope = $this->lookForUnsetAllowedUndefinedExpressions($scope, $arg->value); - } - - if ($calleeReflection !== null) { - $scope = $scope->popInFunctionCall(); - } - - if ($i !== 0 || $closureBindScopeFactory === null) { - continue; - } - - $scope = $scope->restoreOriginalScopeAfterClosureBind($originalScope); - } - - foreach ($deferredInvalidateExpressions as [$invalidateExpressions, $uses]) { - $scope = $this->processImmediatelyCalledCallable($scope, $invalidateExpressions, $uses); - } - - foreach ($deferredByRefClosureResults as $deferredClosureResult) { - $scope = $deferredClosureResult->applyByRefUseScope($scope); - } - - // Type-driven resolved acceptor: the arg types gathered on the evolving - // scope select (and generic-resolve) the acceptor that drives the call's - // return type. Intrinsic overrides are applied on the final scope, - // mirroring the original selectFromArgs(). - // When the selection is not type-driven, the single acceptor IS the - // resolved acceptor - the fast path selectFromArgs() used to take. - $resolvedAcceptor = null; - if ($parametersAcceptors !== []) { - $resolvedAcceptor = $typeDrivenAcceptorSelection - ? $this->selectArgsMetadataAcceptor($args, $gatheredTypes, $parametersAcceptors, $namedArgumentsVariants, $gatheredHasName, $gatheredUnpack, $scope) - : $metadataAcceptor; - } - - if ($resolvedAcceptor !== null && $this->observingTemplateArgumentFrame($scope) !== null) { - $scope = $scope->addTemplateArgumentConstraints($this->templateArgumentObserver->collectCall( - $callLike, - $resolvedAcceptor, - $gatheredTypes, - $callLike instanceof New_ && $calleeReflection instanceof MethodReflection ? $calleeReflection->getDeclaringClass()->getTemplateTypeMap() : null, - )); - } - - // The by-ref OUT writeback reads the metadata acceptor: it is selected from - // the full argument count (stable variant). When that single acceptor still - // carries templates (fast path), its OUT types need generic-resolving from the - // now-complete gathered arg types - the post-loop $resolvedAcceptor is exactly - // that (same variant, resolved); otherwise the metadata acceptor is already resolved. - $writebackAcceptor = $metadataAcceptor; - if ( - $metadataAcceptor !== null - && $argMetadataIsTypeDriven - ) { - $writebackAcceptor = $resolvedAcceptor; - } - $writebackParameters = $writebackAcceptor !== null ? $writebackAcceptor->getParameters() : null; - if ($writebackParameters !== null) { - foreach ($args as $i => $arg) { - $assignByReference = false; - $currentParameter = null; - if (isset($writebackParameters[$i])) { - $currentParameter = $writebackParameters[$i]; - } elseif (count($writebackParameters) > 0 && $writebackAcceptor->isVariadic()) { - $currentParameter = array_last($writebackParameters); - } - - if ($currentParameter !== null) { - $assignByReference = $currentParameter->passedByReference()->createsNewVariable(); - } - - if ($assignByReference) { - $argValue = $arg->value; - if (!$argValue instanceof Variable || $argValue->name !== 'this') { - $paramOutType = $this->getParameterOutExtensionsType($callLike, $calleeReflection, $currentParameter, $scope); - if ($paramOutType !== null) { - $byRefType = $paramOutType; - } elseif ( - $currentParameter instanceof ExtendedParameterReflection - && $currentParameter->getOutType() !== null - ) { - $byRefType = $currentParameter->getOutType(); - } elseif ( - $calleeReflection instanceof MethodReflection - && !$calleeReflection->getDeclaringClass()->isBuiltin() - ) { - $byRefType = $currentParameter->getType(); - } elseif ( - $calleeReflection instanceof FunctionReflection - && !$calleeReflection->isBuiltin() - ) { - $byRefType = $currentParameter->getType(); - } else { - $byRefType = new MixedType(); - } - - $scope = $this->processVirtualAssign( - $scope, - $storage, - $stmt, - $argValue, - new TypeExpr($byRefType), - $nodeCallback, - )->getScope(); - $scope = $this->lookForUnsetAllowedUndefinedExpressions($scope, $argValue); - } - } elseif ($calleeReflection !== null && $calleeReflection->hasSideEffects()->yes()) { - $argType = $this->readArgResult($argResults, $arg->value)->getTypeOnScope($scope, false); - if (!$argType->isObject()->no()) { - $nakedReturnType = null; - if ($nakedMethodReflection !== null) { - $nakedParametersAcceptor = $this->selectArgsAcceptor( - $gatheredTypes, - $nakedMethodReflection->getVariants(), - $nakedMethodReflection->getNamedArgumentsVariants(), - $gatheredHasName, - $gatheredUnpack, - ); - $nakedReturnType = $nakedParametersAcceptor->getReturnType(); - } - if ( - $nakedReturnType === null - || !(new ThisType($nakedMethodReflection->getDeclaringClass()))->isSuperTypeOf($nakedReturnType)->yes() - || $nakedMethodReflection->isPure()->no() - ) { - $this->callNodeCallback($nodeCallback, new InvalidateExprNode($arg->value), $scope, $storage); - $scope = $scope->invalidateExpression($arg->value, true); - } - } elseif (!(new ResourceType())->isSuperTypeOf($argType)->no()) { - $this->callNodeCallback($nodeCallback, new InvalidateExprNode($arg->value), $scope, $storage); - $scope = $scope->invalidateExpression($arg->value, true); - } - } - } - } - - // not storing this, it's scope after processing all args - return new ArgsResult( - $this->expressionResultFactory->create( - $scope, - $scope, - $callLike, - $hasYield, - $isAlwaysTerminating, - $throwPoints, - $impurePoints, - typeCallback: static fn () => new MixedType(), - specifyTypesCallback: SpecifiedTypes::emptySpecifyCallback(), - ), - $resolvedAcceptor, - $argResults, - $byRefArguments, - ); - } - - /** - * Ports the gather-keying of ParametersAcceptorSelector::selectFromArgs(): - * indexes the gathered arg type by name (sets $hasName) vs position, and - * expands unpacked constant arrays / falls back to the iterable value type - * (sets $unpack), so selectFromTypes() picks the matching variant. - * - * @param array $types - */ - private function addGatheredArgType(array &$types, bool &$unpack, bool &$hasName, Node\Arg $originalArg, int $i, Type $type): void - { - if ($originalArg->name !== null) { - $index = $originalArg->name->toString(); - $hasName = true; - } else { - $index = $i; - } - - if ($originalArg->unpack) { - $unpack = true; - $constantArrays = $type->getConstantArrays(); - if (count($constantArrays) > 0) { - foreach ($constantArrays as $constantArray) { - $values = $constantArray->getValueTypes(); - foreach ($constantArray->getKeyTypes() as $j => $keyType) { - $valueType = $values[$j]; - $valueIndex = $keyType->getValue(); - if (is_string($valueIndex)) { - $hasName = true; - } else { - $valueIndex = $i + $j; - } - - $types[$valueIndex] = isset($types[$valueIndex]) - ? TypeCombinator::union($types[$valueIndex], $valueType) - : $valueType; - } - } - } else { - $types[$index] = $type->getIterableValueType(); - } - } else { - $types[$index] = $type; - } - } - - /** - * Whether processing this argument consumes the generic-RESOLVED parameter - * type: a closure/arrow function does - its parameters and body scope are - * typed from the resolved callable(T) - whether it IS the argument or is - * nested anywhere inside it (the enclosing parameter is pushed on the - * in-function-call stack and the nested closure types itself from there). - * Every other argument only reads variant-stable facts off its parameter. - */ - private function argConsumesResolvedParameterType(Expr $value): bool - { - if ($value instanceof Expr\Closure || $value instanceof Expr\ArrowFunction) { - return true; - } - - // cached on the node - args are re-processed across convergence passes - $cached = $value->getAttribute('phpstanArgContainsClosure'); - if ($cached !== null) { - return $cached; - } - - $contains = (new NodeFinder())->findFirst( - [$value], - static fn (Node $node): bool => $node instanceof Expr\Closure || $node instanceof Expr\ArrowFunction, - ) !== null; - $value->setAttribute('phpstanArgContainsClosure', $contains); - - return $contains; - } - - /** - * Resolves the type of a closure/arrow function argument for the generic - * gather, mirroring ParametersAcceptorSelector::selectFromArgs(): the closure - * type is read with the RAW (un-generic-resolved) acceptor parameter pushed - * onto the in-function-call stack, so its body sees the template parameter - * (effectively mixed for an untyped param) rather than a parameter already - * resolved from sibling args. That keeps the inferred return type (the U in - * callable(T): U) faithful to the closure's own declaration. - * - * @param ParametersAcceptor[] $parametersAcceptors - */ - private function gatherClosureArgType(array $parametersAcceptors, int $i, Expr $closureExpr, MutatingScope $scope): Type - { - $rawParameter = null; - if (count($parametersAcceptors) === 1) { - $rawParameters = $parametersAcceptors[0]->getParameters(); - if (isset($rawParameters[$i])) { - $rawParameter = $rawParameters[$i]; - } elseif (count($rawParameters) > 0 && $parametersAcceptors[0]->isVariadic()) { - $rawParameter = array_last($rawParameters); - } - } - - if ($rawParameter !== null) { - $scope = $scope->pushInFunctionCall(null, $rawParameter, false); - } - - return $this->resolveCallableTypeForScope($closureExpr, $scope); - } - - /** - * @param array $types - * @param ParametersAcceptor[] $parametersAcceptors - * @param ParametersAcceptor[]|null $namedArgumentsVariants - */ - private function selectArgsAcceptor(array $types, array $parametersAcceptors, ?array $namedArgumentsVariants, bool $hasName, bool $unpack): ParametersAcceptor - { - return $hasName && $namedArgumentsVariants !== null - ? ParametersAcceptorSelector::selectFromTypes($types, $namedArgumentsVariants, $unpack) - : ParametersAcceptorSelector::selectFromTypes($types, $parametersAcceptors, $unpack); - } - - /** - * Applies the intrinsic argument overrides (array_map/filter/walk/find, - * curl_setopt, implode, Closure::bind) on the arg-to-arg evolved scope via - * the non-reprocessing readers, then type-selects the metadata acceptor over - * the arg types gathered so far. The overrides read sibling arg types - which - * closures-last ordering keeps in scope/$gatheredTypes before any closure. - * - * @param Node\Arg[] $args - * @param array $gatheredTypes - * @param ParametersAcceptor[] $parametersAcceptors - * @param ParametersAcceptor[]|null $namedArgumentsVariants - */ - private function selectArgsMetadataAcceptor(array $args, array $gatheredTypes, array $parametersAcceptors, ?array $namedArgumentsVariants, bool $hasName, bool $unpack, MutatingScope $scope): ParametersAcceptor - { - $overridden = ParametersAcceptorSelector::applyIntrinsicArgOverrides( - $args, - $parametersAcceptors, - $namedArgumentsVariants, - $scope, - fn (Expr $e): Type => $this->readTypeOfMaybeStored($e, $scope), - fn (Expr $e): Type => $this->readTypeOfMaybeStored($e, $scope->doNotTreatPhpDocTypesAsCertain()), - static fn (Type $t): Type => $scope->getIterableValueType($t), - static fn (Type $t): Type => $scope->getIterableKeyType($t), - ); - - return $this->selectArgsAcceptor($gatheredTypes, $overridden, $namedArgumentsVariants, $hasName, $unpack); - } - - /** - * Arguments normalization (reordering, default-filling) can drop an original - * argument from the call processArgs() iterates - duplicate, unknown-named or - * extra arguments in an invalid call. The parameters check still asks their - * types to report the error, so process them too (their result is stored). - * A NoopNodeCallback keeps the dropped arguments out of rule processing, - * matching the behaviour when this guard is off. - */ - public function processDroppedArgs( - Node\Stmt $stmt, - CallLike $originalCall, - CallLike $normalizedCall, - MutatingScope $scope, - ExpressionResultStorage $storage, - ExpressionContext $context, - ): void - { - if ($originalCall === $normalizedCall) { - return; - } - - $keptValueIds = []; - foreach ($normalizedCall->getArgs() as $normalizedArg) { - $keptValueIds[spl_object_id($normalizedArg->value)] = true; - } - - foreach ($originalCall->getArgs() as $originalArg) { - if (isset($keptValueIds[spl_object_id($originalArg->value)])) { - continue; - } - - $this->processExprNode($stmt, $originalArg->value, $scope, $storage, new NoopNodeCallback(), $context->enterDeep()->withoutTemplateArgumentResolution()); - } - } - - /** - * @param MethodReflection|FunctionReflection|null $calleeReflection - */ - private function callCallbackImmediately(?ParameterReflection $parameter, ?Type $parameterType, $calleeReflection): bool - { - $parameterCallableType = null; - if ($parameterType !== null && $calleeReflection instanceof FunctionReflection) { - $parameterCallableType = TypeUtils::findCallableType($parameterType); - } - - if ($parameter instanceof ExtendedParameterReflection) { - $parameterCallImmediately = $parameter->isImmediatelyInvokedCallable(); - if ($parameterCallImmediately->maybe()) { - $callCallbackImmediately = $parameterCallableType !== null; - } else { - $callCallbackImmediately = $parameterCallImmediately->yes(); - } - } else { - $callCallbackImmediately = $parameterCallableType !== null; - } - - return $callCallbackImmediately; - } - - /** - * A callback passed as an argument escapes the current scope and may be invoked, - * so its mutations have to invalidate the outer scope - unless the parameter is - * explicitly marked as later-invoked, in which case the callback only runs after - * the current function returns and its mutations are not visible here yet. - */ - private function shouldInvalidateCallbackExpressions(?ParameterReflection $parameter): bool - { - if ($parameter instanceof ExtendedParameterReflection) { - return !$parameter->isImmediatelyInvokedCallable()->no(); - } - - return true; - } - - /** - * @param MethodReflection|FunctionReflection|null $calleeReflection - */ - private function getParameterTypeFromParameterClosureTypeExtension(CallLike $callLike, $calleeReflection, ParameterReflection $parameter, MutatingScope $scope): ?Type - { - if ($callLike instanceof FuncCall && $calleeReflection instanceof FunctionReflection) { - foreach ($this->functionParameterClosureTypeExtensions->getAll() as $functionParameterClosureTypeExtension) { - if ($functionParameterClosureTypeExtension->isFunctionSupported($calleeReflection, $parameter)) { - return $functionParameterClosureTypeExtension->getTypeFromFunctionCall($calleeReflection, $callLike, $parameter, $scope); - } - } - } elseif ($calleeReflection instanceof MethodReflection) { - if ($callLike instanceof StaticCall) { - foreach ($this->staticMethodParameterClosureTypeExtensions->getAll() as $staticMethodParameterClosureTypeExtension) { - if ($staticMethodParameterClosureTypeExtension->isStaticMethodSupported($calleeReflection, $parameter)) { - return $staticMethodParameterClosureTypeExtension->getTypeFromStaticMethodCall($calleeReflection, $callLike, $parameter, $scope); - } - } - } elseif ($callLike instanceof New_ && $callLike->class instanceof Name) { - $staticCall = new StaticCall( - $callLike->class, - new Identifier('__construct'), - $callLike->getArgs(), - ); - foreach ($this->staticMethodParameterClosureTypeExtensions->getAll() as $staticMethodParameterClosureTypeExtension) { - if ($staticMethodParameterClosureTypeExtension->isStaticMethodSupported($calleeReflection, $parameter)) { - return $staticMethodParameterClosureTypeExtension->getTypeFromStaticMethodCall($calleeReflection, $staticCall, $parameter, $scope); - } - } - } elseif ($callLike instanceof MethodCall) { - foreach ($this->methodParameterClosureTypeExtensions->getAll() as $methodParameterClosureTypeExtension) { - if ($methodParameterClosureTypeExtension->isMethodSupported($calleeReflection, $parameter)) { - return $methodParameterClosureTypeExtension->getTypeFromMethodCall($calleeReflection, $callLike, $parameter, $scope); - } - } - } - } - - return null; - } - - /** - * @param MethodReflection|FunctionReflection|null $calleeReflection - */ - private function getParameterOutExtensionsType(CallLike $callLike, $calleeReflection, ParameterReflection $currentParameter, MutatingScope $scope): ?Type - { - $paramOutTypes = []; - if ($callLike instanceof FuncCall && $calleeReflection instanceof FunctionReflection) { - foreach ($this->functionParameterOutTypeExtensions->getAll() as $functionParameterOutTypeExtension) { - if (!$functionParameterOutTypeExtension->isFunctionSupported($calleeReflection, $currentParameter)) { - continue; - } - - $resolvedType = $functionParameterOutTypeExtension->getParameterOutTypeFromFunctionCall($calleeReflection, $callLike, $currentParameter, $scope); - if ($resolvedType === null) { - continue; - } - $paramOutTypes[] = $resolvedType; - } - } elseif ($callLike instanceof MethodCall && $calleeReflection instanceof MethodReflection) { - foreach ($this->methodParameterOutTypeExtensions->getAll() as $methodParameterOutTypeExtension) { - if (!$methodParameterOutTypeExtension->isMethodSupported($calleeReflection, $currentParameter)) { - continue; - } - - $resolvedType = $methodParameterOutTypeExtension->getParameterOutTypeFromMethodCall($calleeReflection, $callLike, $currentParameter, $scope); - if ($resolvedType === null) { - continue; - } - $paramOutTypes[] = $resolvedType; - } - } elseif ($callLike instanceof StaticCall && $calleeReflection instanceof MethodReflection) { - foreach ($this->staticMethodParameterOutTypeExtensions->getAll() as $staticMethodParameterOutTypeExtension) { - if (!$staticMethodParameterOutTypeExtension->isStaticMethodSupported($calleeReflection, $currentParameter)) { - continue; - } - - $resolvedType = $staticMethodParameterOutTypeExtension->getParameterOutTypeFromStaticMethodCall($calleeReflection, $callLike, $currentParameter, $scope); - if ($resolvedType === null) { - continue; - } - $paramOutTypes[] = $resolvedType; - } - } - - if (count($paramOutTypes) === 1) { - return $paramOutTypes[0]; - } - - if (count($paramOutTypes) > 1) { - return TypeCombinator::union(...$paramOutTypes); - } - - return null; - } - /** * @param callable(Node $node, Scope $scope): void $nodeCallback */ diff --git a/src/Testing/RuleTestCase.php b/src/Testing/RuleTestCase.php index 6f9cc5cc53a..f0eaba56d5e 100644 --- a/src/Testing/RuleTestCase.php +++ b/src/Testing/RuleTestCase.php @@ -5,6 +5,7 @@ use PhpParser\Node; use PHPStan\Analyser\Analyser; use PHPStan\Analyser\AnalyserResultFinalizer; +use PHPStan\Analyser\ArgumentsHandler; use PHPStan\Analyser\Error; use PHPStan\Analyser\ExpressionResultFactory; use PHPStan\Analyser\FileAnalyser; @@ -29,15 +30,6 @@ use PHPStan\Rules\IdentifierRuleError; use PHPStan\Rules\Properties\ReadWritePropertiesExtension; use PHPStan\Rules\Rule; -use PHPStan\Type\FunctionParameterClosureThisExtension; -use PHPStan\Type\FunctionParameterClosureTypeExtension; -use PHPStan\Type\FunctionParameterOutTypeExtension; -use PHPStan\Type\MethodParameterClosureThisExtension; -use PHPStan\Type\MethodParameterClosureTypeExtension; -use PHPStan\Type\MethodParameterOutTypeExtension; -use PHPStan\Type\StaticMethodParameterClosureThisExtension; -use PHPStan\Type\StaticMethodParameterClosureTypeExtension; -use PHPStan\Type\StaticMethodParameterOutTypeExtension; use function array_map; use function array_merge; use function count; @@ -92,23 +84,14 @@ protected function createNodeScopeResolver(): NodeScopeResolver self::getContainer()->getByType(TemplateArgumentObserver::class), $reflectionProvider, self::getContainer()->getByType(FileHelper::class), - self::getContainer()->getExtensionsCollection(FunctionParameterOutTypeExtension::class), - self::getContainer()->getExtensionsCollection(MethodParameterOutTypeExtension::class), - self::getContainer()->getExtensionsCollection(StaticMethodParameterOutTypeExtension::class), $readWritePropertiesExtensions !== [] ? new DirectExtensionsCollection($readWritePropertiesExtensions) : self::getContainer()->getExtensionsCollection(ReadWritePropertiesExtension::class), - self::getContainer()->getExtensionsCollection(FunctionParameterClosureThisExtension::class), - self::getContainer()->getExtensionsCollection(MethodParameterClosureThisExtension::class), - self::getContainer()->getExtensionsCollection(StaticMethodParameterClosureThisExtension::class), - self::getContainer()->getExtensionsCollection(FunctionParameterClosureTypeExtension::class), - self::getContainer()->getExtensionsCollection(MethodParameterClosureTypeExtension::class), - self::getContainer()->getExtensionsCollection(StaticMethodParameterClosureTypeExtension::class), self::getContainer()->getExtensionsCollection(PerFileAnalysisResettable::class), $this->shouldPolluteScopeWithLoopInitialAssignments(), $this->shouldPolluteScopeWithAlwaysIterableForeach(), - self::getContainer()->getParameter('exceptions')['implicitThrows'], $this->shouldTreatPhpDocTypesAsCertain(), self::getContainer()->getByType(ExpressionResultFactory::class), self::getContainer()->getByType(StatementsHandler::class), + self::getContainer()->getByType(ArgumentsHandler::class), ); } diff --git a/src/Testing/TypeInferenceTestCase.php b/src/Testing/TypeInferenceTestCase.php index 4ea3ab8e7fc..60b5d36ea40 100644 --- a/src/Testing/TypeInferenceTestCase.php +++ b/src/Testing/TypeInferenceTestCase.php @@ -6,6 +6,7 @@ use PhpParser\Node; use PhpParser\Node\Expr\StaticCall; use PhpParser\Node\Name; +use PHPStan\Analyser\ArgumentsHandler; use PHPStan\Analyser\ExpressionResultFactory; use PHPStan\Analyser\Generics\TemplateArgumentObserver; use PHPStan\Analyser\MutatingScope; @@ -23,15 +24,6 @@ use PHPStan\ShouldNotHappenException; use PHPStan\TrinaryLogic; use PHPStan\Type\ConstantScalarType; -use PHPStan\Type\FunctionParameterClosureThisExtension; -use PHPStan\Type\FunctionParameterClosureTypeExtension; -use PHPStan\Type\FunctionParameterOutTypeExtension; -use PHPStan\Type\MethodParameterClosureThisExtension; -use PHPStan\Type\MethodParameterClosureTypeExtension; -use PHPStan\Type\MethodParameterOutTypeExtension; -use PHPStan\Type\StaticMethodParameterClosureThisExtension; -use PHPStan\Type\StaticMethodParameterClosureTypeExtension; -use PHPStan\Type\StaticMethodParameterOutTypeExtension; use PHPStan\Type\Type; use PHPStan\Type\VerbosityLevel; use Symfony\Component\Finder\Finder; @@ -67,23 +59,14 @@ protected static function createNodeScopeResolver(): NodeScopeResolver $container->getByType(TemplateArgumentObserver::class), $reflectionProvider, $container->getByType(FileHelper::class), - $container->getExtensionsCollection(FunctionParameterOutTypeExtension::class), - $container->getExtensionsCollection(MethodParameterOutTypeExtension::class), - $container->getExtensionsCollection(StaticMethodParameterOutTypeExtension::class), $container->getExtensionsCollection(ReadWritePropertiesExtension::class), - $container->getExtensionsCollection(FunctionParameterClosureThisExtension::class), - $container->getExtensionsCollection(MethodParameterClosureThisExtension::class), - $container->getExtensionsCollection(StaticMethodParameterClosureThisExtension::class), - $container->getExtensionsCollection(FunctionParameterClosureTypeExtension::class), - $container->getExtensionsCollection(MethodParameterClosureTypeExtension::class), - $container->getExtensionsCollection(StaticMethodParameterClosureTypeExtension::class), $container->getExtensionsCollection(PerFileAnalysisResettable::class), $container->getParameter('polluteScopeWithLoopInitialAssignments'), $container->getParameter('polluteScopeWithAlwaysIterableForeach'), - $container->getParameter('exceptions')['implicitThrows'], $container->getParameter('treatPhpDocTypesAsCertain'), $container->getByType(ExpressionResultFactory::class), $container->getByType(StatementsHandler::class), + $container->getByType(ArgumentsHandler::class), ); } diff --git a/tests/PHPStan/Analyser/AnalyserTest.php b/tests/PHPStan/Analyser/AnalyserTest.php index 591b777d2b8..d69d772ec39 100644 --- a/tests/PHPStan/Analyser/AnalyserTest.php +++ b/tests/PHPStan/Analyser/AnalyserTest.php @@ -23,15 +23,6 @@ use PHPStan\Rules\Properties\ReadWritePropertiesExtension; use PHPStan\Testing\PHPStanTestCase; use PHPStan\Type\FileTypeMapper; -use PHPStan\Type\FunctionParameterClosureThisExtension; -use PHPStan\Type\FunctionParameterClosureTypeExtension; -use PHPStan\Type\FunctionParameterOutTypeExtension; -use PHPStan\Type\MethodParameterClosureThisExtension; -use PHPStan\Type\MethodParameterClosureTypeExtension; -use PHPStan\Type\MethodParameterOutTypeExtension; -use PHPStan\Type\StaticMethodParameterClosureThisExtension; -use PHPStan\Type\StaticMethodParameterClosureTypeExtension; -use PHPStan\Type\StaticMethodParameterOutTypeExtension; use PHPUnit\Framework\Attributes\DataProvider; use function array_map; use function array_merge; @@ -863,23 +854,14 @@ private function createAnalyser(): Analyser $container->getByType(TemplateArgumentObserver::class), $reflectionProvider, $fileHelper, - $container->getExtensionsCollection(FunctionParameterOutTypeExtension::class), - $container->getExtensionsCollection(MethodParameterOutTypeExtension::class), - $container->getExtensionsCollection(StaticMethodParameterOutTypeExtension::class), $container->getExtensionsCollection(ReadWritePropertiesExtension::class), - $container->getExtensionsCollection(FunctionParameterClosureThisExtension::class), - $container->getExtensionsCollection(MethodParameterClosureThisExtension::class), - $container->getExtensionsCollection(StaticMethodParameterClosureThisExtension::class), - $container->getExtensionsCollection(FunctionParameterClosureTypeExtension::class), - $container->getExtensionsCollection(MethodParameterClosureTypeExtension::class), - $container->getExtensionsCollection(StaticMethodParameterClosureTypeExtension::class), $container->getExtensionsCollection(PerFileAnalysisResettable::class), false, true, - true, $this->shouldTreatPhpDocTypesAsCertain(), $container->getByType(ExpressionResultFactory::class), $container->getByType(StatementsHandler::class), + $container->getByType(ArgumentsHandler::class), ); $lexer = new Lexer(); $fileAnalyser = new FileAnalyser( From 137b7d71cc370fd197f355d16c692e4ecebf17e8 Mon Sep 17 00:00:00 2001 From: Ondrej Mirtes Date: Sun, 13 Sep 2026 11:25:17 +0200 Subject: [PATCH 03/10] Extract attribute processing from NodeScopeResolver into AttributesHandler processAttributeGroups() now lives in the AttributesHandler service. The statement handlers and PropertyHooksProcessor inject it; NodeScopeResolver delegates to it from processParamNode(). ReflectionProvider and ArgumentsHandler, used only by attribute processing, leave the NodeScopeResolver constructor. Co-Authored-By: Claude Opus 5 (1M context) Claude-Session: https://claude.ai/code/session_014hgcbwfKQnye2coQXtX5XU --- .../Build/NoScopeTypeReadInEngineRule.php | 1 + src/Analyser/AttributesHandler.php | 71 +++++++++++++++++++ src/Analyser/NodeScopeResolver.php | 50 +------------ src/Analyser/PropertyHooksProcessor.php | 3 +- .../StmtHandler/ClassConstHandler.php | 9 ++- src/Analyser/StmtHandler/ClassLikeHandler.php | 4 +- .../StmtHandler/ClassMethodHandler.php | 4 +- src/Analyser/StmtHandler/EnumCaseHandler.php | 9 ++- src/Analyser/StmtHandler/FunctionHandler.php | 4 +- src/Analyser/StmtHandler/PropertyHandler.php | 4 +- src/Analyser/StmtHandler/TraitUseHandler.php | 4 +- src/Testing/RuleTestCase.php | 6 +- src/Testing/TypeInferenceTestCase.php | 6 +- tests/PHPStan/Analyser/AnalyserTest.php | 3 +- 14 files changed, 112 insertions(+), 66 deletions(-) create mode 100644 src/Analyser/AttributesHandler.php diff --git a/build/PHPStan/Build/NoScopeTypeReadInEngineRule.php b/build/PHPStan/Build/NoScopeTypeReadInEngineRule.php index dd935cef11b..01b5581e2d0 100644 --- a/build/PHPStan/Build/NoScopeTypeReadInEngineRule.php +++ b/build/PHPStan/Build/NoScopeTypeReadInEngineRule.php @@ -28,6 +28,7 @@ final class NoScopeTypeReadInEngineRule implements Rule private const ENGINE_NAMESPACES = [ 'PHPStan\\Analyser\\NodeScopeResolver', 'PHPStan\\Analyser\\ArgumentsHandler', + 'PHPStan\\Analyser\\AttributesHandler', 'PHPStan\\Analyser\\StatementsHandler', 'PHPStan\\Analyser\\StmtHandler\\', 'PHPStan\\Analyser\\ExprHandler\\', diff --git a/src/Analyser/AttributesHandler.php b/src/Analyser/AttributesHandler.php new file mode 100644 index 00000000000..d5a491fde8a --- /dev/null +++ b/src/Analyser/AttributesHandler.php @@ -0,0 +1,71 @@ +attrs as $attr) { + $className = $scope->resolveName($attr->name); + if ($this->reflectionProvider->hasClass($className)) { + $classReflection = $this->reflectionProvider->getClass($className); + if ($classReflection->hasConstructor()) { + $constructorReflection = $classReflection->getConstructor(); + $parametersAcceptor = ParametersAcceptorSelector::combineVariantsForNormalization( + $attr->args, + $constructorReflection->getVariants(), + $constructorReflection->getNamedArgumentsVariants(), + ); + $expr = new New_($attr->name, $attr->args); + $expr = ArgumentsNormalizer::reorderNewArguments($parametersAcceptor, $expr) ?? $expr; + $this->argumentsHandler->processArgs($nodeScopeResolver, $stmt, $constructorReflection, null, $constructorReflection->getVariants(), $constructorReflection->getNamedArgumentsVariants(), $expr, $scope, $storage, $nodeCallback, ExpressionContext::createDeep()); + $nodeScopeResolver->callNodeCallback($nodeCallback, $attr, $scope, $storage); + continue; + } + } + + foreach ($attr->args as $arg) { + $nodeScopeResolver->processExprNode($stmt, $arg->value, $scope, $storage, $nodeCallback, ExpressionContext::createDeep()); + $nodeScopeResolver->callNodeCallback($nodeCallback, $arg, $scope, $storage); + } + $nodeScopeResolver->callNodeCallback($nodeCallback, $attr, $scope, $storage); + } + $nodeScopeResolver->callNodeCallback($nodeCallback, $attrGroup, $scope, $storage); + } + } + +} diff --git a/src/Analyser/NodeScopeResolver.php b/src/Analyser/NodeScopeResolver.php index c380c39030b..11cbe44e38e 100644 --- a/src/Analyser/NodeScopeResolver.php +++ b/src/Analyser/NodeScopeResolver.php @@ -4,7 +4,6 @@ use Closure; use PhpParser\Node; -use PhpParser\Node\AttributeGroup; use PhpParser\Node\Expr; use PhpParser\Node\Expr\ArrayDimFetch; use PhpParser\Node\Expr\FuncCall; @@ -62,9 +61,7 @@ use PHPStan\Reflection\Native\NativeMethodReflection; use PHPStan\Reflection\Native\NativeParameterReflection; use PHPStan\Reflection\ParameterReflection; -use PHPStan\Reflection\ParametersAcceptorSelector; use PHPStan\Reflection\Php\PhpMethodReflection; -use PHPStan\Reflection\ReflectionProvider; use PHPStan\Rules\Properties\ReadWritePropertiesExtension; use PHPStan\ShouldNotHappenException; use PHPStan\TrinaryLogic; @@ -156,7 +153,6 @@ class NodeScopeResolver public function __construct( private readonly Container $container, private readonly TemplateArgumentObserver $templateArgumentObserver, - private readonly ReflectionProvider $reflectionProvider, private readonly FileHelper $fileHelper, #[AutowiredExtensions(of: ReadWritePropertiesExtension::class)] private readonly ExtensionsCollection $readWritePropertiesExtensions, @@ -170,7 +166,7 @@ public function __construct( private readonly bool $treatPhpDocTypesAsCertain, private readonly ExpressionResultFactory $expressionResultFactory, private readonly StatementsHandler $statementsHandler, - private readonly ArgumentsHandler $argumentsHandler, + private readonly AttributesHandler $attributesHandler, ) { self::$guardNewWorld = getenv('PHPSTAN_GUARD_NW') === '1'; @@ -1724,7 +1720,7 @@ public function processParamNode( callable $nodeCallback, ): void { - $this->processAttributeGroups($stmt, $param->attrGroups, $scope, $storage, $nodeCallback); + $this->attributesHandler->processAttributeGroups($this, $stmt, $param->attrGroups, $scope, $storage, $nodeCallback); $this->callNodeCallback($nodeCallback, $param, $scope, $storage); if ($param->type !== null) { $this->callNodeCallback($nodeCallback, $param->type, $scope, $storage); @@ -1736,48 +1732,6 @@ public function processParamNode( $this->processExprNode($stmt, $param->default, $scope, $storage, $nodeCallback, ExpressionContext::createDeep()); } - /** - * @param AttributeGroup[] $attrGroups - * @param callable(Node $node, Scope $scope): void $nodeCallback - */ - public function processAttributeGroups( - Node\Stmt $stmt, - array $attrGroups, - MutatingScope $scope, - ExpressionResultStorage $storage, - callable $nodeCallback, - ): void - { - foreach ($attrGroups as $attrGroup) { - foreach ($attrGroup->attrs as $attr) { - $className = $scope->resolveName($attr->name); - if ($this->reflectionProvider->hasClass($className)) { - $classReflection = $this->reflectionProvider->getClass($className); - if ($classReflection->hasConstructor()) { - $constructorReflection = $classReflection->getConstructor(); - $parametersAcceptor = ParametersAcceptorSelector::combineVariantsForNormalization( - $attr->args, - $constructorReflection->getVariants(), - $constructorReflection->getNamedArgumentsVariants(), - ); - $expr = new New_($attr->name, $attr->args); - $expr = ArgumentsNormalizer::reorderNewArguments($parametersAcceptor, $expr) ?? $expr; - $this->argumentsHandler->processArgs($this, $stmt, $constructorReflection, null, $constructorReflection->getVariants(), $constructorReflection->getNamedArgumentsVariants(), $expr, $scope, $storage, $nodeCallback, ExpressionContext::createDeep()); - $this->callNodeCallback($nodeCallback, $attr, $scope, $storage); - continue; - } - } - - foreach ($attr->args as $arg) { - $this->processExprNode($stmt, $arg->value, $scope, $storage, $nodeCallback, ExpressionContext::createDeep()); - $this->callNodeCallback($nodeCallback, $arg, $scope, $storage); - } - $this->callNodeCallback($nodeCallback, $attr, $scope, $storage); - } - $this->callNodeCallback($nodeCallback, $attrGroup, $scope, $storage); - } - } - /** * @param callable(Node $node, Scope $scope): void $nodeCallback */ diff --git a/src/Analyser/PropertyHooksProcessor.php b/src/Analyser/PropertyHooksProcessor.php index 5b9afdd6a5b..97adc969327 100644 --- a/src/Analyser/PropertyHooksProcessor.php +++ b/src/Analyser/PropertyHooksProcessor.php @@ -30,6 +30,7 @@ final class PropertyHooksProcessor public function __construct( private DeprecatedAttributeResolver $deprecatedAttributeResolver, private PhpDocsResolver $phpDocsResolver, + private AttributesHandler $attributesHandler, ) { } @@ -58,7 +59,7 @@ public function processPropertyHooks( foreach ($hooks as $hook) { $nodeScopeResolver->callNodeCallback($nodeCallback, $hook, $scope, $storage); - $nodeScopeResolver->processAttributeGroups($stmt, $hook->attrGroups, $scope, $storage, $nodeCallback); + $this->attributesHandler->processAttributeGroups($nodeScopeResolver, $stmt, $hook->attrGroups, $scope, $storage, $nodeCallback); [, $phpDocParameterTypes,,,, $phpDocThrowType,,,,,,,, $phpDocComment,,,,,, $resolvedPhpDoc] = $this->phpDocsResolver->getPhpDocs($scope, $hook); diff --git a/src/Analyser/StmtHandler/ClassConstHandler.php b/src/Analyser/StmtHandler/ClassConstHandler.php index 1db93fad6ea..9a00b230f4b 100644 --- a/src/Analyser/StmtHandler/ClassConstHandler.php +++ b/src/Analyser/StmtHandler/ClassConstHandler.php @@ -6,6 +6,7 @@ use PhpParser\Node\Name; use PhpParser\Node\Stmt; use PhpParser\Node\Stmt\ClassConst; +use PHPStan\Analyser\AttributesHandler; use PHPStan\Analyser\ExpressionContext; use PHPStan\Analyser\ExpressionResultStorage; use PHPStan\Analyser\InternalStatementResult; @@ -24,6 +25,12 @@ final class ClassConstHandler implements StmtHandler { + public function __construct( + private AttributesHandler $attributesHandler, + ) + { + } + public function supports(Stmt $stmt): bool { return $stmt instanceof ClassConst; @@ -40,7 +47,7 @@ public function processStmt( { $entryScope = $scope; $impurePoints = []; - $nodeScopeResolver->processAttributeGroups($stmt, $stmt->attrGroups, $scope, $storage, $nodeCallback); + $this->attributesHandler->processAttributeGroups($nodeScopeResolver, $stmt, $stmt->attrGroups, $scope, $storage, $nodeCallback); foreach ($stmt->consts as $const) { $constResult = $nodeScopeResolver->processExprNode($stmt, $const->value, $scope, $storage, $nodeCallback, ExpressionContext::createDeep($context->shouldResolveTemplateArguments())); // the constant's callback fires after its value was processed, so diff --git a/src/Analyser/StmtHandler/ClassLikeHandler.php b/src/Analyser/StmtHandler/ClassLikeHandler.php index 1cc07a26e10..4f205b94613 100644 --- a/src/Analyser/StmtHandler/ClassLikeHandler.php +++ b/src/Analyser/StmtHandler/ClassLikeHandler.php @@ -8,6 +8,7 @@ use PhpParser\Node\Stmt\Class_; use PhpParser\Node\Stmt\ClassLike; use PhpParser\Node\Stmt\Trait_; +use PHPStan\Analyser\AttributesHandler; use PHPStan\Analyser\CalledMethodProcessor; use PHPStan\Analyser\ExpressionResultStorage; use PHPStan\Analyser\InternalStatementResult; @@ -50,6 +51,7 @@ public function __construct( private ClassReflectionFactory $classReflectionFactory, private CalledMethodProcessor $calledMethodProcessor, private ReflectionProvider $reflectionProvider, + private AttributesHandler $attributesHandler, ) { } @@ -104,7 +106,7 @@ public function processStmt( // the class attributes are processed before the InClassNode emission, so // rules firing on it (ClassAttributesRule) read the attribute arguments // from the storage - $nodeScopeResolver->processAttributeGroups($stmt, $stmt->attrGroups, $classScope, $storage, $classStatementsGatherer); + $this->attributesHandler->processAttributeGroups($nodeScopeResolver, $stmt, $stmt->attrGroups, $classScope, $storage, $classStatementsGatherer); $nodeScopeResolver->callNodeCallback($nodeCallback, new InClassNode($stmt, $classReflection), $classScope, $storage); $classLikeStatements = $stmt->stmts; diff --git a/src/Analyser/StmtHandler/ClassMethodHandler.php b/src/Analyser/StmtHandler/ClassMethodHandler.php index 346397d47fc..3ea55c7bdb7 100644 --- a/src/Analyser/StmtHandler/ClassMethodHandler.php +++ b/src/Analyser/StmtHandler/ClassMethodHandler.php @@ -8,6 +8,7 @@ use PhpParser\Node\Stmt; use PhpParser\Node\Stmt\ClassMethod; use PhpParser\Node\Stmt\Return_; +use PHPStan\Analyser\AttributesHandler; use PHPStan\Analyser\DeprecatedAttributeResolver; use PHPStan\Analyser\ExpressionResultStorage; use PHPStan\Analyser\ImpurePoint; @@ -48,6 +49,7 @@ public function __construct( private DeprecatedAttributeResolver $deprecatedAttributeResolver, private PhpDocsResolver $phpDocsResolver, private PropertyHooksProcessor $propertyHooksProcessor, + private AttributesHandler $attributesHandler, ) { } @@ -66,7 +68,7 @@ public function processStmt( StatementContext $context, ): InternalStatementResult { - $nodeScopeResolver->processAttributeGroups($stmt, $stmt->attrGroups, $scope, $storage, $nodeCallback); + $this->attributesHandler->processAttributeGroups($nodeScopeResolver, $stmt, $stmt->attrGroups, $scope, $storage, $nodeCallback); [$templateTypeMap, $phpDocParameterTypes, $phpDocImmediatelyInvokedCallableParameters, $phpDocClosureThisTypeParameters, $phpDocReturnType, $phpDocThrowType, $deprecatedDescription, $isDeprecated, $isInternal, $isFinal, $isPure, $acceptsNamedArguments, $isReadOnly, $phpDocComment, $asserts, $selfOutType, $phpDocParameterOutTypes, , , , $pureUnlessCallableIsImpureParameters] = $this->phpDocsResolver->getPhpDocs($scope, $stmt); foreach ($stmt->params as $param) { diff --git a/src/Analyser/StmtHandler/EnumCaseHandler.php b/src/Analyser/StmtHandler/EnumCaseHandler.php index bbaf1d1beee..c48cfe5061a 100644 --- a/src/Analyser/StmtHandler/EnumCaseHandler.php +++ b/src/Analyser/StmtHandler/EnumCaseHandler.php @@ -4,6 +4,7 @@ use PhpParser\Node\Stmt; use PhpParser\Node\Stmt\EnumCase; +use PHPStan\Analyser\AttributesHandler; use PHPStan\Analyser\ExpressionContext; use PHPStan\Analyser\ExpressionResultStorage; use PHPStan\Analyser\InternalStatementResult; @@ -20,6 +21,12 @@ final class EnumCaseHandler implements StmtHandler { + public function __construct( + private AttributesHandler $attributesHandler, + ) + { + } + public function supports(Stmt $stmt): bool { return $stmt instanceof EnumCase; @@ -34,7 +41,7 @@ public function processStmt( StatementContext $context, ): InternalStatementResult { - $nodeScopeResolver->processAttributeGroups($stmt, $stmt->attrGroups, $scope, $storage, $nodeCallback); + $this->attributesHandler->processAttributeGroups($nodeScopeResolver, $stmt, $stmt->attrGroups, $scope, $storage, $nodeCallback); $impurePoints = []; if ($stmt->expr !== null) { $exprResult = $nodeScopeResolver->processExprNode($stmt, $stmt->expr, $scope, $storage, $nodeCallback, ExpressionContext::createDeep($context->shouldResolveTemplateArguments())); diff --git a/src/Analyser/StmtHandler/FunctionHandler.php b/src/Analyser/StmtHandler/FunctionHandler.php index 600c3aa8f38..bee54b2b148 100644 --- a/src/Analyser/StmtHandler/FunctionHandler.php +++ b/src/Analyser/StmtHandler/FunctionHandler.php @@ -7,6 +7,7 @@ use PhpParser\Node\Stmt; use PhpParser\Node\Stmt\Function_; use PhpParser\Node\Stmt\Return_; +use PHPStan\Analyser\AttributesHandler; use PHPStan\Analyser\DeprecatedAttributeResolver; use PHPStan\Analyser\ExpressionResultStorage; use PHPStan\Analyser\ImpurePoint; @@ -39,6 +40,7 @@ final class FunctionHandler implements StmtHandler public function __construct( private DeprecatedAttributeResolver $deprecatedAttributeResolver, private PhpDocsResolver $phpDocsResolver, + private AttributesHandler $attributesHandler, ) { } @@ -57,7 +59,7 @@ public function processStmt( StatementContext $context, ): InternalStatementResult { - $nodeScopeResolver->processAttributeGroups($stmt, $stmt->attrGroups, $scope, $storage, $nodeCallback); + $this->attributesHandler->processAttributeGroups($nodeScopeResolver, $stmt, $stmt->attrGroups, $scope, $storage, $nodeCallback); [$templateTypeMap, $phpDocParameterTypes, $phpDocImmediatelyInvokedCallableParameters, $phpDocClosureThisTypeParameters, $phpDocReturnType, $phpDocThrowType, $deprecatedDescription, $isDeprecated, $isInternal, , $isPure, $acceptsNamedArguments, , $phpDocComment, $asserts,, $phpDocParameterOutTypes, , , , $pureUnlessCallableIsImpureParameters] = $this->phpDocsResolver->getPhpDocs($scope, $stmt); foreach ($stmt->params as $param) { diff --git a/src/Analyser/StmtHandler/PropertyHandler.php b/src/Analyser/StmtHandler/PropertyHandler.php index 7489cf17ca4..f98095a34c9 100644 --- a/src/Analyser/StmtHandler/PropertyHandler.php +++ b/src/Analyser/StmtHandler/PropertyHandler.php @@ -4,6 +4,7 @@ use PhpParser\Node\Stmt; use PhpParser\Node\Stmt\Property; +use PHPStan\Analyser\AttributesHandler; use PHPStan\Analyser\ExpressionContext; use PHPStan\Analyser\ExpressionResultStorage; use PHPStan\Analyser\InternalStatementResult; @@ -29,6 +30,7 @@ final class PropertyHandler implements StmtHandler public function __construct( private PhpDocsResolver $phpDocsResolver, private PropertyHooksProcessor $propertyHooksProcessor, + private AttributesHandler $attributesHandler, ) { } @@ -47,7 +49,7 @@ public function processStmt( StatementContext $context, ): InternalStatementResult { - $nodeScopeResolver->processAttributeGroups($stmt, $stmt->attrGroups, $scope, $storage, $nodeCallback); + $this->attributesHandler->processAttributeGroups($nodeScopeResolver, $stmt, $stmt->attrGroups, $scope, $storage, $nodeCallback); $nativePropertyType = $stmt->type !== null ? ParserNodeTypeToPHPStanType::resolve($stmt->type, $scope->getClassReflection()) : null; diff --git a/src/Analyser/StmtHandler/TraitUseHandler.php b/src/Analyser/StmtHandler/TraitUseHandler.php index efb57a59973..44e22a2bfab 100644 --- a/src/Analyser/StmtHandler/TraitUseHandler.php +++ b/src/Analyser/StmtHandler/TraitUseHandler.php @@ -6,6 +6,7 @@ use PhpParser\Node; use PhpParser\Node\Stmt; use PhpParser\Node\Stmt\TraitUse; +use PHPStan\Analyser\AttributesHandler; use PHPStan\Analyser\ExpressionResultStorage; use PHPStan\Analyser\InternalStatementResult; use PHPStan\Analyser\MutatingScope; @@ -38,6 +39,7 @@ public function __construct( private FileHelper $fileHelper, #[AutowiredParameter(ref: '@defaultAnalysisParser')] private Parser $parser, + private AttributesHandler $attributesHandler, ) { } @@ -176,7 +178,7 @@ private function processNodesForTraitUse(NodeScopeResolver $nodeScopeResolver, $ // attribute args are not processed as part of the trait statements // but rules like TraitAttributesRule ask about their types - $nodeScopeResolver->processAttributeGroups($node, $node->attrGroups, $traitScope, $storage, new NoopNodeCallback()); + $this->attributesHandler->processAttributeGroups($nodeScopeResolver, $node, $node->attrGroups, $traitScope, $storage, new NoopNodeCallback()); $nodeScopeResolver->callNodeCallback($nodeCallback, new InTraitNode($node, $traitReflection, $scope->getClassReflection()), $traitScope, $storage); $nodeScopeResolver->processStmtNodesInternal($node, $stmts, $traitScope, $storage, $nodeCallback, StatementContext::createTopLevel()); diff --git a/src/Testing/RuleTestCase.php b/src/Testing/RuleTestCase.php index f0eaba56d5e..2fc2b92b9f3 100644 --- a/src/Testing/RuleTestCase.php +++ b/src/Testing/RuleTestCase.php @@ -5,7 +5,7 @@ use PhpParser\Node; use PHPStan\Analyser\Analyser; use PHPStan\Analyser\AnalyserResultFinalizer; -use PHPStan\Analyser\ArgumentsHandler; +use PHPStan\Analyser\AttributesHandler; use PHPStan\Analyser\Error; use PHPStan\Analyser\ExpressionResultFactory; use PHPStan\Analyser\FileAnalyser; @@ -77,12 +77,10 @@ protected function getTypeSpecifier(): TypeSpecifier protected function createNodeScopeResolver(): NodeScopeResolver { $readWritePropertiesExtensions = $this->getReadWritePropertiesExtensions(); - $reflectionProvider = $this->createReflectionProvider(); return new NodeScopeResolver( self::getContainer(), self::getContainer()->getByType(TemplateArgumentObserver::class), - $reflectionProvider, self::getContainer()->getByType(FileHelper::class), $readWritePropertiesExtensions !== [] ? new DirectExtensionsCollection($readWritePropertiesExtensions) : self::getContainer()->getExtensionsCollection(ReadWritePropertiesExtension::class), self::getContainer()->getExtensionsCollection(PerFileAnalysisResettable::class), @@ -91,7 +89,7 @@ protected function createNodeScopeResolver(): NodeScopeResolver $this->shouldTreatPhpDocTypesAsCertain(), self::getContainer()->getByType(ExpressionResultFactory::class), self::getContainer()->getByType(StatementsHandler::class), - self::getContainer()->getByType(ArgumentsHandler::class), + self::getContainer()->getByType(AttributesHandler::class), ); } diff --git a/src/Testing/TypeInferenceTestCase.php b/src/Testing/TypeInferenceTestCase.php index 60b5d36ea40..322ee14d5ba 100644 --- a/src/Testing/TypeInferenceTestCase.php +++ b/src/Testing/TypeInferenceTestCase.php @@ -6,7 +6,7 @@ use PhpParser\Node; use PhpParser\Node\Expr\StaticCall; use PhpParser\Node\Name; -use PHPStan\Analyser\ArgumentsHandler; +use PHPStan\Analyser\AttributesHandler; use PHPStan\Analyser\ExpressionResultFactory; use PHPStan\Analyser\Generics\TemplateArgumentObserver; use PHPStan\Analyser\MutatingScope; @@ -52,12 +52,10 @@ abstract class TypeInferenceTestCase extends PHPStanTestCase protected static function createNodeScopeResolver(): NodeScopeResolver { $container = self::getContainer(); - $reflectionProvider = self::createReflectionProvider(); return new NodeScopeResolver( $container, $container->getByType(TemplateArgumentObserver::class), - $reflectionProvider, $container->getByType(FileHelper::class), $container->getExtensionsCollection(ReadWritePropertiesExtension::class), $container->getExtensionsCollection(PerFileAnalysisResettable::class), @@ -66,7 +64,7 @@ protected static function createNodeScopeResolver(): NodeScopeResolver $container->getParameter('treatPhpDocTypesAsCertain'), $container->getByType(ExpressionResultFactory::class), $container->getByType(StatementsHandler::class), - $container->getByType(ArgumentsHandler::class), + $container->getByType(AttributesHandler::class), ); } diff --git a/tests/PHPStan/Analyser/AnalyserTest.php b/tests/PHPStan/Analyser/AnalyserTest.php index d69d772ec39..d627464f8cd 100644 --- a/tests/PHPStan/Analyser/AnalyserTest.php +++ b/tests/PHPStan/Analyser/AnalyserTest.php @@ -852,7 +852,6 @@ private function createAnalyser(): Analyser $nodeScopeResolver = new NodeScopeResolver( $container, $container->getByType(TemplateArgumentObserver::class), - $reflectionProvider, $fileHelper, $container->getExtensionsCollection(ReadWritePropertiesExtension::class), $container->getExtensionsCollection(PerFileAnalysisResettable::class), @@ -861,7 +860,7 @@ private function createAnalyser(): Analyser $this->shouldTreatPhpDocTypesAsCertain(), $container->getByType(ExpressionResultFactory::class), $container->getByType(StatementsHandler::class), - $container->getByType(ArgumentsHandler::class), + $container->getByType(AttributesHandler::class), ); $lexer = new Lexer(); $fileAnalyser = new FileAnalyser( From a94c37a593a2223c680035fdd3a48a454acd910b Mon Sep 17 00:00:00 2001 From: Ondrej Mirtes Date: Sun, 13 Sep 2026 11:57:19 +0200 Subject: [PATCH 04/10] Extract closure processing from NodeScopeResolver into ClosureProcessor processClosureNode(), processArrowFunctionNode(), processImmediatelyCalledCallable() and refineClosureNodeScope() now live in the ClosureProcessor service, injected by ClosureHandler, ArrowFunctionHandler, FuncCallHandler and ArgumentsHandler. The callable parameter helpers (createCallableParameters(), createNativeCallableParameters(), resolveCallableTypeForScope()) move into ClosureParameterResolver, their only consumer besides the closure walk. Co-Authored-By: Claude Opus 5 (1M context) Claude-Session: https://claude.ai/code/session_014hgcbwfKQnye2coQXtX5XU --- .../Build/NoScopeTypeReadInEngineRule.php | 1 + src/Analyser/ArgumentsHandler.php | 14 +- src/Analyser/ClosureProcessor.php | 522 +++++++++++++++ .../ExprHandler/ArrowFunctionHandler.php | 4 +- src/Analyser/ExprHandler/ClosureHandler.php | 4 +- src/Analyser/ExprHandler/FuncCallHandler.php | 4 +- .../Helper/ClosureParameterResolver.php | 148 ++++- .../Helper/ClosureTypeResolver.php | 6 +- src/Analyser/NodeScopeResolver.php | 620 ------------------ 9 files changed, 688 insertions(+), 635 deletions(-) create mode 100644 src/Analyser/ClosureProcessor.php diff --git a/build/PHPStan/Build/NoScopeTypeReadInEngineRule.php b/build/PHPStan/Build/NoScopeTypeReadInEngineRule.php index 01b5581e2d0..fd6a7e706ad 100644 --- a/build/PHPStan/Build/NoScopeTypeReadInEngineRule.php +++ b/build/PHPStan/Build/NoScopeTypeReadInEngineRule.php @@ -29,6 +29,7 @@ final class NoScopeTypeReadInEngineRule implements Rule 'PHPStan\\Analyser\\NodeScopeResolver', 'PHPStan\\Analyser\\ArgumentsHandler', 'PHPStan\\Analyser\\AttributesHandler', + 'PHPStan\\Analyser\\ClosureProcessor', 'PHPStan\\Analyser\\StatementsHandler', 'PHPStan\\Analyser\\StmtHandler\\', 'PHPStan\\Analyser\\ExprHandler\\', diff --git a/src/Analyser/ArgumentsHandler.php b/src/Analyser/ArgumentsHandler.php index 9600870db18..8e957aba59f 100644 --- a/src/Analyser/ArgumentsHandler.php +++ b/src/Analyser/ArgumentsHandler.php @@ -15,6 +15,7 @@ use PhpParser\Node\Name; use PhpParser\NodeFinder; use PHPStan\Analyser\ExprHandler\ClosureHandler; +use PHPStan\Analyser\ExprHandler\Helper\ClosureParameterResolver; use PHPStan\Analyser\ExprHandler\Helper\ClosureTypeResolver; use PHPStan\Analyser\Generics\TemplateArgumentObserver; use PHPStan\DependencyInjection\AutowiredExtensions; @@ -95,6 +96,7 @@ public function __construct( private Container $container, private TemplateArgumentObserver $templateArgumentObserver, private ExpressionResultFactory $expressionResultFactory, + private ClosureProcessor $closureProcessor, #[AutowiredExtensions(of: FunctionParameterOutTypeExtension::class)] private ExtensionsCollection $functionParameterOutTypeExtensions, #[AutowiredExtensions(of: MethodParameterOutTypeExtension::class)] @@ -228,7 +230,7 @@ public function processArgs( // resolution (see gatherClosureArgType()). $originalArgForGather = $arg->getAttribute(ArgumentsNormalizer::ORIGINAL_ARG_ATTRIBUTE) ?? $arg; $gatheredArgTypeByIndex[$i] = $typeDrivenAcceptorSelection - ? $this->gatherClosureArgType($nodeScopeResolver, $parametersAcceptors, $i, $arg->value, $scope) + ? $this->gatherClosureArgType($parametersAcceptors, $i, $arg->value, $scope) : $this->container->getByType(ClosureTypeResolver::class)->getClosureType($scope, $arg->value, true, $storage); $this->addGatheredArgType($gatheredTypes, $gatheredUnpack, $gatheredHasName, $originalArgForGather, $i, $gatheredArgTypeByIndex[$i]); } @@ -408,7 +410,7 @@ public function processArgs( } } - $closureResult = $nodeScopeResolver->processClosureNode($stmt, $arg->value, $scopeToPass, $storage, $nodeCallback, $context, $parameterType, $parameterNativeType); + $closureResult = $this->closureProcessor->processClosureNode($nodeScopeResolver, $stmt, $arg->value, $scopeToPass, $storage, $nodeCallback, $context, $parameterType, $parameterNativeType); if ($this->callCallbackImmediately($parameter, $parameterType, $calleeReflection)) { $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()); @@ -556,7 +558,7 @@ public function processArgs( } } - $arrowFunctionResult = $nodeScopeResolver->processArrowFunctionNode($stmt, $arg->value, $scopeToPass, $storage, $nodeCallback, $parameterType, $parameterNativeType, $context); + $arrowFunctionResult = $this->closureProcessor->processArrowFunctionNode($nodeScopeResolver, $stmt, $arg->value, $scopeToPass, $storage, $nodeCallback, $parameterType, $parameterNativeType, $context); $arrowFunctionExprResult = $arrowFunctionResult->getExpressionResult(); if ($this->callCallbackImmediately($parameter, $parameterType, $calleeReflection)) { $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), $arrowFunctionExprResult->getThrowPoints())); @@ -686,7 +688,7 @@ public function processArgs( } foreach ($deferredInvalidateExpressions as [$invalidateExpressions, $uses]) { - $scope = $nodeScopeResolver->processImmediatelyCalledCallable($scope, $invalidateExpressions, $uses); + $scope = $this->closureProcessor->processImmediatelyCalledCallable($scope, $invalidateExpressions, $uses); } foreach ($deferredByRefClosureResults as $deferredClosureResult) { @@ -911,7 +913,7 @@ private function argConsumesResolvedParameterType(Expr $value): bool * * @param ParametersAcceptor[] $parametersAcceptors */ - private function gatherClosureArgType(NodeScopeResolver $nodeScopeResolver, array $parametersAcceptors, int $i, Expr $closureExpr, MutatingScope $scope): Type + private function gatherClosureArgType(array $parametersAcceptors, int $i, Expr $closureExpr, MutatingScope $scope): Type { $rawParameter = null; if (count($parametersAcceptors) === 1) { @@ -927,7 +929,7 @@ private function gatherClosureArgType(NodeScopeResolver $nodeScopeResolver, arra $scope = $scope->pushInFunctionCall(null, $rawParameter, false); } - return $nodeScopeResolver->resolveCallableTypeForScope($closureExpr, $scope); + return $this->container->getByType(ClosureParameterResolver::class)->resolveCallableTypeForScope($closureExpr, $scope); } /** diff --git a/src/Analyser/ClosureProcessor.php b/src/Analyser/ClosureProcessor.php new file mode 100644 index 00000000000..f0192700c5a --- /dev/null +++ b/src/Analyser/ClosureProcessor.php @@ -0,0 +1,522 @@ +processClosureNodeInternal($nodeScopeResolver, $stmt, $expr, $scope, $storage, $nodeCallback, $context, $passedToType, $nativePassedToType); + } + + /** + * @param callable(Node $node, Scope $scope): void $nodeCallback + */ + private function processClosureNodeInternal( + NodeScopeResolver $nodeScopeResolver, + Node\Stmt $stmt, + Expr\Closure $expr, + MutatingScope $scope, + ExpressionResultStorage $storage, + callable $nodeCallback, + ExpressionContext $context, + ?Type $passedToType, + ?Type $nativePassedToType = null, + ): ProcessClosureResult + { + foreach ($expr->params as $param) { + $nodeScopeResolver->processParamNode($stmt, $param, $scope, $storage, $nodeCallback); + } + + $byRefUses = []; + + $closureCallArgs = $expr->getAttribute(ClosureArgVisitor::ATTRIBUTE_NAME); + $parameterTypes = $this->container->getByType(ClosureParameterResolver::class)->resolve($scope, $expr, $storage, $closureCallArgs, $passedToType, $nativePassedToType); + $callableParameters = $parameterTypes->parameters; + $nativeCallableParameters = $parameterTypes->nativeParameters; + + $useScope = $scope; + foreach ($expr->uses as $use) { + if ($use->byRef) { + $byRefUses[] = $use; + $useScope = $useScope->enterExpressionAssign($use->var); + + $inAssignRightSideVariableName = $context->getInAssignRightSideVariableName(); + $inAssignRightSideExpr = $context->getInAssignRightSideExpr(); + if ( + $inAssignRightSideVariableName === $use->var->name + && $inAssignRightSideExpr !== null + ) { + // a call's type is carried by the context (see + // ExpressionContext::enterAssignRightSideCallArgs()); a closure + // right side resolves through the closure type resolver + $inAssignRightSideType = $context->getInAssignRightSideType() ?? $this->container->getByType(ClosureParameterResolver::class)->resolveCallableTypeForScope($inAssignRightSideExpr, $scope); + if ($inAssignRightSideType instanceof ClosureType) { + $variableType = $inAssignRightSideType; + } else { + $alreadyHasVariableType = $scope->hasVariableType($inAssignRightSideVariableName); + if ($alreadyHasVariableType->no()) { + $variableType = TypeCombinator::union(new NullType(), $inAssignRightSideType); + } else { + $variableType = TypeCombinator::union($scope->getVariableType($inAssignRightSideVariableName), $inAssignRightSideType); + } + } + $inAssignRightSideNativeType = $context->getInAssignRightSideNativeType() ?? $this->container->getByType(ClosureParameterResolver::class)->resolveCallableTypeForScope($inAssignRightSideExpr, $scope->doNotTreatPhpDocTypesAsCertain()); + if ($inAssignRightSideNativeType instanceof ClosureType) { + $variableNativeType = $inAssignRightSideNativeType; + } else { + $alreadyHasVariableType = $scope->hasVariableType($inAssignRightSideVariableName); + if ($alreadyHasVariableType->no()) { + $variableNativeType = TypeCombinator::union(new NullType(), $inAssignRightSideNativeType); + } else { + $variableNativeType = TypeCombinator::union($scope->getVariableType($inAssignRightSideVariableName), $inAssignRightSideNativeType); + } + } + $scope = $scope->assignVariable($inAssignRightSideVariableName, $variableType, $variableNativeType, TrinaryLogic::createYes()); + } + } + $nodeScopeResolver->processExprNode($stmt, $use->var, $useScope, $storage, $nodeCallback, $context->withoutValueFlow()); + if (!$use->byRef) { + continue; + } + + $useScope = $useScope->exitExpressionAssign($use->var); + } + + if ($expr->returnType !== null) { + $nodeScopeResolver->callNodeCallback($nodeCallback, $expr->returnType, $scope, $storage); + } + + $closureScope = $scope->enterAnonymousFunction($expr, $callableParameters, $nativeCallableParameters); + $closureScope = $closureScope->processClosureScope($scope, null, $byRefUses); + $closureType = $closureScope->getAnonymousFunctionReflection(); + if (!$closureType instanceof ClosureType) { + throw new ShouldNotHappenException(); + } + + $nodeScopeResolver->callNodeCallback($nodeCallback, new InClosureNode($closureType, $expr), $closureScope, $storage); + + $executionEnds = []; + $gatheredReturnStatements = []; + $gatheredReturnStatementsAfterFinally = []; + $gatheredReturnStatementsWithScope = []; + $gatheredYieldStatements = []; + $gatheredYieldStatementsWithScope = []; + $closureImpurePoints = []; + $invalidateExpressions = []; + $closureStmtsGatherer = static function (Node $node, Scope $scope) use (&$executionEnds, &$gatheredReturnStatements, &$gatheredReturnStatementsAfterFinally, &$gatheredReturnStatementsWithScope, &$gatheredYieldStatements, &$gatheredYieldStatementsWithScope, &$closureScope, &$closureImpurePoints, &$invalidateExpressions): void { + if ($scope->getAnonymousFunctionReflection() !== $closureScope->getAnonymousFunctionReflection()) { + return; + } + if ($node instanceof PropertyAssignNode) { + $closureImpurePoints[] = new ImpurePoint( + $scope, + $node, + 'propertyAssign', + 'property assignment', + true, + ); + $invalidateExpressions[] = new InvalidateExprNode($node->getPropertyFetch()); + return; + } + if ($node instanceof ExecutionEndNode) { + $executionEnds[] = $node; + return; + } + if ($node instanceof ReturnAfterFinallyNode) { + $gatheredReturnStatementsAfterFinally[] = new ReturnStatement($scope, $node->getReturnNode()); + return; + } + if ($node instanceof InvalidateExprNode) { + $invalidateExpressions[] = $node; + return; + } + if ($node instanceof Expr\Yield_ || $node instanceof Expr\YieldFrom) { + $gatheredYieldStatements[] = $node; + $gatheredYieldStatementsWithScope[] = [$node, $scope]; + } + if (!$node instanceof Return_) { + return; + } + + $gatheredReturnStatements[] = new ReturnStatement($scope, $node); + $gatheredReturnStatementsWithScope[] = [$node, $scope]; + }; + + if (count($byRefUses) === 0) { + $nodeScopeResolver->pushNodeGatherer($closureStmtsGatherer); + try { + $statementResult = $nodeScopeResolver->processStmtNodesInternal($expr, $expr->stmts, $closureScope, $storage, $nodeCallback, StatementContext::createTopLevel($context->shouldResolveTemplateArguments())); + } finally { + $nodeScopeResolver->popNodeGatherer(); + } + $publicStatementResult = $statementResult->toPublic(); + $closureReturnStatementsNodeScope = $this->refineClosureNodeScope($closureScope, $scope, $expr, $gatheredReturnStatementsWithScope, $gatheredYieldStatementsWithScope, $executionEnds, $statementResult->getThrowPoints(), array_merge($closureImpurePoints, $statementResult->getImpurePoints()), $invalidateExpressions, $storage); + $nodeScopeResolver->callNodeCallback($nodeCallback, new ClosureReturnStatementsNode( + $expr, + $gatheredReturnStatements, + $gatheredReturnStatementsAfterFinally, + $gatheredYieldStatements, + $publicStatementResult, + $executionEnds, + array_merge($publicStatementResult->getImpurePoints(), $closureImpurePoints), + ), $closureReturnStatementsNodeScope, $storage); + $nodeScopeResolver->callNodeCallback($nodeCallback, VariableLivenessResolver::resolve($expr, $statementResult->getVariableFlow()), $closureReturnStatementsNodeScope, $storage); + + return new ProcessClosureResult( + $scope->addTemplateArgumentConstraints($statementResult->getScope()->getTemplateArgumentConstraints()), + $statementResult->getThrowPoints(), + $statementResult->getImpurePoints(), + $invalidateExpressions, + $gatheredReturnStatementsWithScope, + $gatheredYieldStatementsWithScope, + $executionEnds, + array_merge($closureImpurePoints, $statementResult->getImpurePoints()), + ); + } + + $originalStorage = $storage; + + $count = 0; + $closureResultScope = null; + $replayBodyRecording = null; + $replayPassStorage = null; + $replayPassResult = null; + $replayEntryScope = null; + $bodyIsReplayable = $nodeScopeResolver->isReplayableConvergenceBody($expr, $expr->stmts); + do { + $prevScope = $closureScope; + + $storage = $originalStorage->duplicate(); + $bodyRecording = $bodyIsReplayable ? new RecordingNodeCallback() : new NoopNodeCallback(); + // deep context, like the loop handlers' own convergence passes: inner + // loops walk single-pass here and only the final walk below (top-level) + // runs their full convergence - otherwise every closure-convergence + // pass would re-converge every inner loop from scratch + $intermediaryClosureScopeResult = $nodeScopeResolver->processStmtNodesInternal($expr, $expr->stmts, $closureScope, $storage, $bodyRecording, StatementContext::createDeep(resolveTemplateArguments: false)); + // the candidate to replace the final walk when this pass's entry + // turns out to be the fixpoint + if ($bodyRecording instanceof RecordingNodeCallback) { + $replayBodyRecording = $bodyRecording; + $replayPassStorage = $storage; + $replayPassResult = $intermediaryClosureScopeResult; + $replayEntryScope = $prevScope; + } + $intermediaryClosureScope = $intermediaryClosureScopeResult->getScope(); + foreach ($intermediaryClosureScopeResult->getExitPoints() as $exitPoint) { + $intermediaryClosureScope = $intermediaryClosureScope->mergeWith($exitPoint->getScope()); + } + + if ($expr->getAttribute(ImmediatelyInvokedClosureVisitor::ATTRIBUTE_NAME) === true) { + $closureResultScope = $intermediaryClosureScope; + break; + } + + $closureScope = $scope->enterAnonymousFunction($expr, $callableParameters, $nativeCallableParameters); + $closureScope = $closureScope->processClosureScope($intermediaryClosureScope, $prevScope, $byRefUses); + + if ($closureScope->equals($prevScope)) { + break; + } + if ($count >= NodeScopeResolver::GENERALIZE_AFTER_ITERATION) { + $closureScope = $prevScope->generalizeWith($closureScope); + } + $count++; + } while ($count < NodeScopeResolver::LOOP_SCOPE_ITERATIONS); + + if ($closureResultScope === null) { + $closureResultScope = $closureScope; + } + + $storage = $originalStorage; + $nodeScopeResolver->pushNodeGatherer($closureStmtsGatherer); + try { + if ( + $replayBodyRecording !== null && $replayPassStorage !== null + && $replayPassResult !== null && $replayEntryScope !== null + && $closureScope->equals($replayEntryScope) + ) { + // the final walk would repeat the recorded fixpoint pass exactly + // (same entry scope, deterministic walk) - adopt the pass's result + // and replay its emissions through the real callback instead. + // The pass's own entry scope takes over: the recorded pairs carry + // its anonymous-function reflection, which the gatherer's filter + // compares by identity (the state is equals-identical anyway). + $closureScope = $replayEntryScope; + $originalStorage->mergeResults($replayPassStorage); + $nodeScopeResolver->replayRecording($replayBodyRecording, $nodeCallback, $originalStorage, $closureScope); + $statementResult = $replayPassResult; + } else { + $statementResult = $nodeScopeResolver->processStmtNodesInternal($expr, $expr->stmts, $closureScope, $storage, $nodeCallback, StatementContext::createTopLevel($context->shouldResolveTemplateArguments())); + } + } finally { + $nodeScopeResolver->popNodeGatherer(); + } + $publicStatementResult = $statementResult->toPublic(); + $closureReturnStatementsNodeScope = $this->refineClosureNodeScope($closureScope, $scope, $expr, $gatheredReturnStatementsWithScope, $gatheredYieldStatementsWithScope, $executionEnds, $statementResult->getThrowPoints(), array_merge($closureImpurePoints, $statementResult->getImpurePoints()), $invalidateExpressions, $storage); + $nodeScopeResolver->callNodeCallback($nodeCallback, new ClosureReturnStatementsNode( + $expr, + $gatheredReturnStatements, + $gatheredReturnStatementsAfterFinally, + $gatheredYieldStatements, + $publicStatementResult, + $executionEnds, + array_merge($publicStatementResult->getImpurePoints(), $closureImpurePoints), + ), $closureReturnStatementsNodeScope, $storage); + $nodeScopeResolver->callNodeCallback($nodeCallback, VariableLivenessResolver::resolve($expr, $statementResult->getVariableFlow()), $closureReturnStatementsNodeScope, $storage); + + return new ProcessClosureResult( + $scope->addTemplateArgumentConstraints($statementResult->getScope()->getTemplateArgumentConstraints()), + $statementResult->getThrowPoints(), + $statementResult->getImpurePoints(), + $invalidateExpressions, + $gatheredReturnStatementsWithScope, + $gatheredYieldStatementsWithScope, + $executionEnds, + array_merge($closureImpurePoints, $statementResult->getImpurePoints()), + $closureResultScope, + $byRefUses, + ); + } + + /** + * The closure scope was entered with a shallow reflection (parameters + + * declared return, no body walk - see ClosureTypeResolver::getClosureType() + * with $shallow). Now that the single body walk has gathered the returns, + * build the refined ClosureType from them (no second walk) and swap it onto + * the scope the ClosureReturnStatementsNode fires with, so the return-type + * rules see the refined expected return (e.g. Bar&Foo, not just Foo). + * + * @param list $gatheredReturnStatementsWithScope + * @param list $gatheredYieldStatementsWithScope + * @param list $executionEnds + * @param InternalThrowPoint[] $throwPoints + * @param ImpurePoint[] $impurePoints + * @param InvalidateExprNode[] $invalidateExpressions + */ + private function refineClosureNodeScope( + MutatingScope $closureScope, + MutatingScope $scope, + Expr\Closure $expr, + array $gatheredReturnStatementsWithScope, + array $gatheredYieldStatementsWithScope, + array $executionEnds, + array $throwPoints, + array $impurePoints, + array $invalidateExpressions, + ExpressionResultStorage $storage, + ): MutatingScope + { + $refinedClosureType = $this->container->getByType(ClosureTypeResolver::class)->buildClosureTypeForClosure( + $scope, + $expr, + $gatheredReturnStatementsWithScope, + $gatheredYieldStatementsWithScope, + $executionEnds, + $throwPoints, + $impurePoints, + $invalidateExpressions, + false, + $storage, + ); + + return $closureScope->withAnonymousFunctionReflection($refinedClosureType); + } + + /** + * @param InvalidateExprNode[] $invalidatedExpressions + * @param string[] $uses + */ + public function processImmediatelyCalledCallable(MutatingScope $scope, array $invalidatedExpressions, array $uses): MutatingScope + { + if ($scope->isInClass()) { + $uses[] = 'this'; + } + + $finder = new NodeFinder(); + foreach ($invalidatedExpressions as $invalidateExpression) { + $result = $finder->findFirst([$invalidateExpression->getExpr()], static fn ($node) => $node instanceof Variable && in_array($node->name, $uses, true)); + if ($result === null) { + continue; + } + + $requireMoreCharacters = $invalidateExpression->getExpr() instanceof Variable; + $scope = $scope->invalidateExpression($invalidateExpression->getExpr(), $requireMoreCharacters); + } + + return $scope; + } + + /** + * @param callable(Node $node, Scope $scope): void $nodeCallback + */ + public function processArrowFunctionNode( + NodeScopeResolver $nodeScopeResolver, + Node\Stmt $stmt, + Expr\ArrowFunction $expr, + MutatingScope $scope, + ExpressionResultStorage $storage, + callable $nodeCallback, + ?Type $passedToType, + ?Type $nativePassedToType = null, + ?ExpressionContext $context = null, + ): ProcessArrowFunctionResult + { + $context ??= ExpressionContext::createTopLevel(); + foreach ($expr->params as $param) { + $nodeScopeResolver->processParamNode($stmt, $param, $scope, $storage, $nodeCallback); + } + if ($expr->returnType !== null) { + $nodeScopeResolver->callNodeCallback($nodeCallback, $expr->returnType, $scope, $storage); + } + + $arrowFunctionCallArgs = $expr->getAttribute(ArrowFunctionArgVisitor::ATTRIBUTE_NAME); + $parameterTypes = $this->container->getByType(ClosureParameterResolver::class)->resolve($scope, $expr, $storage, $arrowFunctionCallArgs, $passedToType, $nativePassedToType); + $callableParameters = $parameterTypes->parameters; + $nativeCallableParameters = $parameterTypes->nativeParameters; + $arrowFunctionScope = $scope->enterArrowFunction($expr, $callableParameters, $nativeCallableParameters); + if ($arrowFunctionScope->getAnonymousFunctionReflection() === null) { + throw new ShouldNotHappenException(); + } + + // Gather the property-assign impure points and invalidate expressions the + // arrow function type needs (mirroring ClosureTypeResolver::getClosureType()), + // on top of the regular rule node callback, so the single body walk here + // feeds ClosureTypeResolver::buildClosureTypeForArrowFunction(). + $arrowFunctionImpurePoints = []; + $invalidateExpressions = []; + $arrowFunctionStmtsGatherer = static function (Node $node, Scope $innerScope) use ($arrowFunctionScope, &$arrowFunctionImpurePoints, &$invalidateExpressions): void { + if ($innerScope->getAnonymousFunctionReflection() !== $arrowFunctionScope->getAnonymousFunctionReflection()) { + return; + } + + if ($node instanceof InvalidateExprNode) { + $invalidateExpressions[] = $node; + return; + } + + if (!$node instanceof PropertyAssignNode) { + return; + } + + $arrowFunctionImpurePoints[] = new ImpurePoint( + $innerScope, + $node, + 'propertyAssign', + 'property assignment', + true, + ); + $invalidateExpressions[] = new InvalidateExprNode($node->getPropertyFetch()); + }; + + $nodeScopeResolver->pushNodeGatherer($arrowFunctionStmtsGatherer); + try { + $exprResult = $nodeScopeResolver->processExprNode($stmt, $expr->expr, $arrowFunctionScope, $storage, $nodeCallback, ExpressionContext::createTopLevel($context->shouldResolveTemplateArguments())); + } finally { + $nodeScopeResolver->popNodeGatherer(); + } + $scope = $scope->addTemplateArgumentConstraints($exprResult->getScope()->getTemplateArgumentConstraints()); + $scope = $scope->addTemplateArgumentConstraints($nodeScopeResolver->collectReturnSend($arrowFunctionScope, $exprResult)); + + $closureTypeThrowPoints = array_map(static fn (InternalThrowPoint $throwPoint) => $throwPoint->toPublic(), $exprResult->getThrowPoints()); + $closureTypeImpurePoints = array_merge($arrowFunctionImpurePoints, $exprResult->getImpurePoints()); + + // The arrow scope was entered with a shallow reflection (parameters + + // declared return, no body walk). Now that the single body walk above has + // run, build the refined arrow function type from the body expression's + // stored type (no second walk) and fire InArrowFunctionNode with it, so the + // node and the return-type rules see the refined expected return. + $refinedArrowFunctionType = $this->container->getByType(ClosureTypeResolver::class)->buildClosureTypeForArrowFunction( + $scope, + $expr, + $arrowFunctionScope, + $closureTypeThrowPoints, + $closureTypeImpurePoints, + $invalidateExpressions, + false, + $storage, + ); + $refinedArrowFunctionScope = $arrowFunctionScope->withAnonymousFunctionReflection($refinedArrowFunctionType); + $nodeScopeResolver->callNodeCallback($nodeCallback, new InArrowFunctionNode($refinedArrowFunctionType, $expr), $refinedArrowFunctionScope, $storage); + + return new ProcessArrowFunctionResult( + $this->expressionResultFactory->create( + $scope, + beforeScope: $scope, + expr: $expr, + hasYield: false, + isAlwaysTerminating: $exprResult->isAlwaysTerminating(), + variableFlow: ArrowFunctionHandler::getVariableFlow($expr, $exprResult), + throwPoints: $exprResult->getThrowPoints(), + impurePoints: $exprResult->getImpurePoints(), + typeCallback: static fn () => new MixedType(), + specifyTypesCallback: SpecifiedTypes::emptySpecifyCallback(), + ), + $arrowFunctionScope, + $closureTypeThrowPoints, + $closureTypeImpurePoints, + $invalidateExpressions, + ); + } + +} diff --git a/src/Analyser/ExprHandler/ArrowFunctionHandler.php b/src/Analyser/ExprHandler/ArrowFunctionHandler.php index c5ecb82d682..75d4c5c6859 100644 --- a/src/Analyser/ExprHandler/ArrowFunctionHandler.php +++ b/src/Analyser/ExprHandler/ArrowFunctionHandler.php @@ -5,6 +5,7 @@ use PhpParser\Node\Expr; use PhpParser\Node\Expr\ArrowFunction; use PhpParser\Node\Stmt; +use PHPStan\Analyser\ClosureProcessor; use PHPStan\Analyser\ExpressionContext; use PHPStan\Analyser\ExpressionResult; use PHPStan\Analyser\ExpressionResultFactory; @@ -30,6 +31,7 @@ public function __construct( private ClosureTypeResolver $closureTypeResolver, private ExpressionResultFactory $expressionResultFactory, private DefaultNarrowingHelper $defaultNarrowingHelper, + private ClosureProcessor $closureProcessor, ) { } @@ -41,7 +43,7 @@ public function supports(Expr $expr): bool public function processExpr(NodeScopeResolver $nodeScopeResolver, Stmt $stmt, Expr $expr, MutatingScope $scope, ExpressionResultStorage $storage, callable $nodeCallback, ExpressionContext $context): ExpressionResult { - $arrowFunctionResult = $nodeScopeResolver->processArrowFunctionNode($stmt, $expr, $scope, $storage, $nodeCallback, $context->getPassedToType(), $context->getNativePassedToType(), $context); + $arrowFunctionResult = $this->closureProcessor->processArrowFunctionNode($nodeScopeResolver, $stmt, $expr, $scope, $storage, $nodeCallback, $context->getPassedToType(), $context->getNativePassedToType(), $context); $result = $arrowFunctionResult->getExpressionResult(); // A plain typeCallback recursing through getClosureType() would re-walk diff --git a/src/Analyser/ExprHandler/ClosureHandler.php b/src/Analyser/ExprHandler/ClosureHandler.php index 116d6bc0d8b..ea60026b066 100644 --- a/src/Analyser/ExprHandler/ClosureHandler.php +++ b/src/Analyser/ExprHandler/ClosureHandler.php @@ -5,6 +5,7 @@ use PhpParser\Node\Expr; use PhpParser\Node\Expr\Closure; use PhpParser\Node\Stmt; +use PHPStan\Analyser\ClosureProcessor; use PHPStan\Analyser\ExpressionContext; use PHPStan\Analyser\ExpressionResult; use PHPStan\Analyser\ExpressionResultFactory; @@ -30,6 +31,7 @@ public function __construct( private ClosureTypeResolver $closureTypeResolver, private ExpressionResultFactory $expressionResultFactory, private DefaultNarrowingHelper $defaultNarrowingHelper, + private ClosureProcessor $closureProcessor, ) { } @@ -41,7 +43,7 @@ public function supports(Expr $expr): bool public function processExpr(NodeScopeResolver $nodeScopeResolver, Stmt $stmt, Expr $expr, MutatingScope $scope, ExpressionResultStorage $storage, callable $nodeCallback, ExpressionContext $context): ExpressionResult { - $processClosureResult = $nodeScopeResolver->processClosureNode($stmt, $expr, $scope, $storage, $nodeCallback, $context, $context->getPassedToType(), $context->getNativePassedToType()); + $processClosureResult = $this->closureProcessor->processClosureNode($nodeScopeResolver, $stmt, $expr, $scope, $storage, $nodeCallback, $context, $context->getPassedToType(), $context->getNativePassedToType()); // A plain typeCallback recursing through getClosureType() would re-walk // the body each getType() ask before the cache populates and hang; diff --git a/src/Analyser/ExprHandler/FuncCallHandler.php b/src/Analyser/ExprHandler/FuncCallHandler.php index 3682474ee5a..77b8fd8d34d 100644 --- a/src/Analyser/ExprHandler/FuncCallHandler.php +++ b/src/Analyser/ExprHandler/FuncCallHandler.php @@ -15,6 +15,7 @@ use PHPStan\Analyser\ArgsResult; use PHPStan\Analyser\ArgumentsHandler; use PHPStan\Analyser\ArgumentsNormalizer; +use PHPStan\Analyser\ClosureProcessor; use PHPStan\Analyser\ExpressionContext; use PHPStan\Analyser\ExpressionResult; use PHPStan\Analyser\ExpressionResultFactory; @@ -105,6 +106,7 @@ public function __construct( private ImpossibleCheckTypeHelper $impossibleCheckTypeHelper, private ClosureTypeResolver $closureTypeResolver, private ArgumentsHandler $argumentsHandler, + private ClosureProcessor $closureProcessor, ) { } @@ -176,7 +178,7 @@ public function processExpr(NodeScopeResolver $nodeScopeResolver, Stmt $stmt, Ex $throwPoints = array_merge($throwPoints, $callableThrowPoints); $impurePoints = array_merge($impurePoints, array_map(static fn (SimpleImpurePoint $impurePoint) => new ImpurePoint($scope, $expr, $impurePoint->getIdentifier(), $impurePoint->getDescription(), $impurePoint->isCertain()), $parametersAcceptor->getImpurePoints())); - $scope = $nodeScopeResolver->processImmediatelyCalledCallable($scope, $parametersAcceptor->getInvalidateExpressions(), $parametersAcceptor->getUsedVariables()); + $scope = $this->closureProcessor->processImmediatelyCalledCallable($scope, $parametersAcceptor->getInvalidateExpressions(), $parametersAcceptor->getUsedVariables()); } } elseif ($this->reflectionProvider->hasFunction($expr->name, $scope)) { $functionReflection = $this->reflectionProvider->getFunction($expr->name, $scope); diff --git a/src/Analyser/ExprHandler/Helper/ClosureParameterResolver.php b/src/Analyser/ExprHandler/Helper/ClosureParameterResolver.php index cefe53f9fa8..ff0d74a5d01 100644 --- a/src/Analyser/ExprHandler/Helper/ClosureParameterResolver.php +++ b/src/Analyser/ExprHandler/Helper/ClosureParameterResolver.php @@ -2,24 +2,38 @@ namespace PHPStan\Analyser\ExprHandler\Helper; +use Closure; use PhpParser\Node; +use PhpParser\Node\Expr; use PHPStan\Analyser\ClosureParameterTypes; use PHPStan\Analyser\ExpressionResultStorage; use PHPStan\Analyser\MutatingScope; use PHPStan\Analyser\NodeScopeResolver; use PHPStan\DependencyInjection\AutowiredService; +use PHPStan\DependencyInjection\Container; use PHPStan\Parser\ArrayMapArgVisitor; use PHPStan\Parser\ImmediatelyInvokedClosureVisitor; +use PHPStan\Reflection\Native\NativeParameterReflection; +use PHPStan\Reflection\ParameterReflection; use PHPStan\Reflection\PassedByReference; use PHPStan\Reflection\Php\DummyParameter; use PHPStan\Type\Type; +use PHPStan\Type\TypeCombinator; +use PHPStan\Type\UnionType; +use function array_key_exists; +use function array_map; +use function count; +use function max; /** Resolve the same contextual parameters for body analysis and closure type inference. */ #[AutowiredService] final class ClosureParameterResolver { - public function __construct(private NodeScopeResolver $nodeScopeResolver) + public function __construct( + private NodeScopeResolver $nodeScopeResolver, + private Container $container, + ) { } @@ -38,8 +52,8 @@ public function resolve( $intrinsicArgs = $arrayMapArgs ?? $immediatelyInvokedArgs; if ($intrinsicArgs === null) { return new ClosureParameterTypes( - $this->nodeScopeResolver->createCallableParameters($scope, $expr, $callArgs, $passedToType), - $this->nodeScopeResolver->createNativeCallableParameters($scope, $expr, $callArgs, $nativePassedToType), + $this->createCallableParameters($scope, $expr, $callArgs, $passedToType), + $this->createNativeCallableParameters($scope, $expr, $callArgs, $nativePassedToType), ); } @@ -60,4 +74,132 @@ public function resolve( return new ClosureParameterTypes($parameters, $nativeParameters); } + /** + * @param Node\Arg[]|null $args + * @return ParameterReflection[]|null + */ + private function createCallableParameters(MutatingScope $scope, Expr $closureExpr, ?array $args, ?Type $passedToType): ?array + { + return $this->doCreateCallableParameters($scope, $closureExpr, $args, $passedToType, fn (MutatingScope $s, Expr $e): Type => $this->resolveCallableTypeForScope($e, $s)); + } + + /** + * @param Node\Arg[]|null $args + * @return ParameterReflection[]|null + */ + private function createNativeCallableParameters(MutatingScope $scope, Expr $closureExpr, ?array $args, ?Type $nativePassedToType): ?array + { + return $this->doCreateCallableParameters($scope, $closureExpr, $args, $nativePassedToType, fn (MutatingScope $s, Expr $e): Type => $this->resolveCallableTypeForScope($e, $s->doNotTreatPhpDocTypesAsCertain())); + } + + /** + * Resolves the type of an expression a callable parameter is derived from - + * either the closure/arrow function whose acceptors describe the parameters, + * or a call argument refining them. A closure/arrow function is resolved + * directly through ClosureTypeResolver (as Scope::getType() would), not by + * processing it on demand: createCallableParameters() runs while that very + * closure is being processed, so on-demand processing would re-enter + * processClosureNodeInternal() endlessly. + */ + public function resolveCallableTypeForScope(Expr $expr, MutatingScope $scope): Type + { + if ($expr instanceof Expr\Closure || $expr instanceof Expr\ArrowFunction) { + return $this->container->getByType(ClosureTypeResolver::class)->getClosureType($scope, $expr, false, $scope->getCurrentExpressionResultStorage()); + } + + return $this->nodeScopeResolver->readTypeOfMaybeStored($expr, $scope); + } + + /** + * @param Node\Arg[]|null $args + * @param Closure(MutatingScope, Expr): Type $typeGetter + * @return ParameterReflection[]|null + */ + private function doCreateCallableParameters(MutatingScope $scope, Expr $closureExpr, ?array $args, ?Type $passedToType, Closure $typeGetter): ?array + { + $callableParameters = null; + if ($args !== null) { + $closureType = $typeGetter($scope, $closureExpr); + + if ($closureType->isCallable()->no()) { + return null; + } + + $acceptors = $closureType->getCallableParametersAcceptors($scope); + if (count($acceptors) === 1) { + $callableParameters = $acceptors[0]->getParameters(); + + foreach ($callableParameters as $index => $callableParameter) { + if (!isset($args[$index])) { + continue; + } + + if ($callableParameter->isVariadic()) { + $argTypes = []; + $argNumber = count($args); + for ($j = $index; $j < $argNumber; $j++) { + $argTypes[] = $typeGetter($scope, $args[$j]->value); + } + $type = TypeCombinator::union(...$argTypes); + } else { + $type = $typeGetter($scope, $args[$index]->value); + } + $callableParameters[$index] = new NativeParameterReflection( + $callableParameter->getName(), + $callableParameter->isOptional(), + $type, + $callableParameter->passedByReference(), + $callableParameter->isVariadic(), + $callableParameter->getDefaultValue(), + ); + } + } + } elseif ($passedToType !== null && !$passedToType->isCallable()->no()) { + if ($passedToType instanceof UnionType) { + $passedToType = $passedToType->filterTypes(static fn (Type $innerType) => $innerType->isCallable()->yes()); + + if ($passedToType->isCallable()->no()) { + return null; + } + } + + $acceptors = $passedToType->getCallableParametersAcceptors($scope); + foreach ($acceptors as $acceptor) { + $acceptorParameters = array_map(static fn (ParameterReflection $callableParameter) => new NativeParameterReflection( + $callableParameter->getName(), + $callableParameter->isOptional(), + $callableParameter->getType(), + $callableParameter->passedByReference(), + $callableParameter->isVariadic(), + $callableParameter->getDefaultValue(), + ), $acceptor->getParameters()); + + if ($callableParameters === null) { + $callableParameters = $acceptorParameters; + continue; + } + + $newParameters = []; + $parameterCount = max(count($callableParameters), count($acceptorParameters)); + for ($i = 0; $i < $parameterCount; $i++) { + if (!array_key_exists($i, $acceptorParameters)) { + $newParameters[] = $callableParameters[$i]->toOptional(); + continue; + } + + if (!array_key_exists($i, $callableParameters)) { + $newParameters[] = $acceptorParameters[$i]->toOptional(); + continue; + } + + $newParameters[] = $callableParameters[$i]->union($acceptorParameters[$i]); + } + + $callableParameters = $newParameters; + } + } + + return $callableParameters; + } + } diff --git a/src/Analyser/ExprHandler/Helper/ClosureTypeResolver.php b/src/Analyser/ExprHandler/Helper/ClosureTypeResolver.php index d62b0d070b0..6cd05da4e36 100644 --- a/src/Analyser/ExprHandler/Helper/ClosureTypeResolver.php +++ b/src/Analyser/ExprHandler/Helper/ClosureTypeResolver.php @@ -129,7 +129,7 @@ public function getClosureType( // ENTRY (enterAnonymousFunction()/enterArrowFunction()) so entering a // closure/arrow scope never re-walks the body - the refined return type is // built afterwards from the single body walk's gathered returns and carried - // on the node/rule scope (see NodeScopeResolver::processClosureNodeInternal() + // on the node/rule scope (see ClosureProcessor::processClosureNodeInternal() // and processArrowFunctionNode()). if ($shallow) { return new ClosureType( @@ -286,7 +286,7 @@ static function (Node $node, Scope $scope) use ($arrowScope, &$arrowFunctionImpu /** * Constructs a closure type from data the engine already gathered while - * walking the body once (see NodeScopeResolver::processClosureNode()), + * walking the body once (see ClosureProcessor::processClosureNode()), * without a second walk. The return/yield expression types are read from * their stored results. * @@ -341,7 +341,7 @@ public function buildClosureTypeForClosure( /** * Constructs an arrow function type from data the engine already gathered - * while walking the body once (see NodeScopeResolver:: + * while walking the body once (see ClosureProcessor:: * processArrowFunctionNode()), without a second walk. The return/yield * expression types are read from their stored results on $arrowScope. * diff --git a/src/Analyser/NodeScopeResolver.php b/src/Analyser/NodeScopeResolver.php index 11cbe44e38e..c7e63809eea 100644 --- a/src/Analyser/NodeScopeResolver.php +++ b/src/Analyser/NodeScopeResolver.php @@ -13,7 +13,6 @@ use PhpParser\Node\Expr\PropertyFetch; use PhpParser\Node\Expr\StaticCall; use PhpParser\Node\Expr\StaticPropertyFetch; -use PhpParser\Node\Expr\Variable; use PhpParser\Node\Stmt\Class_; use PhpParser\Node\Stmt\Echo_; use PhpParser\Node\Stmt\Foreach_; @@ -22,10 +21,7 @@ use PhpParser\Node\Stmt\Static_; use PhpParser\Node\Stmt\Switch_; use PhpParser\NodeFinder; -use PHPStan\Analyser\ExprHandler\ArrowFunctionHandler; use PHPStan\Analyser\ExprHandler\AssignHandler; -use PHPStan\Analyser\ExprHandler\Helper\ClosureParameterResolver; -use PHPStan\Analyser\ExprHandler\Helper\ClosureTypeResolver; use PHPStan\Analyser\ExprHandler\Helper\NonNullabilityHelper; use PHPStan\Analyser\ExprHandler\Helper\VirtualExprResultHelper; use PHPStan\Analyser\Generics\TemplateArgumentConstraints; @@ -38,52 +34,29 @@ use PHPStan\DependencyInjection\Container; use PHPStan\DependencyInjection\ExtensionsCollection; use PHPStan\File\FileHelper; -use PHPStan\Node\ClosureReturnStatementsNode; -use PHPStan\Node\ExecutionEndNode; use PHPStan\Node\Expr\NativeTypeExpr; use PHPStan\Node\Expr\TypeExpr; use PHPStan\Node\FunctionCallableNode; use PHPStan\Node\FunctionCallExpressionNode; -use PHPStan\Node\InArrowFunctionNode; -use PHPStan\Node\InClosureNode; use PHPStan\Node\InstantiationCallableNode; -use PHPStan\Node\InvalidateExprNode; use PHPStan\Node\MethodCallableNode; use PHPStan\Node\MethodCallExpressionNode; -use PHPStan\Node\PropertyAssignNode; -use PHPStan\Node\ReturnAfterFinallyNode; -use PHPStan\Node\ReturnStatement; use PHPStan\Node\StaticMethodCallableNode; use PHPStan\Node\StaticMethodCallExpressionNode; -use PHPStan\Parser\ArrowFunctionArgVisitor; -use PHPStan\Parser\ClosureArgVisitor; -use PHPStan\Parser\ImmediatelyInvokedClosureVisitor; use PHPStan\Reflection\Native\NativeMethodReflection; -use PHPStan\Reflection\Native\NativeParameterReflection; -use PHPStan\Reflection\ParameterReflection; use PHPStan\Reflection\Php\PhpMethodReflection; use PHPStan\Rules\Properties\ReadWritePropertiesExtension; use PHPStan\ShouldNotHappenException; -use PHPStan\TrinaryLogic; -use PHPStan\Type\ClosureType; use PHPStan\Type\ErrorType; use PHPStan\Type\MixedType; -use PHPStan\Type\NullType; use PHPStan\Type\Type; -use PHPStan\Type\TypeCombinator; use PHPStan\Type\TypeUtils; -use PHPStan\Type\UnionType; -use function array_key_exists; -use function array_map; use function array_merge; use function array_pop; -use function count; use function get_class; use function getenv; -use function in_array; use function is_array; use function is_string; -use function max; use function spl_object_id; use function sprintf; @@ -1116,599 +1089,6 @@ public function callNodeCallback( $nodeCallback($node, $scope->toNodeCallbackScope()); } - /** - * @param callable(Node $node, Scope $scope): void $nodeCallback - */ - public function processClosureNode( - Node\Stmt $stmt, - Expr\Closure $expr, - MutatingScope $scope, - ExpressionResultStorage $storage, - callable $nodeCallback, - ExpressionContext $context, - ?Type $passedToType, - ?Type $nativePassedToType = null, - ): ProcessClosureResult - { - return $this->processClosureNodeInternal($stmt, $expr, $scope, $storage, $nodeCallback, $context, $passedToType, $nativePassedToType); - } - - /** - * @param callable(Node $node, Scope $scope): void $nodeCallback - */ - private function processClosureNodeInternal( - Node\Stmt $stmt, - Expr\Closure $expr, - MutatingScope $scope, - ExpressionResultStorage $storage, - callable $nodeCallback, - ExpressionContext $context, - ?Type $passedToType, - ?Type $nativePassedToType = null, - ): ProcessClosureResult - { - foreach ($expr->params as $param) { - $this->processParamNode($stmt, $param, $scope, $storage, $nodeCallback); - } - - $byRefUses = []; - - $closureCallArgs = $expr->getAttribute(ClosureArgVisitor::ATTRIBUTE_NAME); - $parameterTypes = $this->container->getByType(ClosureParameterResolver::class)->resolve($scope, $expr, $storage, $closureCallArgs, $passedToType, $nativePassedToType); - $callableParameters = $parameterTypes->parameters; - $nativeCallableParameters = $parameterTypes->nativeParameters; - - $useScope = $scope; - foreach ($expr->uses as $use) { - if ($use->byRef) { - $byRefUses[] = $use; - $useScope = $useScope->enterExpressionAssign($use->var); - - $inAssignRightSideVariableName = $context->getInAssignRightSideVariableName(); - $inAssignRightSideExpr = $context->getInAssignRightSideExpr(); - if ( - $inAssignRightSideVariableName === $use->var->name - && $inAssignRightSideExpr !== null - ) { - // a call's type is carried by the context (see - // ExpressionContext::enterAssignRightSideCallArgs()); a closure - // right side resolves through the closure type resolver - $inAssignRightSideType = $context->getInAssignRightSideType() ?? $this->resolveCallableTypeForScope($inAssignRightSideExpr, $scope); - if ($inAssignRightSideType instanceof ClosureType) { - $variableType = $inAssignRightSideType; - } else { - $alreadyHasVariableType = $scope->hasVariableType($inAssignRightSideVariableName); - if ($alreadyHasVariableType->no()) { - $variableType = TypeCombinator::union(new NullType(), $inAssignRightSideType); - } else { - $variableType = TypeCombinator::union($scope->getVariableType($inAssignRightSideVariableName), $inAssignRightSideType); - } - } - $inAssignRightSideNativeType = $context->getInAssignRightSideNativeType() ?? $this->resolveCallableTypeForScope($inAssignRightSideExpr, $scope->doNotTreatPhpDocTypesAsCertain()); - if ($inAssignRightSideNativeType instanceof ClosureType) { - $variableNativeType = $inAssignRightSideNativeType; - } else { - $alreadyHasVariableType = $scope->hasVariableType($inAssignRightSideVariableName); - if ($alreadyHasVariableType->no()) { - $variableNativeType = TypeCombinator::union(new NullType(), $inAssignRightSideNativeType); - } else { - $variableNativeType = TypeCombinator::union($scope->getVariableType($inAssignRightSideVariableName), $inAssignRightSideNativeType); - } - } - $scope = $scope->assignVariable($inAssignRightSideVariableName, $variableType, $variableNativeType, TrinaryLogic::createYes()); - } - } - $this->processExprNode($stmt, $use->var, $useScope, $storage, $nodeCallback, $context->withoutValueFlow()); - if (!$use->byRef) { - continue; - } - - $useScope = $useScope->exitExpressionAssign($use->var); - } - - if ($expr->returnType !== null) { - $this->callNodeCallback($nodeCallback, $expr->returnType, $scope, $storage); - } - - $closureScope = $scope->enterAnonymousFunction($expr, $callableParameters, $nativeCallableParameters); - $closureScope = $closureScope->processClosureScope($scope, null, $byRefUses); - $closureType = $closureScope->getAnonymousFunctionReflection(); - if (!$closureType instanceof ClosureType) { - throw new ShouldNotHappenException(); - } - - $this->callNodeCallback($nodeCallback, new InClosureNode($closureType, $expr), $closureScope, $storage); - - $executionEnds = []; - $gatheredReturnStatements = []; - $gatheredReturnStatementsAfterFinally = []; - $gatheredReturnStatementsWithScope = []; - $gatheredYieldStatements = []; - $gatheredYieldStatementsWithScope = []; - $closureImpurePoints = []; - $invalidateExpressions = []; - $closureStmtsGatherer = static function (Node $node, Scope $scope) use (&$executionEnds, &$gatheredReturnStatements, &$gatheredReturnStatementsAfterFinally, &$gatheredReturnStatementsWithScope, &$gatheredYieldStatements, &$gatheredYieldStatementsWithScope, &$closureScope, &$closureImpurePoints, &$invalidateExpressions): void { - if ($scope->getAnonymousFunctionReflection() !== $closureScope->getAnonymousFunctionReflection()) { - return; - } - if ($node instanceof PropertyAssignNode) { - $closureImpurePoints[] = new ImpurePoint( - $scope, - $node, - 'propertyAssign', - 'property assignment', - true, - ); - $invalidateExpressions[] = new InvalidateExprNode($node->getPropertyFetch()); - return; - } - if ($node instanceof ExecutionEndNode) { - $executionEnds[] = $node; - return; - } - if ($node instanceof ReturnAfterFinallyNode) { - $gatheredReturnStatementsAfterFinally[] = new ReturnStatement($scope, $node->getReturnNode()); - return; - } - if ($node instanceof InvalidateExprNode) { - $invalidateExpressions[] = $node; - return; - } - if ($node instanceof Expr\Yield_ || $node instanceof Expr\YieldFrom) { - $gatheredYieldStatements[] = $node; - $gatheredYieldStatementsWithScope[] = [$node, $scope]; - } - if (!$node instanceof Return_) { - return; - } - - $gatheredReturnStatements[] = new ReturnStatement($scope, $node); - $gatheredReturnStatementsWithScope[] = [$node, $scope]; - }; - - if (count($byRefUses) === 0) { - $this->pushNodeGatherer($closureStmtsGatherer); - try { - $statementResult = $this->processStmtNodesInternal($expr, $expr->stmts, $closureScope, $storage, $nodeCallback, StatementContext::createTopLevel($context->shouldResolveTemplateArguments())); - } finally { - $this->popNodeGatherer(); - } - $publicStatementResult = $statementResult->toPublic(); - $closureReturnStatementsNodeScope = $this->refineClosureNodeScope($closureScope, $scope, $expr, $gatheredReturnStatementsWithScope, $gatheredYieldStatementsWithScope, $executionEnds, $statementResult->getThrowPoints(), array_merge($closureImpurePoints, $statementResult->getImpurePoints()), $invalidateExpressions, $storage); - $this->callNodeCallback($nodeCallback, new ClosureReturnStatementsNode( - $expr, - $gatheredReturnStatements, - $gatheredReturnStatementsAfterFinally, - $gatheredYieldStatements, - $publicStatementResult, - $executionEnds, - array_merge($publicStatementResult->getImpurePoints(), $closureImpurePoints), - ), $closureReturnStatementsNodeScope, $storage); - $this->callNodeCallback($nodeCallback, VariableLivenessResolver::resolve($expr, $statementResult->getVariableFlow()), $closureReturnStatementsNodeScope, $storage); - - return new ProcessClosureResult( - $scope->addTemplateArgumentConstraints($statementResult->getScope()->getTemplateArgumentConstraints()), - $statementResult->getThrowPoints(), - $statementResult->getImpurePoints(), - $invalidateExpressions, - $gatheredReturnStatementsWithScope, - $gatheredYieldStatementsWithScope, - $executionEnds, - array_merge($closureImpurePoints, $statementResult->getImpurePoints()), - ); - } - - $originalStorage = $storage; - - $count = 0; - $closureResultScope = null; - $replayBodyRecording = null; - $replayPassStorage = null; - $replayPassResult = null; - $replayEntryScope = null; - $bodyIsReplayable = $this->isReplayableConvergenceBody($expr, $expr->stmts); - do { - $prevScope = $closureScope; - - $storage = $originalStorage->duplicate(); - $bodyRecording = $bodyIsReplayable ? new RecordingNodeCallback() : new NoopNodeCallback(); - // deep context, like the loop handlers' own convergence passes: inner - // loops walk single-pass here and only the final walk below (top-level) - // runs their full convergence - otherwise every closure-convergence - // pass would re-converge every inner loop from scratch - $intermediaryClosureScopeResult = $this->processStmtNodesInternal($expr, $expr->stmts, $closureScope, $storage, $bodyRecording, StatementContext::createDeep(resolveTemplateArguments: false)); - // the candidate to replace the final walk when this pass's entry - // turns out to be the fixpoint - if ($bodyRecording instanceof RecordingNodeCallback) { - $replayBodyRecording = $bodyRecording; - $replayPassStorage = $storage; - $replayPassResult = $intermediaryClosureScopeResult; - $replayEntryScope = $prevScope; - } - $intermediaryClosureScope = $intermediaryClosureScopeResult->getScope(); - foreach ($intermediaryClosureScopeResult->getExitPoints() as $exitPoint) { - $intermediaryClosureScope = $intermediaryClosureScope->mergeWith($exitPoint->getScope()); - } - - if ($expr->getAttribute(ImmediatelyInvokedClosureVisitor::ATTRIBUTE_NAME) === true) { - $closureResultScope = $intermediaryClosureScope; - break; - } - - $closureScope = $scope->enterAnonymousFunction($expr, $callableParameters, $nativeCallableParameters); - $closureScope = $closureScope->processClosureScope($intermediaryClosureScope, $prevScope, $byRefUses); - - if ($closureScope->equals($prevScope)) { - break; - } - if ($count >= self::GENERALIZE_AFTER_ITERATION) { - $closureScope = $prevScope->generalizeWith($closureScope); - } - $count++; - } while ($count < self::LOOP_SCOPE_ITERATIONS); - - if ($closureResultScope === null) { - $closureResultScope = $closureScope; - } - - $storage = $originalStorage; - $this->pushNodeGatherer($closureStmtsGatherer); - try { - if ( - $replayBodyRecording !== null && $replayPassStorage !== null - && $replayPassResult !== null && $replayEntryScope !== null - && $closureScope->equals($replayEntryScope) - ) { - // the final walk would repeat the recorded fixpoint pass exactly - // (same entry scope, deterministic walk) - adopt the pass's result - // and replay its emissions through the real callback instead. - // The pass's own entry scope takes over: the recorded pairs carry - // its anonymous-function reflection, which the gatherer's filter - // compares by identity (the state is equals-identical anyway). - $closureScope = $replayEntryScope; - $originalStorage->mergeResults($replayPassStorage); - $this->replayRecording($replayBodyRecording, $nodeCallback, $originalStorage, $closureScope); - $statementResult = $replayPassResult; - } else { - $statementResult = $this->processStmtNodesInternal($expr, $expr->stmts, $closureScope, $storage, $nodeCallback, StatementContext::createTopLevel($context->shouldResolveTemplateArguments())); - } - } finally { - $this->popNodeGatherer(); - } - $publicStatementResult = $statementResult->toPublic(); - $closureReturnStatementsNodeScope = $this->refineClosureNodeScope($closureScope, $scope, $expr, $gatheredReturnStatementsWithScope, $gatheredYieldStatementsWithScope, $executionEnds, $statementResult->getThrowPoints(), array_merge($closureImpurePoints, $statementResult->getImpurePoints()), $invalidateExpressions, $storage); - $this->callNodeCallback($nodeCallback, new ClosureReturnStatementsNode( - $expr, - $gatheredReturnStatements, - $gatheredReturnStatementsAfterFinally, - $gatheredYieldStatements, - $publicStatementResult, - $executionEnds, - array_merge($publicStatementResult->getImpurePoints(), $closureImpurePoints), - ), $closureReturnStatementsNodeScope, $storage); - $this->callNodeCallback($nodeCallback, VariableLivenessResolver::resolve($expr, $statementResult->getVariableFlow()), $closureReturnStatementsNodeScope, $storage); - - return new ProcessClosureResult( - $scope->addTemplateArgumentConstraints($statementResult->getScope()->getTemplateArgumentConstraints()), - $statementResult->getThrowPoints(), - $statementResult->getImpurePoints(), - $invalidateExpressions, - $gatheredReturnStatementsWithScope, - $gatheredYieldStatementsWithScope, - $executionEnds, - array_merge($closureImpurePoints, $statementResult->getImpurePoints()), - $closureResultScope, - $byRefUses, - ); - } - - /** - * The closure scope was entered with a shallow reflection (parameters + - * declared return, no body walk - see ClosureTypeResolver::getClosureType() - * with $shallow). Now that the single body walk has gathered the returns, - * build the refined ClosureType from them (no second walk) and swap it onto - * the scope the ClosureReturnStatementsNode fires with, so the return-type - * rules see the refined expected return (e.g. Bar&Foo, not just Foo). - * - * @param list $gatheredReturnStatementsWithScope - * @param list $gatheredYieldStatementsWithScope - * @param list $executionEnds - * @param InternalThrowPoint[] $throwPoints - * @param ImpurePoint[] $impurePoints - * @param InvalidateExprNode[] $invalidateExpressions - */ - private function refineClosureNodeScope( - MutatingScope $closureScope, - MutatingScope $scope, - Expr\Closure $expr, - array $gatheredReturnStatementsWithScope, - array $gatheredYieldStatementsWithScope, - array $executionEnds, - array $throwPoints, - array $impurePoints, - array $invalidateExpressions, - ExpressionResultStorage $storage, - ): MutatingScope - { - $refinedClosureType = $this->container->getByType(ClosureTypeResolver::class)->buildClosureTypeForClosure( - $scope, - $expr, - $gatheredReturnStatementsWithScope, - $gatheredYieldStatementsWithScope, - $executionEnds, - $throwPoints, - $impurePoints, - $invalidateExpressions, - false, - $storage, - ); - - return $closureScope->withAnonymousFunctionReflection($refinedClosureType); - } - - /** - * @param InvalidateExprNode[] $invalidatedExpressions - * @param string[] $uses - */ - public function processImmediatelyCalledCallable(MutatingScope $scope, array $invalidatedExpressions, array $uses): MutatingScope - { - if ($scope->isInClass()) { - $uses[] = 'this'; - } - - $finder = new NodeFinder(); - foreach ($invalidatedExpressions as $invalidateExpression) { - $result = $finder->findFirst([$invalidateExpression->getExpr()], static fn ($node) => $node instanceof Variable && in_array($node->name, $uses, true)); - if ($result === null) { - continue; - } - - $requireMoreCharacters = $invalidateExpression->getExpr() instanceof Variable; - $scope = $scope->invalidateExpression($invalidateExpression->getExpr(), $requireMoreCharacters); - } - - return $scope; - } - - /** - * @param callable(Node $node, Scope $scope): void $nodeCallback - */ - public function processArrowFunctionNode( - Node\Stmt $stmt, - Expr\ArrowFunction $expr, - MutatingScope $scope, - ExpressionResultStorage $storage, - callable $nodeCallback, - ?Type $passedToType, - ?Type $nativePassedToType = null, - ?ExpressionContext $context = null, - ): ProcessArrowFunctionResult - { - $context ??= ExpressionContext::createTopLevel(); - foreach ($expr->params as $param) { - $this->processParamNode($stmt, $param, $scope, $storage, $nodeCallback); - } - if ($expr->returnType !== null) { - $this->callNodeCallback($nodeCallback, $expr->returnType, $scope, $storage); - } - - $arrowFunctionCallArgs = $expr->getAttribute(ArrowFunctionArgVisitor::ATTRIBUTE_NAME); - $parameterTypes = $this->container->getByType(ClosureParameterResolver::class)->resolve($scope, $expr, $storage, $arrowFunctionCallArgs, $passedToType, $nativePassedToType); - $callableParameters = $parameterTypes->parameters; - $nativeCallableParameters = $parameterTypes->nativeParameters; - $arrowFunctionScope = $scope->enterArrowFunction($expr, $callableParameters, $nativeCallableParameters); - if ($arrowFunctionScope->getAnonymousFunctionReflection() === null) { - throw new ShouldNotHappenException(); - } - - // Gather the property-assign impure points and invalidate expressions the - // arrow function type needs (mirroring ClosureTypeResolver::getClosureType()), - // on top of the regular rule node callback, so the single body walk here - // feeds ClosureTypeResolver::buildClosureTypeForArrowFunction(). - $arrowFunctionImpurePoints = []; - $invalidateExpressions = []; - $arrowFunctionStmtsGatherer = static function (Node $node, Scope $innerScope) use ($arrowFunctionScope, &$arrowFunctionImpurePoints, &$invalidateExpressions): void { - if ($innerScope->getAnonymousFunctionReflection() !== $arrowFunctionScope->getAnonymousFunctionReflection()) { - return; - } - - if ($node instanceof InvalidateExprNode) { - $invalidateExpressions[] = $node; - return; - } - - if (!$node instanceof PropertyAssignNode) { - return; - } - - $arrowFunctionImpurePoints[] = new ImpurePoint( - $innerScope, - $node, - 'propertyAssign', - 'property assignment', - true, - ); - $invalidateExpressions[] = new InvalidateExprNode($node->getPropertyFetch()); - }; - - $this->pushNodeGatherer($arrowFunctionStmtsGatherer); - try { - $exprResult = $this->processExprNode($stmt, $expr->expr, $arrowFunctionScope, $storage, $nodeCallback, ExpressionContext::createTopLevel($context->shouldResolveTemplateArguments())); - } finally { - $this->popNodeGatherer(); - } - $scope = $scope->addTemplateArgumentConstraints($exprResult->getScope()->getTemplateArgumentConstraints()); - $scope = $scope->addTemplateArgumentConstraints($this->collectReturnSend($arrowFunctionScope, $exprResult)); - - $closureTypeThrowPoints = array_map(static fn (InternalThrowPoint $throwPoint) => $throwPoint->toPublic(), $exprResult->getThrowPoints()); - $closureTypeImpurePoints = array_merge($arrowFunctionImpurePoints, $exprResult->getImpurePoints()); - - // The arrow scope was entered with a shallow reflection (parameters + - // declared return, no body walk). Now that the single body walk above has - // run, build the refined arrow function type from the body expression's - // stored type (no second walk) and fire InArrowFunctionNode with it, so the - // node and the return-type rules see the refined expected return. - $refinedArrowFunctionType = $this->container->getByType(ClosureTypeResolver::class)->buildClosureTypeForArrowFunction( - $scope, - $expr, - $arrowFunctionScope, - $closureTypeThrowPoints, - $closureTypeImpurePoints, - $invalidateExpressions, - false, - $storage, - ); - $refinedArrowFunctionScope = $arrowFunctionScope->withAnonymousFunctionReflection($refinedArrowFunctionType); - $this->callNodeCallback($nodeCallback, new InArrowFunctionNode($refinedArrowFunctionType, $expr), $refinedArrowFunctionScope, $storage); - - return new ProcessArrowFunctionResult( - $this->expressionResultFactory->create( - $scope, - beforeScope: $scope, - expr: $expr, - hasYield: false, - isAlwaysTerminating: $exprResult->isAlwaysTerminating(), - variableFlow: ArrowFunctionHandler::getVariableFlow($expr, $exprResult), - throwPoints: $exprResult->getThrowPoints(), - impurePoints: $exprResult->getImpurePoints(), - typeCallback: static fn () => new MixedType(), - specifyTypesCallback: SpecifiedTypes::emptySpecifyCallback(), - ), - $arrowFunctionScope, - $closureTypeThrowPoints, - $closureTypeImpurePoints, - $invalidateExpressions, - ); - } - - /** - * @param Node\Arg[]|null $args - * @return ParameterReflection[]|null - */ - public function createCallableParameters(MutatingScope $scope, Expr $closureExpr, ?array $args, ?Type $passedToType): ?array - { - return $this->doCreateCallableParameters($scope, $closureExpr, $args, $passedToType, fn (MutatingScope $s, Expr $e): Type => $this->resolveCallableTypeForScope($e, $s)); - } - - /** - * @param Node\Arg[]|null $args - * @return ParameterReflection[]|null - */ - public function createNativeCallableParameters(MutatingScope $scope, Expr $closureExpr, ?array $args, ?Type $nativePassedToType): ?array - { - return $this->doCreateCallableParameters($scope, $closureExpr, $args, $nativePassedToType, fn (MutatingScope $s, Expr $e): Type => $this->resolveCallableTypeForScope($e, $s->doNotTreatPhpDocTypesAsCertain())); - } - - /** - * Resolves the type of an expression a callable parameter is derived from - - * either the closure/arrow function whose acceptors describe the parameters, - * or a call argument refining them. A closure/arrow function is resolved - * directly through ClosureTypeResolver (as Scope::getType() would), not by - * processing it on demand: createCallableParameters() runs while that very - * closure is being processed, so on-demand processing would re-enter - * processClosureNodeInternal() endlessly. - */ - public function resolveCallableTypeForScope(Expr $expr, MutatingScope $scope): Type - { - if ($expr instanceof Expr\Closure || $expr instanceof Expr\ArrowFunction) { - return $this->container->getByType(ClosureTypeResolver::class)->getClosureType($scope, $expr, false, $scope->getCurrentExpressionResultStorage()); - } - - return $this->readTypeOfMaybeStored($expr, $scope); - } - - /** - * @param Node\Arg[]|null $args - * @param Closure(MutatingScope, Expr): Type $typeGetter - * @return ParameterReflection[]|null - */ - private function doCreateCallableParameters(MutatingScope $scope, Expr $closureExpr, ?array $args, ?Type $passedToType, Closure $typeGetter): ?array - { - $callableParameters = null; - if ($args !== null) { - $closureType = $typeGetter($scope, $closureExpr); - - if ($closureType->isCallable()->no()) { - return null; - } - - $acceptors = $closureType->getCallableParametersAcceptors($scope); - if (count($acceptors) === 1) { - $callableParameters = $acceptors[0]->getParameters(); - - foreach ($callableParameters as $index => $callableParameter) { - if (!isset($args[$index])) { - continue; - } - - if ($callableParameter->isVariadic()) { - $argTypes = []; - $argNumber = count($args); - for ($j = $index; $j < $argNumber; $j++) { - $argTypes[] = $typeGetter($scope, $args[$j]->value); - } - $type = TypeCombinator::union(...$argTypes); - } else { - $type = $typeGetter($scope, $args[$index]->value); - } - $callableParameters[$index] = new NativeParameterReflection( - $callableParameter->getName(), - $callableParameter->isOptional(), - $type, - $callableParameter->passedByReference(), - $callableParameter->isVariadic(), - $callableParameter->getDefaultValue(), - ); - } - } - } elseif ($passedToType !== null && !$passedToType->isCallable()->no()) { - if ($passedToType instanceof UnionType) { - $passedToType = $passedToType->filterTypes(static fn (Type $innerType) => $innerType->isCallable()->yes()); - - if ($passedToType->isCallable()->no()) { - return null; - } - } - - $acceptors = $passedToType->getCallableParametersAcceptors($scope); - foreach ($acceptors as $acceptor) { - $acceptorParameters = array_map(static fn (ParameterReflection $callableParameter) => new NativeParameterReflection( - $callableParameter->getName(), - $callableParameter->isOptional(), - $callableParameter->getType(), - $callableParameter->passedByReference(), - $callableParameter->isVariadic(), - $callableParameter->getDefaultValue(), - ), $acceptor->getParameters()); - - if ($callableParameters === null) { - $callableParameters = $acceptorParameters; - continue; - } - - $newParameters = []; - $parameterCount = max(count($callableParameters), count($acceptorParameters)); - for ($i = 0; $i < $parameterCount; $i++) { - if (!array_key_exists($i, $acceptorParameters)) { - $newParameters[] = $callableParameters[$i]->toOptional(); - continue; - } - - if (!array_key_exists($i, $callableParameters)) { - $newParameters[] = $acceptorParameters[$i]->toOptional(); - continue; - } - - $newParameters[] = $callableParameters[$i]->union($acceptorParameters[$i]); - } - - $callableParameters = $newParameters; - } - } - - return $callableParameters; - } - /** * @param callable(Node $node, Scope $scope): void $nodeCallback */ From 4fa962c57f2e3c08d9bb67c9f7954a0dc9d0e71d Mon Sep 17 00:00:00 2001 From: Ondrej Mirtes Date: Sun, 13 Sep 2026 11:59:13 +0200 Subject: [PATCH 05/10] Extract parameter processing from NodeScopeResolver into ParametersProcessor processParamNode() becomes ParametersProcessor::processParams(), which takes the whole Param[] list, so the function, method, property hook, closure and arrow function callers no longer loop over their parameters. AttributesHandler, used only by parameter processing, leaves the NodeScopeResolver constructor. Co-Authored-By: Claude Opus 5 (1M context) Claude-Session: https://claude.ai/code/session_014hgcbwfKQnye2coQXtX5XU --- .../Build/NoScopeTypeReadInEngineRule.php | 1 + src/Analyser/ClosureProcessor.php | 8 +--- src/Analyser/NodeScopeResolver.php | 24 ---------- src/Analyser/ParametersProcessor.php | 45 +++++++++++++++++++ src/Analyser/PropertyHooksProcessor.php | 5 +-- .../StmtHandler/ClassMethodHandler.php | 6 +-- src/Analyser/StmtHandler/FunctionHandler.php | 6 +-- src/Testing/RuleTestCase.php | 2 - src/Testing/TypeInferenceTestCase.php | 2 - tests/PHPStan/Analyser/AnalyserTest.php | 1 - 10 files changed, 56 insertions(+), 44 deletions(-) create mode 100644 src/Analyser/ParametersProcessor.php diff --git a/build/PHPStan/Build/NoScopeTypeReadInEngineRule.php b/build/PHPStan/Build/NoScopeTypeReadInEngineRule.php index fd6a7e706ad..69238150079 100644 --- a/build/PHPStan/Build/NoScopeTypeReadInEngineRule.php +++ b/build/PHPStan/Build/NoScopeTypeReadInEngineRule.php @@ -30,6 +30,7 @@ final class NoScopeTypeReadInEngineRule implements Rule 'PHPStan\\Analyser\\ArgumentsHandler', 'PHPStan\\Analyser\\AttributesHandler', 'PHPStan\\Analyser\\ClosureProcessor', + 'PHPStan\\Analyser\\ParametersProcessor', 'PHPStan\\Analyser\\StatementsHandler', 'PHPStan\\Analyser\\StmtHandler\\', 'PHPStan\\Analyser\\ExprHandler\\', diff --git a/src/Analyser/ClosureProcessor.php b/src/Analyser/ClosureProcessor.php index f0192700c5a..d5bed0ae177 100644 --- a/src/Analyser/ClosureProcessor.php +++ b/src/Analyser/ClosureProcessor.php @@ -84,9 +84,7 @@ private function processClosureNodeInternal( ?Type $nativePassedToType = null, ): ProcessClosureResult { - foreach ($expr->params as $param) { - $nodeScopeResolver->processParamNode($stmt, $param, $scope, $storage, $nodeCallback); - } + $this->container->getByType(ParametersProcessor::class)->processParams($nodeScopeResolver, $stmt, $expr->params, $scope, $storage, $nodeCallback); $byRefUses = []; @@ -423,9 +421,7 @@ public function processArrowFunctionNode( ): ProcessArrowFunctionResult { $context ??= ExpressionContext::createTopLevel(); - foreach ($expr->params as $param) { - $nodeScopeResolver->processParamNode($stmt, $param, $scope, $storage, $nodeCallback); - } + $this->container->getByType(ParametersProcessor::class)->processParams($nodeScopeResolver, $stmt, $expr->params, $scope, $storage, $nodeCallback); if ($expr->returnType !== null) { $nodeScopeResolver->callNodeCallback($nodeCallback, $expr->returnType, $scope, $storage); } diff --git a/src/Analyser/NodeScopeResolver.php b/src/Analyser/NodeScopeResolver.php index c7e63809eea..3deac1c6194 100644 --- a/src/Analyser/NodeScopeResolver.php +++ b/src/Analyser/NodeScopeResolver.php @@ -139,7 +139,6 @@ public function __construct( private readonly bool $treatPhpDocTypesAsCertain, private readonly ExpressionResultFactory $expressionResultFactory, private readonly StatementsHandler $statementsHandler, - private readonly AttributesHandler $attributesHandler, ) { self::$guardNewWorld = getenv('PHPSTAN_GUARD_NW') === '1'; @@ -1089,29 +1088,6 @@ public function callNodeCallback( $nodeCallback($node, $scope->toNodeCallbackScope()); } - /** - * @param callable(Node $node, Scope $scope): void $nodeCallback - */ - public function processParamNode( - Node\Stmt $stmt, - Node\Param $param, - MutatingScope $scope, - ExpressionResultStorage $storage, - callable $nodeCallback, - ): void - { - $this->attributesHandler->processAttributeGroups($this, $stmt, $param->attrGroups, $scope, $storage, $nodeCallback); - $this->callNodeCallback($nodeCallback, $param, $scope, $storage); - if ($param->type !== null) { - $this->callNodeCallback($nodeCallback, $param->type, $scope, $storage); - } - if ($param->default === null) { - return; - } - - $this->processExprNode($stmt, $param->default, $scope, $storage, $nodeCallback, ExpressionContext::createDeep()); - } - /** * @param callable(Node $node, Scope $scope): void $nodeCallback */ diff --git a/src/Analyser/ParametersProcessor.php b/src/Analyser/ParametersProcessor.php new file mode 100644 index 00000000000..9d0abdd370c --- /dev/null +++ b/src/Analyser/ParametersProcessor.php @@ -0,0 +1,45 @@ +attributesHandler->processAttributeGroups($nodeScopeResolver, $stmt, $param->attrGroups, $scope, $storage, $nodeCallback); + $nodeScopeResolver->callNodeCallback($nodeCallback, $param, $scope, $storage); + if ($param->type !== null) { + $nodeScopeResolver->callNodeCallback($nodeCallback, $param->type, $scope, $storage); + } + if ($param->default === null) { + continue; + } + + $nodeScopeResolver->processExprNode($stmt, $param->default, $scope, $storage, $nodeCallback, ExpressionContext::createDeep()); + } + } + +} diff --git a/src/Analyser/PropertyHooksProcessor.php b/src/Analyser/PropertyHooksProcessor.php index 97adc969327..0bbbc636d7f 100644 --- a/src/Analyser/PropertyHooksProcessor.php +++ b/src/Analyser/PropertyHooksProcessor.php @@ -31,6 +31,7 @@ public function __construct( private DeprecatedAttributeResolver $deprecatedAttributeResolver, private PhpDocsResolver $phpDocsResolver, private AttributesHandler $attributesHandler, + private ParametersProcessor $parametersProcessor, ) { } @@ -63,9 +64,7 @@ public function processPropertyHooks( [, $phpDocParameterTypes,,,, $phpDocThrowType,,,,,,,, $phpDocComment,,,,,, $resolvedPhpDoc] = $this->phpDocsResolver->getPhpDocs($scope, $hook); - foreach ($hook->params as $param) { - $nodeScopeResolver->processParamNode($stmt, $param, $scope, $storage, $nodeCallback); - } + $this->parametersProcessor->processParams($nodeScopeResolver, $stmt, $hook->params, $scope, $storage, $nodeCallback); [$isDeprecated, $deprecatedDescription] = $this->deprecatedAttributeResolver->getDeprecatedAttribute($scope, $hook); diff --git a/src/Analyser/StmtHandler/ClassMethodHandler.php b/src/Analyser/StmtHandler/ClassMethodHandler.php index 3ea55c7bdb7..a7bb190c5e8 100644 --- a/src/Analyser/StmtHandler/ClassMethodHandler.php +++ b/src/Analyser/StmtHandler/ClassMethodHandler.php @@ -15,6 +15,7 @@ use PHPStan\Analyser\InternalStatementResult; use PHPStan\Analyser\MutatingScope; use PHPStan\Analyser\NodeScopeResolver; +use PHPStan\Analyser\ParametersProcessor; use PHPStan\Analyser\PhpDocsResolver; use PHPStan\Analyser\PropertyHooksProcessor; use PHPStan\Analyser\Scope; @@ -50,6 +51,7 @@ public function __construct( private PhpDocsResolver $phpDocsResolver, private PropertyHooksProcessor $propertyHooksProcessor, private AttributesHandler $attributesHandler, + private ParametersProcessor $parametersProcessor, ) { } @@ -71,9 +73,7 @@ public function processStmt( $this->attributesHandler->processAttributeGroups($nodeScopeResolver, $stmt, $stmt->attrGroups, $scope, $storage, $nodeCallback); [$templateTypeMap, $phpDocParameterTypes, $phpDocImmediatelyInvokedCallableParameters, $phpDocClosureThisTypeParameters, $phpDocReturnType, $phpDocThrowType, $deprecatedDescription, $isDeprecated, $isInternal, $isFinal, $isPure, $acceptsNamedArguments, $isReadOnly, $phpDocComment, $asserts, $selfOutType, $phpDocParameterOutTypes, , , , $pureUnlessCallableIsImpureParameters] = $this->phpDocsResolver->getPhpDocs($scope, $stmt); - foreach ($stmt->params as $param) { - $nodeScopeResolver->processParamNode($stmt, $param, $scope, $storage, $nodeCallback); - } + $this->parametersProcessor->processParams($nodeScopeResolver, $stmt, $stmt->params, $scope, $storage, $nodeCallback); if ($stmt->returnType !== null) { $nodeScopeResolver->callNodeCallback($nodeCallback, $stmt->returnType, $scope, $storage); diff --git a/src/Analyser/StmtHandler/FunctionHandler.php b/src/Analyser/StmtHandler/FunctionHandler.php index bee54b2b148..bb95212ae43 100644 --- a/src/Analyser/StmtHandler/FunctionHandler.php +++ b/src/Analyser/StmtHandler/FunctionHandler.php @@ -14,6 +14,7 @@ use PHPStan\Analyser\InternalStatementResult; use PHPStan\Analyser\MutatingScope; use PHPStan\Analyser\NodeScopeResolver; +use PHPStan\Analyser\ParametersProcessor; use PHPStan\Analyser\PhpDocsResolver; use PHPStan\Analyser\Scope; use PHPStan\Analyser\StatementContext; @@ -41,6 +42,7 @@ public function __construct( private DeprecatedAttributeResolver $deprecatedAttributeResolver, private PhpDocsResolver $phpDocsResolver, private AttributesHandler $attributesHandler, + private ParametersProcessor $parametersProcessor, ) { } @@ -62,9 +64,7 @@ public function processStmt( $this->attributesHandler->processAttributeGroups($nodeScopeResolver, $stmt, $stmt->attrGroups, $scope, $storage, $nodeCallback); [$templateTypeMap, $phpDocParameterTypes, $phpDocImmediatelyInvokedCallableParameters, $phpDocClosureThisTypeParameters, $phpDocReturnType, $phpDocThrowType, $deprecatedDescription, $isDeprecated, $isInternal, , $isPure, $acceptsNamedArguments, , $phpDocComment, $asserts,, $phpDocParameterOutTypes, , , , $pureUnlessCallableIsImpureParameters] = $this->phpDocsResolver->getPhpDocs($scope, $stmt); - foreach ($stmt->params as $param) { - $nodeScopeResolver->processParamNode($stmt, $param, $scope, $storage, $nodeCallback); - } + $this->parametersProcessor->processParams($nodeScopeResolver, $stmt, $stmt->params, $scope, $storage, $nodeCallback); if ($stmt->returnType !== null) { $nodeScopeResolver->callNodeCallback($nodeCallback, $stmt->returnType, $scope, $storage); diff --git a/src/Testing/RuleTestCase.php b/src/Testing/RuleTestCase.php index 2fc2b92b9f3..dbadf96c96f 100644 --- a/src/Testing/RuleTestCase.php +++ b/src/Testing/RuleTestCase.php @@ -5,7 +5,6 @@ use PhpParser\Node; use PHPStan\Analyser\Analyser; use PHPStan\Analyser\AnalyserResultFinalizer; -use PHPStan\Analyser\AttributesHandler; use PHPStan\Analyser\Error; use PHPStan\Analyser\ExpressionResultFactory; use PHPStan\Analyser\FileAnalyser; @@ -89,7 +88,6 @@ protected function createNodeScopeResolver(): NodeScopeResolver $this->shouldTreatPhpDocTypesAsCertain(), self::getContainer()->getByType(ExpressionResultFactory::class), self::getContainer()->getByType(StatementsHandler::class), - self::getContainer()->getByType(AttributesHandler::class), ); } diff --git a/src/Testing/TypeInferenceTestCase.php b/src/Testing/TypeInferenceTestCase.php index 322ee14d5ba..98a3c996c38 100644 --- a/src/Testing/TypeInferenceTestCase.php +++ b/src/Testing/TypeInferenceTestCase.php @@ -6,7 +6,6 @@ use PhpParser\Node; use PhpParser\Node\Expr\StaticCall; use PhpParser\Node\Name; -use PHPStan\Analyser\AttributesHandler; use PHPStan\Analyser\ExpressionResultFactory; use PHPStan\Analyser\Generics\TemplateArgumentObserver; use PHPStan\Analyser\MutatingScope; @@ -64,7 +63,6 @@ protected static function createNodeScopeResolver(): NodeScopeResolver $container->getParameter('treatPhpDocTypesAsCertain'), $container->getByType(ExpressionResultFactory::class), $container->getByType(StatementsHandler::class), - $container->getByType(AttributesHandler::class), ); } diff --git a/tests/PHPStan/Analyser/AnalyserTest.php b/tests/PHPStan/Analyser/AnalyserTest.php index d627464f8cd..64cdbae59c7 100644 --- a/tests/PHPStan/Analyser/AnalyserTest.php +++ b/tests/PHPStan/Analyser/AnalyserTest.php @@ -860,7 +860,6 @@ private function createAnalyser(): Analyser $this->shouldTreatPhpDocTypesAsCertain(), $container->getByType(ExpressionResultFactory::class), $container->getByType(StatementsHandler::class), - $container->getByType(AttributesHandler::class), ); $lexer = new Lexer(); $fileAnalyser = new FileAnalyser( From 5d6003a1ad343ae0c607a4c2d8f014d79dc0baf2 Mon Sep 17 00:00:00 2001 From: Ondrej Mirtes Date: Sun, 13 Sep 2026 12:01:22 +0200 Subject: [PATCH 06/10] Move processVirtualAssign() from NodeScopeResolver to AssignHandler The virtual assign is a thin wrapper over AssignHandler::prepareTarget() and applyWrite(), so it now lives next to them as a public method and uses the handler's own VirtualExprResultHelper and ExpressionResultFactory instead of container lookups. The inc/dec handlers, FuncCallHandler, FuncCallScopeEffectsHelper, ForeachHandler, UnsetHandler and ArgumentsHandler inject AssignHandler. Co-Authored-By: Claude Opus 5 (1M context) Claude-Session: https://claude.ai/code/session_014hgcbwfKQnye2coQXtX5XU --- src/Analyser/ArgumentsHandler.php | 5 +- src/Analyser/ExprHandler/AssignHandler.php | 55 ++++++++++++++++++ src/Analyser/ExprHandler/FuncCallHandler.php | 7 ++- .../Helper/FuncCallScopeEffectsHelper.php | 23 +++++--- src/Analyser/ExprHandler/PostDecHandler.php | 4 +- src/Analyser/ExprHandler/PostIncHandler.php | 4 +- src/Analyser/ExprHandler/PreDecHandler.php | 4 +- src/Analyser/ExprHandler/PreIncHandler.php | 4 +- src/Analyser/NodeScopeResolver.php | 58 ------------------- src/Analyser/StmtHandler/ForeachHandler.php | 8 ++- src/Analyser/StmtHandler/UnsetHandler.php | 9 ++- 11 files changed, 105 insertions(+), 76 deletions(-) diff --git a/src/Analyser/ArgumentsHandler.php b/src/Analyser/ArgumentsHandler.php index 8e957aba59f..bb5806eb02c 100644 --- a/src/Analyser/ArgumentsHandler.php +++ b/src/Analyser/ArgumentsHandler.php @@ -14,6 +14,7 @@ use PhpParser\Node\Identifier; use PhpParser\Node\Name; use PhpParser\NodeFinder; +use PHPStan\Analyser\ExprHandler\AssignHandler; use PHPStan\Analyser\ExprHandler\ClosureHandler; use PHPStan\Analyser\ExprHandler\Helper\ClosureParameterResolver; use PHPStan\Analyser\ExprHandler\Helper\ClosureTypeResolver; @@ -117,6 +118,7 @@ public function __construct( private ExtensionsCollection $staticMethodParameterClosureTypeExtensions, #[AutowiredParameter(ref: '%exceptions.implicitThrows%')] private bool $implicitThrows, + private AssignHandler $assignHandler, ) { } @@ -769,7 +771,8 @@ public function processArgs( $byRefType = new MixedType(); } - $scope = $nodeScopeResolver->processVirtualAssign( + $scope = $this->assignHandler->processVirtualAssign( + $nodeScopeResolver, $scope, $storage, $stmt, diff --git a/src/Analyser/ExprHandler/AssignHandler.php b/src/Analyser/ExprHandler/AssignHandler.php index b86f94d00ae..7ff29aef635 100644 --- a/src/Analyser/ExprHandler/AssignHandler.php +++ b/src/Analyser/ExprHandler/AssignHandler.php @@ -51,9 +51,11 @@ use PHPStan\Analyser\VarAnnotationProcessor; use PHPStan\Analyser\VariableFlow; use PHPStan\Analyser\VariableFlowBuilder; +use PHPStan\Analyser\VirtualAssignNodeCallback; use PHPStan\DependencyInjection\AutowiredService; use PHPStan\Node\Expr\ExistingArrayDimFetch; use PHPStan\Node\Expr\IntertwinedVariableByReferenceWithExpr; +use PHPStan\Node\Expr\NativeTypeExpr; use PHPStan\Node\Expr\SetExistingOffsetValueTypeExpr; use PHPStan\Node\Expr\SetOffsetValueTypeExpr; use PHPStan\Node\Expr\TypeExpr; @@ -547,6 +549,59 @@ private function createSpecifyTypesCallback(Assign $expr, ExpressionResult $assi }; } + /** + * @param callable(Node $node, Scope $scope): void $nodeCallback + */ + public function processVirtualAssign(NodeScopeResolver $nodeScopeResolver, MutatingScope $scope, ExpressionResultStorage $storage, Node\Stmt $stmt, Expr $var, Expr $assignedExpr, callable $nodeCallback, ?ExpressionResult $assignedExprResult = null): ExpressionResult + { + // work off an available result for the assigned expr: passed by the + // caller, or fabricated from a type-carrying virtual node - threaded + // straight into applyWrite() so its reads compose instead of falling + // back to on-demand pricing of the type, the truthy/falsey narrowing, + // and the synthetic sentinel comparisons + if ( + $assignedExprResult === null + && ($assignedExpr instanceof TypeExpr || $assignedExpr instanceof NativeTypeExpr) + && $storage->findExpressionResult($assignedExpr) === null + ) { + $assignedExprResult = $this->virtualExprResultHelper->createTypeExprResult($scope, $assignedExpr); + } + + $virtualAssignNodeCallback = VirtualAssignNodeCallback::create($nodeCallback); + $target = $this->prepareTarget( + $nodeScopeResolver, + $scope, + $storage, + $stmt, + $var, + $assignedExpr, + $virtualAssignNodeCallback, + ExpressionContext::createDeep(), + AssignTargetWalkMode::virtualAssign(), + ); + + return $this->applyWrite( + $nodeScopeResolver, + $target, + $this->expressionResultFactory->create( + $target->getScope(), + beforeScope: $target->getScope(), + expr: $assignedExpr, + hasYield: false, + isAlwaysTerminating: false, + throwPoints: [], + impurePoints: [], + typeCallback: static fn () => new MixedType(), + specifyTypesCallback: SpecifiedTypes::emptySpecifyCallback(), + ), + $assignedExprResult, + $stmt, + $storage, + $virtualAssignNodeCallback, + ExpressionContext::createDeep(), + ); + } + /** * The pre-value half of an assignment: walks the target's sub-expressions * (root, dimensions, receiver, dynamic name) in PHP's evaluation order and diff --git a/src/Analyser/ExprHandler/FuncCallHandler.php b/src/Analyser/ExprHandler/FuncCallHandler.php index 77b8fd8d34d..f5bf2db635e 100644 --- a/src/Analyser/ExprHandler/FuncCallHandler.php +++ b/src/Analyser/ExprHandler/FuncCallHandler.php @@ -107,6 +107,7 @@ public function __construct( private ClosureTypeResolver $closureTypeResolver, private ArgumentsHandler $argumentsHandler, private ClosureProcessor $closureProcessor, + private AssignHandler $assignHandler, ) { } @@ -228,7 +229,8 @@ public function processExpr(NodeScopeResolver $nodeScopeResolver, Stmt $stmt, Ex $propertyAttributes = $normalizedExpr->getAttributes(); $propertyAttributes['inCloneWith'] = true; if (count($clonePropertyKeyTypeScalars) === 1) { - $nodeScopeResolver->processVirtualAssign( + $this->assignHandler->processVirtualAssign( + $nodeScopeResolver, $scope, $storage, $stmt, @@ -239,7 +241,8 @@ public function processExpr(NodeScopeResolver $nodeScopeResolver, Stmt $stmt, Ex continue; } - $nodeScopeResolver->processVirtualAssign( + $this->assignHandler->processVirtualAssign( + $nodeScopeResolver, $scope, $storage, $stmt, diff --git a/src/Analyser/ExprHandler/Helper/FuncCallScopeEffectsHelper.php b/src/Analyser/ExprHandler/Helper/FuncCallScopeEffectsHelper.php index 379dc823178..5e4f91d4e79 100644 --- a/src/Analyser/ExprHandler/Helper/FuncCallScopeEffectsHelper.php +++ b/src/Analyser/ExprHandler/Helper/FuncCallScopeEffectsHelper.php @@ -11,6 +11,7 @@ use PhpParser\Node\Stmt; use PHPStan\Analyser\ArgsResult; use PHPStan\Analyser\ExpressionResultStorage; +use PHPStan\Analyser\ExprHandler\AssignHandler; use PHPStan\Analyser\MutatingScope; use PHPStan\Analyser\NodeScopeResolver; use PHPStan\Analyser\Scope; @@ -64,6 +65,7 @@ public function __construct( private OutputBufferHelper $outputBufferHelper, #[AutowiredParameter] private bool $rememberPossiblyImpureFunctionValues, + private AssignHandler $assignHandler, ) { } @@ -82,7 +84,8 @@ public function applyArrayWalkResult(NodeScopeResolver $nodeScopeResolver, Stmt $newArrayType = $arrayWalkOriginalArrayType->mapValueType(static fn (Type $type): Type => $arrayWalkValueType); $newArrayNativeType = $arrayWalkOriginalArrayNativeType->mapValueType(static fn (Type $type): Type => $arrayWalkValueNativeType); - $scope = $nodeScopeResolver->processVirtualAssign( + $scope = $this->assignHandler->processVirtualAssign( + $nodeScopeResolver, $scope, $storage, $stmt, @@ -169,7 +172,8 @@ public function applyCallScopeEffects(NodeScopeResolver $nodeScopeResolver, Stmt $arrayArgNativeType = $arrayArgResult->getTypeOnScope($scope, true); $isArrayPop = $functionReflection->getName() === 'array_pop'; - $scope = $nodeScopeResolver->processVirtualAssign( + $scope = $this->assignHandler->processVirtualAssign( + $nodeScopeResolver, $scope, $storage, $stmt, @@ -189,7 +193,8 @@ public function applyCallScopeEffects(NodeScopeResolver $nodeScopeResolver, Stmt ) { $arrayArg = $normalizedExpr->getArgs()[0]->value; - $scope = $nodeScopeResolver->processVirtualAssign( + $scope = $this->assignHandler->processVirtualAssign( + $nodeScopeResolver, $scope, $storage, $stmt, @@ -215,7 +220,8 @@ public function applyCallScopeEffects(NodeScopeResolver $nodeScopeResolver, Stmt ) { $arrayArg = $normalizedExpr->getArgs()[0]->value; - $scope = $nodeScopeResolver->processVirtualAssign( + $scope = $this->assignHandler->processVirtualAssign( + $nodeScopeResolver, $scope, $storage, $stmt, @@ -255,7 +261,8 @@ public function applyCallScopeEffects(NodeScopeResolver $nodeScopeResolver, Stmt $replacementNativeType = new ConstantArrayType([], []); } - $scope = $nodeScopeResolver->processVirtualAssign( + $scope = $this->assignHandler->processVirtualAssign( + $nodeScopeResolver, $scope, $storage, $stmt, @@ -275,7 +282,8 @@ public function applyCallScopeEffects(NodeScopeResolver $nodeScopeResolver, Stmt ) { $arrayArg = $normalizedExpr->getArgs()[0]->value; - $scope = $nodeScopeResolver->processVirtualAssign( + $scope = $this->assignHandler->processVirtualAssign( + $nodeScopeResolver, $scope, $storage, $stmt, @@ -292,7 +300,8 @@ public function applyCallScopeEffects(NodeScopeResolver $nodeScopeResolver, Stmt ) { $arrayArg = $normalizedExpr->getArgs()[0]->value; - $scope = $nodeScopeResolver->processVirtualAssign( + $scope = $this->assignHandler->processVirtualAssign( + $nodeScopeResolver, $scope, $storage, $stmt, diff --git a/src/Analyser/ExprHandler/PostDecHandler.php b/src/Analyser/ExprHandler/PostDecHandler.php index 6c97d45b54c..1bbf61f00bf 100644 --- a/src/Analyser/ExprHandler/PostDecHandler.php +++ b/src/Analyser/ExprHandler/PostDecHandler.php @@ -34,6 +34,7 @@ public function __construct( private ExpressionResultFactory $expressionResultFactory, private DefaultNarrowingHelper $defaultNarrowingHelper, private IncDecTypeHelper $incDecTypeHelper, + private AssignHandler $assignHandler, ) { } @@ -70,7 +71,8 @@ public function processExpr(NodeScopeResolver $nodeScopeResolver, Stmt $stmt, Ex // rule-side asks about it answer from the storage $nodeScopeResolver->storeExpressionResult($storage, $virtualExpr, $virtualExprResult); - $assignedScope = $nodeScopeResolver->processVirtualAssign( + $assignedScope = $this->assignHandler->processVirtualAssign( + $nodeScopeResolver, $varResult->getScope(), $storage, $stmt, diff --git a/src/Analyser/ExprHandler/PostIncHandler.php b/src/Analyser/ExprHandler/PostIncHandler.php index 83368839776..17a385712d2 100644 --- a/src/Analyser/ExprHandler/PostIncHandler.php +++ b/src/Analyser/ExprHandler/PostIncHandler.php @@ -34,6 +34,7 @@ public function __construct( private ExpressionResultFactory $expressionResultFactory, private DefaultNarrowingHelper $defaultNarrowingHelper, private IncDecTypeHelper $incDecTypeHelper, + private AssignHandler $assignHandler, ) { } @@ -70,7 +71,8 @@ public function processExpr(NodeScopeResolver $nodeScopeResolver, Stmt $stmt, Ex // rule-side asks about it answer from the storage $nodeScopeResolver->storeExpressionResult($storage, $virtualExpr, $virtualExprResult); - $assignedScope = $nodeScopeResolver->processVirtualAssign( + $assignedScope = $this->assignHandler->processVirtualAssign( + $nodeScopeResolver, $varResult->getScope(), $storage, $stmt, diff --git a/src/Analyser/ExprHandler/PreDecHandler.php b/src/Analyser/ExprHandler/PreDecHandler.php index c646ea565cc..2346942353e 100644 --- a/src/Analyser/ExprHandler/PreDecHandler.php +++ b/src/Analyser/ExprHandler/PreDecHandler.php @@ -32,6 +32,7 @@ public function __construct( private ExpressionResultFactory $expressionResultFactory, private IncDecTypeHelper $incDecTypeHelper, private DefaultNarrowingHelper $defaultNarrowingHelper, + private AssignHandler $assignHandler, ) { } @@ -72,7 +73,8 @@ public function processExpr(NodeScopeResolver $nodeScopeResolver, Stmt $stmt, Ex // final result after this handler returns $nodeScopeResolver->storeExpressionResult($storage, $expr, $incDecValueResult); - $assignedScope = $nodeScopeResolver->processVirtualAssign( + $assignedScope = $this->assignHandler->processVirtualAssign( + $nodeScopeResolver, $varResult->getScope(), $storage, $stmt, diff --git a/src/Analyser/ExprHandler/PreIncHandler.php b/src/Analyser/ExprHandler/PreIncHandler.php index ba7b208166d..ec72ef2f4cc 100644 --- a/src/Analyser/ExprHandler/PreIncHandler.php +++ b/src/Analyser/ExprHandler/PreIncHandler.php @@ -32,6 +32,7 @@ public function __construct( private ExpressionResultFactory $expressionResultFactory, private IncDecTypeHelper $incDecTypeHelper, private DefaultNarrowingHelper $defaultNarrowingHelper, + private AssignHandler $assignHandler, ) { } @@ -72,7 +73,8 @@ public function processExpr(NodeScopeResolver $nodeScopeResolver, Stmt $stmt, Ex // final result after this handler returns $nodeScopeResolver->storeExpressionResult($storage, $expr, $incDecValueResult); - $assignedScope = $nodeScopeResolver->processVirtualAssign( + $assignedScope = $this->assignHandler->processVirtualAssign( + $nodeScopeResolver, $varResult->getScope(), $storage, $stmt, diff --git a/src/Analyser/NodeScopeResolver.php b/src/Analyser/NodeScopeResolver.php index 3deac1c6194..cf3ec68be7f 100644 --- a/src/Analyser/NodeScopeResolver.php +++ b/src/Analyser/NodeScopeResolver.php @@ -21,9 +21,7 @@ use PhpParser\Node\Stmt\Static_; use PhpParser\Node\Stmt\Switch_; use PhpParser\NodeFinder; -use PHPStan\Analyser\ExprHandler\AssignHandler; use PHPStan\Analyser\ExprHandler\Helper\NonNullabilityHelper; -use PHPStan\Analyser\ExprHandler\Helper\VirtualExprResultHelper; use PHPStan\Analyser\Generics\TemplateArgumentConstraints; use PHPStan\Analyser\Generics\TemplateArgumentFrame; use PHPStan\Analyser\Generics\TemplateArgumentObserver; @@ -34,8 +32,6 @@ use PHPStan\DependencyInjection\Container; use PHPStan\DependencyInjection\ExtensionsCollection; use PHPStan\File\FileHelper; -use PHPStan\Node\Expr\NativeTypeExpr; -use PHPStan\Node\Expr\TypeExpr; use PHPStan\Node\FunctionCallableNode; use PHPStan\Node\FunctionCallExpressionNode; use PHPStan\Node\InstantiationCallableNode; @@ -1088,60 +1084,6 @@ public function callNodeCallback( $nodeCallback($node, $scope->toNodeCallbackScope()); } - /** - * @param callable(Node $node, Scope $scope): void $nodeCallback - */ - public function processVirtualAssign(MutatingScope $scope, ExpressionResultStorage $storage, Node\Stmt $stmt, Expr $var, Expr $assignedExpr, callable $nodeCallback, ?ExpressionResult $assignedExprResult = null): ExpressionResult - { - // work off an available result for the assigned expr: passed by the - // caller, or fabricated from a type-carrying virtual node - threaded - // straight into applyWrite() so its reads compose instead of falling - // back to on-demand pricing of the type, the truthy/falsey narrowing, - // and the synthetic sentinel comparisons - if ( - $assignedExprResult === null - && ($assignedExpr instanceof TypeExpr || $assignedExpr instanceof NativeTypeExpr) - && $storage->findExpressionResult($assignedExpr) === null - ) { - $assignedExprResult = $this->container->getByType(VirtualExprResultHelper::class)->createTypeExprResult($scope, $assignedExpr); - } - - $assignHandler = $this->container->getByType(AssignHandler::class); - $virtualAssignNodeCallback = VirtualAssignNodeCallback::create($nodeCallback); - $target = $assignHandler->prepareTarget( - $this, - $scope, - $storage, - $stmt, - $var, - $assignedExpr, - $virtualAssignNodeCallback, - ExpressionContext::createDeep(), - AssignTargetWalkMode::virtualAssign(), - ); - - return $assignHandler->applyWrite( - $this, - $target, - $this->expressionResultFactory->create( - $target->getScope(), - beforeScope: $target->getScope(), - expr: $assignedExpr, - hasYield: false, - isAlwaysTerminating: false, - throwPoints: [], - impurePoints: [], - typeCallback: static fn () => new MixedType(), - specifyTypesCallback: SpecifiedTypes::emptySpecifyCallback(), - ), - $assignedExprResult, - $stmt, - $storage, - $virtualAssignNodeCallback, - ExpressionContext::createDeep(), - ); - } - /** * The template argument frame of the body being walked while it observes * a body that created unresolved template arguments - null otherwise, so diff --git a/src/Analyser/StmtHandler/ForeachHandler.php b/src/Analyser/StmtHandler/ForeachHandler.php index 9d8921de38d..cce0ac1a3c3 100644 --- a/src/Analyser/StmtHandler/ForeachHandler.php +++ b/src/Analyser/StmtHandler/ForeachHandler.php @@ -23,6 +23,7 @@ use PHPStan\Analyser\ExpressionContext; use PHPStan\Analyser\ExpressionResultStorage; use PHPStan\Analyser\ExpressionTypeHolder; +use PHPStan\Analyser\ExprHandler\AssignHandler; use PHPStan\Analyser\ExprHandler\Helper\IdenticalNarrowingHelper; use PHPStan\Analyser\InternalStatementResult; use PHPStan\Analyser\InternalThrowPoint; @@ -83,6 +84,7 @@ public function __construct( #[AutowiredParameter(ref: '%exceptions.implicitThrows%')] private bool $implicitThrows, private VarAnnotationProcessor $varAnnotationProcessor, + private AssignHandler $assignHandler, ) { } @@ -531,7 +533,8 @@ private function enterForeach(NodeScopeResolver $nodeScopeResolver, MutatingScop $vars[] = $keyVarName; } } else { - $scope = $nodeScopeResolver->processVirtualAssign( + $scope = $this->assignHandler->processVirtualAssign( + $nodeScopeResolver, $scope, $storage, $stmt, @@ -550,7 +553,8 @@ private function enterForeach(NodeScopeResolver $nodeScopeResolver, MutatingScop $scope = $scope->enterForeachKey($originalScope, $stmt->expr, $iterateeType, $nativeIterateeType, $stmt->keyVar->name); $vars[] = $stmt->keyVar->name; } elseif ($stmt->keyVar !== null) { - $scope = $nodeScopeResolver->processVirtualAssign( + $scope = $this->assignHandler->processVirtualAssign( + $nodeScopeResolver, $scope, $storage, $stmt, diff --git a/src/Analyser/StmtHandler/UnsetHandler.php b/src/Analyser/StmtHandler/UnsetHandler.php index 2c798f90687..7874a82009b 100644 --- a/src/Analyser/StmtHandler/UnsetHandler.php +++ b/src/Analyser/StmtHandler/UnsetHandler.php @@ -11,6 +11,7 @@ use PhpParser\Node\Stmt\Unset_; use PHPStan\Analyser\ExpressionContext; use PHPStan\Analyser\ExpressionResultStorage; +use PHPStan\Analyser\ExprHandler\AssignHandler; use PHPStan\Analyser\ExprHandler\Helper\MethodThrowPointHelper; use PHPStan\Analyser\ExprHandler\Helper\VirtualExprResultHelper; use PHPStan\Analyser\ImpurePoint; @@ -48,7 +49,10 @@ public function supports(Stmt $stmt): bool return $stmt instanceof Unset_; } - public function __construct(private Container $container) + public function __construct( + private Container $container, + private AssignHandler $assignHandler, + ) { } @@ -109,7 +113,8 @@ public function processStmt( }; $clonedVar = $buildExistingChain($var->var); $unsetOffsetExpr = new UnsetOffsetExpr($var->var, $var->dim); - $scope = $nodeScopeResolver->processVirtualAssign( + $scope = $this->assignHandler->processVirtualAssign( + $nodeScopeResolver, $scope, $storage, $stmt, From 4bf4bcf8d6b8717a8bb4b4a7cd5ed58a0256bb34 Mon Sep 17 00:00:00 2001 From: Ondrej Mirtes Date: Sun, 13 Sep 2026 12:20:32 +0200 Subject: [PATCH 07/10] Inject analysis settings into statement handlers instead of reading them from NodeScopeResolver IfHandler, ForHandler, WhileHandler, DoWhileHandler and ForeachHandler receive treatPhpDocTypesAsCertain, polluteScopeWithLoopInitialAssignments and polluteScopeWithAlwaysIterableForeach as #[AutowiredParameter]s, and ClassLikeHandler receives the ReadWritePropertiesExtension collection. NodeScopeResolver drops the accessors and the constructor parameters. Tests configure these through getAdditionalConfigFiles() now: cases that ran with non-default values move into sibling test classes backed by shared neon files, and the ReadWritePropertiesExtension doubles become services. RuleTestCase::getReadWritePropertiesExtensions(), shouldPolluteScopeWithLoopInitialAssignments() and shouldPolluteScopeWithAlwaysIterableForeach() no longer configure the analysis - overriding them with a value that differs from the configuration fails the test instead of silently changing its meaning. Co-Authored-By: Claude Opus 5 (1M context) Claude-Session: https://claude.ai/code/session_014hgcbwfKQnye2coQXtX5XU --- src/Analyser/NodeScopeResolver.php | 32 - src/Analyser/StmtHandler/ClassLikeHandler.php | 10 +- src/Analyser/StmtHandler/DoWhileHandler.php | 10 +- src/Analyser/StmtHandler/ForHandler.php | 18 +- src/Analyser/StmtHandler/ForeachHandler.php | 8 +- src/Analyser/StmtHandler/IfHandler.php | 12 +- src/Analyser/StmtHandler/WhileHandler.php | 18 +- src/Testing/RuleTestCase.php | 32 +- src/Testing/TypeInferenceTestCase.php | 5 - tests/PHPStan/Analyser/AnalyserTest.php | 5 - .../Classes/ImpossibleInstanceOfRuleTest.php | 125 +- ...anceOfRuleWithUncertainPhpDocTypesTest.php | 149 ++ .../BooleanOrConstantConditionRuleTest.php | 25 - ...ditionRuleWithUncertainPhpDocTypesTest.php | 128 ++ ...mpossibleCheckTypeFunctionCallRuleTest.php | 163 +- ...onCallRuleWithUncertainPhpDocTypesTest.php | 264 ++++ ...isonOperatorsConstantConditionRuleTest.php | 13 - ...nditionRuleWithoutForeachPollutionTest.php | 45 + ...rictComparisonOfDifferentTypesRuleTest.php | 13 - ...ntTypesRuleWithoutForeachPollutionTest.php | 48 + .../DeadCode/UnreachableStatementRuleTest.php | 160 -- ...tementRuleWithUncertainPhpDocTypesTest.php | 212 +++ .../Rules/DeadCode/UnusedVariableRuleTest.php | 13 - ...ariableRuleWithoutForeachPollutionTest.php | 36 + ...ReadOnlyByPhpDocPropertyAssignRuleTest.php | 37 +- .../MissingReadOnlyPropertyAssignRuleTest.php | 57 +- .../UninitializedPropertyRuleTest.php | 47 - .../EntityIdReadWritePropertiesExtension.php | 32 + ...AnnotationReadWritePropertiesExtension.php | 27 + ...edPropertyReadWritePropertiesExtension.php | 25 + ...AnnotationReadWritePropertiesExtension.php | 27 + ...ocEntityIdReadWritePropertiesExtension.php | 32 + ...adonly-by-phpdoc-property-assign-rule.neon | 5 + ...missing-readonly-property-assign-rule.neon | 9 + .../uninitialized-property-rule.neon | 8 + .../Variables/DefinedVariableRuleTest.php | 1351 +---------------- ...RuleWithoutLoopAndForeachPollutionTest.php | 129 ++ ...edVariableRuleWithoutLoopPollutionTest.php | 1124 ++++++++++++++ .../PHPStan/Rules/uncertain-phpdoc-types.neon | 2 + .../Rules/without-foreach-pollution.neon | 2 + .../PHPStan/Rules/without-loop-pollution.neon | 2 + 41 files changed, 2425 insertions(+), 2035 deletions(-) create mode 100644 tests/PHPStan/Rules/Classes/ImpossibleInstanceOfRuleWithUncertainPhpDocTypesTest.php create mode 100644 tests/PHPStan/Rules/Comparison/BooleanOrConstantConditionRuleWithUncertainPhpDocTypesTest.php create mode 100644 tests/PHPStan/Rules/Comparison/ImpossibleCheckTypeFunctionCallRuleWithUncertainPhpDocTypesTest.php create mode 100644 tests/PHPStan/Rules/Comparison/NumberComparisonOperatorsConstantConditionRuleWithoutForeachPollutionTest.php create mode 100644 tests/PHPStan/Rules/Comparison/StrictComparisonOfDifferentTypesRuleWithoutForeachPollutionTest.php create mode 100644 tests/PHPStan/Rules/DeadCode/UnreachableStatementRuleWithUncertainPhpDocTypesTest.php create mode 100644 tests/PHPStan/Rules/DeadCode/UnusedVariableRuleWithoutForeachPollutionTest.php create mode 100644 tests/PHPStan/Rules/Properties/data/EntityIdReadWritePropertiesExtension.php create mode 100644 tests/PHPStan/Rules/Properties/data/InitAnnotationReadWritePropertiesExtension.php create mode 100644 tests/PHPStan/Rules/Properties/data/InitedPropertyReadWritePropertiesExtension.php create mode 100644 tests/PHPStan/Rules/Properties/data/InjectAnnotationReadWritePropertiesExtension.php create mode 100644 tests/PHPStan/Rules/Properties/data/PhpDocEntityIdReadWritePropertiesExtension.php create mode 100644 tests/PHPStan/Rules/Properties/missing-readonly-by-phpdoc-property-assign-rule.neon create mode 100644 tests/PHPStan/Rules/Properties/missing-readonly-property-assign-rule.neon create mode 100644 tests/PHPStan/Rules/Variables/DefinedVariableRuleWithoutLoopAndForeachPollutionTest.php create mode 100644 tests/PHPStan/Rules/Variables/DefinedVariableRuleWithoutLoopPollutionTest.php create mode 100644 tests/PHPStan/Rules/uncertain-phpdoc-types.neon create mode 100644 tests/PHPStan/Rules/without-foreach-pollution.neon create mode 100644 tests/PHPStan/Rules/without-loop-pollution.neon diff --git a/src/Analyser/NodeScopeResolver.php b/src/Analyser/NodeScopeResolver.php index cf3ec68be7f..1ef087a82b7 100644 --- a/src/Analyser/NodeScopeResolver.php +++ b/src/Analyser/NodeScopeResolver.php @@ -27,7 +27,6 @@ use PHPStan\Analyser\Generics\TemplateArgumentObserver; use PHPStan\Analyser\Generics\TemplateArgumentStats; use PHPStan\DependencyInjection\AutowiredExtensions; -use PHPStan\DependencyInjection\AutowiredParameter; use PHPStan\DependencyInjection\AutowiredService; use PHPStan\DependencyInjection\Container; use PHPStan\DependencyInjection\ExtensionsCollection; @@ -41,7 +40,6 @@ use PHPStan\Node\StaticMethodCallExpressionNode; use PHPStan\Reflection\Native\NativeMethodReflection; use PHPStan\Reflection\Php\PhpMethodReflection; -use PHPStan\Rules\Properties\ReadWritePropertiesExtension; use PHPStan\ShouldNotHappenException; use PHPStan\Type\ErrorType; use PHPStan\Type\MixedType; @@ -116,23 +114,14 @@ class NodeScopeResolver public static array $guardProcessedExprIds = []; /** - * @param ExtensionsCollection $readWritePropertiesExtensions * @param ExtensionsCollection $perFileAnalysisResettables */ public function __construct( private readonly Container $container, private readonly TemplateArgumentObserver $templateArgumentObserver, private readonly FileHelper $fileHelper, - #[AutowiredExtensions(of: ReadWritePropertiesExtension::class)] - private readonly ExtensionsCollection $readWritePropertiesExtensions, #[AutowiredExtensions(of: PerFileAnalysisResettable::class)] private readonly ExtensionsCollection $perFileAnalysisResettables, - #[AutowiredParameter] - private readonly bool $polluteScopeWithLoopInitialAssignments, - #[AutowiredParameter] - private readonly bool $polluteScopeWithAlwaysIterableForeach, - #[AutowiredParameter] - private readonly bool $treatPhpDocTypesAsCertain, private readonly ExpressionResultFactory $expressionResultFactory, private readonly StatementsHandler $statementsHandler, ) @@ -431,27 +420,6 @@ public function isAnalysedFile(string $fileName): bool return isset($this->analysedFiles[$fileName]); } - public function shouldPolluteScopeWithLoopInitialAssignments(): bool - { - return $this->polluteScopeWithLoopInitialAssignments; - } - - public function shouldPolluteScopeWithAlwaysIterableForeach(): bool - { - return $this->polluteScopeWithAlwaysIterableForeach; - } - - public function shouldTreatPhpDocTypesAsCertain(): bool - { - return $this->treatPhpDocTypesAsCertain; - } - - /** @return ExtensionsCollection */ - public function getReadWritePropertiesExtensions(): ExtensionsCollection - { - return $this->readWritePropertiesExtensions; - } - /** Whether an on-demand walk answers already processed real nodes from their stored results (see processExprOnDemand()). */ public function isReturningStoredExpressionResults(): bool { diff --git a/src/Analyser/StmtHandler/ClassLikeHandler.php b/src/Analyser/StmtHandler/ClassLikeHandler.php index 4f205b94613..8b7f9dc0d6c 100644 --- a/src/Analyser/StmtHandler/ClassLikeHandler.php +++ b/src/Analyser/StmtHandler/ClassLikeHandler.php @@ -22,7 +22,9 @@ use PHPStan\BetterReflection\Reflector\Reflector; use PHPStan\BetterReflection\SourceLocator\Ast\Strategy\NodeToReflection; use PHPStan\BetterReflection\SourceLocator\Located\LocatedSource; +use PHPStan\DependencyInjection\AutowiredExtensions; use PHPStan\DependencyInjection\AutowiredService; +use PHPStan\DependencyInjection\ExtensionsCollection; use PHPStan\File\FileReader; use PHPStan\Node\ClassConstantsNode; use PHPStan\Node\ClassMethodsNode; @@ -32,6 +34,7 @@ use PHPStan\Reflection\ClassReflection; use PHPStan\Reflection\ClassReflectionFactory; use PHPStan\Reflection\ReflectionProvider; +use PHPStan\Rules\Properties\ReadWritePropertiesExtension; use PHPStan\ShouldNotHappenException; use function sprintf; use function usort; @@ -46,12 +49,17 @@ final class ClassLikeHandler implements StmtHandler { + /** + * @param ExtensionsCollection $readWritePropertiesExtensions + */ public function __construct( private Reflector $reflector, private ClassReflectionFactory $classReflectionFactory, private CalledMethodProcessor $calledMethodProcessor, private ReflectionProvider $reflectionProvider, private AttributesHandler $attributesHandler, + #[AutowiredExtensions(of: ReadWritePropertiesExtension::class)] + private ExtensionsCollection $readWritePropertiesExtensions, ) { } @@ -129,7 +137,7 @@ public function processStmt( // Class members have their own inference context, including when the class // declaration is visited during an enclosing body's observation pass. $nodeScopeResolver->processStmtNodesInternal($stmt, $classLikeStatements, $classScope, $storage, $classStatementsGatherer, StatementContext::createTopLevel()); - $nodeScopeResolver->callNodeCallback($nodeCallback, new ClassPropertiesNode($stmt, $nodeScopeResolver->getReadWritePropertiesExtensions(), $classStatementsGatherer->getProperties(), $classStatementsGatherer->getPropertyUsages(), $classStatementsGatherer->getMethodCalls(), $classStatementsGatherer->getReturnStatementsNodes(), $classStatementsGatherer->getPropertyAssigns(), $classReflection), $classScope, $storage); + $nodeScopeResolver->callNodeCallback($nodeCallback, new ClassPropertiesNode($stmt, $this->readWritePropertiesExtensions, $classStatementsGatherer->getProperties(), $classStatementsGatherer->getPropertyUsages(), $classStatementsGatherer->getMethodCalls(), $classStatementsGatherer->getReturnStatementsNodes(), $classStatementsGatherer->getPropertyAssigns(), $classReflection), $classScope, $storage); $nodeScopeResolver->callNodeCallback($nodeCallback, new ClassMethodsNode($stmt, $classStatementsGatherer->getMethods(), $classStatementsGatherer->getMethodCalls(), $classReflection), $classScope, $storage); $nodeScopeResolver->callNodeCallback($nodeCallback, new ClassConstantsNode($stmt, $classStatementsGatherer->getConstants(), $classStatementsGatherer->getConstantFetches(), $classReflection), $classScope, $storage); $classReflection->evictPrivateSymbols(); diff --git a/src/Analyser/StmtHandler/DoWhileHandler.php b/src/Analyser/StmtHandler/DoWhileHandler.php index ae63fdc64e1..b273f7153b1 100644 --- a/src/Analyser/StmtHandler/DoWhileHandler.php +++ b/src/Analyser/StmtHandler/DoWhileHandler.php @@ -16,6 +16,7 @@ use PHPStan\Analyser\StatementContext; use PHPStan\Analyser\StmtHandler; use PHPStan\Analyser\VariableFlow; +use PHPStan\DependencyInjection\AutowiredParameter; use PHPStan\DependencyInjection\AutowiredService; use PHPStan\Node\DoWhileLoopConditionNode; use function array_merge; @@ -28,6 +29,13 @@ final class DoWhileHandler implements StmtHandler { + public function __construct( + #[AutowiredParameter] + private bool $treatPhpDocTypesAsCertain, + ) + { + } + public function supports(Stmt $stmt): bool { return $stmt instanceof Do_; @@ -137,7 +145,7 @@ public function processStmt( $alwaysIterates = false; if ($context->isTopLevel()) { - $condBooleanType = ($nodeScopeResolver->shouldTreatPhpDocTypesAsCertain() ? $condResult->getType() : $condResult->getNativeType())->toBoolean(); + $condBooleanType = ($this->treatPhpDocTypesAsCertain ? $condResult->getType() : $condResult->getNativeType())->toBoolean(); $alwaysIterates = $condBooleanType->isTrue()->yes(); } diff --git a/src/Analyser/StmtHandler/ForHandler.php b/src/Analyser/StmtHandler/ForHandler.php index 445afa016b1..3191663e97f 100644 --- a/src/Analyser/StmtHandler/ForHandler.php +++ b/src/Analyser/StmtHandler/ForHandler.php @@ -25,6 +25,7 @@ use PHPStan\Analyser\TypeSpecifierContext; use PHPStan\Analyser\VariableFlow; use PHPStan\Analyser\VariableFlowBuilder; +use PHPStan\DependencyInjection\AutowiredParameter; use PHPStan\DependencyInjection\AutowiredService; use PHPStan\TrinaryLogic; use function array_last; @@ -41,6 +42,15 @@ final class ForHandler implements StmtHandler { + public function __construct( + #[AutowiredParameter] + private bool $polluteScopeWithLoopInitialAssignments, + #[AutowiredParameter] + private bool $treatPhpDocTypesAsCertain, + ) + { + } + public function supports(Stmt $stmt): bool { return $stmt instanceof For_; @@ -166,7 +176,7 @@ public function processStmt( // only the last condition expression is relevant whether the loop continues // see https://www.php.net/manual/en/control-structures.for.php if ($condExpr === $lastCondExpr) { - $condTruthiness = ($nodeScopeResolver->shouldTreatPhpDocTypesAsCertain() ? $condResult->getType() : $condResult->getNativeType())->toBoolean(); + $condTruthiness = ($this->treatPhpDocTypesAsCertain ? $condResult->getType() : $condResult->getNativeType())->toBoolean(); $isIterableAtLeastOnce = $isIterableAtLeastOnce->and($condTruthiness->isTrue()); } @@ -285,20 +295,20 @@ public function processStmt( } if ($isIterableAtLeastOnce->no() || $finalScopeResult->isAlwaysTerminating() || ($backEdgeDead && count($breakExitPoints) === 0)) { - if ($nodeScopeResolver->shouldPolluteScopeWithLoopInitialAssignments()) { + if ($this->polluteScopeWithLoopInitialAssignments) { $finalScope = $initScope; } else { $finalScope = $scope; } } elseif ($isIterableAtLeastOnce->maybe()) { - if ($nodeScopeResolver->shouldPolluteScopeWithLoopInitialAssignments()) { + if ($this->polluteScopeWithLoopInitialAssignments) { $finalScope = $finalScope->mergeWith($initScope); } else { $finalScope = $finalScope->mergeWith($scope); } } else { - if (!$nodeScopeResolver->shouldPolluteScopeWithLoopInitialAssignments()) { + if (!$this->polluteScopeWithLoopInitialAssignments) { $finalScope = $finalScope->mergeWith($scope); } } diff --git a/src/Analyser/StmtHandler/ForeachHandler.php b/src/Analyser/StmtHandler/ForeachHandler.php index cce0ac1a3c3..057ad69e12f 100644 --- a/src/Analyser/StmtHandler/ForeachHandler.php +++ b/src/Analyser/StmtHandler/ForeachHandler.php @@ -85,6 +85,8 @@ public function __construct( private bool $implicitThrows, private VarAnnotationProcessor $varAnnotationProcessor, private AssignHandler $assignHandler, + #[AutowiredParameter] + private bool $polluteScopeWithAlwaysIterableForeach, ) { } @@ -151,7 +153,7 @@ public function processStmt( // walked synthetic would delegate to); the walk is the composition's // miss seam $nonEmptyIterateeScope = $scope; - if ($nodeScopeResolver->shouldPolluteScopeWithAlwaysIterableForeach()) { + if ($this->polluteScopeWithAlwaysIterableForeach) { $identicalNarrowingHelper = $this->container->getByType(IdenticalNarrowingHelper::class); $emptyArrayType = new ConstantArrayType([], []); $nonEmptyTypes = $identicalNarrowingHelper->specifyIdenticalAgainstType( @@ -460,7 +462,7 @@ static function () use ($condResult, $emptyArrayType): Type { ), TypeSpecifierContext::createTruthy())); } elseif ($isIterableAtLeastOnce->no() || $finalScopeResult->isAlwaysTerminating()) { $finalScope = $scope; - } elseif (!$nodeScopeResolver->shouldPolluteScopeWithAlwaysIterableForeach()) { + } elseif (!$this->polluteScopeWithAlwaysIterableForeach) { $finalScope = $scope->processAlwaysIterableForeachScopeWithoutPollute($finalScope); // get types from finalScope, but don't create new variables } @@ -484,7 +486,7 @@ static function () use ($condResult, $emptyArrayType): Type { VariableFlowBuilder::targetWrite($stmt->valueVar, VariableWrite::KIND_FOREACH_VALUE, $finalScope, $storage), $stmt->byRef && $stmt->valueVar instanceof Variable && is_string($stmt->valueVar->name) ? VariableFlow::escape($stmt->valueVar->name) : null, ); - $loopFlow = VariableFlow::loop($traversableThrowPoint !== null ? VariableFlow::throwing($traversableThrowPoint->getType(), true) : null, VariableFlow::sequence($bindingFlow, $finalScopeResult->getVariableFlow()), null, $isIterableAtLeastOnce->yes() && $nodeScopeResolver->shouldPolluteScopeWithAlwaysIterableForeach(), true); + $loopFlow = VariableFlow::loop($traversableThrowPoint !== null ? VariableFlow::throwing($traversableThrowPoint->getType(), true) : null, VariableFlow::sequence($bindingFlow, $finalScopeResult->getVariableFlow()), null, $isIterableAtLeastOnce->yes() && $this->polluteScopeWithAlwaysIterableForeach, true); $bindingWrites = VariableFlowBuilder::writes($bindingFlow); $bindings = []; foreach ($bindingWrites as $write) { diff --git a/src/Analyser/StmtHandler/IfHandler.php b/src/Analyser/StmtHandler/IfHandler.php index 59526da6eac..6595d804528 100644 --- a/src/Analyser/StmtHandler/IfHandler.php +++ b/src/Analyser/StmtHandler/IfHandler.php @@ -13,6 +13,7 @@ use PHPStan\Analyser\StatementContext; use PHPStan\Analyser\StmtHandler; use PHPStan\Analyser\VariableFlow; +use PHPStan\DependencyInjection\AutowiredParameter; use PHPStan\DependencyInjection\AutowiredService; use function array_merge; use function array_reverse; @@ -25,6 +26,13 @@ final class IfHandler implements StmtHandler { + public function __construct( + #[AutowiredParameter] + private bool $treatPhpDocTypesAsCertain, + ) + { + } + public function supports(Stmt $stmt): bool { return $stmt instanceof If_; @@ -44,7 +52,7 @@ public function processStmt( $elseFlow = null; $condResult = $nodeScopeResolver->processExprNode($stmt, $stmt->cond, $scope, $storage, $nodeCallback, ExpressionContext::createDeep($context->shouldResolveTemplateArguments())); $nodeScopeResolver->callNodeCallback($nodeCallback, $stmt, $entryScope, $storage); - $conditionType = ($nodeScopeResolver->shouldTreatPhpDocTypesAsCertain() ? $condResult->getType() : $condResult->getNativeType())->toBoolean(); + $conditionType = ($this->treatPhpDocTypesAsCertain ? $condResult->getType() : $condResult->getNativeType())->toBoolean(); $ifAlwaysTrue = $conditionType->isTrue()->yes(); $exitPoints = []; $throwPoints = $condResult->getThrowPoints(); @@ -79,7 +87,7 @@ public function processStmt( foreach ($stmt->elseifs as $elseif) { $condResult = $nodeScopeResolver->processExprNode($stmt, $elseif->cond, $condScope, $storage, $nodeCallback, ExpressionContext::createDeep($context->shouldResolveTemplateArguments())); $nodeScopeResolver->callNodeCallback($nodeCallback, $elseif, $scope, $storage); - $elseIfConditionType = ($nodeScopeResolver->shouldTreatPhpDocTypesAsCertain() ? $condResult->getType() : $condResult->getNativeType())->toBoolean(); + $elseIfConditionType = ($this->treatPhpDocTypesAsCertain ? $condResult->getType() : $condResult->getNativeType())->toBoolean(); $throwPoints = array_merge($throwPoints, $condResult->getThrowPoints()); $impurePoints = array_merge($impurePoints, $condResult->getImpurePoints()); $branchScopeStatementResult = $nodeScopeResolver->processStmtNodesInternal($elseif, $elseif->stmts, $condResult->getTruthyScope(), $storage, $nodeCallback, $context); diff --git a/src/Analyser/StmtHandler/WhileHandler.php b/src/Analyser/StmtHandler/WhileHandler.php index ba044123c0d..44bc28b83b1 100644 --- a/src/Analyser/StmtHandler/WhileHandler.php +++ b/src/Analyser/StmtHandler/WhileHandler.php @@ -17,6 +17,7 @@ use PHPStan\Analyser\StmtHandler; use PHPStan\Analyser\TypeSpecifierContext; use PHPStan\Analyser\VariableFlow; +use PHPStan\DependencyInjection\AutowiredParameter; use PHPStan\DependencyInjection\AutowiredService; use PHPStan\Node\BreaklessWhileLoopNode; use function array_merge; @@ -29,6 +30,15 @@ final class WhileHandler implements StmtHandler { + public function __construct( + #[AutowiredParameter] + private bool $polluteScopeWithLoopInitialAssignments, + #[AutowiredParameter] + private bool $treatPhpDocTypesAsCertain, + ) + { + } + public function supports(Stmt $stmt): bool { return $stmt instanceof While_; @@ -51,10 +61,10 @@ public function processStmt( $scope->pushExpressionResultStorage($storage); try { $condResult = $nodeScopeResolver->processExprNode($stmt, $stmt->cond, $scope, $storage, new NoopNodeCallback(), ExpressionContext::createDeep(resolveTemplateArguments: false)); - $beforeCondBooleanType = ($nodeScopeResolver->shouldTreatPhpDocTypesAsCertain() ? $condResult->getType() : $condResult->getNativeType())->toBoolean(); + $beforeCondBooleanType = ($this->treatPhpDocTypesAsCertain ? $condResult->getType() : $condResult->getNativeType())->toBoolean(); $condScope = $condResult->getFalseyScope(); if (!$context->isTopLevel() && $beforeCondBooleanType->isFalse()->yes()) { - if (!$nodeScopeResolver->shouldPolluteScopeWithLoopInitialAssignments()) { + if (!$this->polluteScopeWithLoopInitialAssignments) { $scope = $condScope->mergeWith($scope); } @@ -173,7 +183,7 @@ public function processStmt( $alwaysIterates = false; $neverIterates = false; if ($context->isTopLevel()) { - $condBooleanType = ($nodeScopeResolver->shouldTreatPhpDocTypesAsCertain() ? $bodyCondResult->getType() : $bodyCondResult->getNativeType())->toBoolean(); + $condBooleanType = ($this->treatPhpDocTypesAsCertain ? $bodyCondResult->getType() : $bodyCondResult->getNativeType())->toBoolean(); $alwaysIterates = $condBooleanType->isTrue()->yes(); $neverIterates = $condBooleanType->isFalse()->yes(); } @@ -200,7 +210,7 @@ public function processStmt( $finalScope = null; } if (!$isIterableAtLeastOnce) { - if (!$nodeScopeResolver->shouldPolluteScopeWithLoopInitialAssignments()) { + if (!$this->polluteScopeWithLoopInitialAssignments) { $condScope = $condScope->mergeWith($scope); } $finalScope = $finalScope === null ? $condScope : $finalScope->mergeWith($condScope); diff --git a/src/Testing/RuleTestCase.php b/src/Testing/RuleTestCase.php index dbadf96c96f..82f16583c30 100644 --- a/src/Testing/RuleTestCase.php +++ b/src/Testing/RuleTestCase.php @@ -21,7 +21,6 @@ use PHPStan\Collectors\Registry as CollectorRegistry; use PHPStan\Dependency\DependencyResolver; use PHPStan\Dependency\PackageDependencyResolver; -use PHPStan\DependencyInjection\DirectExtensionsCollection; use PHPStan\File\FileHelper; use PHPStan\File\FileReader; use PHPStan\Fixable\Patcher; @@ -29,6 +28,7 @@ use PHPStan\Rules\IdentifierRuleError; use PHPStan\Rules\Properties\ReadWritePropertiesExtension; use PHPStan\Rules\Rule; +use PHPStan\ShouldNotHappenException; use function array_map; use function array_merge; use function count; @@ -61,6 +61,10 @@ protected function getCollectors(): array } /** + * No longer configures the analysis - register the extensions as services + * in a config file returned from getAdditionalConfigFiles() instead. + * Returning any extension fails the test. + * * @return ReadWritePropertiesExtension[] */ protected function getReadWritePropertiesExtensions(): array @@ -75,17 +79,19 @@ protected function getTypeSpecifier(): TypeSpecifier protected function createNodeScopeResolver(): NodeScopeResolver { - $readWritePropertiesExtensions = $this->getReadWritePropertiesExtensions(); + if ( + $this->getReadWritePropertiesExtensions() !== [] + || $this->shouldPolluteScopeWithLoopInitialAssignments() !== self::getContainer()->getParameter('polluteScopeWithLoopInitialAssignments') + || $this->shouldPolluteScopeWithAlwaysIterableForeach() !== self::getContainer()->getParameter('polluteScopeWithAlwaysIterableForeach') + ) { + throw new ShouldNotHappenException('getReadWritePropertiesExtensions(), shouldPolluteScopeWithLoopInitialAssignments() and shouldPolluteScopeWithAlwaysIterableForeach() no longer configure the analysis. Register the extensions as services and set the parameters in a config file returned from getAdditionalConfigFiles() instead.'); + } return new NodeScopeResolver( self::getContainer(), self::getContainer()->getByType(TemplateArgumentObserver::class), self::getContainer()->getByType(FileHelper::class), - $readWritePropertiesExtensions !== [] ? new DirectExtensionsCollection($readWritePropertiesExtensions) : self::getContainer()->getExtensionsCollection(ReadWritePropertiesExtension::class), self::getContainer()->getExtensionsCollection(PerFileAnalysisResettable::class), - $this->shouldPolluteScopeWithLoopInitialAssignments(), - $this->shouldPolluteScopeWithAlwaysIterableForeach(), - $this->shouldTreatPhpDocTypesAsCertain(), self::getContainer()->getByType(ExpressionResultFactory::class), self::getContainer()->getByType(StatementsHandler::class), ); @@ -306,14 +312,24 @@ private function gatherAnalyserErrorsWithDelayedErrors(array $files): array ]; } + /** + * No longer configures the analysis - set the polluteScopeWithLoopInitialAssignments + * parameter in a config file returned from getAdditionalConfigFiles() instead. + * Returning a different value than the parameter fails the test. + */ protected function shouldPolluteScopeWithLoopInitialAssignments(): bool { - return true; + return self::getContainer()->getParameter('polluteScopeWithLoopInitialAssignments'); } + /** + * No longer configures the analysis - set the polluteScopeWithAlwaysIterableForeach + * parameter in a config file returned from getAdditionalConfigFiles() instead. + * Returning a different value than the parameter fails the test. + */ protected function shouldPolluteScopeWithAlwaysIterableForeach(): bool { - return true; + return self::getContainer()->getParameter('polluteScopeWithAlwaysIterableForeach'); } protected function shouldFailOnPhpErrors(): bool diff --git a/src/Testing/TypeInferenceTestCase.php b/src/Testing/TypeInferenceTestCase.php index 98a3c996c38..071bb4bb190 100644 --- a/src/Testing/TypeInferenceTestCase.php +++ b/src/Testing/TypeInferenceTestCase.php @@ -19,7 +19,6 @@ use PHPStan\Node\InClassNode; use PHPStan\PhpDoc\TypeStringResolver; use PHPStan\Reflection\ReflectionProvider; -use PHPStan\Rules\Properties\ReadWritePropertiesExtension; use PHPStan\ShouldNotHappenException; use PHPStan\TrinaryLogic; use PHPStan\Type\ConstantScalarType; @@ -56,11 +55,7 @@ protected static function createNodeScopeResolver(): NodeScopeResolver $container, $container->getByType(TemplateArgumentObserver::class), $container->getByType(FileHelper::class), - $container->getExtensionsCollection(ReadWritePropertiesExtension::class), $container->getExtensionsCollection(PerFileAnalysisResettable::class), - $container->getParameter('polluteScopeWithLoopInitialAssignments'), - $container->getParameter('polluteScopeWithAlwaysIterableForeach'), - $container->getParameter('treatPhpDocTypesAsCertain'), $container->getByType(ExpressionResultFactory::class), $container->getByType(StatementsHandler::class), ); diff --git a/tests/PHPStan/Analyser/AnalyserTest.php b/tests/PHPStan/Analyser/AnalyserTest.php index 64cdbae59c7..adb02c6e81b 100644 --- a/tests/PHPStan/Analyser/AnalyserTest.php +++ b/tests/PHPStan/Analyser/AnalyserTest.php @@ -20,7 +20,6 @@ use PHPStan\Parser\RichParser; use PHPStan\Rules\AlwaysFailRule; use PHPStan\Rules\DirectRegistry as DirectRuleRegistry; -use PHPStan\Rules\Properties\ReadWritePropertiesExtension; use PHPStan\Testing\PHPStanTestCase; use PHPStan\Type\FileTypeMapper; use PHPUnit\Framework\Attributes\DataProvider; @@ -853,11 +852,7 @@ private function createAnalyser(): Analyser $container, $container->getByType(TemplateArgumentObserver::class), $fileHelper, - $container->getExtensionsCollection(ReadWritePropertiesExtension::class), $container->getExtensionsCollection(PerFileAnalysisResettable::class), - false, - true, - $this->shouldTreatPhpDocTypesAsCertain(), $container->getByType(ExpressionResultFactory::class), $container->getByType(StatementsHandler::class), ); diff --git a/tests/PHPStan/Rules/Classes/ImpossibleInstanceOfRuleTest.php b/tests/PHPStan/Rules/Classes/ImpossibleInstanceOfRuleTest.php index e701bf91f4e..81d5ef2aee3 100644 --- a/tests/PHPStan/Rules/Classes/ImpossibleInstanceOfRuleTest.php +++ b/tests/PHPStan/Rules/Classes/ImpossibleInstanceOfRuleTest.php @@ -11,8 +11,6 @@ use PHPStan\Testing\RuleTestCase; use PHPUnit\Framework\Attributes\DataProvider; use PHPUnit\Framework\Attributes\RequiresPhp; -use function sprintf; -use const PHP_VERSION_ID; /** * @extends RuleTestCase @@ -20,8 +18,6 @@ class ImpossibleInstanceOfRuleTest extends RuleTestCase { - private bool $treatPhpDocTypesAsCertain; - private bool $reportAlwaysTrueInLastCondition = false; protected function getRule(): Rule @@ -43,7 +39,7 @@ protected function getRule(): Rule $ruleLevelHelper, new PossiblyImpureTipHelper(true), self::getContainer()->getByType(ConstantConditionInTraitHelper::class), - treatPhpDocTypesAsCertain: $this->treatPhpDocTypesAsCertain, + treatPhpDocTypesAsCertain: true, reportAlwaysTrueInLastCondition: $this->reportAlwaysTrueInLastCondition, treatPhpDocTypesAsCertainTip: true, ), @@ -51,14 +47,8 @@ protected function getRule(): Rule ]); } - protected function shouldTreatPhpDocTypesAsCertain(): bool - { - return $this->treatPhpDocTypesAsCertain; - } - public function testInstanceof(): void { - $this->treatPhpDocTypesAsCertain = true; $tipText = 'Because the type is coming from a PHPDoc, you can turn off this check by setting treatPhpDocTypesAsCertain: false in your %configurationFile%.'; $this->analyse( [__DIR__ . '/data/impossible-instanceof.php'], @@ -213,32 +203,8 @@ public function testInstanceof(): void ); } - public function testDoNotReportTypesFromPhpDocs(): void - { - $this->treatPhpDocTypesAsCertain = false; - $this->analyse([__DIR__ . '/data/impossible-instanceof-not-phpdoc.php'], [ - [ - 'Instanceof between stdClass and stdClass will always evaluate to true.', - 12, - ], - [ - 'Instanceof between stdClass and Exception will always evaluate to false.', - 15, - ], - [ - 'Instanceof between DateTimeInterface and DateTimeInterface will always evaluate to true.', - 27, - ], - [ - 'Instanceof between DateTimeInterface and ImpossibleInstanceofNotPhpDoc\SomeFinalClass will always evaluate to false.', - 30, - ], - ]); - } - public function testReportTypesFromPhpDocs(): void { - $this->treatPhpDocTypesAsCertain = true; $this->analyse([__DIR__ . '/data/impossible-instanceof-not-phpdoc.php'], [ [ 'Instanceof between stdClass and stdClass will always evaluate to true.', @@ -271,26 +237,17 @@ public function testReportTypesFromPhpDocs(): void public function testBug3096(): void { - $this->treatPhpDocTypesAsCertain = true; $this->analyse([__DIR__ . '/data/bug-3096.php'], []); } public function testBug6213(): void { - $this->treatPhpDocTypesAsCertain = true; $this->analyse([__DIR__ . '/data/bug-6213.php'], []); } - public function testBug5333(): void - { - $this->treatPhpDocTypesAsCertain = false; - $this->analyse([__DIR__ . '/data/bug-5333.php'], []); - } - #[RequiresPhp('>= 8.0.0')] public function testBug8042(): void { - $this->treatPhpDocTypesAsCertain = true; $this->analyse([__DIR__ . '/data/bug-8042.php'], [ [ 'Instanceof between Bug8042\B and Bug8042\B will always evaluate to true.', @@ -308,13 +265,11 @@ public function testBug8042(): void #[RequiresPhp('>= 8.1.0')] public function testBug7721(): void { - $this->treatPhpDocTypesAsCertain = true; $this->analyse([__DIR__ . '/data/bug-7721.php'], []); } public function testUnreachableIfBranches(): void { - $this->treatPhpDocTypesAsCertain = true; $this->analyse([__DIR__ . '/../Comparison/data/unreachable-if-branches.php'], [ [ 'Instanceof between stdClass and stdClass will always evaluate to true.', @@ -336,30 +291,8 @@ public function testUnreachableIfBranches(): void ]); } - public function testIfBranchesDoNotReportPhpDoc(): void - { - $this->treatPhpDocTypesAsCertain = false; - $this->analyse([__DIR__ . '/../Comparison/data/unreachable-if-branches-not-phpdoc.php'], [ - [ - 'Instanceof between UnreachableIfBranchesNotPhpDoc\Foo and UnreachableIfBranchesNotPhpDoc\Foo will always evaluate to true.', - 16, - ], - [ - 'Instanceof between UnreachableIfBranchesNotPhpDoc\Foo and UnreachableIfBranchesNotPhpDoc\Foo will always evaluate to true.', - 26, - 'Remove remaining cases below this one and this error will disappear too.', - ], - [ - 'Instanceof between UnreachableIfBranchesNotPhpDoc\Foo and UnreachableIfBranchesNotPhpDoc\Foo will always evaluate to true.', - 36, - 'Remove remaining cases below this one and this error will disappear too.', - ], - ]); - } - public function testIfBranchesReportPhpDoc(): void { - $this->treatPhpDocTypesAsCertain = true; $tipText = 'Because the type is coming from a PHPDoc, you can turn off this check by setting treatPhpDocTypesAsCertain: false in your %configurationFile%.'; $this->analyse([__DIR__ . '/../Comparison/data/unreachable-if-branches-not-phpdoc.php'], [ [ @@ -396,7 +329,6 @@ public function testIfBranchesReportPhpDoc(): void public function testUnreachableTernaryElse(): void { - $this->treatPhpDocTypesAsCertain = true; $this->analyse([__DIR__ . '/../Comparison/data/unreachable-ternary-else-branch.php'], [ [ 'Instanceof between stdClass and stdClass will always evaluate to true.', @@ -409,28 +341,8 @@ public function testUnreachableTernaryElse(): void ]); } - public function testTernaryElseDoNotReportPhpDoc(): void - { - $this->treatPhpDocTypesAsCertain = false; - $this->analyse([__DIR__ . '/../Comparison/data/unreachable-ternary-else-branch-not-phpdoc.php'], [ - [ - 'Instanceof between UnreachableTernaryElseBranchNotPhpDoc\Foo and UnreachableTernaryElseBranchNotPhpDoc\Foo will always evaluate to true.', - 16, - ], - [ - 'Instanceof between UnreachableTernaryElseBranchNotPhpDoc\Foo and UnreachableTernaryElseBranchNotPhpDoc\Foo will always evaluate to true.', - 17, - ], - [ - 'Instanceof between UnreachableTernaryElseBranchNotPhpDoc\Foo and UnreachableTernaryElseBranchNotPhpDoc\Foo will always evaluate to true.', - 20, - ], - ]); - } - public function testTernaryElseReportPhpDoc(): void { - $this->treatPhpDocTypesAsCertain = true; $tipText = 'Because the type is coming from a PHPDoc, you can turn off this check by setting treatPhpDocTypesAsCertain: false in your %configurationFile%.'; $this->analyse([__DIR__ . '/../Comparison/data/unreachable-ternary-else-branch-not-phpdoc.php'], [ [ @@ -453,12 +365,6 @@ public function testTernaryElseReportPhpDoc(): void ]); } - public function testBug4689(): void - { - $this->treatPhpDocTypesAsCertain = false; - $this->analyse([__DIR__ . '/data/bug-4689.php'], []); - } - public static function dataReportAlwaysTrueInLastCondition(): iterable { yield [false, [ @@ -490,7 +396,6 @@ public static function dataReportAlwaysTrueInLastCondition(): iterable #[DataProvider('dataReportAlwaysTrueInLastCondition')] public function testReportAlwaysTrueInLastCondition(bool $reportAlwaysTrueInLastCondition, array $expectedErrors): void { - $this->treatPhpDocTypesAsCertain = true; $this->reportAlwaysTrueInLastCondition = $reportAlwaysTrueInLastCondition; $this->analyse([__DIR__ . '/data/impossible-instanceof-report-always-true-last-condition.php'], $expectedErrors); } @@ -498,7 +403,6 @@ public function testReportAlwaysTrueInLastCondition(bool $reportAlwaysTrueInLast #[RequiresPhp('>= 8.1.0')] public function testBug10201(): void { - $this->treatPhpDocTypesAsCertain = true; $this->analyse([__DIR__ . '/../../Analyser/nsrt/bug-10201.php'], [ [ 'Instanceof between string and Bug10201\Hello will always evaluate to false.', @@ -509,8 +413,6 @@ public function testBug10201(): void public function testBug3632(): void { - $this->treatPhpDocTypesAsCertain = true; - $tipText = 'Because the type is coming from a PHPDoc, you can turn off this check by setting treatPhpDocTypesAsCertain: false in your %configurationFile%.'; $this->analyse([__DIR__ . '/data/bug-3632.php'], [ [ @@ -531,8 +433,6 @@ public function testBug3632(): void public function testBug10036(): void { - $this->treatPhpDocTypesAsCertain = true; - $this->analyse([__DIR__ . '/data/bug-10036.php'], [ [ 'Instanceof between stdClass and string|null results in an error.', @@ -551,20 +451,17 @@ public function testBug10036(): void public function testBug10353(): void { - $this->treatPhpDocTypesAsCertain = true; $this->analyse([__DIR__ . '/data/bug-10353.php'], []); } public function testBug12267(): void { - $this->treatPhpDocTypesAsCertain = true; $this->analyse([__DIR__ . '/data/bug-12267.php'], []); } #[RequiresPhp('>= 8.0.0')] public function testNewIsAlwaysFinalClass(): void { - $this->treatPhpDocTypesAsCertain = true; $this->analyse([__DIR__ . '/data/impossible-instanceof-new-is-always-final.php'], [ [ 'Instanceof between ImpossibleInstanceofNewIsAlwaysFinal\Bar and ImpossibleInstanceofNewIsAlwaysFinal\Foo will always evaluate to false.', @@ -597,17 +494,6 @@ public function testNewIsAlwaysFinalClass(): void ]); } - public function testBug13469(): void - { - $this->treatPhpDocTypesAsCertain = false; - $this->analyse([__DIR__ . '/data/bug-13469.php'], [ - [ - sprintf('Instanceof between Bug13469\Foo and Stringable will always evaluate to %s.', PHP_VERSION_ID >= 80000 ? 'true' : 'false'), - 23, - ], - ]); - } - public static function dataBug13975(): iterable { yield [__DIR__ . '/data/bug-13975-a.php']; @@ -617,13 +503,11 @@ public static function dataBug13975(): iterable #[DataProvider('dataBug13975')] public function testBug13975(string $file): void { - $this->treatPhpDocTypesAsCertain = true; $this->analyse([$file], []); } public function testPossiblyImpureTip(): void { - $this->treatPhpDocTypesAsCertain = true; $learnMore = ' Learn more: https://phpstan.org/blog/remembering-and-forgetting-returned-values'; $this->analyse([__DIR__ . '/data/possibly-impure-instanceof-tip.php'], [ // maybe-impure: tip expected @@ -643,7 +527,6 @@ public function testPossiblyImpureTip(): void public function testInTrait(): void { - $this->treatPhpDocTypesAsCertain = true; $tipText = 'Because the type is coming from a PHPDoc, you can turn off this check by setting treatPhpDocTypesAsCertain: false in your %configurationFile%.'; $this->analyse([__DIR__ . '/data/impossible-instanceof-in-trait.php'], [ [ @@ -659,10 +542,4 @@ public function testInTrait(): void ]); } - public function testBug5271(): void - { - $this->treatPhpDocTypesAsCertain = false; - $this->analyse([__DIR__ . '/../../Analyser/nsrt/bug-5271.php'], []); - } - } diff --git a/tests/PHPStan/Rules/Classes/ImpossibleInstanceOfRuleWithUncertainPhpDocTypesTest.php b/tests/PHPStan/Rules/Classes/ImpossibleInstanceOfRuleWithUncertainPhpDocTypesTest.php new file mode 100644 index 00000000000..2ee8db20e0d --- /dev/null +++ b/tests/PHPStan/Rules/Classes/ImpossibleInstanceOfRuleWithUncertainPhpDocTypesTest.php @@ -0,0 +1,149 @@ + + */ +class ImpossibleInstanceOfRuleWithUncertainPhpDocTypesTest extends RuleTestCase +{ + + protected function getRule(): Rule + { + $ruleLevelHelper = new RuleLevelHelper( + self::createReflectionProvider(), + checkNullables: true, + checkThisOnly: false, + checkUnionTypes: true, + checkExplicitMixed: false, + checkImplicitMixed: false, + checkBenevolentUnionTypes: false, + discoveringSymbolsTip: true, + ); + + // @phpstan-ignore argument.type + return new CompositeRule([ + new ImpossibleInstanceOfRule( + $ruleLevelHelper, + new PossiblyImpureTipHelper(true), + self::getContainer()->getByType(ConstantConditionInTraitHelper::class), + treatPhpDocTypesAsCertain: false, + reportAlwaysTrueInLastCondition: false, + treatPhpDocTypesAsCertainTip: true, + ), + new ConstantConditionInTraitRule(), + ]); + } + + public static function getAdditionalConfigFiles(): array + { + return array_merge( + parent::getAdditionalConfigFiles(), + [ + __DIR__ . '/../uncertain-phpdoc-types.neon', + ], + ); + } + + protected function shouldTreatPhpDocTypesAsCertain(): bool + { + return false; + } + + public function testDoNotReportTypesFromPhpDocs(): void + { + $this->analyse([__DIR__ . '/data/impossible-instanceof-not-phpdoc.php'], [ + [ + 'Instanceof between stdClass and stdClass will always evaluate to true.', + 12, + ], + [ + 'Instanceof between stdClass and Exception will always evaluate to false.', + 15, + ], + [ + 'Instanceof between DateTimeInterface and DateTimeInterface will always evaluate to true.', + 27, + ], + [ + 'Instanceof between DateTimeInterface and ImpossibleInstanceofNotPhpDoc\SomeFinalClass will always evaluate to false.', + 30, + ], + ]); + } + + public function testBug5333(): void + { + $this->analyse([__DIR__ . '/data/bug-5333.php'], []); + } + + public function testIfBranchesDoNotReportPhpDoc(): void + { + $this->analyse([__DIR__ . '/../Comparison/data/unreachable-if-branches-not-phpdoc.php'], [ + [ + 'Instanceof between UnreachableIfBranchesNotPhpDoc\Foo and UnreachableIfBranchesNotPhpDoc\Foo will always evaluate to true.', + 16, + ], + [ + 'Instanceof between UnreachableIfBranchesNotPhpDoc\Foo and UnreachableIfBranchesNotPhpDoc\Foo will always evaluate to true.', + 26, + 'Remove remaining cases below this one and this error will disappear too.', + ], + [ + 'Instanceof between UnreachableIfBranchesNotPhpDoc\Foo and UnreachableIfBranchesNotPhpDoc\Foo will always evaluate to true.', + 36, + 'Remove remaining cases below this one and this error will disappear too.', + ], + ]); + } + + public function testTernaryElseDoNotReportPhpDoc(): void + { + $this->analyse([__DIR__ . '/../Comparison/data/unreachable-ternary-else-branch-not-phpdoc.php'], [ + [ + 'Instanceof between UnreachableTernaryElseBranchNotPhpDoc\Foo and UnreachableTernaryElseBranchNotPhpDoc\Foo will always evaluate to true.', + 16, + ], + [ + 'Instanceof between UnreachableTernaryElseBranchNotPhpDoc\Foo and UnreachableTernaryElseBranchNotPhpDoc\Foo will always evaluate to true.', + 17, + ], + [ + 'Instanceof between UnreachableTernaryElseBranchNotPhpDoc\Foo and UnreachableTernaryElseBranchNotPhpDoc\Foo will always evaluate to true.', + 20, + ], + ]); + } + + public function testBug4689(): void + { + $this->analyse([__DIR__ . '/data/bug-4689.php'], []); + } + + public function testBug13469(): void + { + $this->analyse([__DIR__ . '/data/bug-13469.php'], [ + [ + sprintf('Instanceof between Bug13469\Foo and Stringable will always evaluate to %s.', PHP_VERSION_ID >= 80000 ? 'true' : 'false'), + 23, + ], + ]); + } + + public function testBug5271(): void + { + $this->analyse([__DIR__ . '/../../Analyser/nsrt/bug-5271.php'], []); + } + +} diff --git a/tests/PHPStan/Rules/Comparison/BooleanOrConstantConditionRuleTest.php b/tests/PHPStan/Rules/Comparison/BooleanOrConstantConditionRuleTest.php index 837668739b0..348fc70f524 100644 --- a/tests/PHPStan/Rules/Comparison/BooleanOrConstantConditionRuleTest.php +++ b/tests/PHPStan/Rules/Comparison/BooleanOrConstantConditionRuleTest.php @@ -237,21 +237,6 @@ public function testRuleLogicalOr(): void ]); } - public function testDoNotReportPhpDoc(): void - { - $this->treatPhpDocTypesAsCertain = false; - $this->analyse([__DIR__ . '/data/boolean-or-not-phpdoc.php'], [ - [ - 'Left side of || is always true.', - 24, - ], - [ - 'Right side of || is always true.', - 30, - ], - ]); - } - public function testReportPhpDoc(): void { $this->treatPhpDocTypesAsCertain = true; @@ -288,9 +273,6 @@ public static function dataTreatPhpDocTypesAsCertainRegression(): array [ true, ], - [ - false, - ], ]; } @@ -441,13 +423,6 @@ public function testBug14473(): void $this->analyse([__DIR__ . '/data/bug-14473.php'], []); } - public function testBug15169(): void - { - $this->treatPhpDocTypesAsCertain = false; - - $this->analyse([__DIR__ . '/../DeadCode/data/bug-15169.php'], []); - } - public function testBug15169TreatPhpDocTypesAsCertain(): void { $this->treatPhpDocTypesAsCertain = true; diff --git a/tests/PHPStan/Rules/Comparison/BooleanOrConstantConditionRuleWithUncertainPhpDocTypesTest.php b/tests/PHPStan/Rules/Comparison/BooleanOrConstantConditionRuleWithUncertainPhpDocTypesTest.php new file mode 100644 index 00000000000..b78733e32bc --- /dev/null +++ b/tests/PHPStan/Rules/Comparison/BooleanOrConstantConditionRuleWithUncertainPhpDocTypesTest.php @@ -0,0 +1,128 @@ + + */ +class BooleanOrConstantConditionRuleWithUncertainPhpDocTypesTest extends RuleTestCase +{ + + private bool $treatPhpDocTypesAsCertain; + + protected function getRule(): Rule + { + // @phpstan-ignore argument.type + return new CompositeRule([ + new BooleanOrConstantConditionRule( + new ConstantConditionRuleHelper( + $this->treatPhpDocTypesAsCertain, + ), + new PossiblyImpureTipHelper(true), + self::getContainer()->getByType(ConstantConditionInTraitHelper::class), + self::getContainer()->getByType(FunctionCallConstantConditionHelper::class), + $this->treatPhpDocTypesAsCertain, + false, + true, + ), + new ImpossibleCheckTypeFunctionCallRule( + new ImpossibleCheckTypeHelper( + self::createReflectionProvider(), + $this->treatPhpDocTypesAsCertain, + ), + new PossiblyImpureTipHelper(true), + self::getContainer()->getByType(ConstantConditionInTraitHelper::class), + self::getContainer()->getByType(FunctionCallConstantConditionHelper::class), + $this->treatPhpDocTypesAsCertain, + false, + true, + ), + new ImpossibleCheckTypeMethodCallRule( + new ImpossibleCheckTypeHelper( + self::createReflectionProvider(), + $this->treatPhpDocTypesAsCertain, + ), + new PossiblyImpureTipHelper(true), + self::getContainer()->getByType(ConstantConditionInTraitHelper::class), + self::getContainer()->getByType(FunctionCallConstantConditionHelper::class), + $this->treatPhpDocTypesAsCertain, + false, + true, + ), + new ImpossibleCheckTypeStaticMethodCallRule( + new ImpossibleCheckTypeHelper( + self::createReflectionProvider(), + $this->treatPhpDocTypesAsCertain, + ), + new PossiblyImpureTipHelper(true), + self::getContainer()->getByType(ConstantConditionInTraitHelper::class), + self::getContainer()->getByType(FunctionCallConstantConditionHelper::class), + $this->treatPhpDocTypesAsCertain, + false, + true, + ), + new FunctionCallConstantConditionRule(), + new ConstantConditionInTraitRule(), + ]); + } + + public static function getAdditionalConfigFiles(): array + { + return array_merge( + parent::getAdditionalConfigFiles(), + [ + __DIR__ . '/../uncertain-phpdoc-types.neon', + ], + ); + } + + protected function shouldTreatPhpDocTypesAsCertain(): bool + { + return $this->treatPhpDocTypesAsCertain; + } + + public function testDoNotReportPhpDoc(): void + { + $this->treatPhpDocTypesAsCertain = false; + $this->analyse([__DIR__ . '/data/boolean-or-not-phpdoc.php'], [ + [ + 'Left side of || is always true.', + 24, + ], + [ + 'Right side of || is always true.', + 30, + ], + ]); + } + + public static function dataTreatPhpDocTypesAsCertainRegression(): array + { + return [ + [ + false, + ], + ]; + } + + #[DataProvider('dataTreatPhpDocTypesAsCertainRegression')] + public function testTreatPhpDocTypesAsCertainRegression(bool $treatPhpDocTypesAsCertain): void + { + $this->treatPhpDocTypesAsCertain = $treatPhpDocTypesAsCertain; + $this->analyse([__DIR__ . '/data/boolean-or-treat-phpdoc-types-regression.php'], []); + } + + public function testBug15169(): void + { + $this->treatPhpDocTypesAsCertain = false; + + $this->analyse([__DIR__ . '/../DeadCode/data/bug-15169.php'], []); + } + +} diff --git a/tests/PHPStan/Rules/Comparison/ImpossibleCheckTypeFunctionCallRuleTest.php b/tests/PHPStan/Rules/Comparison/ImpossibleCheckTypeFunctionCallRuleTest.php index 7dff486519c..dc0b44131f7 100644 --- a/tests/PHPStan/Rules/Comparison/ImpossibleCheckTypeFunctionCallRuleTest.php +++ b/tests/PHPStan/Rules/Comparison/ImpossibleCheckTypeFunctionCallRuleTest.php @@ -7,10 +7,7 @@ use PHPStan\Testing\RuleTestCase; use PHPUnit\Framework\Attributes\DataProvider; use PHPUnit\Framework\Attributes\RequiresPhp; -use function array_filter; use function array_map; -use function array_values; -use function count; /** * @extends RuleTestCase @@ -18,8 +15,6 @@ class ImpossibleCheckTypeFunctionCallRuleTest extends RuleTestCase { - private bool $treatPhpDocTypesAsCertain; - private bool $reportAlwaysTrueInLastCondition = false; protected function getRule(): Rule @@ -29,12 +24,12 @@ protected function getRule(): Rule new ImpossibleCheckTypeFunctionCallRule( new ImpossibleCheckTypeHelper( self::createReflectionProvider(), - $this->treatPhpDocTypesAsCertain, + true, ), new PossiblyImpureTipHelper(true), self::getContainer()->getByType(ConstantConditionInTraitHelper::class), self::getContainer()->getByType(FunctionCallConstantConditionHelper::class), - $this->treatPhpDocTypesAsCertain, + true, $this->reportAlwaysTrueInLastCondition, true, ), @@ -42,15 +37,9 @@ protected function getRule(): Rule ]); } - protected function shouldTreatPhpDocTypesAsCertain(): bool - { - return $this->treatPhpDocTypesAsCertain; - } - #[RequiresPhp('>= 8.0.0')] public function testImpossibleCheckTypeFunctionCall(): void { - $this->treatPhpDocTypesAsCertain = true; $this->analyse( [__DIR__ . '/data/check-type-function-call.php'], [ @@ -290,14 +279,12 @@ public function testImpossibleCheckTypeFunctionCall(): void public function testBug7898(): void { - $this->treatPhpDocTypesAsCertain = true; $this->analyse([__DIR__ . '/data/bug-7898.php'], []); } #[RequiresPhp('>= 8.1.0')] public function testStructExists(): void { - $this->treatPhpDocTypesAsCertain = true; $this->analyse([__DIR__ . '/data/check-type-function-call-struct-exists.php'], [ ['Call to function class_exists() with \'CheckTypeFunctionCall\\\\_Interface\' will always evaluate to false.', 23], ['Call to function class_exists() with \'CheckTypeFunctionCall\\\\_Trait\' will always evaluate to false.', 25], @@ -313,20 +300,8 @@ public function testStructExists(): void ]); } - public function testDoNotReportTypesFromPhpDocs(): void - { - $this->treatPhpDocTypesAsCertain = false; - $this->analyse([__DIR__ . '/data/check-type-function-call-not-phpdoc.php'], [ - [ - 'Call to function is_int() with int will always evaluate to true.', - 16, - ], - ]); - } - public function testReportTypesFromPhpDocs(): void { - $this->treatPhpDocTypesAsCertain = true; $this->analyse([__DIR__ . '/data/check-type-function-call-not-phpdoc.php'], [ [ 'Call to function is_int() with int will always evaluate to true.', @@ -352,44 +327,27 @@ public function testReportTypesFromPhpDocs(): void public function testBug2550(): void { - $this->treatPhpDocTypesAsCertain = true; $this->analyse([__DIR__ . '/data/bug-2550.php'], []); } public function testBug3994(): void { - $this->treatPhpDocTypesAsCertain = true; $this->analyse([__DIR__ . '/data/bug-3994.php'], []); } public function testBug1613(): void { - $this->treatPhpDocTypesAsCertain = true; $this->analyse([__DIR__ . '/data/bug-1613.php'], []); } public function testBug2714(): void { - $this->treatPhpDocTypesAsCertain = true; $this->analyse([__DIR__ . '/data/bug-2714.php'], []); } - public function testBug4657(): void - { - $this->treatPhpDocTypesAsCertain = false; - $this->analyse([__DIR__ . '/data/bug-4657.php'], []); - } - - public function testBug4999(): void - { - $this->treatPhpDocTypesAsCertain = false; - $this->analyse([__DIR__ . '/data/bug-4999.php'], []); - } - #[RequiresPhp('>= 8.1.0')] public function testArrayIsList(): void { - $this->treatPhpDocTypesAsCertain = true; $this->analyse([__DIR__ . '/data/array-is-list.php'], [ [ 'Call to function array_is_list() with array will always evaluate to false.', @@ -409,13 +367,11 @@ public function testArrayIsList(): void public function testBug3766(): void { - $this->treatPhpDocTypesAsCertain = true; $this->analyse([__DIR__ . '/data/bug-3766.php'], []); } public function testBug6305(): void { - $this->treatPhpDocTypesAsCertain = true; $this->analyse([__DIR__ . '/data/bug-6305.php'], [ [ 'Call to function is_subclass_of() with Bug6305\B and \'Bug6305\\\A\' will always evaluate to true.', @@ -430,13 +386,11 @@ public function testBug6305(): void public function testBug6305b(): void { - $this->treatPhpDocTypesAsCertain = true; $this->analyse([__DIR__ . '/data/bug-6305b.php'], []); } public function testBug13713(): void { - $this->treatPhpDocTypesAsCertain = true; $this->analyse([__DIR__ . '/data/bug-13713.php'], [ [ "Call to function is_subclass_of() with arguments Bug13713\\test, 'stdClass' and false will always evaluate to true.", @@ -452,19 +406,16 @@ public function testBug13713(): void public function testBug6698(): void { - $this->treatPhpDocTypesAsCertain = true; $this->analyse([__DIR__ . '/data/bug-6698.php'], []); } public function testBug5369(): void { - $this->treatPhpDocTypesAsCertain = true; $this->analyse([__DIR__ . '/data/bug-5369.php'], []); } public function testBugInArrayDateFormat(): void { - $this->treatPhpDocTypesAsCertain = true; $this->analyse([__DIR__ . '/data/in-array-date-format.php'], [ [ 'Call to function in_array() with arguments \'a\', non-empty-array and true will always evaluate to true.', @@ -490,62 +441,52 @@ public function testBugInArrayDateFormat(): void public function testBug5496(): void { - $this->treatPhpDocTypesAsCertain = true; $this->analyse([__DIR__ . '/data/bug-5496.php'], []); } public function testBug3892(): void { - $this->treatPhpDocTypesAsCertain = true; $this->analyse([__DIR__ . '/data/bug-3892.php'], []); } public function testBug3314(): void { - $this->treatPhpDocTypesAsCertain = true; $this->analyse([__DIR__ . '/data/bug-3314.php'], []); } public function testBug2870(): void { - $this->treatPhpDocTypesAsCertain = true; $this->analyse([__DIR__ . '/data/bug-2870.php'], []); } public function testBug5354(): void { - $this->treatPhpDocTypesAsCertain = true; $this->analyse([__DIR__ . '/data/bug-5354.php'], []); } public function testSlevomatCsInArrayBug(): void { - $this->treatPhpDocTypesAsCertain = true; $this->analyse([__DIR__ . '/data/slevomat-cs-in-array.php'], []); } public function testNonEmptySpecifiedString(): void { - $this->treatPhpDocTypesAsCertain = true; $this->analyse([__DIR__ . '/data/non-empty-string-impossible-type.php'], []); } public function testBug2755(): void { - $this->treatPhpDocTypesAsCertain = true; $this->analyse([__DIR__ . '/data/bug-2755.php'], []); } public function testBug7079(): void { - $this->treatPhpDocTypesAsCertain = true; $this->analyse([__DIR__ . '/data/bug-7079.php'], []); } #[RequiresPhp('>= 8.0.0')] public function testConditionalTypesInference(): void { - $this->treatPhpDocTypesAsCertain = true; $this->analyse([__DIR__ . '/../../Analyser/nsrt/conditional-types-inference.php'], [ [ 'Call to function testIsInt() with string will always evaluate to false.', @@ -572,56 +513,47 @@ public function testConditionalTypesInference(): void public function testBug6697(): void { - $this->treatPhpDocTypesAsCertain = true; $this->analyse([__DIR__ . '/data/bug-6697.php'], []); } #[RequiresPhp('>= 8.0.0')] public function testBug6443(): void { - $this->treatPhpDocTypesAsCertain = true; $this->analyse([__DIR__ . '/data/bug-6443.php'], []); } public function testBug7684(): void { - $this->treatPhpDocTypesAsCertain = true; $this->analyse([__DIR__ . '/data/bug-7684.php'], []); } public function testBug7224(): void { - $this->treatPhpDocTypesAsCertain = true; $this->analyse([__DIR__ . '/../../Analyser/nsrt/bug-7224.php'], []); } public function testBug4708(): void { - $this->treatPhpDocTypesAsCertain = true; $this->analyse([__DIR__ . '/data/bug-4708.php'], []); } public function testBug3821(): void { - $this->treatPhpDocTypesAsCertain = true; $this->analyse([__DIR__ . '/data/bug-3821.php'], []); } public function testBug6599(): void { - $this->treatPhpDocTypesAsCertain = true; $this->analyse([__DIR__ . '/data/bug-6599.php'], []); } public function testBug7914(): void { - $this->treatPhpDocTypesAsCertain = true; $this->analyse([__DIR__ . '/data/bug-7914.php'], []); } public function testDocblockAssertEquality(): void { - $this->treatPhpDocTypesAsCertain = true; $this->analyse([__DIR__ . '/data/docblock-assert-equality.php'], [ [ 'Call to function isAnInteger() with int will always evaluate to true.', @@ -632,56 +564,36 @@ public function testDocblockAssertEquality(): void public function testBug8076(): void { - $this->treatPhpDocTypesAsCertain = true; $this->analyse([__DIR__ . '/data/bug-8076.php'], []); } public function testBug8562(): void { - $this->treatPhpDocTypesAsCertain = true; $this->analyse([__DIR__ . '/data/bug-8562.php'], []); } - public function testBug6938(): void - { - $this->treatPhpDocTypesAsCertain = false; - $this->analyse([__DIR__ . '/data/bug-6938.php'], []); - } - public function testBug8727(): void { - $this->treatPhpDocTypesAsCertain = true; $this->analyse([__DIR__ . '/data/bug-8727.php'], []); } public function testBug8474(): void { - $this->treatPhpDocTypesAsCertain = true; $this->analyse([__DIR__ . '/data/bug-8474.php'], []); } - public function testBug5695(): void - { - $this->treatPhpDocTypesAsCertain = false; - $this->analyse([__DIR__ . '/data/bug-5695.php'], []); - } - public function testBug8752(): void { - $this->treatPhpDocTypesAsCertain = true; $this->analyse([__DIR__ . '/../../Analyser/nsrt/bug-8752.php'], []); } public function testDiscussion9134(): void { - $this->treatPhpDocTypesAsCertain = true; $this->analyse([__DIR__ . '/../../Analyser/nsrt/discussion-9134.php'], []); } public function testImpossibleMethodExistOnGenericClassString(): void { - $this->treatPhpDocTypesAsCertain = true; - $tipText = 'Because the type is coming from a PHPDoc, you can turn off this check by setting treatPhpDocTypesAsCertain: false in your %configurationFile%.'; $this->analyse([__DIR__ . '/data/impossible-method-exists-on-generic-class-string.php'], [ [ @@ -740,14 +652,12 @@ public static function dataReportAlwaysTrueInLastCondition(): iterable #[DataProvider('dataReportAlwaysTrueInLastCondition')] public function testReportAlwaysTrueInLastCondition(bool $reportAlwaysTrueInLastCondition, array $expectedErrors): void { - $this->treatPhpDocTypesAsCertain = true; $this->reportAlwaysTrueInLastCondition = $reportAlwaysTrueInLastCondition; $this->analyse([__DIR__ . '/data/impossible-function-report-always-true-last-condition.php'], $expectedErrors); } public function testObjectShapes(): void { - $this->treatPhpDocTypesAsCertain = true; $this->analyse([__DIR__ . '/data/property-exists-object-shapes.php'], [ [ 'Call to function property_exists() with object{foo: int, bar?: string} and \'baz\' will always evaluate to false.', @@ -920,7 +830,6 @@ private static function getLooseComparisonAgainsEnumsIssues(): array #[RequiresPhp('>= 8.1.0')] public function testLooseComparisonAgainstEnums(): void { - $this->treatPhpDocTypesAsCertain = true; $issues = array_map( static function (array $i): array { if (($i[2] ?? null) === 'BUG') { @@ -936,7 +845,6 @@ static function (array $i): array { public function testNonStrictInArray(): void { - $this->treatPhpDocTypesAsCertain = true; $this->analyse([__DIR__ . '/../../Analyser/nsrt/bug-9662.php'], []); } @@ -945,7 +853,6 @@ public function testNonStrictInArrayEnums(): void { $tipText = 'Because the type is coming from a PHPDoc, you can turn off this check by setting treatPhpDocTypesAsCertain: false in your %configurationFile%.'; - $this->treatPhpDocTypesAsCertain = true; $this->analyse([__DIR__ . '/../../Analyser/nsrt/bug-9662-enums.php'], [ [ "Call to function in_array() with 'NotAnEnumCase' and array will always evaluate to false.", @@ -968,18 +875,8 @@ public function testNonStrictInArrayEnums(): void ]); } - #[RequiresPhp('>= 8.1.0')] - public function testLooseComparisonAgainstEnumsNoPhpdoc(): void - { - $this->treatPhpDocTypesAsCertain = false; - $issues = self::getLooseComparisonAgainsEnumsIssues(); - $issues = array_values(array_filter($issues, static fn (array $i) => count($i) === 2)); - $this->analyse([__DIR__ . '/data/loose-comparison-against-enums.php'], $issues); - } - public function testBug4890b(): void { - $this->treatPhpDocTypesAsCertain = true; $this->analyse([__DIR__ . '/data/bug-4890b.php'], []); } @@ -987,7 +884,6 @@ public function testBug10502(): void { $tipText = 'Because the type is coming from a PHPDoc, you can turn off this check by setting treatPhpDocTypesAsCertain: false in your %configurationFile%.'; - $this->treatPhpDocTypesAsCertain = true; $this->analyse([__DIR__ . '/data/bug-10502.php'], [ [ "Call to function is_callable() with array{ArrayObject<0, 0>, 'count'} will always evaluate to true.", @@ -1003,32 +899,27 @@ public function testBug10502(): void public function testAlwaysTruePregMatch(): void { - $this->treatPhpDocTypesAsCertain = true; $this->analyse([__DIR__ . '/data/always-true-preg-match.php'], []); } public function testBug3979(): void { - $this->treatPhpDocTypesAsCertain = true; $this->analyse([__DIR__ . '/data/bug-3979.php'], []); } #[RequiresPhp('>= 8.0.0')] public function testBug8464(): void { - $this->treatPhpDocTypesAsCertain = true; $this->analyse([__DIR__ . '/data/bug-8464.php'], []); } public function testBug8954(): void { - $this->treatPhpDocTypesAsCertain = true; $this->analyse([__DIR__ . '/data/bug-8954.php'], []); } public function testBugPR3404(): void { - $this->treatPhpDocTypesAsCertain = true; $this->analyse([__DIR__ . '/data/bug-pr-3404.php'], [ [ 'Call to function is_a() with arguments BugPR3404\Location, \'BugPR3404\\\\Location\' and true will always evaluate to true.', @@ -1039,13 +930,11 @@ public function testBugPR3404(): void public function testBug13151(): void { - $this->treatPhpDocTypesAsCertain = true; $this->analyse([__DIR__ . '/data/bug-13151.php'], []); } public function testBug8818(): void { - $this->treatPhpDocTypesAsCertain = true; $this->analyse([__DIR__ . '/data/bug-8818.php'], []); } @@ -1053,7 +942,6 @@ public function testBug12755(): void { $tipText = 'Because the type is coming from a PHPDoc, you can turn off this check by setting treatPhpDocTypesAsCertain: false in your %configurationFile%.'; - $this->treatPhpDocTypesAsCertain = true; $this->analyse([__DIR__ . '/data/bug-12755.php'], [ [ 'Call to function in_array() with arguments null, array{key1: bool|null, key2: null} and true will always evaluate to true.', @@ -1065,19 +953,16 @@ public function testBug12755(): void public function testBugStrictRule147(): void { - $this->treatPhpDocTypesAsCertain = true; $this->analyse([__DIR__ . '/data/bug-strict-147.php'], []); } public function testBugStrictRule143(): void { - $this->treatPhpDocTypesAsCertain = true; $this->analyse([__DIR__ . '/data/bug-strict-143.php'], []); } public function testBug12412(): void { - $this->treatPhpDocTypesAsCertain = true; $this->analyse([__DIR__ . '/data/bug-12412.php'], []); } @@ -1085,7 +970,6 @@ public function testBug14873(): void { $tipText = 'Because the type is coming from a PHPDoc, you can turn off this check by setting treatPhpDocTypesAsCertain: false in your %configurationFile%.'; - $this->treatPhpDocTypesAsCertain = true; $this->analyse([__DIR__ . '/data/bug-14873.php'], [ [ 'Call to function in_array() with arguments \'a\'|\'b\'|\'c\', array{\'a\', \'b\', \'c\'} and true will always evaluate to true.', @@ -1102,7 +986,6 @@ public function testBug14873(): void public function testBug2730(): void { - $this->treatPhpDocTypesAsCertain = true; $this->analyse([__DIR__ . '/data/bug-2730.php'], [ [ 'Call to function is_object() with int will always evaluate to false.', @@ -1114,20 +997,17 @@ public function testBug2730(): void #[RequiresPhp('>= 8.2.0')] public function testBug13291(): void { - $this->treatPhpDocTypesAsCertain = true; $this->analyse([__DIR__ . '/data/bug-13291.php'], []); } public function testBug6788(): void { - $this->treatPhpDocTypesAsCertain = true; $this->analyse([__DIR__ . '/data/bug-6788.php'], []); } #[RequiresPhp('>= 8.0.0')] public function testBug13268(): void { - $this->treatPhpDocTypesAsCertain = true; $this->analyse([__DIR__ . '/data/bug-13268.php'], []); } @@ -1136,7 +1016,6 @@ public function testBug12087(): void { $tipText = 'Because the type is coming from a PHPDoc, you can turn off this check by setting treatPhpDocTypesAsCertain: false in your %configurationFile%.'; - $this->treatPhpDocTypesAsCertain = true; $this->analyse([__DIR__ . '/data/bug-12087.php'], [ [ 'Call to function is_null() with null will always evaluate to true.', @@ -1151,7 +1030,6 @@ public function testBug12087c(): void { $tipText = 'Because the type is coming from a PHPDoc, you can turn off this check by setting treatPhpDocTypesAsCertain: false in your %configurationFile%.'; - $this->treatPhpDocTypesAsCertain = true; $this->analyse([__DIR__ . '/data/bug-12087c.php'], [ [ 'Call to function is_null() with null will always evaluate to true.', @@ -1171,64 +1049,54 @@ public function testBug12087c(): void public function testBug10394(): void { - $this->treatPhpDocTypesAsCertain = true; $this->analyse([__DIR__ . '/data/bug-10394.php'], []); } public function testBug12930(): void { - $this->treatPhpDocTypesAsCertain = true; $this->analyse([__DIR__ . '/data/bug-12930.php'], []); } public function testBug13628(): void { - $this->treatPhpDocTypesAsCertain = true; $this->analyse([__DIR__ . '/data/bug-13628.php'], []); } public function testBug13023(): void { - $this->treatPhpDocTypesAsCertain = true; $this->analyse([__DIR__ . '/data/bug-13023.php'], []); } public function testBug9095(): void { - $this->treatPhpDocTypesAsCertain = true; $this->analyse([__DIR__ . '/data/bug-9095.php'], []); } #[RequiresPhp('>= 8.1.0')] public function testBug7599(): void { - $this->treatPhpDocTypesAsCertain = true; $this->analyse([__DIR__ . '/data/bug-7599.php'], []); } #[RequiresPhp('>= 8.0.0')] public function testBug13474(): void { - $this->treatPhpDocTypesAsCertain = true; $this->analyse([__DIR__ . '/data/bug-13474.php'], []); } public function testBug13687(): void { - $this->treatPhpDocTypesAsCertain = true; $this->analyse([__DIR__ . '/data/bug-13687.php'], []); } #[RequiresPhp('>= 8.1.0')] public function testBug12798(): void { - $this->treatPhpDocTypesAsCertain = true; $this->analyse([__DIR__ . '/data/bug-12798.php'], []); } public function testBug4570(): void { - $this->treatPhpDocTypesAsCertain = true; $this->analyse([__DIR__ . '/data/bug-4570.php'], []); } @@ -1236,7 +1104,6 @@ public function testBug9666(): void { $tipText = 'Because the type is coming from a PHPDoc, you can turn off this check by setting treatPhpDocTypesAsCertain: false in your %configurationFile%.'; - $this->treatPhpDocTypesAsCertain = true; $this->analyse([__DIR__ . '/data/bug-9666.php'], [ [ 'Call to function is_bool() with bool will always evaluate to true.', @@ -1249,31 +1116,26 @@ public function testBug9666(): void #[RequiresPhp('>= 8.0.0')] public function testBug9445(): void { - $this->treatPhpDocTypesAsCertain = true; $this->analyse([__DIR__ . '/data/bug-9445.php'], []); } public function testBug7773(): void { - $this->treatPhpDocTypesAsCertain = true; $this->analyse([__DIR__ . '/data/bug-7773.php'], []); } public function testPr4375(): void { - $this->treatPhpDocTypesAsCertain = true; $this->analyse([__DIR__ . '/data/pr-4375.php'], []); } public function testBug14177(): void { - $this->treatPhpDocTypesAsCertain = true; $this->analyse([__DIR__ . '/data/bug-14177.php'], []); } public function testBug13566(): void { - $this->treatPhpDocTypesAsCertain = true; $this->analyse([__DIR__ . '/data/bug-13566.php'], [ [ 'Call to function is_numeric() with 123 will always evaluate to true.', @@ -1286,22 +1148,13 @@ public function testBug13566(): void ]); } - #[RequiresPhp('>= 8.1.0')] - public function testBug14429(): void - { - $this->treatPhpDocTypesAsCertain = false; - $this->analyse([__DIR__ . '/data/bug-14429.php'], []); - } - public function testBug13705(): void { - $this->treatPhpDocTypesAsCertain = true; $this->analyse([__DIR__ . '/../../Analyser/nsrt/bug-13705.php'], []); } public function testBug13799(): void { - $this->treatPhpDocTypesAsCertain = true; $this->analyse([__DIR__ . '/data/bug-13799.php'], [ [ 'Call to function in_array() with arguments \'c\', list<\'a\'|\'b\'> and true will always evaluate to false.', @@ -1313,7 +1166,6 @@ public function testBug13799(): void public function testInTrait(): void { - $this->treatPhpDocTypesAsCertain = true; $this->analyse([__DIR__ . '/data/impossible-function-call-in-trait.php'], [ [ 'Call to function is_string() with int will always evaluate to false.', @@ -1324,38 +1176,27 @@ public function testInTrait(): void public function testBug12063(): void { - $this->treatPhpDocTypesAsCertain = true; $this->analyse([__DIR__ . '/data/bug-12063.php'], []); } - public function testBug11014(): void - { - $this->treatPhpDocTypesAsCertain = false; - $this->analyse([__DIR__ . '/../../Analyser/nsrt/bug-11014.php'], []); - } - public function testBug8217(): void { - $this->treatPhpDocTypesAsCertain = true; $this->analyse([__DIR__ . '/data/bug-8217.php'], []); } public function testBug8980(): void { - $this->treatPhpDocTypesAsCertain = true; $this->analyse([__DIR__ . '/data/bug-8980.php'], []); } #[RequiresPhp('>= 8.1.0')] public function testBug14908(): void { - $this->treatPhpDocTypesAsCertain = true; $this->analyse([__DIR__ . '/data/bug-14908.php'], []); } public function testBug6211(): void { - $this->treatPhpDocTypesAsCertain = true; $this->analyse([__DIR__ . '/data/bug-6211.php'], [ [ 'Call to function method_exists() with Bug6211\Hell and \'test\' will always evaluate to true.', diff --git a/tests/PHPStan/Rules/Comparison/ImpossibleCheckTypeFunctionCallRuleWithUncertainPhpDocTypesTest.php b/tests/PHPStan/Rules/Comparison/ImpossibleCheckTypeFunctionCallRuleWithUncertainPhpDocTypesTest.php new file mode 100644 index 00000000000..74a451a4dc2 --- /dev/null +++ b/tests/PHPStan/Rules/Comparison/ImpossibleCheckTypeFunctionCallRuleWithUncertainPhpDocTypesTest.php @@ -0,0 +1,264 @@ + + */ +class ImpossibleCheckTypeFunctionCallRuleWithUncertainPhpDocTypesTest extends RuleTestCase +{ + + protected function getRule(): Rule + { + // @phpstan-ignore argument.type + return new CompositeRule([ + new ImpossibleCheckTypeFunctionCallRule( + new ImpossibleCheckTypeHelper( + self::createReflectionProvider(), + false, + ), + new PossiblyImpureTipHelper(true), + self::getContainer()->getByType(ConstantConditionInTraitHelper::class), + self::getContainer()->getByType(FunctionCallConstantConditionHelper::class), + false, + false, + true, + ), + new ConstantConditionInTraitRule(), + ]); + } + + public static function getAdditionalConfigFiles(): array + { + return array_merge( + parent::getAdditionalConfigFiles(), + [ + __DIR__ . '/../uncertain-phpdoc-types.neon', + ], + ); + } + + protected function shouldTreatPhpDocTypesAsCertain(): bool + { + return false; + } + + public function testDoNotReportTypesFromPhpDocs(): void + { + $this->analyse([__DIR__ . '/data/check-type-function-call-not-phpdoc.php'], [ + [ + 'Call to function is_int() with int will always evaluate to true.', + 16, + ], + ]); + } + + public function testBug4657(): void + { + $this->analyse([__DIR__ . '/data/bug-4657.php'], []); + } + + public function testBug4999(): void + { + $this->analyse([__DIR__ . '/data/bug-4999.php'], []); + } + + public function testBug6938(): void + { + $this->analyse([__DIR__ . '/data/bug-6938.php'], []); + } + + public function testBug5695(): void + { + $this->analyse([__DIR__ . '/data/bug-5695.php'], []); + } + + /** @return list */ + private static function getLooseComparisonAgainsEnumsIssues(): array + { + $tipText = 'Because the type is coming from a PHPDoc, you can turn off this check by setting treatPhpDocTypesAsCertain: false in your %configurationFile%.'; + return [ + [ + 'Call to function in_array() with LooseComparisonAgainstEnums\\FooUnitEnum and array{\'A\'} will always evaluate to false.', + 21, + ], + [ + 'Call to function in_array() with arguments LooseComparisonAgainstEnums\\FooUnitEnum, array{\'A\'} and false will always evaluate to false.', + 24, + ], + [ + 'Call to function in_array() with LooseComparisonAgainstEnums\\FooBackedEnum and array{\'A\'} will always evaluate to false.', + 27, + ], + [ + 'Call to function in_array() with arguments LooseComparisonAgainstEnums\\FooBackedEnum, array{\'A\'} and false will always evaluate to false.', + 30, + ], + [ + 'Call to function in_array() with arguments LooseComparisonAgainstEnums\\FooBackedEnum|LooseComparisonAgainstEnums\\FooUnitEnum, array{\'A\'} and false will always evaluate to false.', + 33, + ], + [ + 'Call to function in_array() with \'A\' and array{LooseComparisonAgainstEnums\\FooUnitEnum} will always evaluate to false.', + 39, + ], + [ + 'Call to function in_array() with arguments \'A\', array{LooseComparisonAgainstEnums\\FooUnitEnum} and false will always evaluate to false.', + 42, + ], + [ + 'Call to function in_array() with \'A\' and array{LooseComparisonAgainstEnums\\FooBackedEnum} will always evaluate to false.', + 45, + ], + [ + 'Call to function in_array() with arguments \'A\', array{LooseComparisonAgainstEnums\\FooBackedEnum} and false will always evaluate to false.', + 48, + ], + [ + 'Call to function in_array() with arguments \'A\', array{LooseComparisonAgainstEnums\\FooBackedEnum|LooseComparisonAgainstEnums\\FooUnitEnum} and false will always evaluate to false.', + 51, + ], + [ + 'Call to function in_array() with LooseComparisonAgainstEnums\FooUnitEnum and array{bool} will always evaluate to false.', + 57, + ], + [ + 'Call to function in_array() with arguments LooseComparisonAgainstEnums\FooUnitEnum, array{bool} and false will always evaluate to false.', + 60, + ], + [ + 'Call to function in_array() with LooseComparisonAgainstEnums\FooBackedEnum and array{bool} will always evaluate to false.', + 63, + ], + [ + 'Call to function in_array() with arguments LooseComparisonAgainstEnums\FooBackedEnum, array{bool} and false will always evaluate to false.', + 66, + ], + [ + 'Call to function in_array() with arguments LooseComparisonAgainstEnums\FooBackedEnum|LooseComparisonAgainstEnums\FooUnitEnum, array{bool} and false will always evaluate to false.', + 69, + ], + [ + 'Call to function in_array() with bool and array{LooseComparisonAgainstEnums\FooUnitEnum} will always evaluate to false.', + 75, + ], + [ + 'Call to function in_array() with arguments bool, array{LooseComparisonAgainstEnums\FooUnitEnum} and false will always evaluate to false.', + 78, + ], + [ + 'Call to function in_array() with bool and array{LooseComparisonAgainstEnums\FooBackedEnum} will always evaluate to false.', + 81, + ], + [ + 'Call to function in_array() with arguments bool, array{LooseComparisonAgainstEnums\FooBackedEnum} and false will always evaluate to false.', + 84, + ], + [ + 'Call to function in_array() with arguments bool, array{LooseComparisonAgainstEnums\FooBackedEnum|LooseComparisonAgainstEnums\FooUnitEnum} and false will always evaluate to false.', + 87, + ], + [ + 'Call to function in_array() with LooseComparisonAgainstEnums\FooUnitEnum and array{null} will always evaluate to false.', + 93, + ], + [ + 'Call to function in_array() with null and array{LooseComparisonAgainstEnums\FooBackedEnum} will always evaluate to false.', + 96, + ], + [ + 'Call to function in_array() with LooseComparisonAgainstEnums\FooUnitEnum and array will always evaluate to false.', + 125, + $tipText, + ], + [ + 'Call to function in_array() with arguments LooseComparisonAgainstEnums\FooUnitEnum, array and false will always evaluate to false.', + 128, + $tipText, + ], + [ + 'Call to function in_array() with arguments LooseComparisonAgainstEnums\FooUnitEnum, array and true will always evaluate to false.', + 131, + $tipText, + ], + [ + 'Call to function in_array() with string and array will always evaluate to false.', + 143, + $tipText, + ], + [ + 'Call to function in_array() with arguments string, array and false will always evaluate to false.', + 146, + $tipText, + ], + [ + 'Call to function in_array() with arguments string, array and true will always evaluate to false.', + 149, + $tipText, + ], + [ + 'Call to function in_array() with LooseComparisonAgainstEnums\FooUnitEnum::B and non-empty-array will always evaluate to false.', + 159, + $tipText, + ], + [ + 'Call to function in_array() with LooseComparisonAgainstEnums\FooUnitEnum::A and non-empty-array will always evaluate to true.', + 162, + $tipText, + ], + [ + 'Call to function in_array() with arguments LooseComparisonAgainstEnums\FooUnitEnum::A, non-empty-array and false will always evaluate to true.', + 165, + 'BUG', + //$tipText, + ], + [ + 'Call to function in_array() with arguments LooseComparisonAgainstEnums\FooUnitEnum::A, non-empty-array and true will always evaluate to true.', + 168, + 'BUG', + //$tipText, + ], + [ + 'Call to function in_array() with arguments LooseComparisonAgainstEnums\FooUnitEnum::B, non-empty-array and false will always evaluate to false.', + 171, + 'BUG', + //$tipText, + ], + [ + 'Call to function in_array() with arguments LooseComparisonAgainstEnums\FooUnitEnum::B, non-empty-array and true will always evaluate to false.', + 174, + 'BUG', + //$tipText, + ], + ]; + } + + #[RequiresPhp('>= 8.1.0')] + public function testLooseComparisonAgainstEnumsNoPhpdoc(): void + { + $issues = self::getLooseComparisonAgainsEnumsIssues(); + $issues = array_values(array_filter($issues, static fn (array $i) => count($i) === 2)); + $this->analyse([__DIR__ . '/data/loose-comparison-against-enums.php'], $issues); + } + + #[RequiresPhp('>= 8.1.0')] + public function testBug14429(): void + { + $this->analyse([__DIR__ . '/data/bug-14429.php'], []); + } + + public function testBug11014(): void + { + $this->analyse([__DIR__ . '/../../Analyser/nsrt/bug-11014.php'], []); + } + +} diff --git a/tests/PHPStan/Rules/Comparison/NumberComparisonOperatorsConstantConditionRuleTest.php b/tests/PHPStan/Rules/Comparison/NumberComparisonOperatorsConstantConditionRuleTest.php index 6011cadf288..dcbc8672de2 100644 --- a/tests/PHPStan/Rules/Comparison/NumberComparisonOperatorsConstantConditionRuleTest.php +++ b/tests/PHPStan/Rules/Comparison/NumberComparisonOperatorsConstantConditionRuleTest.php @@ -16,8 +16,6 @@ class NumberComparisonOperatorsConstantConditionRuleTest extends RuleTestCase private bool $treatPhpDocTypesAsCertain = true; - private bool $polluteScopeWithAlwaysIterableForeach = true; - protected function getRule(): Rule { // @phpstan-ignore argument.type @@ -32,11 +30,6 @@ protected function getRule(): Rule ]); } - protected function shouldPolluteScopeWithAlwaysIterableForeach(): bool - { - return $this->polluteScopeWithAlwaysIterableForeach; - } - public function testBug8277(): void { $this->analyse([__DIR__ . '/data/bug-8277.php'], []); @@ -288,12 +281,6 @@ public function testBug3387(): void $this->analyse([__DIR__ . '/data/bug-3387.php'], []); } - public function testBug13984(): void - { - $this->polluteScopeWithAlwaysIterableForeach = false; - $this->analyse([__DIR__ . '/data/bug-13984.php'], []); - } - public function testBug13874(): void { $this->analyse([__DIR__ . '/data/bug-13874.php'], []); diff --git a/tests/PHPStan/Rules/Comparison/NumberComparisonOperatorsConstantConditionRuleWithoutForeachPollutionTest.php b/tests/PHPStan/Rules/Comparison/NumberComparisonOperatorsConstantConditionRuleWithoutForeachPollutionTest.php new file mode 100644 index 00000000000..035af5cacba --- /dev/null +++ b/tests/PHPStan/Rules/Comparison/NumberComparisonOperatorsConstantConditionRuleWithoutForeachPollutionTest.php @@ -0,0 +1,45 @@ + + */ +class NumberComparisonOperatorsConstantConditionRuleWithoutForeachPollutionTest extends RuleTestCase +{ + + protected function getRule(): Rule + { + // @phpstan-ignore argument.type + return new CompositeRule([ + new NumberComparisonOperatorsConstantConditionRule( + new PossiblyImpureTipHelper(true), + self::getContainer()->getByType(ConstantConditionInTraitHelper::class), + true, + true, + ), + new ConstantConditionInTraitRule(), + ]); + } + + public static function getAdditionalConfigFiles(): array + { + return array_merge( + parent::getAdditionalConfigFiles(), + [ + __DIR__ . '/../without-foreach-pollution.neon', + ], + ); + } + + public function testBug13984(): void + { + $this->analyse([__DIR__ . '/data/bug-13984.php'], []); + } + +} diff --git a/tests/PHPStan/Rules/Comparison/StrictComparisonOfDifferentTypesRuleTest.php b/tests/PHPStan/Rules/Comparison/StrictComparisonOfDifferentTypesRuleTest.php index c59730e3b94..debc27ee4e3 100644 --- a/tests/PHPStan/Rules/Comparison/StrictComparisonOfDifferentTypesRuleTest.php +++ b/tests/PHPStan/Rules/Comparison/StrictComparisonOfDifferentTypesRuleTest.php @@ -21,8 +21,6 @@ class StrictComparisonOfDifferentTypesRuleTest extends RuleTestCase private bool $treatPhpDocTypesAsCertain = true; - private bool $polluteScopeWithAlwaysIterableForeach = true; - protected function getRule(): Rule { // @phpstan-ignore argument.type @@ -44,11 +42,6 @@ protected function shouldTreatPhpDocTypesAsCertain(): bool return $this->treatPhpDocTypesAsCertain; } - protected function shouldPolluteScopeWithAlwaysIterableForeach(): bool - { - return $this->polluteScopeWithAlwaysIterableForeach; - } - public function testStrictComparison(): void { $tipText = 'Because the type is coming from a PHPDoc, you can turn off this check by setting treatPhpDocTypesAsCertain: false in your %configurationFile%.'; @@ -1235,12 +1228,6 @@ public function testBug13421(): void $this->analyse([__DIR__ . '/../../Analyser/nsrt/bug-13421.php'], []); } - public function testBug14446(): void - { - $this->polluteScopeWithAlwaysIterableForeach = false; - $this->analyse([__DIR__ . '/../../Analyser/data/bug-14446.php'], []); - } - public function testBug13444(): void { $this->analyse([__DIR__ . '/data/bug-13444.php'], []); diff --git a/tests/PHPStan/Rules/Comparison/StrictComparisonOfDifferentTypesRuleWithoutForeachPollutionTest.php b/tests/PHPStan/Rules/Comparison/StrictComparisonOfDifferentTypesRuleWithoutForeachPollutionTest.php new file mode 100644 index 00000000000..10d86a63661 --- /dev/null +++ b/tests/PHPStan/Rules/Comparison/StrictComparisonOfDifferentTypesRuleWithoutForeachPollutionTest.php @@ -0,0 +1,48 @@ + + */ +class StrictComparisonOfDifferentTypesRuleWithoutForeachPollutionTest extends RuleTestCase +{ + + protected function getRule(): Rule + { + // @phpstan-ignore argument.type + return new CompositeRule([ + new StrictComparisonOfDifferentTypesRule( + self::getContainer()->getByType(RicherScopeGetTypeHelper::class), + new PossiblyImpureTipHelper(true), + self::getContainer()->getByType(ConstantConditionInTraitHelper::class), + true, + false, + true, + ), + new ConstantConditionInTraitRule(), + ]); + } + + public static function getAdditionalConfigFiles(): array + { + return array_merge( + parent::getAdditionalConfigFiles(), + [ + __DIR__ . '/../without-foreach-pollution.neon', + ], + ); + } + + public function testBug14446(): void + { + $this->analyse([__DIR__ . '/../../Analyser/data/bug-14446.php'], []); + } + +} diff --git a/tests/PHPStan/Rules/DeadCode/UnreachableStatementRuleTest.php b/tests/PHPStan/Rules/DeadCode/UnreachableStatementRuleTest.php index 4e04595e917..e2085ce0830 100644 --- a/tests/PHPStan/Rules/DeadCode/UnreachableStatementRuleTest.php +++ b/tests/PHPStan/Rules/DeadCode/UnreachableStatementRuleTest.php @@ -77,9 +77,6 @@ public static function dataBugWithoutGitHubIssue1(): array [ true, ], - [ - false, - ], ]; } @@ -224,23 +221,6 @@ public function testBug11179(): void $this->analyse([__DIR__ . '/data/bug-11179.php'], []); } - public function testBug11992(): void - { - $this->treatPhpDocTypesAsCertain = false; - $this->analyse([__DIR__ . '/data/bug-11992.php'], []); - } - - public function testBug7531(): void - { - $this->treatPhpDocTypesAsCertain = false; - $this->analyse([__DIR__ . '/data/bug-7531.php'], [ - [ - 'Unreachable statement - code above always terminates.', - 22, - ], - ]); - } - public function testMultipleUnreachable(): void { $this->treatPhpDocTypesAsCertain = true; @@ -252,127 +232,6 @@ public function testMultipleUnreachable(): void ]); } - #[RequiresPhp('>= 8.1.0')] - public function testBug11909(): void - { - $this->treatPhpDocTypesAsCertain = false; - $this->analyse([__DIR__ . '/data/bug-11909.php'], [ - [ - 'Unreachable statement - code above always terminates.', - 10, - ], - ]); - } - - #[RequiresPhp('>= 8.1.0')] - public function testBug13232a(): void - { - $this->treatPhpDocTypesAsCertain = false; - $this->analyse([__DIR__ . '/data/bug-13232a.php'], [ - [ - 'Unreachable statement - code above always terminates.', - 10, - ], - [ - 'Unreachable statement - code above always terminates.', - 17, - ], - [ - 'Unreachable statement - code above always terminates.', - 23, - ], - [ - 'Unreachable statement - code above always terminates.', - 32, - ], - [ - 'Unreachable statement - code above always terminates.', - 38, - ], - [ - 'Unreachable statement - code above always terminates.', - 44, - ], - [ - 'Unreachable statement - code above always terminates.', - 52, - ], - [ - 'Unreachable statement - code above always terminates.', - 61, - ], - [ - 'Unreachable statement - code above always terminates.', - 70, - ], - ]); - } - - #[RequiresPhp('>= 8.1.0')] - public function testBug13232b(): void - { - $this->treatPhpDocTypesAsCertain = false; - $this->analyse([__DIR__ . '/data/bug-13232b.php'], [ - [ - 'Unreachable statement - code above always terminates.', - 19, - ], - ]); - } - - #[RequiresPhp('>= 8.1.0')] - public function testBug13232c(): void - { - $this->treatPhpDocTypesAsCertain = false; - $this->analyse([__DIR__ . '/data/bug-13232c.php'], [ - [ - 'Unreachable statement - code above always terminates.', - 12, - ], - [ - 'Unreachable statement - code above always terminates.', - 20, - ], - ]); - } - - #[RequiresPhp('>= 8.1.0')] - public function testBug13232d(): void - { - $this->treatPhpDocTypesAsCertain = false; - $this->analyse([__DIR__ . '/data/bug-13232d.php'], [ - [ - 'Unreachable statement - code above always terminates.', - 11, - ], - ]); - } - - #[RequiresPhp('>= 8.1.0')] - public function testBug13288(): void - { - $this->treatPhpDocTypesAsCertain = false; - $this->analyse([__DIR__ . '/data/bug-13288.php'], []); - } - - public function testBug13311(): void - { - $this->treatPhpDocTypesAsCertain = false; - $this->analyse([__DIR__ . '/data/bug-13311.php'], []); - } - - public function testBug13307(): void - { - $this->treatPhpDocTypesAsCertain = false; - $this->analyse([__DIR__ . '/data/bug-13307.php'], []); - } - - public function testBug13331(): void - { - $this->treatPhpDocTypesAsCertain = false; - $this->analyse([__DIR__ . '/data/bug-13331.php'], []); - } - #[RequiresPhp('>= 8.2.0')] public function testBug14328(): void { @@ -434,12 +293,6 @@ public function testBug11731(): void ]); } - public function testBug15169(): void - { - $this->treatPhpDocTypesAsCertain = false; - $this->analyse([__DIR__ . '/data/bug-15169.php'], []); - } - public function testBug15169TreatPhpDocTypesAsCertain(): void { $this->treatPhpDocTypesAsCertain = true; @@ -471,12 +324,6 @@ public function testBug15169TreatPhpDocTypesAsCertain(): void ]); } - public function testBug15169Analogous(): void - { - $this->treatPhpDocTypesAsCertain = false; - $this->analyse([__DIR__ . '/data/bug-15169b.php'], []); - } - public function testBug15169AnalogousTreatPhpDocTypesAsCertain(): void { $this->treatPhpDocTypesAsCertain = true; @@ -516,13 +363,6 @@ public function testBug15169AnalogousTreatPhpDocTypesAsCertain(): void ]); } - #[RequiresPhp('>= 8.1.0')] - public function testBug15169FirstClassCallables(): void - { - $this->treatPhpDocTypesAsCertain = false; - $this->analyse([__DIR__ . '/data/bug-15169c.php'], []); - } - #[RequiresPhp('>= 8.1.0')] public function testBug15169FirstClassCallablesTreatPhpDocTypesAsCertain(): void { diff --git a/tests/PHPStan/Rules/DeadCode/UnreachableStatementRuleWithUncertainPhpDocTypesTest.php b/tests/PHPStan/Rules/DeadCode/UnreachableStatementRuleWithUncertainPhpDocTypesTest.php new file mode 100644 index 00000000000..ce32dee53a2 --- /dev/null +++ b/tests/PHPStan/Rules/DeadCode/UnreachableStatementRuleWithUncertainPhpDocTypesTest.php @@ -0,0 +1,212 @@ + + */ +class UnreachableStatementRuleWithUncertainPhpDocTypesTest extends RuleTestCase +{ + + private bool $treatPhpDocTypesAsCertain; + + protected function getRule(): Rule + { + return new UnreachableStatementRule(); + } + + public static function getAdditionalConfigFiles(): array + { + return array_merge( + parent::getAdditionalConfigFiles(), + [ + __DIR__ . '/../uncertain-phpdoc-types.neon', + ], + ); + } + + protected function shouldTreatPhpDocTypesAsCertain(): bool + { + return $this->treatPhpDocTypesAsCertain; + } + + public static function dataBugWithoutGitHubIssue1(): array + { + return [ + [ + false, + ], + ]; + } + + #[DataProvider('dataBugWithoutGitHubIssue1')] + public function testBugWithoutGitHubIssue1(bool $treatPhpDocTypesAsCertain): void + { + $this->treatPhpDocTypesAsCertain = $treatPhpDocTypesAsCertain; + $this->analyse([__DIR__ . '/data/bug-without-issue-1.php'], []); + } + + public function testBug11992(): void + { + $this->treatPhpDocTypesAsCertain = false; + $this->analyse([__DIR__ . '/data/bug-11992.php'], []); + } + + public function testBug7531(): void + { + $this->treatPhpDocTypesAsCertain = false; + $this->analyse([__DIR__ . '/data/bug-7531.php'], [ + [ + 'Unreachable statement - code above always terminates.', + 22, + ], + ]); + } + + #[RequiresPhp('>= 8.1.0')] + public function testBug11909(): void + { + $this->treatPhpDocTypesAsCertain = false; + $this->analyse([__DIR__ . '/data/bug-11909.php'], [ + [ + 'Unreachable statement - code above always terminates.', + 10, + ], + ]); + } + + #[RequiresPhp('>= 8.1.0')] + public function testBug13232a(): void + { + $this->treatPhpDocTypesAsCertain = false; + $this->analyse([__DIR__ . '/data/bug-13232a.php'], [ + [ + 'Unreachable statement - code above always terminates.', + 10, + ], + [ + 'Unreachable statement - code above always terminates.', + 17, + ], + [ + 'Unreachable statement - code above always terminates.', + 23, + ], + [ + 'Unreachable statement - code above always terminates.', + 32, + ], + [ + 'Unreachable statement - code above always terminates.', + 38, + ], + [ + 'Unreachable statement - code above always terminates.', + 44, + ], + [ + 'Unreachable statement - code above always terminates.', + 52, + ], + [ + 'Unreachable statement - code above always terminates.', + 61, + ], + [ + 'Unreachable statement - code above always terminates.', + 70, + ], + ]); + } + + #[RequiresPhp('>= 8.1.0')] + public function testBug13232b(): void + { + $this->treatPhpDocTypesAsCertain = false; + $this->analyse([__DIR__ . '/data/bug-13232b.php'], [ + [ + 'Unreachable statement - code above always terminates.', + 19, + ], + ]); + } + + #[RequiresPhp('>= 8.1.0')] + public function testBug13232c(): void + { + $this->treatPhpDocTypesAsCertain = false; + $this->analyse([__DIR__ . '/data/bug-13232c.php'], [ + [ + 'Unreachable statement - code above always terminates.', + 12, + ], + [ + 'Unreachable statement - code above always terminates.', + 20, + ], + ]); + } + + #[RequiresPhp('>= 8.1.0')] + public function testBug13232d(): void + { + $this->treatPhpDocTypesAsCertain = false; + $this->analyse([__DIR__ . '/data/bug-13232d.php'], [ + [ + 'Unreachable statement - code above always terminates.', + 11, + ], + ]); + } + + #[RequiresPhp('>= 8.1.0')] + public function testBug13288(): void + { + $this->treatPhpDocTypesAsCertain = false; + $this->analyse([__DIR__ . '/data/bug-13288.php'], []); + } + + public function testBug13311(): void + { + $this->treatPhpDocTypesAsCertain = false; + $this->analyse([__DIR__ . '/data/bug-13311.php'], []); + } + + public function testBug13307(): void + { + $this->treatPhpDocTypesAsCertain = false; + $this->analyse([__DIR__ . '/data/bug-13307.php'], []); + } + + public function testBug13331(): void + { + $this->treatPhpDocTypesAsCertain = false; + $this->analyse([__DIR__ . '/data/bug-13331.php'], []); + } + + public function testBug15169(): void + { + $this->treatPhpDocTypesAsCertain = false; + $this->analyse([__DIR__ . '/data/bug-15169.php'], []); + } + + public function testBug15169Analogous(): void + { + $this->treatPhpDocTypesAsCertain = false; + $this->analyse([__DIR__ . '/data/bug-15169b.php'], []); + } + + #[RequiresPhp('>= 8.1.0')] + public function testBug15169FirstClassCallables(): void + { + $this->treatPhpDocTypesAsCertain = false; + $this->analyse([__DIR__ . '/data/bug-15169c.php'], []); + } + +} diff --git a/tests/PHPStan/Rules/DeadCode/UnusedVariableRuleTest.php b/tests/PHPStan/Rules/DeadCode/UnusedVariableRuleTest.php index 0df8d42932a..439b1afb2dc 100644 --- a/tests/PHPStan/Rules/DeadCode/UnusedVariableRuleTest.php +++ b/tests/PHPStan/Rules/DeadCode/UnusedVariableRuleTest.php @@ -13,13 +13,6 @@ class UnusedVariableRuleTest extends RuleTestCase { - private bool $polluteScopeWithAlwaysIterableForeach = true; - - protected function shouldPolluteScopeWithAlwaysIterableForeach(): bool - { - return $this->polluteScopeWithAlwaysIterableForeach; - } - protected function getRule(): Rule { return new UnusedVariableRule(self::getContainer()->getByType(ExprPrinter::class)); @@ -264,12 +257,6 @@ public function testResultFlow(): void ]); } - public function testForeachWithoutPollution(): void - { - $this->polluteScopeWithAlwaysIterableForeach = false; - $this->analyse([__DIR__ . '/data/unused-variable-foreach-pollution.php'], []); - } - public function testUsageFlow(): void { $this->analyse([__DIR__ . '/data/unused-variable-usage-flow.php'], []); diff --git a/tests/PHPStan/Rules/DeadCode/UnusedVariableRuleWithoutForeachPollutionTest.php b/tests/PHPStan/Rules/DeadCode/UnusedVariableRuleWithoutForeachPollutionTest.php new file mode 100644 index 00000000000..044e38339ce --- /dev/null +++ b/tests/PHPStan/Rules/DeadCode/UnusedVariableRuleWithoutForeachPollutionTest.php @@ -0,0 +1,36 @@ + + */ +class UnusedVariableRuleWithoutForeachPollutionTest extends RuleTestCase +{ + + protected function getRule(): Rule + { + return new UnusedVariableRule(self::getContainer()->getByType(ExprPrinter::class)); + } + + public static function getAdditionalConfigFiles(): array + { + return array_merge( + parent::getAdditionalConfigFiles(), + [ + __DIR__ . '/../without-foreach-pollution.neon', + ], + ); + } + + public function testForeachWithoutPollution(): void + { + $this->analyse([__DIR__ . '/data/unused-variable-foreach-pollution.php'], []); + } + +} diff --git a/tests/PHPStan/Rules/Properties/MissingReadOnlyByPhpDocPropertyAssignRuleTest.php b/tests/PHPStan/Rules/Properties/MissingReadOnlyByPhpDocPropertyAssignRuleTest.php index 7d296db1152..6b028fdade8 100644 --- a/tests/PHPStan/Rules/Properties/MissingReadOnlyByPhpDocPropertyAssignRuleTest.php +++ b/tests/PHPStan/Rules/Properties/MissingReadOnlyByPhpDocPropertyAssignRuleTest.php @@ -4,11 +4,10 @@ use PHPStan\Reflection\AdditionalConstructorsExtension; use PHPStan\Reflection\ConstructorsHelper; -use PHPStan\Reflection\PropertyReflection; use PHPStan\Rules\Rule; use PHPStan\Testing\RuleTestCase; use PHPUnit\Framework\Attributes\RequiresPhp; -use function in_array; +use function array_merge; /** * @extends RuleTestCase @@ -28,34 +27,14 @@ protected function getRule(): Rule ); } - protected function getReadWritePropertiesExtensions(): array + public static function getAdditionalConfigFiles(): array { - return [ - new class() implements ReadWritePropertiesExtension { - - public function isAlwaysRead(PropertyReflection $property, string $propertyName): bool - { - return $this->isEntityId($property, $propertyName); - } - - public function isAlwaysWritten(PropertyReflection $property, string $propertyName): bool - { - return $this->isEntityId($property, $propertyName); - } - - public function isInitialized(PropertyReflection $property, string $propertyName): bool - { - return $this->isEntityId($property, $propertyName); - } - - private function isEntityId(PropertyReflection $property, string $propertyName): bool - { - return $property->getDeclaringClass()->getName() === 'MissingReadOnlyPropertyAssignPhpDoc\\Entity' - && in_array($propertyName, ['id'], true); - } - - }, - ]; + return array_merge( + parent::getAdditionalConfigFiles(), + [ + __DIR__ . '/missing-readonly-by-phpdoc-property-assign-rule.neon', + ], + ); } public function testRule(): void diff --git a/tests/PHPStan/Rules/Properties/MissingReadOnlyPropertyAssignRuleTest.php b/tests/PHPStan/Rules/Properties/MissingReadOnlyPropertyAssignRuleTest.php index 6e9a1b98c9a..1e4af3d4f4a 100644 --- a/tests/PHPStan/Rules/Properties/MissingReadOnlyPropertyAssignRuleTest.php +++ b/tests/PHPStan/Rules/Properties/MissingReadOnlyPropertyAssignRuleTest.php @@ -4,12 +4,10 @@ use PHPStan\Reflection\AdditionalConstructorsExtension; use PHPStan\Reflection\ConstructorsHelper; -use PHPStan\Reflection\PropertyReflection; use PHPStan\Rules\Rule; use PHPStan\Testing\RuleTestCase; use PHPUnit\Framework\Attributes\RequiresPhp; -use function in_array; -use function strpos; +use function array_merge; /** * @extends RuleTestCase @@ -33,53 +31,14 @@ protected function getRule(): Rule ); } - protected function getReadWritePropertiesExtensions(): array + public static function getAdditionalConfigFiles(): array { - return [ - new class() implements ReadWritePropertiesExtension { - - public function isAlwaysRead(PropertyReflection $property, string $propertyName): bool - { - return $this->isEntityId($property, $propertyName); - } - - public function isAlwaysWritten(PropertyReflection $property, string $propertyName): bool - { - return $this->isEntityId($property, $propertyName); - } - - public function isInitialized(PropertyReflection $property, string $propertyName): bool - { - return $this->isEntityId($property, $propertyName); - } - - private function isEntityId(PropertyReflection $property, string $propertyName): bool - { - return $property->getDeclaringClass()->getName() === 'MissingReadOnlyPropertyAssign\\Entity' - && in_array($propertyName, ['id'], true); - } - - }, - new class() implements ReadWritePropertiesExtension { - - public function isAlwaysRead(PropertyReflection $property, string $propertyName): bool - { - return false; - } - - public function isAlwaysWritten(PropertyReflection $property, string $propertyName): bool - { - return $this->isInitialized($property, $propertyName); - } - - public function isInitialized(PropertyReflection $property, string $propertyName): bool - { - return $property->isPublic() && - strpos($property->getDocComment() ?? '', '@init') !== false; - } - - }, - ]; + return array_merge( + parent::getAdditionalConfigFiles(), + [ + __DIR__ . '/missing-readonly-property-assign-rule.neon', + ], + ); } #[RequiresPhp('>= 8.1.0')] diff --git a/tests/PHPStan/Rules/Properties/UninitializedPropertyRuleTest.php b/tests/PHPStan/Rules/Properties/UninitializedPropertyRuleTest.php index b89b1e75959..c0f6a1a9262 100644 --- a/tests/PHPStan/Rules/Properties/UninitializedPropertyRuleTest.php +++ b/tests/PHPStan/Rules/Properties/UninitializedPropertyRuleTest.php @@ -4,12 +4,10 @@ use PHPStan\Reflection\AdditionalConstructorsExtension; use PHPStan\Reflection\ConstructorsHelper; -use PHPStan\Reflection\PropertyReflection; use PHPStan\Rules\Rule; use PHPStan\Testing\RuleTestCase; use PHPUnit\Framework\Attributes\RequiresPhp; use function array_merge; -use function strpos; /** * @extends RuleTestCase @@ -33,51 +31,6 @@ protected function getRule(): Rule ); } - protected function getReadWritePropertiesExtensions(): array - { - return [ - new class() implements ReadWritePropertiesExtension { - - public function isAlwaysRead(PropertyReflection $property, string $propertyName): bool - { - return false; - } - - public function isAlwaysWritten(PropertyReflection $property, string $propertyName): bool - { - return false; - } - - public function isInitialized(PropertyReflection $property, string $propertyName): bool - { - return $property->getDeclaringClass()->getName() === 'UninitializedProperty\\TestExtension' && $propertyName === 'inited'; - } - - }, - - // bug-9619 - new class() implements ReadWritePropertiesExtension { - - public function isAlwaysRead(PropertyReflection $property, string $propertyName): bool - { - return false; - } - - public function isAlwaysWritten(PropertyReflection $property, string $propertyName): bool - { - return $this->isInitialized($property, $propertyName); - } - - public function isInitialized(PropertyReflection $property, string $propertyName): bool - { - return $property->isPublic() && - strpos($property->getDocComment() ?? '', '@inject') !== false; - } - - }, - ]; - } - public static function getAdditionalConfigFiles(): array { return array_merge( diff --git a/tests/PHPStan/Rules/Properties/data/EntityIdReadWritePropertiesExtension.php b/tests/PHPStan/Rules/Properties/data/EntityIdReadWritePropertiesExtension.php new file mode 100644 index 00000000000..116e81159e8 --- /dev/null +++ b/tests/PHPStan/Rules/Properties/data/EntityIdReadWritePropertiesExtension.php @@ -0,0 +1,32 @@ +isEntityId($property, $propertyName); + } + + public function isAlwaysWritten(PropertyReflection $property, string $propertyName): bool + { + return $this->isEntityId($property, $propertyName); + } + + public function isInitialized(PropertyReflection $property, string $propertyName): bool + { + return $this->isEntityId($property, $propertyName); + } + + private function isEntityId(PropertyReflection $property, string $propertyName): bool + { + return $property->getDeclaringClass()->getName() === 'MissingReadOnlyPropertyAssign\\Entity' + && in_array($propertyName, ['id'], true); + } + +} diff --git a/tests/PHPStan/Rules/Properties/data/InitAnnotationReadWritePropertiesExtension.php b/tests/PHPStan/Rules/Properties/data/InitAnnotationReadWritePropertiesExtension.php new file mode 100644 index 00000000000..30bc41b0ca9 --- /dev/null +++ b/tests/PHPStan/Rules/Properties/data/InitAnnotationReadWritePropertiesExtension.php @@ -0,0 +1,27 @@ +isInitialized($property, $propertyName); + } + + public function isInitialized(PropertyReflection $property, string $propertyName): bool + { + return $property->isPublic() && + strpos($property->getDocComment() ?? '', '@init') !== false; + } + +} diff --git a/tests/PHPStan/Rules/Properties/data/InitedPropertyReadWritePropertiesExtension.php b/tests/PHPStan/Rules/Properties/data/InitedPropertyReadWritePropertiesExtension.php new file mode 100644 index 00000000000..3271beb0a2d --- /dev/null +++ b/tests/PHPStan/Rules/Properties/data/InitedPropertyReadWritePropertiesExtension.php @@ -0,0 +1,25 @@ +getDeclaringClass()->getName() === 'UninitializedProperty\\TestExtension' && $propertyName === 'inited'; + } + +} diff --git a/tests/PHPStan/Rules/Properties/data/InjectAnnotationReadWritePropertiesExtension.php b/tests/PHPStan/Rules/Properties/data/InjectAnnotationReadWritePropertiesExtension.php new file mode 100644 index 00000000000..2d05c78539e --- /dev/null +++ b/tests/PHPStan/Rules/Properties/data/InjectAnnotationReadWritePropertiesExtension.php @@ -0,0 +1,27 @@ +isInitialized($property, $propertyName); + } + + public function isInitialized(PropertyReflection $property, string $propertyName): bool + { + return $property->isPublic() && + strpos($property->getDocComment() ?? '', '@inject') !== false; + } + +} diff --git a/tests/PHPStan/Rules/Properties/data/PhpDocEntityIdReadWritePropertiesExtension.php b/tests/PHPStan/Rules/Properties/data/PhpDocEntityIdReadWritePropertiesExtension.php new file mode 100644 index 00000000000..8f5bb0d1324 --- /dev/null +++ b/tests/PHPStan/Rules/Properties/data/PhpDocEntityIdReadWritePropertiesExtension.php @@ -0,0 +1,32 @@ +isEntityId($property, $propertyName); + } + + public function isAlwaysWritten(PropertyReflection $property, string $propertyName): bool + { + return $this->isEntityId($property, $propertyName); + } + + public function isInitialized(PropertyReflection $property, string $propertyName): bool + { + return $this->isEntityId($property, $propertyName); + } + + private function isEntityId(PropertyReflection $property, string $propertyName): bool + { + return $property->getDeclaringClass()->getName() === 'MissingReadOnlyPropertyAssignPhpDoc\\Entity' + && in_array($propertyName, ['id'], true); + } + +} diff --git a/tests/PHPStan/Rules/Properties/missing-readonly-by-phpdoc-property-assign-rule.neon b/tests/PHPStan/Rules/Properties/missing-readonly-by-phpdoc-property-assign-rule.neon new file mode 100644 index 00000000000..7e3c57da61e --- /dev/null +++ b/tests/PHPStan/Rules/Properties/missing-readonly-by-phpdoc-property-assign-rule.neon @@ -0,0 +1,5 @@ +services: + - + class: PHPStan\Rules\Properties\PhpDocEntityIdReadWritePropertiesExtension + tags: + - phpstan.properties.readWriteExtension diff --git a/tests/PHPStan/Rules/Properties/missing-readonly-property-assign-rule.neon b/tests/PHPStan/Rules/Properties/missing-readonly-property-assign-rule.neon new file mode 100644 index 00000000000..a41724bd7d7 --- /dev/null +++ b/tests/PHPStan/Rules/Properties/missing-readonly-property-assign-rule.neon @@ -0,0 +1,9 @@ +services: + - + class: PHPStan\Rules\Properties\EntityIdReadWritePropertiesExtension + tags: + - phpstan.properties.readWriteExtension + - + class: PHPStan\Rules\Properties\InitAnnotationReadWritePropertiesExtension + tags: + - phpstan.properties.readWriteExtension diff --git a/tests/PHPStan/Rules/Properties/uninitialized-property-rule.neon b/tests/PHPStan/Rules/Properties/uninitialized-property-rule.neon index 6362843ec84..552e3fdda76 100644 --- a/tests/PHPStan/Rules/Properties/uninitialized-property-rule.neon +++ b/tests/PHPStan/Rules/Properties/uninitialized-property-rule.neon @@ -3,3 +3,11 @@ services: class: PHPStan\Rules\Properties\TestInitializedProperty tags: - phpstan.additionalConstructorsExtension + - + class: PHPStan\Rules\Properties\InitedPropertyReadWritePropertiesExtension + tags: + - phpstan.properties.readWriteExtension + - + class: PHPStan\Rules\Properties\InjectAnnotationReadWritePropertiesExtension + tags: + - phpstan.properties.readWriteExtension diff --git a/tests/PHPStan/Rules/Variables/DefinedVariableRuleTest.php b/tests/PHPStan/Rules/Variables/DefinedVariableRuleTest.php index 97095f70866..c01fabdb376 100644 --- a/tests/PHPStan/Rules/Variables/DefinedVariableRuleTest.php +++ b/tests/PHPStan/Rules/Variables/DefinedVariableRuleTest.php @@ -17,10 +17,6 @@ class DefinedVariableRuleTest extends RuleTestCase private bool $checkMaybeUndefinedVariables; - private bool $polluteScopeWithLoopInitialAssignments; - - private bool $polluteScopeWithAlwaysIterableForeach; - protected function getRule(): Rule { return new DefinedVariableRule( @@ -29,313 +25,9 @@ protected function getRule(): Rule ); } - protected function shouldPolluteScopeWithLoopInitialAssignments(): bool - { - return $this->polluteScopeWithLoopInitialAssignments; - } - - protected function shouldPolluteScopeWithAlwaysIterableForeach(): bool - { - return $this->polluteScopeWithAlwaysIterableForeach; - } - - public function testDefinedVariables(): void - { - require_once __DIR__ . '/data/defined-variables-definition.php'; - $this->cliArgumentsVariablesRegistered = true; - $this->polluteScopeWithLoopInitialAssignments = false; - $this->checkMaybeUndefinedVariables = true; - $this->polluteScopeWithAlwaysIterableForeach = true; - $this->analyse([__DIR__ . '/data/defined-variables.php'], [ - [ - 'Undefined variable: $definedLater', - 5, - ], - [ - 'Variable $definedInIfOnly might not be defined.', - 10, - ], - [ - 'Variable $definedInCases might not be defined.', - 21, - ], - [ - 'Undefined variable: $fooParameterBeforeDeclaration', - 29, - ], - [ - 'Undefined variable: $parseStrParameter', - 34, - ], - [ - 'Undefined variable: $parseStrParameter', - 36, - ], - [ - 'Undefined variable: $foo', - 39, - ], - [ - 'Undefined variable: $willBeUnset', - 44, - ], - [ - 'Undefined variable: $mustAlreadyExistWhenDividing', - 50, - ], - [ - 'Undefined variable: $arrayDoesNotExist', - 57, - ], - [ - 'Undefined variable: $undefinedVariable', - 59, - ], - [ - 'Undefined variable: $this', - 96, - ], - [ - 'Undefined variable: $this', - 99, - ], - [ - 'Undefined variable: $variableInEmpty', - 145, - ], - [ - 'Undefined variable: $negatedVariableInEmpty', - 152, - ], - [ - 'Undefined variable: $variableInEmpty', - 155, - ], - [ - 'Undefined variable: $negatedVariableInEmpty', - 156, - ], - [ - 'Undefined variable: $variableInIsset', - 159, - ], - [ - 'Undefined variable: $anotherVariableInIsset', - 159, - ], - [ - 'Undefined variable: $variableInIsset', - 161, - ], - [ - 'Undefined variable: $anotherVariableInIsset', - 161, - ], - [ - 'Undefined variable: $http_response_header', - 185, - ], - [ - 'Undefined variable: $http_response_header', - 191, - ], - [ - 'Undefined variable: $assignedInKey', - 203, - ], - [ - 'Undefined variable: $assignedInKey', - 204, - ], - [ - 'Variable $forI might not be defined.', - 250, - ], - [ - 'Variable $forJ might not be defined.', - 251, - ], - [ - 'Variable $variableAvailableInAllCatches might not be defined.', - 266, - ], - [ - 'Variable $variableDefinedOnlyInOneCatch might not be defined.', - 267, - ], - [ - 'Undefined variable: $variableInBitwiseAndAssign', - 277, - ], - [ - 'Variable $mightBeUndefinedInDoWhile might not be defined.', - 282, - ], - [ - 'Undefined variable: $variableInSecondCase', - 290, - ], - [ - 'Variable $variableInSecondCase might not be defined.', - 293, - ], - [ - 'Undefined variable: $variableAssignedInSecondCase', - 300, - ], - [ - 'Variable $variableInFallthroughCase might not be defined.', - 302, - ], - [ - 'Variable $variableFromDefaultFirst might not be defined.', - 312, - ], - [ - 'Undefined variable: $undefinedVariableInForeach', - 315, - ], - [ - 'Variable $anotherForLoopVariable might not be defined.', - 328, - ], - [ - 'Variable $maybeDefinedInTernary might not be defined.', - 351, - ], - [ - 'Variable $anotherMaybeDefinedInTernary might not be defined.', - 354, - ], - [ - 'Variable $whileVariableUsedAndThenDefined might not be defined.', - 356, - ], - [ - 'Variable $forVariableUsedAndThenDefined might not be defined.', - 360, - ], - [ - 'Variable $unknownVariablePassedToReset might not be defined.', - 368, - ], - [ - 'Variable $unknownVariablePassedToReset might not be defined.', - 369, - ], - [ - 'Variable $variableInAssign might not be defined.', - 384, - ], - [ - 'Variable $undefinedArrayIndex might not be defined.', - 409, - ], - [ - 'Variable $anotherUndefinedArrayIndex might not be defined.', - 409, - ], - [ - 'Variable $str might not be defined.', - 423, - ], - [ - 'Variable $str might not be defined.', - 428, - ], - ]); - } - - public function testDefinedVariablesInClosures(): void - { - $this->cliArgumentsVariablesRegistered = true; - $this->polluteScopeWithLoopInitialAssignments = false; - $this->checkMaybeUndefinedVariables = true; - $this->polluteScopeWithAlwaysIterableForeach = true; - $this->analyse([__DIR__ . '/data/defined-variables-closures.php'], [ - [ - 'Undefined variable: $this', - 14, - ], - ]); - } - - public function testDefinedVariablesInShortArrayDestructuringSyntax(): void - { - $this->cliArgumentsVariablesRegistered = true; - $this->polluteScopeWithLoopInitialAssignments = false; - $this->checkMaybeUndefinedVariables = true; - $this->polluteScopeWithAlwaysIterableForeach = true; - $this->analyse([__DIR__ . '/data/defined-variables-array-destructuring-short-syntax.php'], [ - [ - 'Undefined variable: $f', - 11, - ], - [ - 'Undefined variable: $f', - 14, - ], - [ - 'Undefined variable: $var3', - 32, - ], - ]); - } - - public function testCliArgumentsVariablesNotRegistered(): void - { - $this->cliArgumentsVariablesRegistered = false; - $this->polluteScopeWithLoopInitialAssignments = false; - $this->checkMaybeUndefinedVariables = true; - $this->polluteScopeWithAlwaysIterableForeach = true; - $this->analyse([__DIR__ . '/data/cli-arguments-variables.php'], [ - [ - 'Variable $argc might not be defined.', - 3, - ], - [ - 'Undefined variable: $argc', - 5, - ], - ]); - } - - public function testCliArgumentsVariablesRegistered(): void - { - $this->cliArgumentsVariablesRegistered = true; - $this->polluteScopeWithLoopInitialAssignments = false; - $this->checkMaybeUndefinedVariables = true; - $this->polluteScopeWithAlwaysIterableForeach = true; - $this->analyse([__DIR__ . '/data/cli-arguments-variables.php'], [ - [ - 'Undefined variable: $argc', - 5, - ], - ]); - } - public static function dataLoopInitialAssignments(): array { return [ - [ - false, - false, - [], - ], - [ - false, - true, - [ - [ - 'Variable $i might not be defined.', - 7, - ], - [ - 'Variable $whileVar might not be defined.', - 13, - ], - ], - ], [ true, false, @@ -358,632 +50,53 @@ public function testLoopInitialAssignments( bool $checkMaybeUndefinedVariables, array $expectedErrors, ): void - { - $this->cliArgumentsVariablesRegistered = false; - $this->polluteScopeWithLoopInitialAssignments = $polluteScopeWithLoopInitialAssignments; - $this->checkMaybeUndefinedVariables = $checkMaybeUndefinedVariables; - $this->polluteScopeWithAlwaysIterableForeach = true; - $this->analyse([__DIR__ . '/data/loop-initial-assignments.php'], $expectedErrors); - } - - public function testDefineVariablesInClass(): void - { - $this->cliArgumentsVariablesRegistered = true; - $this->polluteScopeWithLoopInitialAssignments = false; - $this->checkMaybeUndefinedVariables = true; - $this->polluteScopeWithAlwaysIterableForeach = true; - $this->analyse([__DIR__ . '/data/define-variables-class.php'], []); - } - - public function testDeadBranches(): void - { - $this->cliArgumentsVariablesRegistered = true; - $this->polluteScopeWithLoopInitialAssignments = false; - $this->checkMaybeUndefinedVariables = true; - $this->polluteScopeWithAlwaysIterableForeach = true; - $this->analyse([__DIR__ . '/data/dead-branches.php'], [ - [ - 'Undefined variable: $test', - 21, - ], - [ - 'Undefined variable: $test', - 33, - ], - [ - 'Undefined variable: $test', - 55, - ], - [ - 'Undefined variable: $test', - 66, - ], - [ - 'Undefined variable: $test', - 94, - ], - ]); - } - - public function testForeach(): void - { - $this->cliArgumentsVariablesRegistered = true; - $this->polluteScopeWithLoopInitialAssignments = false; - $this->checkMaybeUndefinedVariables = true; - $this->polluteScopeWithAlwaysIterableForeach = true; - $this->analyse([__DIR__ . '/data/foreach.php'], [ - [ - 'Variable $val might not be defined.', - 9, - ], - [ - 'Variable $test might not be defined.', - 10, - ], - [ - 'Undefined variable: $val', - 46, - ], - [ - 'Undefined variable: $test', - 47, - ], - [ - 'Variable $val might not be defined.', - 62, - ], - [ - 'Variable $test might not be defined.', - 63, - ], - [ - 'Undefined variable: $val', - 200, - ], - [ - 'Undefined variable: $test', - 201, - ], - [ - 'Undefined variable: $val', - 216, - ], - [ - 'Undefined variable: $test', - 217, - ], - [ - 'Variable $val might not be defined.', - 246, - ], - [ - 'Variable $test might not be defined.', - 247, - ], - ]); - } - - public static function dataForeachPolluteScopeWithAlwaysIterableForeach(): array - { - return [ - [ - true, - [ - [ - 'Undefined variable: $key', - 8, - ], - [ - 'Undefined variable: $val', - 9, - ], - [ - 'Undefined variable: $test', - 10, - ], - [ - 'Variable $test might not be defined.', - 34, - ], - [ - 'Variable $key might not be defined.', - 47, - ], - [ - 'Variable $test might not be defined.', - 48, - ], - [ - 'Variable $key might not be defined.', - 61, - ], - [ - 'Variable $test might not be defined.', - 62, - ], - ], - ], - [ - false, - [ - [ - 'Undefined variable: $key', - 8, - ], - [ - 'Undefined variable: $val', - 9, - ], - [ - 'Undefined variable: $test', - 10, - ], - [ - 'Variable $key might not be defined.', - 19, - ], - [ - 'Variable $val might not be defined.', - 20, - ], - [ - 'Variable $test might not be defined.', - 21, - ], - [ - 'Variable $key might not be defined.', - 32, - ], - [ - 'Variable $val might not be defined.', - 33, - ], - [ - 'Variable $test might not be defined.', - 34, - ], - [ - 'Variable $key might not be defined.', - 47, - ], - [ - 'Variable $test might not be defined.', - 48, - ], - [ - 'Variable $key might not be defined.', - 61, - ], - [ - 'Variable $test might not be defined.', - 62, - ], - [ - 'Variable $key might not be defined.', - 75, - ], - [ - 'Variable $test might not be defined.', - 76, - ], - ], - ], - ]; - } - - /** - * @param list $errors - */ - #[DataProvider('dataForeachPolluteScopeWithAlwaysIterableForeach')] - public function testForeachPolluteScopeWithAlwaysIterableForeach(bool $polluteScopeWithAlwaysIterableForeach, array $errors): void - { - $this->cliArgumentsVariablesRegistered = true; - $this->polluteScopeWithLoopInitialAssignments = false; - $this->checkMaybeUndefinedVariables = true; - $this->polluteScopeWithAlwaysIterableForeach = $polluteScopeWithAlwaysIterableForeach; - $this->analyse([__DIR__ . '/data/foreach-always-iterable.php'], $errors); - } - - public function testBooleanOperatorsTruthyFalsey(): void - { - $this->cliArgumentsVariablesRegistered = true; - $this->polluteScopeWithLoopInitialAssignments = false; - $this->checkMaybeUndefinedVariables = true; - $this->polluteScopeWithAlwaysIterableForeach = true; - $this->analyse([__DIR__ . '/data/boolean-op-truthy-falsey.php'], [ - [ - 'Variable $matches might not be defined.', - 9, - ], - [ - 'Variable $matches might not be defined.', - 15, - ], - ]); - } - - public function testArrowFunctions(): void - { - $this->cliArgumentsVariablesRegistered = true; - $this->polluteScopeWithLoopInitialAssignments = false; - $this->checkMaybeUndefinedVariables = true; - $this->polluteScopeWithAlwaysIterableForeach = true; - $this->analyse([__DIR__ . '/data/defined-variables-arrow-functions.php'], [ - [ - 'Undefined variable: $a', - 10, - ], - [ - 'Undefined variable: $this', - 19, - ], - ]); - } - - public function testCoalesceAssign(): void - { - $this->cliArgumentsVariablesRegistered = true; - $this->polluteScopeWithLoopInitialAssignments = false; - $this->checkMaybeUndefinedVariables = true; - $this->polluteScopeWithAlwaysIterableForeach = true; - $this->analyse([__DIR__ . '/data/defined-variables-coalesce-assign.php'], [ - [ - 'Undefined variable: $b', - 16, - ], - ]); - } - - public function testBug2748(): void - { - $this->cliArgumentsVariablesRegistered = true; - $this->polluteScopeWithLoopInitialAssignments = false; - $this->checkMaybeUndefinedVariables = true; - $this->polluteScopeWithAlwaysIterableForeach = true; - $this->analyse([__DIR__ . '/data/bug-2748.php'], [ - [ - 'Undefined variable: $foo', - 10, - ], - [ - 'Undefined variable: $foo', - 15, - ], - ]); - } - - public function testGlobalVariables(): void - { - $this->cliArgumentsVariablesRegistered = true; - $this->polluteScopeWithLoopInitialAssignments = false; - $this->checkMaybeUndefinedVariables = true; - $this->polluteScopeWithAlwaysIterableForeach = true; - $this->analyse([__DIR__ . '/data/global-variables.php'], []); - } - - public function testRootScopeMaybeDefined(): void - { - $this->cliArgumentsVariablesRegistered = true; - $this->polluteScopeWithLoopInitialAssignments = false; - $this->checkMaybeUndefinedVariables = false; - $this->polluteScopeWithAlwaysIterableForeach = true; - $this->analyse([__DIR__ . '/data/root-scope-maybe.php'], []); - } - - public function testRootScopeMaybeDefinedCheck(): void - { - $this->cliArgumentsVariablesRegistered = true; - $this->polluteScopeWithLoopInitialAssignments = false; - $this->checkMaybeUndefinedVariables = true; - $this->polluteScopeWithAlwaysIterableForeach = true; - $this->analyse([__DIR__ . '/data/root-scope-maybe.php'], [ - [ - 'Variable $maybe might not be defined.', - 3, - ], - [ - 'Variable $this might not be defined.', - 5, - ], - ]); - } - - public function testFormerThisVariableRule(): void - { - $this->cliArgumentsVariablesRegistered = true; - $this->polluteScopeWithLoopInitialAssignments = false; - $this->checkMaybeUndefinedVariables = true; - $this->polluteScopeWithAlwaysIterableForeach = true; - $this->analyse([__DIR__ . '/data/this.php'], [ - [ - 'Undefined variable: $this', - 16, - ], - [ - 'Undefined variable: $this', - 20, - ], - [ - 'Undefined variable: $this', - 26, - ], - [ - 'Undefined variable: $this', - 38, - ], - ]); - } - - public function testClosureUse(): void - { - $this->cliArgumentsVariablesRegistered = true; - $this->polluteScopeWithLoopInitialAssignments = false; - $this->checkMaybeUndefinedVariables = true; - $this->polluteScopeWithAlwaysIterableForeach = true; - $this->analyse([__DIR__ . '/data/defined-variables-anonymous-function-use.php'], [ - [ - 'Variable $bar might not be defined.', - 5, - ], - [ - 'Variable $wrongErrorHandler might not be defined.', - 22, - ], - [ - 'Variable $onlyInIf might not be defined.', - 23, - ], - [ - 'Variable $forI might not be defined.', - 24, - ], - [ - 'Variable $forJ might not be defined.', - 25, - ], - [ - 'Variable $anotherVariableFromForCond might not be defined.', - 26, - ], - ]); - } - - public function testNullsafeIsset(): void - { - $this->cliArgumentsVariablesRegistered = true; - $this->polluteScopeWithLoopInitialAssignments = false; - $this->checkMaybeUndefinedVariables = true; - $this->polluteScopeWithAlwaysIterableForeach = true; - $this->analyse([__DIR__ . '/data/variable-nullsafe-isset.php'], []); - } - - #[RequiresPhp('>= 8.0.0')] - public function testBug7291(): void - { - $this->cliArgumentsVariablesRegistered = true; - $this->polluteScopeWithLoopInitialAssignments = false; - $this->checkMaybeUndefinedVariables = true; - $this->polluteScopeWithAlwaysIterableForeach = true; - $this->analyse([__DIR__ . '/data/bug-7291.php'], [ - [ - 'Variable $a might not be defined.', - 23, - ], - [ - 'Variable $b might not be defined.', - 32, - ], - [ - 'Variable $c might not be defined.', - 41, - ], - [ - 'Variable $d might not be defined.', - 50, - ], - ]); - } - - public function testBug1306(): void - { - $this->cliArgumentsVariablesRegistered = true; - $this->polluteScopeWithLoopInitialAssignments = false; - $this->checkMaybeUndefinedVariables = true; - $this->polluteScopeWithAlwaysIterableForeach = true; - $this->analyse([__DIR__ . '/data/bug-1306.php'], []); - } - - public function testBug3515(): void - { - $this->cliArgumentsVariablesRegistered = true; - $this->polluteScopeWithLoopInitialAssignments = false; - $this->checkMaybeUndefinedVariables = true; - $this->polluteScopeWithAlwaysIterableForeach = true; - $this->analyse([__DIR__ . '/data/bug-3515.php'], [ - [ - 'Undefined variable: $anArray', - 19, - ], - [ - 'Undefined variable: $anArray', - 20, - ], - ]); - } - - public function testBug4412(): void - { - $this->cliArgumentsVariablesRegistered = true; - $this->polluteScopeWithLoopInitialAssignments = false; - $this->checkMaybeUndefinedVariables = true; - $this->polluteScopeWithAlwaysIterableForeach = true; - $this->analyse([__DIR__ . '/data/bug-4412.php'], [ - [ - 'Undefined variable: $a', - 17, - ], - ]); - } - - public function testBug3283(): void - { - $this->cliArgumentsVariablesRegistered = true; - $this->polluteScopeWithLoopInitialAssignments = false; - $this->checkMaybeUndefinedVariables = true; - $this->polluteScopeWithAlwaysIterableForeach = true; - $this->analyse([__DIR__ . '/data/bug-3283.php'], []); - } - - public function testFirstClassCallables(): void - { - $this->cliArgumentsVariablesRegistered = true; - $this->polluteScopeWithLoopInitialAssignments = false; - $this->checkMaybeUndefinedVariables = true; - $this->polluteScopeWithAlwaysIterableForeach = true; - $this->analyse([__DIR__ . '/data/first-class-callables.php'], [ - [ - 'Undefined variable: $foo', - 10, - ], - [ - 'Undefined variable: $foo', - 11, - ], - [ - 'Undefined variable: $foo', - 29, - ], - [ - 'Undefined variable: $foo', - 30, - ], - [ - 'Undefined variable: $foo', - 48, - ], - [ - 'Undefined variable: $foo', - 49, - ], - ]); - } - - public function testBug6112(): void - { - $this->cliArgumentsVariablesRegistered = true; - $this->polluteScopeWithLoopInitialAssignments = false; - $this->checkMaybeUndefinedVariables = true; - $this->polluteScopeWithAlwaysIterableForeach = true; - $this->analyse([__DIR__ . '/data/bug-6112.php'], []); - } - - public function testBug3601(): void - { - $this->cliArgumentsVariablesRegistered = true; - $this->polluteScopeWithLoopInitialAssignments = true; - $this->checkMaybeUndefinedVariables = true; - $this->polluteScopeWithAlwaysIterableForeach = true; - $this->analyse([__DIR__ . '/data/bug-3601.php'], []); - } - - public function testBug1016(): void - { - $this->cliArgumentsVariablesRegistered = true; - $this->polluteScopeWithLoopInitialAssignments = true; - $this->checkMaybeUndefinedVariables = true; - $this->polluteScopeWithAlwaysIterableForeach = true; - $this->analyse([__DIR__ . '/data/bug-1016.php'], []); - } - - public function testBug1016b(): void - { - $this->cliArgumentsVariablesRegistered = true; - $this->polluteScopeWithLoopInitialAssignments = true; - $this->checkMaybeUndefinedVariables = true; - $this->polluteScopeWithAlwaysIterableForeach = true; - $this->analyse([__DIR__ . '/data/bug-1016b.php'], []); - } - - #[RequiresPhp('>= 8.0.0')] - public function testBug8142(): void - { - $this->cliArgumentsVariablesRegistered = true; - $this->polluteScopeWithLoopInitialAssignments = true; - $this->checkMaybeUndefinedVariables = true; - $this->polluteScopeWithAlwaysIterableForeach = true; - $this->analyse([__DIR__ . '/data/bug-8142.php'], []); - } - - public function testBug5401(): void - { - $this->cliArgumentsVariablesRegistered = true; - $this->polluteScopeWithLoopInitialAssignments = false; - $this->checkMaybeUndefinedVariables = true; - $this->polluteScopeWithAlwaysIterableForeach = true; - $this->analyse([__DIR__ . '/data/bug-5401.php'], []); + { + $this->cliArgumentsVariablesRegistered = false; + $this->checkMaybeUndefinedVariables = $checkMaybeUndefinedVariables; + $this->analyse([__DIR__ . '/data/loop-initial-assignments.php'], $expectedErrors); } - public function testBug8212(): void + public function testBug3601(): void { $this->cliArgumentsVariablesRegistered = true; - $this->polluteScopeWithLoopInitialAssignments = true; $this->checkMaybeUndefinedVariables = true; - $this->polluteScopeWithAlwaysIterableForeach = true; - $this->analyse([__DIR__ . '/data/bug-8212.php'], []); + $this->analyse([__DIR__ . '/data/bug-3601.php'], []); } - public function testBug4173(): void + public function testBug1016(): void { $this->cliArgumentsVariablesRegistered = true; - $this->polluteScopeWithLoopInitialAssignments = false; $this->checkMaybeUndefinedVariables = true; - $this->polluteScopeWithAlwaysIterableForeach = true; - $this->analyse([__DIR__ . '/data/bug-4173.php'], []); + $this->analyse([__DIR__ . '/data/bug-1016.php'], []); } - public function testBug5805(): void + public function testBug1016b(): void { $this->cliArgumentsVariablesRegistered = true; - $this->polluteScopeWithLoopInitialAssignments = false; $this->checkMaybeUndefinedVariables = true; - $this->polluteScopeWithAlwaysIterableForeach = true; - $this->analyse([__DIR__ . '/data/bug-5805.php'], []); + $this->analyse([__DIR__ . '/data/bug-1016b.php'], []); } - public function testBug8467c(): void + #[RequiresPhp('>= 8.0.0')] + public function testBug8142(): void { $this->cliArgumentsVariablesRegistered = true; - $this->polluteScopeWithLoopInitialAssignments = false; $this->checkMaybeUndefinedVariables = true; - $this->polluteScopeWithAlwaysIterableForeach = false; - $this->analyse([__DIR__ . '/data/bug-8467c.php'], [ - [ - 'Variable $v might not be defined.', - 16, - ], - [ - 'Variable $v might not be defined.', - 18, - ], - ]); + $this->analyse([__DIR__ . '/data/bug-8142.php'], []); } - public function testBug393(): void + public function testBug8212(): void { $this->cliArgumentsVariablesRegistered = true; - $this->polluteScopeWithLoopInitialAssignments = false; $this->checkMaybeUndefinedVariables = true; - $this->polluteScopeWithAlwaysIterableForeach = true; - $this->analyse([__DIR__ . '/data/bug-393.php'], []); + $this->analyse([__DIR__ . '/data/bug-8212.php'], []); } #[RequiresPhp('>= 8.1.0')] public function testBug9474(): void { $this->cliArgumentsVariablesRegistered = true; - $this->polluteScopeWithLoopInitialAssignments = true; $this->checkMaybeUndefinedVariables = true; - $this->polluteScopeWithAlwaysIterableForeach = true; $this->analyse([__DIR__ . '/data/bug-9474.php'], []); } @@ -991,36 +104,28 @@ public function testBug9474(): void public function testEnum(): void { $this->cliArgumentsVariablesRegistered = true; - $this->polluteScopeWithLoopInitialAssignments = true; $this->checkMaybeUndefinedVariables = true; - $this->polluteScopeWithAlwaysIterableForeach = true; $this->analyse([__DIR__ . '/data/defined-variables-enum.php'], []); } public function testBug5326(): void { $this->cliArgumentsVariablesRegistered = true; - $this->polluteScopeWithLoopInitialAssignments = true; $this->checkMaybeUndefinedVariables = true; - $this->polluteScopeWithAlwaysIterableForeach = true; $this->analyse([__DIR__ . '/data/bug-5326.php'], []); } public function testBug5266(): void { $this->cliArgumentsVariablesRegistered = true; - $this->polluteScopeWithLoopInitialAssignments = true; $this->checkMaybeUndefinedVariables = true; - $this->polluteScopeWithAlwaysIterableForeach = true; $this->analyse([__DIR__ . '/data/bug-5266.php'], []); } public function testIsStringNarrowsCertainty(): void { $this->cliArgumentsVariablesRegistered = true; - $this->polluteScopeWithLoopInitialAssignments = true; $this->checkMaybeUndefinedVariables = true; - $this->polluteScopeWithAlwaysIterableForeach = true; $this->analyse([__DIR__ . '/data/isstring-certainty.php'], [ [ 'Variable $a might not be defined.', @@ -1036,9 +141,7 @@ public function testIsStringNarrowsCertainty(): void public function testBug12364(): void { $this->cliArgumentsVariablesRegistered = true; - $this->polluteScopeWithLoopInitialAssignments = true; $this->checkMaybeUndefinedVariables = true; - $this->polluteScopeWithAlwaysIterableForeach = true; $this->analyse([__DIR__ . '/data/bug-12364.php'], [ [ 'Variable $z might not be defined.', @@ -1054,9 +157,7 @@ public function testBug12364(): void public function testDiscussion10252(): void { $this->cliArgumentsVariablesRegistered = true; - $this->polluteScopeWithLoopInitialAssignments = true; $this->checkMaybeUndefinedVariables = true; - $this->polluteScopeWithAlwaysIterableForeach = true; $this->analyse([__DIR__ . '/data/discussion-10252.php'], []); } @@ -1064,9 +165,7 @@ public function testDiscussion10252(): void public function testBug10418(): void { $this->cliArgumentsVariablesRegistered = true; - $this->polluteScopeWithLoopInitialAssignments = true; $this->checkMaybeUndefinedVariables = true; - $this->polluteScopeWithAlwaysIterableForeach = true; $this->analyse([__DIR__ . '/data/bug-10418.php'], []); } @@ -1074,9 +173,7 @@ public function testBug10418(): void public function testPassByReferenceIntoNotNullable(): void { $this->cliArgumentsVariablesRegistered = true; - $this->polluteScopeWithLoopInitialAssignments = true; $this->checkMaybeUndefinedVariables = true; - $this->polluteScopeWithAlwaysIterableForeach = true; $this->analyse([__DIR__ . '/data/pass-by-reference-into-not-nullable.php'], [ [ 'Undefined variable: $three', @@ -1088,18 +185,14 @@ public function testPassByReferenceIntoNotNullable(): void public function testBug10228(): void { $this->cliArgumentsVariablesRegistered = true; - $this->polluteScopeWithLoopInitialAssignments = true; $this->checkMaybeUndefinedVariables = true; - $this->polluteScopeWithAlwaysIterableForeach = true; $this->analyse([__DIR__ . '/data/bug-10228.php'], []); } public function testBug9426(): void { $this->cliArgumentsVariablesRegistered = true; - $this->polluteScopeWithLoopInitialAssignments = true; $this->checkMaybeUndefinedVariables = true; - $this->polluteScopeWithAlwaysIterableForeach = true; $this->analyse([__DIR__ . '/data/bug-9426.php'], []); } @@ -1107,9 +200,7 @@ public function testBug9426(): void public function testPropertyHooks(): void { $this->cliArgumentsVariablesRegistered = true; - $this->polluteScopeWithLoopInitialAssignments = true; $this->checkMaybeUndefinedVariables = true; - $this->polluteScopeWithAlwaysIterableForeach = true; $this->analyse([__DIR__ . '/data/property-hooks.php'], [ [ 'Undefined variable: $val', @@ -1133,9 +224,7 @@ public function testPropertyHooks(): void public function testDynamicAccess(): void { $this->cliArgumentsVariablesRegistered = true; - $this->polluteScopeWithLoopInitialAssignments = true; $this->checkMaybeUndefinedVariables = true; - $this->polluteScopeWithAlwaysIterableForeach = true; $this->analyse([__DIR__ . '/data/dynamic-access.php'], [ [ 'Undefined variable: $bar', @@ -1187,9 +276,7 @@ public function testDynamicAccess(): void public function testBug8719(): void { $this->cliArgumentsVariablesRegistered = true; - $this->polluteScopeWithLoopInitialAssignments = true; $this->checkMaybeUndefinedVariables = true; - $this->polluteScopeWithAlwaysIterableForeach = true; $this->analyse([__DIR__ . '/data/bug-8719.php'], []); } @@ -1198,9 +285,7 @@ public function testBug8719(): void public function testBug13353(): void { $this->cliArgumentsVariablesRegistered = true; - $this->polluteScopeWithLoopInitialAssignments = true; $this->checkMaybeUndefinedVariables = true; - $this->polluteScopeWithAlwaysIterableForeach = true; $this->analyse([__DIR__ . '/data/bug-13353.php'], []); } @@ -1208,9 +293,7 @@ public function testBug13353(): void public function testBug13694(): void { $this->cliArgumentsVariablesRegistered = true; - $this->polluteScopeWithLoopInitialAssignments = true; $this->checkMaybeUndefinedVariables = true; - $this->polluteScopeWithAlwaysIterableForeach = true; $this->analyse([__DIR__ . '/data/bug-13694.php'], []); } @@ -1219,9 +302,7 @@ public function testBug13694(): void public function testBug5191(): void { $this->cliArgumentsVariablesRegistered = true; - $this->polluteScopeWithLoopInitialAssignments = true; $this->checkMaybeUndefinedVariables = true; - $this->polluteScopeWithAlwaysIterableForeach = true; $this->analyse([__DIR__ . '/data/bug-5191.php'], [ [ @@ -1235,9 +316,7 @@ public function testBug5191(): void public function testBug10909(): void { $this->cliArgumentsVariablesRegistered = true; - $this->polluteScopeWithLoopInitialAssignments = true; $this->checkMaybeUndefinedVariables = true; - $this->polluteScopeWithAlwaysIterableForeach = true; $this->analyse([__DIR__ . '/data/bug-10909.php'], []); } @@ -1246,9 +325,7 @@ public function testBug10909(): void public function testBug13981(): void { $this->cliArgumentsVariablesRegistered = true; - $this->polluteScopeWithLoopInitialAssignments = true; $this->checkMaybeUndefinedVariables = true; - $this->polluteScopeWithAlwaysIterableForeach = true; $this->analyse([__DIR__ . '/data/bug-13981.php'], [ [ @@ -1266,9 +343,7 @@ public function testBug13981(): void public function testBug7705(): void { $this->cliArgumentsVariablesRegistered = true; - $this->polluteScopeWithLoopInitialAssignments = true; $this->checkMaybeUndefinedVariables = true; - $this->polluteScopeWithAlwaysIterableForeach = true; $this->analyse([__DIR__ . '/data/bug-7705.php'], []); } @@ -1277,40 +352,16 @@ public function testBug7705(): void public function testBug12944(): void { $this->cliArgumentsVariablesRegistered = true; - $this->polluteScopeWithLoopInitialAssignments = true; $this->checkMaybeUndefinedVariables = true; - $this->polluteScopeWithAlwaysIterableForeach = true; $this->analyse([__DIR__ . '/data/bug-12944.php'], []); } - public function testBug9023(): void - { - $this->cliArgumentsVariablesRegistered = true; - $this->polluteScopeWithLoopInitialAssignments = false; - $this->checkMaybeUndefinedVariables = true; - $this->polluteScopeWithAlwaysIterableForeach = true; - - $this->analyse([__DIR__ . '/data/bug-9023.php'], []); - } - - public function testBug11984(): void - { - $this->cliArgumentsVariablesRegistered = true; - $this->polluteScopeWithLoopInitialAssignments = false; - $this->checkMaybeUndefinedVariables = true; - $this->polluteScopeWithAlwaysIterableForeach = true; - - $this->analyse([__DIR__ . '/data/bug-11984.php'], []); - } - #[DataProvider('dataBug11545')] public function testBug11545(bool $polluteScopeWithLoopInitialAssignments): void { $this->cliArgumentsVariablesRegistered = true; - $this->polluteScopeWithLoopInitialAssignments = $polluteScopeWithLoopInitialAssignments; $this->checkMaybeUndefinedVariables = true; - $this->polluteScopeWithAlwaysIterableForeach = true; $errors = []; if (!$polluteScopeWithLoopInitialAssignments) { @@ -1326,294 +377,22 @@ public function testBug11545(bool $polluteScopeWithLoopInitialAssignments): void /** @return iterable */ public static function dataBug11545(): iterable { - yield [false]; yield [true]; } - public function testBug10245(): void - { - $this->cliArgumentsVariablesRegistered = true; - $this->polluteScopeWithLoopInitialAssignments = false; - $this->checkMaybeUndefinedVariables = true; - $this->polluteScopeWithAlwaysIterableForeach = true; - - $this->analyse([__DIR__ . '/data/bug-10245.php'], []); - } - - public function testBug5919(): void - { - $this->cliArgumentsVariablesRegistered = true; - $this->polluteScopeWithLoopInitialAssignments = false; - $this->checkMaybeUndefinedVariables = true; - $this->polluteScopeWithAlwaysIterableForeach = true; - - $this->analyse([__DIR__ . '/data/bug-5919.php'], []); - } - - public function testBug8430(): void - { - $this->cliArgumentsVariablesRegistered = true; - $this->polluteScopeWithLoopInitialAssignments = false; - $this->checkMaybeUndefinedVariables = true; - $this->polluteScopeWithAlwaysIterableForeach = true; - - $this->analyse([__DIR__ . '/data/bug-8430.php'], []); - } - - public function testBug8430b(): void - { - $this->cliArgumentsVariablesRegistered = true; - $this->polluteScopeWithLoopInitialAssignments = false; - $this->checkMaybeUndefinedVariables = true; - $this->polluteScopeWithAlwaysIterableForeach = true; - - $this->analyse([__DIR__ . '/data/bug-8430b.php'], []); - } - - public function testBug5477(): void - { - $this->cliArgumentsVariablesRegistered = true; - $this->polluteScopeWithLoopInitialAssignments = false; - $this->checkMaybeUndefinedVariables = true; - $this->polluteScopeWithAlwaysIterableForeach = true; - - $this->analyse([__DIR__ . '/data/bug-5477.php'], []); - } - - public function testBug10657(): void - { - $this->cliArgumentsVariablesRegistered = true; - $this->polluteScopeWithLoopInitialAssignments = false; - $this->checkMaybeUndefinedVariables = true; - $this->polluteScopeWithAlwaysIterableForeach = true; - - $this->analyse([__DIR__ . '/data/bug-10657.php'], []); - } - - public function testBug6830(): void - { - $this->cliArgumentsVariablesRegistered = true; - $this->polluteScopeWithLoopInitialAssignments = false; - $this->checkMaybeUndefinedVariables = true; - $this->polluteScopeWithAlwaysIterableForeach = true; - - $this->analyse([__DIR__ . '/data/bug-6830.php'], []); - } - - public function testBug14019(): void - { - $this->cliArgumentsVariablesRegistered = true; - $this->polluteScopeWithLoopInitialAssignments = false; - $this->checkMaybeUndefinedVariables = true; - $this->polluteScopeWithAlwaysIterableForeach = true; - - $this->analyse([__DIR__ . '/../../Analyser/nsrt/bug-14019.php'], []); - } - - public function testBug9349(): void - { - $this->cliArgumentsVariablesRegistered = true; - $this->polluteScopeWithLoopInitialAssignments = false; - $this->checkMaybeUndefinedVariables = true; - $this->polluteScopeWithAlwaysIterableForeach = true; - - $this->analyse([__DIR__ . '/data/bug-9349.php'], [ - [ - 'Variable $sql might not be defined.', - 19, - ], - [ - 'Variable $sql might not be defined.', - 78, - ], - ]); - } - - public function testBug14318(): void - { - $this->cliArgumentsVariablesRegistered = true; - $this->polluteScopeWithLoopInitialAssignments = false; - $this->checkMaybeUndefinedVariables = true; - $this->polluteScopeWithAlwaysIterableForeach = true; - - $this->analyse([__DIR__ . '/data/bug-14318.php'], []); - } - - public function testBug14323(): void - { - $this->cliArgumentsVariablesRegistered = true; - $this->polluteScopeWithLoopInitialAssignments = false; - $this->checkMaybeUndefinedVariables = true; - $this->polluteScopeWithAlwaysIterableForeach = true; - - $this->analyse([__DIR__ . '/data/bug-14323.php'], [ - [ - 'Variable $command might not be defined.', - 24, - ], - [ - 'Variable $command might not be defined.', - 50, - ], - [ - 'Variable $command might not be defined.', - 119, - ], - ]); - } - - #[RequiresPhp('>= 8.0.0')] - public function testBug14274(): void - { - $this->cliArgumentsVariablesRegistered = true; - $this->polluteScopeWithLoopInitialAssignments = false; - $this->checkMaybeUndefinedVariables = true; - $this->polluteScopeWithAlwaysIterableForeach = true; - - $this->analyse([__DIR__ . '/data/bug-14274.php'], []); - } - - #[RequiresPhp('>= 8.0.0')] - public function testBug12373(): void - { - $this->cliArgumentsVariablesRegistered = true; - $this->polluteScopeWithLoopInitialAssignments = false; - $this->checkMaybeUndefinedVariables = true; - $this->polluteScopeWithAlwaysIterableForeach = true; - - $this->analyse([__DIR__ . '/data/bug-12373.php'], []); - } - public function testBug13920(): void { $this->cliArgumentsVariablesRegistered = true; - $this->polluteScopeWithLoopInitialAssignments = true; $this->checkMaybeUndefinedVariables = true; - $this->polluteScopeWithAlwaysIterableForeach = true; $this->analyse([__DIR__ . '/data/bug-13920.php'], []); } - public function testBug12992(): void - { - $this->cliArgumentsVariablesRegistered = true; - $this->polluteScopeWithLoopInitialAssignments = false; - $this->checkMaybeUndefinedVariables = true; - $this->polluteScopeWithAlwaysIterableForeach = true; - $this->analyse([__DIR__ . '/data/bug-12992.php'], []); - } - - public function testBug14227(): void - { - $this->cliArgumentsVariablesRegistered = true; - $this->polluteScopeWithLoopInitialAssignments = false; - $this->checkMaybeUndefinedVariables = true; - $this->polluteScopeWithAlwaysIterableForeach = true; - $this->analyse([__DIR__ . '/data/bug-14227.php'], []); - } - - public function testBug14117(): void - { - $this->cliArgumentsVariablesRegistered = true; - $this->polluteScopeWithLoopInitialAssignments = false; - $this->checkMaybeUndefinedVariables = true; - $this->polluteScopeWithAlwaysIterableForeach = true; - - $this->analyse([__DIR__ . '/data/bug-14117.php'], [ - [ - 'Variable $value might not be defined.', - 49, - ], - [ - 'Undefined variable: $value', - 65, - ], - [ - 'Undefined variable: $value', - 81, - ], - ]); - } - - public function testBug12597(): void - { - $this->cliArgumentsVariablesRegistered = true; - $this->polluteScopeWithLoopInitialAssignments = false; - $this->checkMaybeUndefinedVariables = true; - $this->polluteScopeWithAlwaysIterableForeach = true; - $this->analyse([__DIR__ . '/data/bug-12597.php'], []); - } - - public function testBug11218(): void - { - $this->cliArgumentsVariablesRegistered = true; - $this->polluteScopeWithLoopInitialAssignments = false; - $this->checkMaybeUndefinedVariables = true; - $this->polluteScopeWithAlwaysIterableForeach = true; - $this->analyse([__DIR__ . '/data/bug-11218.php'], []); - } - - public function testBug4352(): void - { - $this->cliArgumentsVariablesRegistered = true; - $this->polluteScopeWithLoopInitialAssignments = false; - $this->checkMaybeUndefinedVariables = true; - $this->polluteScopeWithAlwaysIterableForeach = true; - $this->analyse([__DIR__ . '/data/bug-4352.php'], []); - } - - public function testBug6688(): void - { - $this->cliArgumentsVariablesRegistered = true; - $this->polluteScopeWithLoopInitialAssignments = false; - $this->checkMaybeUndefinedVariables = true; - $this->polluteScopeWithAlwaysIterableForeach = true; - $this->analyse([__DIR__ . '/data/bug-6688.php'], [ - [ - 'Variable $a might not be defined.', - 5, - ], - ]); - } - - #[RequiresPhp('>= 8.0.0')] - public function testBug10729(): void - { - $this->cliArgumentsVariablesRegistered = true; - $this->polluteScopeWithLoopInitialAssignments = false; - $this->checkMaybeUndefinedVariables = true; - $this->polluteScopeWithAlwaysIterableForeach = true; - $this->analyse([__DIR__ . '/data/bug-10729.php'], [ - [ - 'Variable $format might not be defined.', - 12, - ], - [ - 'Undefined variable: $format', - 25, - ], - [ - 'Variable $format might not be defined.', - 31, - ], - [ - 'Variable $value might not be defined.', - 32, - ], - [ - 'Variable $format might not be defined.', - 38, - ], - ]); - } - #[RequiresPhp('>= 8.0.0')] public function testBug6833(): void { $this->cliArgumentsVariablesRegistered = true; - $this->polluteScopeWithLoopInitialAssignments = true; $this->checkMaybeUndefinedVariables = true; - $this->polluteScopeWithAlwaysIterableForeach = true; $this->analyse([__DIR__ . '/../../Analyser/nsrt/bug-6833.php'], [ [ 'Variable $file might not be defined.', @@ -1666,114 +445,14 @@ public function testBug6833(): void public function testBug9392(): void { $this->cliArgumentsVariablesRegistered = true; - $this->polluteScopeWithLoopInitialAssignments = true; $this->checkMaybeUndefinedVariables = true; - $this->polluteScopeWithAlwaysIterableForeach = true; $this->analyse([__DIR__ . '/data/bug-9392.php'], []); } - public function testBug10090(): void - { - $this->cliArgumentsVariablesRegistered = true; - $this->polluteScopeWithLoopInitialAssignments = false; - $this->checkMaybeUndefinedVariables = true; - $this->polluteScopeWithAlwaysIterableForeach = true; - $this->analyse([__DIR__ . '/../../Analyser/nsrt/bug-10090.php'], [ - [ - 'Undefined variable: $shortcut_id', - 18, - ], - [ - 'Undefined variable: $shortcut_id', - 19, - ], - [ - 'Variable $x might not be defined.', - 35, - ], - [ - 'Variable $x might not be defined.', - 36, - ], - [ - 'Variable $x might not be defined.', - 80, - ], - [ - 'Variable $x might not be defined.', - 81, - ], - [ - 'Undefined variable: $x', - 96, - ], - ]); - } - - public function testBug2032(): void - { - $this->cliArgumentsVariablesRegistered = true; - $this->polluteScopeWithLoopInitialAssignments = false; - $this->checkMaybeUndefinedVariables = true; - $this->polluteScopeWithAlwaysIterableForeach = true; - $this->analyse([__DIR__ . '/data/bug-2032.php'], [ - [ - 'Undefined variable: $undefined', - 6, - ], - [ - 'Undefined variable: $undefined', - 9, - ], - [ - 'Undefined variable: $undefined', - 15, - ], - ]); - } - - public function testBug13833(): void - { - $this->cliArgumentsVariablesRegistered = true; - $this->polluteScopeWithLoopInitialAssignments = false; - $this->checkMaybeUndefinedVariables = true; - $this->polluteScopeWithAlwaysIterableForeach = true; - $this->analyse([__DIR__ . '/data/bug-13833.php'], []); - } - - public function testBug9685(): void - { - $this->cliArgumentsVariablesRegistered = true; - $this->polluteScopeWithLoopInitialAssignments = false; - $this->checkMaybeUndefinedVariables = true; - $this->polluteScopeWithAlwaysIterableForeach = true; - $this->analyse([__DIR__ . '/data/bug-9685.php'], []); - } - - public function testBug7706(): void - { - $this->cliArgumentsVariablesRegistered = true; - $this->polluteScopeWithLoopInitialAssignments = false; - $this->checkMaybeUndefinedVariables = true; - $this->polluteScopeWithAlwaysIterableForeach = true; - $this->analyse([__DIR__ . '/data/bug-7706.php'], []); - } - - public function testBug8360(): void - { - $this->cliArgumentsVariablesRegistered = true; - $this->polluteScopeWithLoopInitialAssignments = false; - $this->checkMaybeUndefinedVariables = true; - $this->polluteScopeWithAlwaysIterableForeach = true; - $this->analyse([__DIR__ . '/data/bug-8360.php'], []); - } - public function testBug14418(): void { $this->cliArgumentsVariablesRegistered = true; - $this->polluteScopeWithLoopInitialAssignments = true; $this->checkMaybeUndefinedVariables = true; - $this->polluteScopeWithAlwaysIterableForeach = true; $this->analyse([__DIR__ . '/data/bug-14418.php'], []); } diff --git a/tests/PHPStan/Rules/Variables/DefinedVariableRuleWithoutLoopAndForeachPollutionTest.php b/tests/PHPStan/Rules/Variables/DefinedVariableRuleWithoutLoopAndForeachPollutionTest.php new file mode 100644 index 00000000000..205bb1c0cfc --- /dev/null +++ b/tests/PHPStan/Rules/Variables/DefinedVariableRuleWithoutLoopAndForeachPollutionTest.php @@ -0,0 +1,129 @@ + + */ +class DefinedVariableRuleWithoutLoopAndForeachPollutionTest extends RuleTestCase +{ + + protected function getRule(): Rule + { + return new DefinedVariableRule( + true, + true, + ); + } + + public static function getAdditionalConfigFiles(): array + { + return array_merge( + parent::getAdditionalConfigFiles(), + [ + __DIR__ . '/../without-loop-pollution.neon', + __DIR__ . '/../without-foreach-pollution.neon', + ], + ); + } + + public static function dataForeachPolluteScopeWithAlwaysIterableForeach(): array + { + return [ + [ + false, + [ + [ + 'Undefined variable: $key', + 8, + ], + [ + 'Undefined variable: $val', + 9, + ], + [ + 'Undefined variable: $test', + 10, + ], + [ + 'Variable $key might not be defined.', + 19, + ], + [ + 'Variable $val might not be defined.', + 20, + ], + [ + 'Variable $test might not be defined.', + 21, + ], + [ + 'Variable $key might not be defined.', + 32, + ], + [ + 'Variable $val might not be defined.', + 33, + ], + [ + 'Variable $test might not be defined.', + 34, + ], + [ + 'Variable $key might not be defined.', + 47, + ], + [ + 'Variable $test might not be defined.', + 48, + ], + [ + 'Variable $key might not be defined.', + 61, + ], + [ + 'Variable $test might not be defined.', + 62, + ], + [ + 'Variable $key might not be defined.', + 75, + ], + [ + 'Variable $test might not be defined.', + 76, + ], + ], + ], + ]; + } + + /** + * @param list $errors + */ + #[DataProvider('dataForeachPolluteScopeWithAlwaysIterableForeach')] + public function testForeachPolluteScopeWithAlwaysIterableForeach(bool $polluteScopeWithAlwaysIterableForeach, array $errors): void + { + $this->analyse([__DIR__ . '/data/foreach-always-iterable.php'], $errors); + } + + public function testBug8467c(): void + { + $this->analyse([__DIR__ . '/data/bug-8467c.php'], [ + [ + 'Variable $v might not be defined.', + 16, + ], + [ + 'Variable $v might not be defined.', + 18, + ], + ]); + } + +} diff --git a/tests/PHPStan/Rules/Variables/DefinedVariableRuleWithoutLoopPollutionTest.php b/tests/PHPStan/Rules/Variables/DefinedVariableRuleWithoutLoopPollutionTest.php new file mode 100644 index 00000000000..a8c90ab2f9f --- /dev/null +++ b/tests/PHPStan/Rules/Variables/DefinedVariableRuleWithoutLoopPollutionTest.php @@ -0,0 +1,1124 @@ + + */ +class DefinedVariableRuleWithoutLoopPollutionTest extends RuleTestCase +{ + + private bool $cliArgumentsVariablesRegistered; + + private bool $checkMaybeUndefinedVariables; + + protected function getRule(): Rule + { + return new DefinedVariableRule( + $this->cliArgumentsVariablesRegistered, + $this->checkMaybeUndefinedVariables, + ); + } + + public static function getAdditionalConfigFiles(): array + { + return array_merge( + parent::getAdditionalConfigFiles(), + [ + __DIR__ . '/../without-loop-pollution.neon', + ], + ); + } + + public function testDefinedVariables(): void + { + require_once __DIR__ . '/data/defined-variables-definition.php'; + $this->cliArgumentsVariablesRegistered = true; + $this->checkMaybeUndefinedVariables = true; + $this->analyse([__DIR__ . '/data/defined-variables.php'], [ + [ + 'Undefined variable: $definedLater', + 5, + ], + [ + 'Variable $definedInIfOnly might not be defined.', + 10, + ], + [ + 'Variable $definedInCases might not be defined.', + 21, + ], + [ + 'Undefined variable: $fooParameterBeforeDeclaration', + 29, + ], + [ + 'Undefined variable: $parseStrParameter', + 34, + ], + [ + 'Undefined variable: $parseStrParameter', + 36, + ], + [ + 'Undefined variable: $foo', + 39, + ], + [ + 'Undefined variable: $willBeUnset', + 44, + ], + [ + 'Undefined variable: $mustAlreadyExistWhenDividing', + 50, + ], + [ + 'Undefined variable: $arrayDoesNotExist', + 57, + ], + [ + 'Undefined variable: $undefinedVariable', + 59, + ], + [ + 'Undefined variable: $this', + 96, + ], + [ + 'Undefined variable: $this', + 99, + ], + [ + 'Undefined variable: $variableInEmpty', + 145, + ], + [ + 'Undefined variable: $negatedVariableInEmpty', + 152, + ], + [ + 'Undefined variable: $variableInEmpty', + 155, + ], + [ + 'Undefined variable: $negatedVariableInEmpty', + 156, + ], + [ + 'Undefined variable: $variableInIsset', + 159, + ], + [ + 'Undefined variable: $anotherVariableInIsset', + 159, + ], + [ + 'Undefined variable: $variableInIsset', + 161, + ], + [ + 'Undefined variable: $anotherVariableInIsset', + 161, + ], + [ + 'Undefined variable: $http_response_header', + 185, + ], + [ + 'Undefined variable: $http_response_header', + 191, + ], + [ + 'Undefined variable: $assignedInKey', + 203, + ], + [ + 'Undefined variable: $assignedInKey', + 204, + ], + [ + 'Variable $forI might not be defined.', + 250, + ], + [ + 'Variable $forJ might not be defined.', + 251, + ], + [ + 'Variable $variableAvailableInAllCatches might not be defined.', + 266, + ], + [ + 'Variable $variableDefinedOnlyInOneCatch might not be defined.', + 267, + ], + [ + 'Undefined variable: $variableInBitwiseAndAssign', + 277, + ], + [ + 'Variable $mightBeUndefinedInDoWhile might not be defined.', + 282, + ], + [ + 'Undefined variable: $variableInSecondCase', + 290, + ], + [ + 'Variable $variableInSecondCase might not be defined.', + 293, + ], + [ + 'Undefined variable: $variableAssignedInSecondCase', + 300, + ], + [ + 'Variable $variableInFallthroughCase might not be defined.', + 302, + ], + [ + 'Variable $variableFromDefaultFirst might not be defined.', + 312, + ], + [ + 'Undefined variable: $undefinedVariableInForeach', + 315, + ], + [ + 'Variable $anotherForLoopVariable might not be defined.', + 328, + ], + [ + 'Variable $maybeDefinedInTernary might not be defined.', + 351, + ], + [ + 'Variable $anotherMaybeDefinedInTernary might not be defined.', + 354, + ], + [ + 'Variable $whileVariableUsedAndThenDefined might not be defined.', + 356, + ], + [ + 'Variable $forVariableUsedAndThenDefined might not be defined.', + 360, + ], + [ + 'Variable $unknownVariablePassedToReset might not be defined.', + 368, + ], + [ + 'Variable $unknownVariablePassedToReset might not be defined.', + 369, + ], + [ + 'Variable $variableInAssign might not be defined.', + 384, + ], + [ + 'Variable $undefinedArrayIndex might not be defined.', + 409, + ], + [ + 'Variable $anotherUndefinedArrayIndex might not be defined.', + 409, + ], + [ + 'Variable $str might not be defined.', + 423, + ], + [ + 'Variable $str might not be defined.', + 428, + ], + ]); + } + + public function testDefinedVariablesInClosures(): void + { + $this->cliArgumentsVariablesRegistered = true; + $this->checkMaybeUndefinedVariables = true; + $this->analyse([__DIR__ . '/data/defined-variables-closures.php'], [ + [ + 'Undefined variable: $this', + 14, + ], + ]); + } + + public function testDefinedVariablesInShortArrayDestructuringSyntax(): void + { + $this->cliArgumentsVariablesRegistered = true; + $this->checkMaybeUndefinedVariables = true; + $this->analyse([__DIR__ . '/data/defined-variables-array-destructuring-short-syntax.php'], [ + [ + 'Undefined variable: $f', + 11, + ], + [ + 'Undefined variable: $f', + 14, + ], + [ + 'Undefined variable: $var3', + 32, + ], + ]); + } + + public function testCliArgumentsVariablesNotRegistered(): void + { + $this->cliArgumentsVariablesRegistered = false; + $this->checkMaybeUndefinedVariables = true; + $this->analyse([__DIR__ . '/data/cli-arguments-variables.php'], [ + [ + 'Variable $argc might not be defined.', + 3, + ], + [ + 'Undefined variable: $argc', + 5, + ], + ]); + } + + public function testCliArgumentsVariablesRegistered(): void + { + $this->cliArgumentsVariablesRegistered = true; + $this->checkMaybeUndefinedVariables = true; + $this->analyse([__DIR__ . '/data/cli-arguments-variables.php'], [ + [ + 'Undefined variable: $argc', + 5, + ], + ]); + } + + public static function dataLoopInitialAssignments(): array + { + return [ + [ + false, + false, + [], + ], + [ + false, + true, + [ + [ + 'Variable $i might not be defined.', + 7, + ], + [ + 'Variable $whileVar might not be defined.', + 13, + ], + ], + ], + ]; + } + + /** + * @param list $expectedErrors + */ + #[DataProvider('dataLoopInitialAssignments')] + public function testLoopInitialAssignments( + bool $polluteScopeWithLoopInitialAssignments, + bool $checkMaybeUndefinedVariables, + array $expectedErrors, + ): void + { + $this->cliArgumentsVariablesRegistered = false; + $this->checkMaybeUndefinedVariables = $checkMaybeUndefinedVariables; + $this->analyse([__DIR__ . '/data/loop-initial-assignments.php'], $expectedErrors); + } + + public function testDefineVariablesInClass(): void + { + $this->cliArgumentsVariablesRegistered = true; + $this->checkMaybeUndefinedVariables = true; + $this->analyse([__DIR__ . '/data/define-variables-class.php'], []); + } + + public function testDeadBranches(): void + { + $this->cliArgumentsVariablesRegistered = true; + $this->checkMaybeUndefinedVariables = true; + $this->analyse([__DIR__ . '/data/dead-branches.php'], [ + [ + 'Undefined variable: $test', + 21, + ], + [ + 'Undefined variable: $test', + 33, + ], + [ + 'Undefined variable: $test', + 55, + ], + [ + 'Undefined variable: $test', + 66, + ], + [ + 'Undefined variable: $test', + 94, + ], + ]); + } + + public function testForeach(): void + { + $this->cliArgumentsVariablesRegistered = true; + $this->checkMaybeUndefinedVariables = true; + $this->analyse([__DIR__ . '/data/foreach.php'], [ + [ + 'Variable $val might not be defined.', + 9, + ], + [ + 'Variable $test might not be defined.', + 10, + ], + [ + 'Undefined variable: $val', + 46, + ], + [ + 'Undefined variable: $test', + 47, + ], + [ + 'Variable $val might not be defined.', + 62, + ], + [ + 'Variable $test might not be defined.', + 63, + ], + [ + 'Undefined variable: $val', + 200, + ], + [ + 'Undefined variable: $test', + 201, + ], + [ + 'Undefined variable: $val', + 216, + ], + [ + 'Undefined variable: $test', + 217, + ], + [ + 'Variable $val might not be defined.', + 246, + ], + [ + 'Variable $test might not be defined.', + 247, + ], + ]); + } + + public static function dataForeachPolluteScopeWithAlwaysIterableForeach(): array + { + return [ + [ + true, + [ + [ + 'Undefined variable: $key', + 8, + ], + [ + 'Undefined variable: $val', + 9, + ], + [ + 'Undefined variable: $test', + 10, + ], + [ + 'Variable $test might not be defined.', + 34, + ], + [ + 'Variable $key might not be defined.', + 47, + ], + [ + 'Variable $test might not be defined.', + 48, + ], + [ + 'Variable $key might not be defined.', + 61, + ], + [ + 'Variable $test might not be defined.', + 62, + ], + ], + ], + ]; + } + + /** + * @param list $errors + */ + #[DataProvider('dataForeachPolluteScopeWithAlwaysIterableForeach')] + public function testForeachPolluteScopeWithAlwaysIterableForeach(bool $polluteScopeWithAlwaysIterableForeach, array $errors): void + { + $this->cliArgumentsVariablesRegistered = true; + $this->checkMaybeUndefinedVariables = true; + $this->analyse([__DIR__ . '/data/foreach-always-iterable.php'], $errors); + } + + public function testBooleanOperatorsTruthyFalsey(): void + { + $this->cliArgumentsVariablesRegistered = true; + $this->checkMaybeUndefinedVariables = true; + $this->analyse([__DIR__ . '/data/boolean-op-truthy-falsey.php'], [ + [ + 'Variable $matches might not be defined.', + 9, + ], + [ + 'Variable $matches might not be defined.', + 15, + ], + ]); + } + + public function testArrowFunctions(): void + { + $this->cliArgumentsVariablesRegistered = true; + $this->checkMaybeUndefinedVariables = true; + $this->analyse([__DIR__ . '/data/defined-variables-arrow-functions.php'], [ + [ + 'Undefined variable: $a', + 10, + ], + [ + 'Undefined variable: $this', + 19, + ], + ]); + } + + public function testCoalesceAssign(): void + { + $this->cliArgumentsVariablesRegistered = true; + $this->checkMaybeUndefinedVariables = true; + $this->analyse([__DIR__ . '/data/defined-variables-coalesce-assign.php'], [ + [ + 'Undefined variable: $b', + 16, + ], + ]); + } + + public function testBug2748(): void + { + $this->cliArgumentsVariablesRegistered = true; + $this->checkMaybeUndefinedVariables = true; + $this->analyse([__DIR__ . '/data/bug-2748.php'], [ + [ + 'Undefined variable: $foo', + 10, + ], + [ + 'Undefined variable: $foo', + 15, + ], + ]); + } + + public function testGlobalVariables(): void + { + $this->cliArgumentsVariablesRegistered = true; + $this->checkMaybeUndefinedVariables = true; + $this->analyse([__DIR__ . '/data/global-variables.php'], []); + } + + public function testRootScopeMaybeDefined(): void + { + $this->cliArgumentsVariablesRegistered = true; + $this->checkMaybeUndefinedVariables = false; + $this->analyse([__DIR__ . '/data/root-scope-maybe.php'], []); + } + + public function testRootScopeMaybeDefinedCheck(): void + { + $this->cliArgumentsVariablesRegistered = true; + $this->checkMaybeUndefinedVariables = true; + $this->analyse([__DIR__ . '/data/root-scope-maybe.php'], [ + [ + 'Variable $maybe might not be defined.', + 3, + ], + [ + 'Variable $this might not be defined.', + 5, + ], + ]); + } + + public function testFormerThisVariableRule(): void + { + $this->cliArgumentsVariablesRegistered = true; + $this->checkMaybeUndefinedVariables = true; + $this->analyse([__DIR__ . '/data/this.php'], [ + [ + 'Undefined variable: $this', + 16, + ], + [ + 'Undefined variable: $this', + 20, + ], + [ + 'Undefined variable: $this', + 26, + ], + [ + 'Undefined variable: $this', + 38, + ], + ]); + } + + public function testClosureUse(): void + { + $this->cliArgumentsVariablesRegistered = true; + $this->checkMaybeUndefinedVariables = true; + $this->analyse([__DIR__ . '/data/defined-variables-anonymous-function-use.php'], [ + [ + 'Variable $bar might not be defined.', + 5, + ], + [ + 'Variable $wrongErrorHandler might not be defined.', + 22, + ], + [ + 'Variable $onlyInIf might not be defined.', + 23, + ], + [ + 'Variable $forI might not be defined.', + 24, + ], + [ + 'Variable $forJ might not be defined.', + 25, + ], + [ + 'Variable $anotherVariableFromForCond might not be defined.', + 26, + ], + ]); + } + + public function testNullsafeIsset(): void + { + $this->cliArgumentsVariablesRegistered = true; + $this->checkMaybeUndefinedVariables = true; + $this->analyse([__DIR__ . '/data/variable-nullsafe-isset.php'], []); + } + + #[RequiresPhp('>= 8.0.0')] + public function testBug7291(): void + { + $this->cliArgumentsVariablesRegistered = true; + $this->checkMaybeUndefinedVariables = true; + $this->analyse([__DIR__ . '/data/bug-7291.php'], [ + [ + 'Variable $a might not be defined.', + 23, + ], + [ + 'Variable $b might not be defined.', + 32, + ], + [ + 'Variable $c might not be defined.', + 41, + ], + [ + 'Variable $d might not be defined.', + 50, + ], + ]); + } + + public function testBug1306(): void + { + $this->cliArgumentsVariablesRegistered = true; + $this->checkMaybeUndefinedVariables = true; + $this->analyse([__DIR__ . '/data/bug-1306.php'], []); + } + + public function testBug3515(): void + { + $this->cliArgumentsVariablesRegistered = true; + $this->checkMaybeUndefinedVariables = true; + $this->analyse([__DIR__ . '/data/bug-3515.php'], [ + [ + 'Undefined variable: $anArray', + 19, + ], + [ + 'Undefined variable: $anArray', + 20, + ], + ]); + } + + public function testBug4412(): void + { + $this->cliArgumentsVariablesRegistered = true; + $this->checkMaybeUndefinedVariables = true; + $this->analyse([__DIR__ . '/data/bug-4412.php'], [ + [ + 'Undefined variable: $a', + 17, + ], + ]); + } + + public function testBug3283(): void + { + $this->cliArgumentsVariablesRegistered = true; + $this->checkMaybeUndefinedVariables = true; + $this->analyse([__DIR__ . '/data/bug-3283.php'], []); + } + + public function testFirstClassCallables(): void + { + $this->cliArgumentsVariablesRegistered = true; + $this->checkMaybeUndefinedVariables = true; + $this->analyse([__DIR__ . '/data/first-class-callables.php'], [ + [ + 'Undefined variable: $foo', + 10, + ], + [ + 'Undefined variable: $foo', + 11, + ], + [ + 'Undefined variable: $foo', + 29, + ], + [ + 'Undefined variable: $foo', + 30, + ], + [ + 'Undefined variable: $foo', + 48, + ], + [ + 'Undefined variable: $foo', + 49, + ], + ]); + } + + public function testBug6112(): void + { + $this->cliArgumentsVariablesRegistered = true; + $this->checkMaybeUndefinedVariables = true; + $this->analyse([__DIR__ . '/data/bug-6112.php'], []); + } + + public function testBug5401(): void + { + $this->cliArgumentsVariablesRegistered = true; + $this->checkMaybeUndefinedVariables = true; + $this->analyse([__DIR__ . '/data/bug-5401.php'], []); + } + + public function testBug4173(): void + { + $this->cliArgumentsVariablesRegistered = true; + $this->checkMaybeUndefinedVariables = true; + $this->analyse([__DIR__ . '/data/bug-4173.php'], []); + } + + public function testBug5805(): void + { + $this->cliArgumentsVariablesRegistered = true; + $this->checkMaybeUndefinedVariables = true; + $this->analyse([__DIR__ . '/data/bug-5805.php'], []); + } + + public function testBug393(): void + { + $this->cliArgumentsVariablesRegistered = true; + $this->checkMaybeUndefinedVariables = true; + $this->analyse([__DIR__ . '/data/bug-393.php'], []); + } + + public function testBug9023(): void + { + $this->cliArgumentsVariablesRegistered = true; + $this->checkMaybeUndefinedVariables = true; + + $this->analyse([__DIR__ . '/data/bug-9023.php'], []); + } + + public function testBug11984(): void + { + $this->cliArgumentsVariablesRegistered = true; + $this->checkMaybeUndefinedVariables = true; + + $this->analyse([__DIR__ . '/data/bug-11984.php'], []); + } + + #[DataProvider('dataBug11545')] + public function testBug11545(bool $polluteScopeWithLoopInitialAssignments): void + { + $this->cliArgumentsVariablesRegistered = true; + $this->checkMaybeUndefinedVariables = true; + + $errors = []; + if (!$polluteScopeWithLoopInitialAssignments) { + $errors[] = [ + 'Variable $result might not be defined.', + 24, + ]; + } + + $this->analyse([__DIR__ . '/data/bug-11545.php'], $errors); + } + + /** @return iterable */ + public static function dataBug11545(): iterable + { + yield [false]; + } + + public function testBug10245(): void + { + $this->cliArgumentsVariablesRegistered = true; + $this->checkMaybeUndefinedVariables = true; + + $this->analyse([__DIR__ . '/data/bug-10245.php'], []); + } + + public function testBug5919(): void + { + $this->cliArgumentsVariablesRegistered = true; + $this->checkMaybeUndefinedVariables = true; + + $this->analyse([__DIR__ . '/data/bug-5919.php'], []); + } + + public function testBug8430(): void + { + $this->cliArgumentsVariablesRegistered = true; + $this->checkMaybeUndefinedVariables = true; + + $this->analyse([__DIR__ . '/data/bug-8430.php'], []); + } + + public function testBug8430b(): void + { + $this->cliArgumentsVariablesRegistered = true; + $this->checkMaybeUndefinedVariables = true; + + $this->analyse([__DIR__ . '/data/bug-8430b.php'], []); + } + + public function testBug5477(): void + { + $this->cliArgumentsVariablesRegistered = true; + $this->checkMaybeUndefinedVariables = true; + + $this->analyse([__DIR__ . '/data/bug-5477.php'], []); + } + + public function testBug10657(): void + { + $this->cliArgumentsVariablesRegistered = true; + $this->checkMaybeUndefinedVariables = true; + + $this->analyse([__DIR__ . '/data/bug-10657.php'], []); + } + + public function testBug6830(): void + { + $this->cliArgumentsVariablesRegistered = true; + $this->checkMaybeUndefinedVariables = true; + + $this->analyse([__DIR__ . '/data/bug-6830.php'], []); + } + + public function testBug14019(): void + { + $this->cliArgumentsVariablesRegistered = true; + $this->checkMaybeUndefinedVariables = true; + + $this->analyse([__DIR__ . '/../../Analyser/nsrt/bug-14019.php'], []); + } + + public function testBug9349(): void + { + $this->cliArgumentsVariablesRegistered = true; + $this->checkMaybeUndefinedVariables = true; + + $this->analyse([__DIR__ . '/data/bug-9349.php'], [ + [ + 'Variable $sql might not be defined.', + 19, + ], + [ + 'Variable $sql might not be defined.', + 78, + ], + ]); + } + + public function testBug14318(): void + { + $this->cliArgumentsVariablesRegistered = true; + $this->checkMaybeUndefinedVariables = true; + + $this->analyse([__DIR__ . '/data/bug-14318.php'], []); + } + + public function testBug14323(): void + { + $this->cliArgumentsVariablesRegistered = true; + $this->checkMaybeUndefinedVariables = true; + + $this->analyse([__DIR__ . '/data/bug-14323.php'], [ + [ + 'Variable $command might not be defined.', + 24, + ], + [ + 'Variable $command might not be defined.', + 50, + ], + [ + 'Variable $command might not be defined.', + 119, + ], + ]); + } + + #[RequiresPhp('>= 8.0.0')] + public function testBug14274(): void + { + $this->cliArgumentsVariablesRegistered = true; + $this->checkMaybeUndefinedVariables = true; + + $this->analyse([__DIR__ . '/data/bug-14274.php'], []); + } + + #[RequiresPhp('>= 8.0.0')] + public function testBug12373(): void + { + $this->cliArgumentsVariablesRegistered = true; + $this->checkMaybeUndefinedVariables = true; + + $this->analyse([__DIR__ . '/data/bug-12373.php'], []); + } + + public function testBug12992(): void + { + $this->cliArgumentsVariablesRegistered = true; + $this->checkMaybeUndefinedVariables = true; + $this->analyse([__DIR__ . '/data/bug-12992.php'], []); + } + + public function testBug14227(): void + { + $this->cliArgumentsVariablesRegistered = true; + $this->checkMaybeUndefinedVariables = true; + $this->analyse([__DIR__ . '/data/bug-14227.php'], []); + } + + public function testBug14117(): void + { + $this->cliArgumentsVariablesRegistered = true; + $this->checkMaybeUndefinedVariables = true; + + $this->analyse([__DIR__ . '/data/bug-14117.php'], [ + [ + 'Variable $value might not be defined.', + 49, + ], + [ + 'Undefined variable: $value', + 65, + ], + [ + 'Undefined variable: $value', + 81, + ], + ]); + } + + public function testBug12597(): void + { + $this->cliArgumentsVariablesRegistered = true; + $this->checkMaybeUndefinedVariables = true; + $this->analyse([__DIR__ . '/data/bug-12597.php'], []); + } + + public function testBug11218(): void + { + $this->cliArgumentsVariablesRegistered = true; + $this->checkMaybeUndefinedVariables = true; + $this->analyse([__DIR__ . '/data/bug-11218.php'], []); + } + + public function testBug4352(): void + { + $this->cliArgumentsVariablesRegistered = true; + $this->checkMaybeUndefinedVariables = true; + $this->analyse([__DIR__ . '/data/bug-4352.php'], []); + } + + public function testBug6688(): void + { + $this->cliArgumentsVariablesRegistered = true; + $this->checkMaybeUndefinedVariables = true; + $this->analyse([__DIR__ . '/data/bug-6688.php'], [ + [ + 'Variable $a might not be defined.', + 5, + ], + ]); + } + + #[RequiresPhp('>= 8.0.0')] + public function testBug10729(): void + { + $this->cliArgumentsVariablesRegistered = true; + $this->checkMaybeUndefinedVariables = true; + $this->analyse([__DIR__ . '/data/bug-10729.php'], [ + [ + 'Variable $format might not be defined.', + 12, + ], + [ + 'Undefined variable: $format', + 25, + ], + [ + 'Variable $format might not be defined.', + 31, + ], + [ + 'Variable $value might not be defined.', + 32, + ], + [ + 'Variable $format might not be defined.', + 38, + ], + ]); + } + + public function testBug10090(): void + { + $this->cliArgumentsVariablesRegistered = true; + $this->checkMaybeUndefinedVariables = true; + $this->analyse([__DIR__ . '/../../Analyser/nsrt/bug-10090.php'], [ + [ + 'Undefined variable: $shortcut_id', + 18, + ], + [ + 'Undefined variable: $shortcut_id', + 19, + ], + [ + 'Variable $x might not be defined.', + 35, + ], + [ + 'Variable $x might not be defined.', + 36, + ], + [ + 'Variable $x might not be defined.', + 80, + ], + [ + 'Variable $x might not be defined.', + 81, + ], + [ + 'Undefined variable: $x', + 96, + ], + ]); + } + + public function testBug2032(): void + { + $this->cliArgumentsVariablesRegistered = true; + $this->checkMaybeUndefinedVariables = true; + $this->analyse([__DIR__ . '/data/bug-2032.php'], [ + [ + 'Undefined variable: $undefined', + 6, + ], + [ + 'Undefined variable: $undefined', + 9, + ], + [ + 'Undefined variable: $undefined', + 15, + ], + ]); + } + + public function testBug13833(): void + { + $this->cliArgumentsVariablesRegistered = true; + $this->checkMaybeUndefinedVariables = true; + $this->analyse([__DIR__ . '/data/bug-13833.php'], []); + } + + public function testBug9685(): void + { + $this->cliArgumentsVariablesRegistered = true; + $this->checkMaybeUndefinedVariables = true; + $this->analyse([__DIR__ . '/data/bug-9685.php'], []); + } + + public function testBug7706(): void + { + $this->cliArgumentsVariablesRegistered = true; + $this->checkMaybeUndefinedVariables = true; + $this->analyse([__DIR__ . '/data/bug-7706.php'], []); + } + + public function testBug8360(): void + { + $this->cliArgumentsVariablesRegistered = true; + $this->checkMaybeUndefinedVariables = true; + $this->analyse([__DIR__ . '/data/bug-8360.php'], []); + } + +} diff --git a/tests/PHPStan/Rules/uncertain-phpdoc-types.neon b/tests/PHPStan/Rules/uncertain-phpdoc-types.neon new file mode 100644 index 00000000000..c551b84f1f6 --- /dev/null +++ b/tests/PHPStan/Rules/uncertain-phpdoc-types.neon @@ -0,0 +1,2 @@ +parameters: + treatPhpDocTypesAsCertain: false diff --git a/tests/PHPStan/Rules/without-foreach-pollution.neon b/tests/PHPStan/Rules/without-foreach-pollution.neon new file mode 100644 index 00000000000..3ee516d3be6 --- /dev/null +++ b/tests/PHPStan/Rules/without-foreach-pollution.neon @@ -0,0 +1,2 @@ +parameters: + polluteScopeWithAlwaysIterableForeach: false diff --git a/tests/PHPStan/Rules/without-loop-pollution.neon b/tests/PHPStan/Rules/without-loop-pollution.neon new file mode 100644 index 00000000000..47811f500ec --- /dev/null +++ b/tests/PHPStan/Rules/without-loop-pollution.neon @@ -0,0 +1,2 @@ +parameters: + polluteScopeWithLoopInitialAssignments: false From 26a99c0b13bd16bd4b42910cf53c26b8022d5abe Mon Sep 17 00:00:00 2001 From: Ondrej Mirtes Date: Sun, 13 Sep 2026 12:59:02 +0200 Subject: [PATCH 08/10] Break the closure resolver DI cycle by extracting ContextualClosureParameterResolver ClosureTypeResolver built closure parameters through ClosureParameterResolver, and ClosureParameterResolver priced call arguments through ClosureTypeResolver::getClosureType() - a cycle hidden behind a container lookup. ClosureTypeResolver never passes call arguments, and only the call-argument refinement needs closure types, so the contextual part (array_map() and immediate-invocation values, the passed-to callable type) moves into ContextualClosureParameterResolver, injected into both resolvers. ClosureParameterResolver now injects ClosureTypeResolver directly, and ArgumentsHandler and ClosureProcessor inject both resolvers instead of looking them up. The one lookup left is ClosureProcessor::getParametersProcessor(): closure parameters walk their attributes, attribute arguments are call arguments walked by ArgumentsHandler, and those walk closure arguments through ClosureProcessor again - genuine recursion constructor injection cannot express. Co-Authored-By: Claude Opus 5 (1M context) Claude-Session: https://claude.ai/code/session_014hgcbwfKQnye2coQXtX5XU --- src/Analyser/ArgumentsHandler.php | 28 ++- src/Analyser/ClosureProcessor.php | 29 ++- .../Helper/ClosureParameterResolver.php | 169 +++++------------- .../Helper/ClosureTypeResolver.php | 4 +- .../ContextualClosureParameterResolver.php | 135 ++++++++++++++ 5 files changed, 213 insertions(+), 152 deletions(-) create mode 100644 src/Analyser/ExprHandler/Helper/ContextualClosureParameterResolver.php diff --git a/src/Analyser/ArgumentsHandler.php b/src/Analyser/ArgumentsHandler.php index bb5806eb02c..718d1b8826a 100644 --- a/src/Analyser/ArgumentsHandler.php +++ b/src/Analyser/ArgumentsHandler.php @@ -22,7 +22,6 @@ use PHPStan\DependencyInjection\AutowiredExtensions; use PHPStan\DependencyInjection\AutowiredParameter; use PHPStan\DependencyInjection\AutowiredService; -use PHPStan\DependencyInjection\Container; use PHPStan\DependencyInjection\ExtensionsCollection; use PHPStan\Node\Expr\TypeExpr; use PHPStan\Node\InvalidateExprNode; @@ -94,7 +93,6 @@ final class ArgumentsHandler * @param ExtensionsCollection $staticMethodParameterClosureTypeExtensions */ public function __construct( - private Container $container, private TemplateArgumentObserver $templateArgumentObserver, private ExpressionResultFactory $expressionResultFactory, private ClosureProcessor $closureProcessor, @@ -119,6 +117,8 @@ public function __construct( #[AutowiredParameter(ref: '%exceptions.implicitThrows%')] private bool $implicitThrows, private AssignHandler $assignHandler, + private ClosureTypeResolver $closureTypeResolver, + private ClosureParameterResolver $closureParameterResolver, ) { } @@ -233,7 +233,7 @@ public function processArgs( $originalArgForGather = $arg->getAttribute(ArgumentsNormalizer::ORIGINAL_ARG_ATTRIBUTE) ?? $arg; $gatheredArgTypeByIndex[$i] = $typeDrivenAcceptorSelection ? $this->gatherClosureArgType($parametersAcceptors, $i, $arg->value, $scope) - : $this->container->getByType(ClosureTypeResolver::class)->getClosureType($scope, $arg->value, true, $storage); + : $this->closureTypeResolver->getClosureType($scope, $arg->value, true, $storage); $this->addGatheredArgType($gatheredTypes, $gatheredUnpack, $gatheredHasName, $originalArgForGather, $i, $gatheredArgTypeByIndex[$i]); } @@ -362,7 +362,6 @@ public function processArgs( // a miss means "walk it", not "price it silently": pricing skips // the body walk and never fires the closure's node callbacks if ($storedClosureArgResult === null && $nodeScopeResolver->isReturningStoredExpressionResults()) { - $closureTypeResolver = $this->container->getByType(ClosureTypeResolver::class); $storedClosureArgResult = $this->expressionResultFactory->create( $scopeToPass, beforeScope: $scopeToPass, @@ -371,8 +370,8 @@ public function processArgs( isAlwaysTerminating: false, throwPoints: [], impurePoints: [], - type: $closureTypeResolver->getClosureType($scopeToPass, $arg->value, false, $storage), - nativeType: $closureTypeResolver->getClosureType($scopeToPass->doNotTreatPhpDocTypesAsCertain(), $arg->value, false, $storage), + type: $this->closureTypeResolver->getClosureType($scopeToPass, $arg->value, false, $storage), + nativeType: $this->closureTypeResolver->getClosureType($scopeToPass->doNotTreatPhpDocTypesAsCertain(), $arg->value, false, $storage), typeCallback: null, specifyTypesCallback: SpecifiedTypes::emptySpecifyCallback(), ); @@ -418,7 +417,6 @@ public function processArgs( $impurePoints = array_merge($impurePoints, $closureResult->getImpurePoints()); } - $closureTypeResolver = $this->container->getByType(ClosureTypeResolver::class); $storedClosureResult = $this->expressionResultFactory->create( $closureResult->getScope(), $scopeToPass, @@ -428,7 +426,7 @@ public function processArgs( isAlwaysTerminating: false, throwPoints: [], impurePoints: [], - type: $closureTypeResolver->buildClosureTypeForClosure( + type: $this->closureTypeResolver->buildClosureTypeForClosure( $scopeToPass, $arg->value, $closureResult->getGatheredReturnStatements(), @@ -442,7 +440,7 @@ public function processArgs( ), // the native flavour reads the stored native types off the same // single body walk - no second walk on the promoted scope - nativeType: $closureTypeResolver->buildClosureTypeForClosure( + nativeType: $this->closureTypeResolver->buildClosureTypeForClosure( $scopeToPass, $arg->value, $closureResult->getGatheredReturnStatements(), @@ -512,7 +510,6 @@ public function processArgs( // consume mode alone (the nullsafe call's plain-twin walk) is the // first and only walk of the argument - a miss means walk it if ($storedClosureArgResult === null && $nodeScopeResolver->isReturningStoredExpressionResults()) { - $closureTypeResolver = $this->container->getByType(ClosureTypeResolver::class); $storedClosureArgResult = $this->expressionResultFactory->create( $scopeToPass, beforeScope: $scopeToPass, @@ -521,8 +518,8 @@ public function processArgs( isAlwaysTerminating: false, throwPoints: [], impurePoints: [], - type: $closureTypeResolver->getClosureType($scopeToPass, $arg->value, false, $storage), - nativeType: $closureTypeResolver->getClosureType($scopeToPass->doNotTreatPhpDocTypesAsCertain(), $arg->value, false, $storage), + type: $this->closureTypeResolver->getClosureType($scopeToPass, $arg->value, false, $storage), + nativeType: $this->closureTypeResolver->getClosureType($scopeToPass->doNotTreatPhpDocTypesAsCertain(), $arg->value, false, $storage), typeCallback: null, specifyTypesCallback: SpecifiedTypes::emptySpecifyCallback(), ); @@ -566,13 +563,12 @@ public function processArgs( $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), $arrowFunctionExprResult->getThrowPoints())); $impurePoints = array_merge($impurePoints, $arrowFunctionExprResult->getImpurePoints()); } - $arrowFunctionClosureTypeResolver = $this->container->getByType(ClosureTypeResolver::class); $arrowFunctionScope = $arrowFunctionResult->getArrowFunctionScope(); // both flavours are built from the single body walk (see // ArrowFunctionHandler); the built type also answers the // invalidate-expressions read below without re-walking the // still-unstored node through Scope::getType() - $arrowFunctionType = $arrowFunctionClosureTypeResolver->buildClosureTypeForArrowFunction( + $arrowFunctionType = $this->closureTypeResolver->buildClosureTypeForArrowFunction( $scopeToPass, $arg->value, $arrowFunctionScope, @@ -592,7 +588,7 @@ public function processArgs( throwPoints: $arrowFunctionExprResult->getThrowPoints(), impurePoints: $arrowFunctionExprResult->getImpurePoints(), type: $arrowFunctionType, - nativeType: $arrowFunctionClosureTypeResolver->buildClosureTypeForArrowFunction( + nativeType: $this->closureTypeResolver->buildClosureTypeForArrowFunction( $scopeToPass, $arg->value, $arrowFunctionScope, @@ -932,7 +928,7 @@ private function gatherClosureArgType(array $parametersAcceptors, int $i, Expr $ $scope = $scope->pushInFunctionCall(null, $rawParameter, false); } - return $this->container->getByType(ClosureParameterResolver::class)->resolveCallableTypeForScope($closureExpr, $scope); + return $this->closureParameterResolver->resolveCallableTypeForScope($closureExpr, $scope); } /** diff --git a/src/Analyser/ClosureProcessor.php b/src/Analyser/ClosureProcessor.php index d5bed0ae177..e414b9a0f65 100644 --- a/src/Analyser/ClosureProcessor.php +++ b/src/Analyser/ClosureProcessor.php @@ -47,10 +47,23 @@ final class ClosureProcessor public function __construct( private Container $container, private ExpressionResultFactory $expressionResultFactory, + private ClosureParameterResolver $closureParameterResolver, + private ClosureTypeResolver $closureTypeResolver, ) { } + /** + * Looked up instead of injected: parameters walk their attributes, attribute + * arguments are call arguments walked by ArgumentsHandler, and those walk + * closure arguments through this class - a closure in an attribute argument + * recurses back here, which constructor injection cannot express. + */ + private function getParametersProcessor(): ParametersProcessor + { + return $this->container->getByType(ParametersProcessor::class); + } + /** * @param callable(Node $node, Scope $scope): void $nodeCallback */ @@ -84,12 +97,12 @@ private function processClosureNodeInternal( ?Type $nativePassedToType = null, ): ProcessClosureResult { - $this->container->getByType(ParametersProcessor::class)->processParams($nodeScopeResolver, $stmt, $expr->params, $scope, $storage, $nodeCallback); + $this->getParametersProcessor()->processParams($nodeScopeResolver, $stmt, $expr->params, $scope, $storage, $nodeCallback); $byRefUses = []; $closureCallArgs = $expr->getAttribute(ClosureArgVisitor::ATTRIBUTE_NAME); - $parameterTypes = $this->container->getByType(ClosureParameterResolver::class)->resolve($scope, $expr, $storage, $closureCallArgs, $passedToType, $nativePassedToType); + $parameterTypes = $this->closureParameterResolver->resolve($scope, $expr, $storage, $closureCallArgs, $passedToType, $nativePassedToType); $callableParameters = $parameterTypes->parameters; $nativeCallableParameters = $parameterTypes->nativeParameters; @@ -108,7 +121,7 @@ private function processClosureNodeInternal( // a call's type is carried by the context (see // ExpressionContext::enterAssignRightSideCallArgs()); a closure // right side resolves through the closure type resolver - $inAssignRightSideType = $context->getInAssignRightSideType() ?? $this->container->getByType(ClosureParameterResolver::class)->resolveCallableTypeForScope($inAssignRightSideExpr, $scope); + $inAssignRightSideType = $context->getInAssignRightSideType() ?? $this->closureParameterResolver->resolveCallableTypeForScope($inAssignRightSideExpr, $scope); if ($inAssignRightSideType instanceof ClosureType) { $variableType = $inAssignRightSideType; } else { @@ -119,7 +132,7 @@ private function processClosureNodeInternal( $variableType = TypeCombinator::union($scope->getVariableType($inAssignRightSideVariableName), $inAssignRightSideType); } } - $inAssignRightSideNativeType = $context->getInAssignRightSideNativeType() ?? $this->container->getByType(ClosureParameterResolver::class)->resolveCallableTypeForScope($inAssignRightSideExpr, $scope->doNotTreatPhpDocTypesAsCertain()); + $inAssignRightSideNativeType = $context->getInAssignRightSideNativeType() ?? $this->closureParameterResolver->resolveCallableTypeForScope($inAssignRightSideExpr, $scope->doNotTreatPhpDocTypesAsCertain()); if ($inAssignRightSideNativeType instanceof ClosureType) { $variableNativeType = $inAssignRightSideNativeType; } else { @@ -365,7 +378,7 @@ private function refineClosureNodeScope( ExpressionResultStorage $storage, ): MutatingScope { - $refinedClosureType = $this->container->getByType(ClosureTypeResolver::class)->buildClosureTypeForClosure( + $refinedClosureType = $this->closureTypeResolver->buildClosureTypeForClosure( $scope, $expr, $gatheredReturnStatementsWithScope, @@ -421,13 +434,13 @@ public function processArrowFunctionNode( ): ProcessArrowFunctionResult { $context ??= ExpressionContext::createTopLevel(); - $this->container->getByType(ParametersProcessor::class)->processParams($nodeScopeResolver, $stmt, $expr->params, $scope, $storage, $nodeCallback); + $this->getParametersProcessor()->processParams($nodeScopeResolver, $stmt, $expr->params, $scope, $storage, $nodeCallback); if ($expr->returnType !== null) { $nodeScopeResolver->callNodeCallback($nodeCallback, $expr->returnType, $scope, $storage); } $arrowFunctionCallArgs = $expr->getAttribute(ArrowFunctionArgVisitor::ATTRIBUTE_NAME); - $parameterTypes = $this->container->getByType(ClosureParameterResolver::class)->resolve($scope, $expr, $storage, $arrowFunctionCallArgs, $passedToType, $nativePassedToType); + $parameterTypes = $this->closureParameterResolver->resolve($scope, $expr, $storage, $arrowFunctionCallArgs, $passedToType, $nativePassedToType); $callableParameters = $parameterTypes->parameters; $nativeCallableParameters = $parameterTypes->nativeParameters; $arrowFunctionScope = $scope->enterArrowFunction($expr, $callableParameters, $nativeCallableParameters); @@ -482,7 +495,7 @@ public function processArrowFunctionNode( // run, build the refined arrow function type from the body expression's // stored type (no second walk) and fire InArrowFunctionNode with it, so the // node and the return-type rules see the refined expected return. - $refinedArrowFunctionType = $this->container->getByType(ClosureTypeResolver::class)->buildClosureTypeForArrowFunction( + $refinedArrowFunctionType = $this->closureTypeResolver->buildClosureTypeForArrowFunction( $scope, $expr, $arrowFunctionScope, diff --git a/src/Analyser/ExprHandler/Helper/ClosureParameterResolver.php b/src/Analyser/ExprHandler/Helper/ClosureParameterResolver.php index ff0d74a5d01..2410e505bc2 100644 --- a/src/Analyser/ExprHandler/Helper/ClosureParameterResolver.php +++ b/src/Analyser/ExprHandler/Helper/ClosureParameterResolver.php @@ -10,29 +10,27 @@ use PHPStan\Analyser\MutatingScope; use PHPStan\Analyser\NodeScopeResolver; use PHPStan\DependencyInjection\AutowiredService; -use PHPStan\DependencyInjection\Container; -use PHPStan\Parser\ArrayMapArgVisitor; -use PHPStan\Parser\ImmediatelyInvokedClosureVisitor; use PHPStan\Reflection\Native\NativeParameterReflection; use PHPStan\Reflection\ParameterReflection; -use PHPStan\Reflection\PassedByReference; -use PHPStan\Reflection\Php\DummyParameter; use PHPStan\Type\Type; use PHPStan\Type\TypeCombinator; -use PHPStan\Type\UnionType; -use function array_key_exists; -use function array_map; use function count; -use function max; -/** Resolve the same contextual parameters for body analysis and closure type inference. */ +/** + * Resolves closure parameters for the body walk: the contextual parameters + * (ContextualClosureParameterResolver), refined from the arguments of a call + * that invokes the closure in place. Pricing those arguments needs the closure + * types of nested closures, which is why this part is separate from what + * ClosureTypeResolver itself builds parameters through. + */ #[AutowiredService] final class ClosureParameterResolver { public function __construct( private NodeScopeResolver $nodeScopeResolver, - private Container $container, + private ClosureTypeResolver $closureTypeResolver, + private ContextualClosureParameterResolver $contextualClosureParameterResolver, ) { } @@ -47,49 +45,14 @@ public function resolve( ?Type $nativePassedToType, ): ClosureParameterTypes { - $arrayMapArgs = $expr->getAttribute(ArrayMapArgVisitor::ATTRIBUTE_NAME); - $immediatelyInvokedArgs = $expr->getAttribute(ImmediatelyInvokedClosureVisitor::ARGS_ATTRIBUTE_NAME); - $intrinsicArgs = $arrayMapArgs ?? $immediatelyInvokedArgs; - if ($intrinsicArgs === null) { - return new ClosureParameterTypes( - $this->createCallableParameters($scope, $expr, $callArgs, $passedToType), - $this->createNativeCallableParameters($scope, $expr, $callArgs, $nativePassedToType), - ); + if ($callArgs === null || $this->contextualClosureParameterResolver->hasIntrinsicArgs($expr)) { + return $this->contextualClosureParameterResolver->resolve($scope, $expr, $storage, $passedToType, $nativePassedToType); } - $parameters = []; - $nativeParameters = []; - foreach ($intrinsicArgs as $arg) { - $result = $storage !== null ? $storage->findExpressionResult($arg->value) : null; - $type = $result !== null ? $result->getType() : $this->nodeScopeResolver->readScopeStateOrSyntheticType($arg->value, $scope); - $nativeType = $result !== null ? $result->getNativeType() : $this->nodeScopeResolver->readScopeStateOrSyntheticType($arg->value, $scope->doNotTreatPhpDocTypesAsCertain()); - if ($arrayMapArgs !== null) { - $type = $type->getIterableValueType(); - $nativeType = $nativeType->getIterableValueType(); - } - $parameters[] = new DummyParameter('item', $type, optional: false, passedByReference: PassedByReference::createNo(), variadic: false, defaultValue: null); - $nativeParameters[] = new DummyParameter('item', $nativeType, optional: false, passedByReference: PassedByReference::createNo(), variadic: false, defaultValue: null); - } - - return new ClosureParameterTypes($parameters, $nativeParameters); - } - - /** - * @param Node\Arg[]|null $args - * @return ParameterReflection[]|null - */ - private function createCallableParameters(MutatingScope $scope, Expr $closureExpr, ?array $args, ?Type $passedToType): ?array - { - return $this->doCreateCallableParameters($scope, $closureExpr, $args, $passedToType, fn (MutatingScope $s, Expr $e): Type => $this->resolveCallableTypeForScope($e, $s)); - } - - /** - * @param Node\Arg[]|null $args - * @return ParameterReflection[]|null - */ - private function createNativeCallableParameters(MutatingScope $scope, Expr $closureExpr, ?array $args, ?Type $nativePassedToType): ?array - { - return $this->doCreateCallableParameters($scope, $closureExpr, $args, $nativePassedToType, fn (MutatingScope $s, Expr $e): Type => $this->resolveCallableTypeForScope($e, $s->doNotTreatPhpDocTypesAsCertain())); + return new ClosureParameterTypes( + $this->createCallArgsParameters($scope, $expr, $callArgs, fn (MutatingScope $s, Expr $e): Type => $this->resolveCallableTypeForScope($e, $s)), + $this->createCallArgsParameters($scope, $expr, $callArgs, fn (MutatingScope $s, Expr $e): Type => $this->resolveCallableTypeForScope($e, $s->doNotTreatPhpDocTypesAsCertain())), + ); } /** @@ -97,105 +60,59 @@ private function createNativeCallableParameters(MutatingScope $scope, Expr $clos * either the closure/arrow function whose acceptors describe the parameters, * or a call argument refining them. A closure/arrow function is resolved * directly through ClosureTypeResolver (as Scope::getType() would), not by - * processing it on demand: createCallableParameters() runs while that very + * processing it on demand: createCallArgsParameters() runs while that very * closure is being processed, so on-demand processing would re-enter - * processClosureNodeInternal() endlessly. + * ClosureProcessor::processClosureNodeInternal() endlessly. */ public function resolveCallableTypeForScope(Expr $expr, MutatingScope $scope): Type { if ($expr instanceof Expr\Closure || $expr instanceof Expr\ArrowFunction) { - return $this->container->getByType(ClosureTypeResolver::class)->getClosureType($scope, $expr, false, $scope->getCurrentExpressionResultStorage()); + return $this->closureTypeResolver->getClosureType($scope, $expr, false, $scope->getCurrentExpressionResultStorage()); } return $this->nodeScopeResolver->readTypeOfMaybeStored($expr, $scope); } /** - * @param Node\Arg[]|null $args + * @param Node\Arg[] $args * @param Closure(MutatingScope, Expr): Type $typeGetter * @return ParameterReflection[]|null */ - private function doCreateCallableParameters(MutatingScope $scope, Expr $closureExpr, ?array $args, ?Type $passedToType, Closure $typeGetter): ?array + private function createCallArgsParameters(MutatingScope $scope, Expr $closureExpr, array $args, Closure $typeGetter): ?array { - $callableParameters = null; - if ($args !== null) { - $closureType = $typeGetter($scope, $closureExpr); - - if ($closureType->isCallable()->no()) { - return null; - } - - $acceptors = $closureType->getCallableParametersAcceptors($scope); - if (count($acceptors) === 1) { - $callableParameters = $acceptors[0]->getParameters(); + $closureType = $typeGetter($scope, $closureExpr); + if ($closureType->isCallable()->no()) { + return null; + } - foreach ($callableParameters as $index => $callableParameter) { - if (!isset($args[$index])) { - continue; - } + $callableParameters = null; + $acceptors = $closureType->getCallableParametersAcceptors($scope); + if (count($acceptors) === 1) { + $callableParameters = $acceptors[0]->getParameters(); - if ($callableParameter->isVariadic()) { - $argTypes = []; - $argNumber = count($args); - for ($j = $index; $j < $argNumber; $j++) { - $argTypes[] = $typeGetter($scope, $args[$j]->value); - } - $type = TypeCombinator::union(...$argTypes); - } else { - $type = $typeGetter($scope, $args[$index]->value); - } - $callableParameters[$index] = new NativeParameterReflection( - $callableParameter->getName(), - $callableParameter->isOptional(), - $type, - $callableParameter->passedByReference(), - $callableParameter->isVariadic(), - $callableParameter->getDefaultValue(), - ); + foreach ($callableParameters as $index => $callableParameter) { + if (!isset($args[$index])) { + continue; } - } - } elseif ($passedToType !== null && !$passedToType->isCallable()->no()) { - if ($passedToType instanceof UnionType) { - $passedToType = $passedToType->filterTypes(static fn (Type $innerType) => $innerType->isCallable()->yes()); - if ($passedToType->isCallable()->no()) { - return null; + if ($callableParameter->isVariadic()) { + $argTypes = []; + $argNumber = count($args); + for ($j = $index; $j < $argNumber; $j++) { + $argTypes[] = $typeGetter($scope, $args[$j]->value); + } + $type = TypeCombinator::union(...$argTypes); + } else { + $type = $typeGetter($scope, $args[$index]->value); } - } - - $acceptors = $passedToType->getCallableParametersAcceptors($scope); - foreach ($acceptors as $acceptor) { - $acceptorParameters = array_map(static fn (ParameterReflection $callableParameter) => new NativeParameterReflection( + $callableParameters[$index] = new NativeParameterReflection( $callableParameter->getName(), $callableParameter->isOptional(), - $callableParameter->getType(), + $type, $callableParameter->passedByReference(), $callableParameter->isVariadic(), $callableParameter->getDefaultValue(), - ), $acceptor->getParameters()); - - if ($callableParameters === null) { - $callableParameters = $acceptorParameters; - continue; - } - - $newParameters = []; - $parameterCount = max(count($callableParameters), count($acceptorParameters)); - for ($i = 0; $i < $parameterCount; $i++) { - if (!array_key_exists($i, $acceptorParameters)) { - $newParameters[] = $callableParameters[$i]->toOptional(); - continue; - } - - if (!array_key_exists($i, $callableParameters)) { - $newParameters[] = $acceptorParameters[$i]->toOptional(); - continue; - } - - $newParameters[] = $callableParameters[$i]->union($acceptorParameters[$i]); - } - - $callableParameters = $newParameters; + ); } } diff --git a/src/Analyser/ExprHandler/Helper/ClosureTypeResolver.php b/src/Analyser/ExprHandler/Helper/ClosureTypeResolver.php index 6cd05da4e36..ec9cc7924f8 100644 --- a/src/Analyser/ExprHandler/Helper/ClosureTypeResolver.php +++ b/src/Analyser/ExprHandler/Helper/ClosureTypeResolver.php @@ -80,7 +80,7 @@ final class ClosureTypeResolver implements PerFileAnalysisResettable public function __construct( private NodeScopeResolver $nodeScopeResolver, private InitializerExprTypeResolver $initializerExprTypeResolver, - private ClosureParameterResolver $closureParameterResolver, + private ContextualClosureParameterResolver $contextualClosureParameterResolver, ) { } @@ -801,7 +801,7 @@ private function buildParametersAndAcceptors( $nativePassedToType = $inParameter instanceof ExtendedParameterReflection ? $inParameter->getNativeType() : $inParameter->getType(); } } - $parameterTypes = $this->closureParameterResolver->resolve($scope, $expr, $storage, null, $passedToType, $nativePassedToType); + $parameterTypes = $this->contextualClosureParameterResolver->resolve($scope, $expr, $storage, $passedToType, $nativePassedToType); return [$parameters, $isVariadic, $parameterTypes->parameters, $parameterTypes->nativeParameters]; } diff --git a/src/Analyser/ExprHandler/Helper/ContextualClosureParameterResolver.php b/src/Analyser/ExprHandler/Helper/ContextualClosureParameterResolver.php new file mode 100644 index 00000000000..344f86872f5 --- /dev/null +++ b/src/Analyser/ExprHandler/Helper/ContextualClosureParameterResolver.php @@ -0,0 +1,135 @@ +getAttribute(ArrayMapArgVisitor::ATTRIBUTE_NAME) !== null + || $expr->getAttribute(ImmediatelyInvokedClosureVisitor::ARGS_ATTRIBUTE_NAME) !== null; + } + + public function resolve( + MutatingScope $scope, + Node\Expr\Closure|Node\Expr\ArrowFunction $expr, + ?ExpressionResultStorage $storage, + ?Type $passedToType, + ?Type $nativePassedToType, + ): ClosureParameterTypes + { + $arrayMapArgs = $expr->getAttribute(ArrayMapArgVisitor::ATTRIBUTE_NAME); + $immediatelyInvokedArgs = $expr->getAttribute(ImmediatelyInvokedClosureVisitor::ARGS_ATTRIBUTE_NAME); + $intrinsicArgs = $arrayMapArgs ?? $immediatelyInvokedArgs; + if ($intrinsicArgs === null) { + return new ClosureParameterTypes( + $this->createPassedToTypeParameters($scope, $passedToType), + $this->createPassedToTypeParameters($scope, $nativePassedToType), + ); + } + + $parameters = []; + $nativeParameters = []; + foreach ($intrinsicArgs as $arg) { + $result = $storage !== null ? $storage->findExpressionResult($arg->value) : null; + $type = $result !== null ? $result->getType() : $this->nodeScopeResolver->readScopeStateOrSyntheticType($arg->value, $scope); + $nativeType = $result !== null ? $result->getNativeType() : $this->nodeScopeResolver->readScopeStateOrSyntheticType($arg->value, $scope->doNotTreatPhpDocTypesAsCertain()); + if ($arrayMapArgs !== null) { + $type = $type->getIterableValueType(); + $nativeType = $nativeType->getIterableValueType(); + } + $parameters[] = new DummyParameter('item', $type, optional: false, passedByReference: PassedByReference::createNo(), variadic: false, defaultValue: null); + $nativeParameters[] = new DummyParameter('item', $nativeType, optional: false, passedByReference: PassedByReference::createNo(), variadic: false, defaultValue: null); + } + + return new ClosureParameterTypes($parameters, $nativeParameters); + } + + /** + * @return ParameterReflection[]|null + */ + private function createPassedToTypeParameters(MutatingScope $scope, ?Type $passedToType): ?array + { + if ($passedToType === null || $passedToType->isCallable()->no()) { + return null; + } + + if ($passedToType instanceof UnionType) { + $passedToType = $passedToType->filterTypes(static fn (Type $innerType) => $innerType->isCallable()->yes()); + + if ($passedToType->isCallable()->no()) { + return null; + } + } + + $callableParameters = null; + $acceptors = $passedToType->getCallableParametersAcceptors($scope); + foreach ($acceptors as $acceptor) { + $acceptorParameters = array_map(static fn (ParameterReflection $callableParameter) => new NativeParameterReflection( + $callableParameter->getName(), + $callableParameter->isOptional(), + $callableParameter->getType(), + $callableParameter->passedByReference(), + $callableParameter->isVariadic(), + $callableParameter->getDefaultValue(), + ), $acceptor->getParameters()); + + if ($callableParameters === null) { + $callableParameters = $acceptorParameters; + continue; + } + + $newParameters = []; + $parameterCount = max(count($callableParameters), count($acceptorParameters)); + for ($i = 0; $i < $parameterCount; $i++) { + if (!array_key_exists($i, $acceptorParameters)) { + $newParameters[] = $callableParameters[$i]->toOptional(); + continue; + } + + if (!array_key_exists($i, $callableParameters)) { + $newParameters[] = $acceptorParameters[$i]->toOptional(); + continue; + } + + $newParameters[] = $callableParameters[$i]->union($acceptorParameters[$i]); + } + + $callableParameters = $newParameters; + } + + return $callableParameters; + } + +} From a7d8a9965df67fc8656173ae1fc5986bcc84947f Mon Sep 17 00:00:00 2001 From: Ondrej Mirtes Date: Sun, 13 Sep 2026 13:24:51 +0200 Subject: [PATCH 09/10] Cleanup --- src/Analyser/MutatingScope.php | 2 +- src/Analyser/NodeScopeResolver.php | 51 +++++-------------- .../Php/PhpClassReflectionExtension.php | 3 +- 3 files changed, 17 insertions(+), 39 deletions(-) diff --git a/src/Analyser/MutatingScope.php b/src/Analyser/MutatingScope.php index 483d58981da..d0eb57ef6aa 100644 --- a/src/Analyser/MutatingScope.php +++ b/src/Analyser/MutatingScope.php @@ -1458,7 +1458,7 @@ protected function findSettledStoredResult(Expr $node): ?ExpressionResult return null; } - return $this->container->getByType(NodeScopeResolver::class)->findSettledExpressionResult($storage, $node); + return $storage->findExpressionResult($node); } public function getCurrentExpressionResultStorage(): ?ExpressionResultStorage diff --git a/src/Analyser/NodeScopeResolver.php b/src/Analyser/NodeScopeResolver.php index 1ef087a82b7..8500e4347b8 100644 --- a/src/Analyser/NodeScopeResolver.php +++ b/src/Analyser/NodeScopeResolver.php @@ -204,30 +204,6 @@ public function processNodes( } } - /** The stored result an outside asker may consume. */ - public function findSettledExpressionResult(ExpressionResultStorage $storage, Expr $expr): ?ExpressionResult - { - return $storage->findExpressionResult($expr); - } - - /** An effect-free result carrying eagerly known types, positioned at the given scope. */ - protected function createEagerExpressionResult(MutatingScope $scope, Expr $expr, Type $type, Type $nativeType): ExpressionResult - { - return $this->expressionResultFactory->create( - $scope, - beforeScope: $scope, - expr: $expr, - hasYield: false, - isAlwaysTerminating: false, - throwPoints: [], - impurePoints: [], - typeCallback: null, - specifyTypesCallback: SpecifiedTypes::emptySpecifyCallback(), - type: $type, - nativeType: $nativeType, - ); - } - public function storeExpressionResult(ExpressionResultStorage $storage, Expr $expr, ExpressionResult $expressionResult): void { if (self::$guardNewWorld) { @@ -506,7 +482,20 @@ public function processExprOnDemand(Expr $expr, MutatingScope $scope, Expression ExprHandlerRegistry::resolve($expr, $this->container) === null && !($expr instanceof Expr\CallLike && $expr->isFirstClassCallable()) ) { - return $this->createEagerExpressionResult($scope, $expr, new MixedType(), new MixedType()); + $mixed = new MixedType(); + return $this->expressionResultFactory->create( + $scope, + beforeScope: $scope, + expr: $expr, + hasYield: false, + isAlwaysTerminating: false, + throwPoints: [], + impurePoints: [], + typeCallback: null, + specifyTypesCallback: SpecifiedTypes::emptySpecifyCallback(), + type: $mixed, + nativeType: $mixed, + ); } // save/restore, never reset: on-demand walks nest (a typeCallback @@ -532,18 +521,6 @@ public function processExprOnDemand(Expr $expr, MutatingScope $scope, Expression } } - /** - * Processes an expression for an independent analysis pass - a reflection- - * level lazy computation that builds its own scope from scratch (e.g. - * private property type inference from constructor assignments). Such a - * pass legitimately prices real AST nodes outside the file's main walk, - * on a fresh storage of its own. - */ - public function processIndependentPassExpr(Expr $expr, MutatingScope $scope): ExpressionResult - { - return $this->processExprOnDemand($expr, $scope, new ExpressionResultStorage()); - } - /** * The stored ExpressionResult of a node processExprNode() already processed * into the given storage - the caller asserts the processing order by diff --git a/src/Reflection/Php/PhpClassReflectionExtension.php b/src/Reflection/Php/PhpClassReflectionExtension.php index 441874ecf5b..80874d54ae0 100644 --- a/src/Reflection/Php/PhpClassReflectionExtension.php +++ b/src/Reflection/Php/PhpClassReflectionExtension.php @@ -7,6 +7,7 @@ use PhpParser\Node\Stmt\ClassMethod; use PhpParser\Node\Stmt\Declare_; use PhpParser\Node\Stmt\Namespace_; +use PHPStan\Analyser\ExpressionResultStorage; use PHPStan\Analyser\NodeScopeResolver; use PHPStan\Analyser\OutOfClassScope; use PHPStan\Analyser\PhpDocsResolver; @@ -1239,7 +1240,7 @@ private function inferAndCachePropertyTypes( // an independent lazy pass on its own scope - never read through // Scope::getType(), which is reserved for the file's main walk - $propertyType = $this->nodeScopeResolver->processIndependentPassExpr($expr->expr, $methodScope)->getType(); + $propertyType = $this->nodeScopeResolver->processExprOnDemand($expr->expr, $methodScope, new ExpressionResultStorage())->getType(); if ($propertyType instanceof ErrorType || $propertyType instanceof NeverType) { continue; } From 35188e724789930251c48848ee6badaa1c681a85 Mon Sep 17 00:00:00 2001 From: Ondrej Mirtes Date: Sun, 13 Sep 2026 13:33:17 +0200 Subject: [PATCH 10/10] Move collectOffsetSetUsage() from NodeScopeResolver to AssignHandler AssignHandler is its only caller, so it becomes a private method there, observing through the handler's own TemplateArgumentObserver. Co-Authored-By: Claude Opus 5 (1M context) Claude-Session: https://claude.ai/code/session_014hgcbwfKQnye2coQXtX5XU --- src/Analyser/ExprHandler/AssignHandler.php | 30 +++++++++++++++++++++- src/Analyser/NodeScopeResolver.php | 26 ------------------- 2 files changed, 29 insertions(+), 27 deletions(-) diff --git a/src/Analyser/ExprHandler/AssignHandler.php b/src/Analyser/ExprHandler/AssignHandler.php index 7ff29aef635..abdd17e2fd2 100644 --- a/src/Analyser/ExprHandler/AssignHandler.php +++ b/src/Analyser/ExprHandler/AssignHandler.php @@ -36,6 +36,7 @@ use PHPStan\Analyser\ExprHandler\Helper\MethodThrowPointHelper; use PHPStan\Analyser\ExprHandler\Helper\NonNullabilityHelper; use PHPStan\Analyser\ExprHandler\Helper\VirtualExprResultHelper; +use PHPStan\Analyser\Generics\TemplateArgumentConstraints; use PHPStan\Analyser\Generics\TemplateArgumentObserver; use PHPStan\Analyser\ImpurePoint; use PHPStan\Analyser\InternalThrowPoint; @@ -1492,7 +1493,8 @@ public function applyWrite( && !$setVarType->isArray()->yes() && !(new ObjectType(ArrayAccess::class))->isSuperTypeOf($setVarType)->no() ) { - $scope = $scope->addTemplateArgumentConstraints($nodeScopeResolver->collectOffsetSetUsage( + $scope = $scope->addTemplateArgumentConstraints($this->collectOffsetSetUsage( + $nodeScopeResolver, $scope, $setVarType, $offsetTypes[count($offsetTypes) - 1][0], @@ -1813,6 +1815,32 @@ public function applyWrite( ); } + /** + * A write through ArrayAccess (`$storage[$key] = $value`) reaches the + * object's template arguments exactly like the offsetSet() call it stands + * for, so observe both the key and the written value against that method's + * parameters resolved on the receiver. + */ + private function collectOffsetSetUsage(NodeScopeResolver $nodeScopeResolver, MutatingScope $scope, Type $receiverType, ?Type $keyType, Type $valueType): TemplateArgumentConstraints + { + $constraints = TemplateArgumentConstraints::createEmpty(); + $frame = $nodeScopeResolver->observingTemplateArgumentFrame($scope); + if ($frame === null || !$receiverType->hasMethod('offsetSet')->yes()) { + return $constraints; + } + + $parameters = $receiverType->getMethod('offsetSet', $scope)->getOnlyVariant()->getParameters(); + if ($keyType !== null && isset($parameters[0])) { + $constraints = $constraints->merge($this->templateArgumentObserver->collectArgument($parameters[0]->getType(), $keyType)); + } + if (!isset($parameters[1])) { + return $constraints; + } + + $constraints = $constraints->merge($this->templateArgumentObserver->collectArgument($parameters[1]->getType(), $valueType)); + return $constraints; + } + private function createArrayDimFetchConditionalExpressionHolder( Expr\Variable $keyVar, Expr $arrayArg, diff --git a/src/Analyser/NodeScopeResolver.php b/src/Analyser/NodeScopeResolver.php index 8500e4347b8..3e6a28511b0 100644 --- a/src/Analyser/NodeScopeResolver.php +++ b/src/Analyser/NodeScopeResolver.php @@ -1067,30 +1067,4 @@ public function collectReturnSend(MutatingScope $scope, ExpressionResult $return return $this->templateArgumentObserver->collectSend($declaredReturnType, $returnedResult->getType()); } - /** - * A write through ArrayAccess (`$storage[$key] = $value`) reaches the - * object's template arguments exactly like the offsetSet() call it stands - * for, so observe both the key and the written value against that method's - * parameters resolved on the receiver. - */ - public function collectOffsetSetUsage(MutatingScope $scope, Type $receiverType, ?Type $keyType, Type $valueType): TemplateArgumentConstraints - { - $constraints = TemplateArgumentConstraints::createEmpty(); - $frame = $this->observingTemplateArgumentFrame($scope); - if ($frame === null || !$receiverType->hasMethod('offsetSet')->yes()) { - return $constraints; - } - - $parameters = $receiverType->getMethod('offsetSet', $scope)->getOnlyVariant()->getParameters(); - if ($keyType !== null && isset($parameters[0])) { - $constraints = $constraints->merge($this->templateArgumentObserver->collectArgument($parameters[0]->getType(), $keyType)); - } - if (!isset($parameters[1])) { - return $constraints; - } - - $constraints = $constraints->merge($this->templateArgumentObserver->collectArgument($parameters[1]->getType(), $valueType)); - return $constraints; - } - }