From f9d803e2ea8934961fbee624682b6781837223c4 Mon Sep 17 00:00:00 2001 From: Ondrej Mirtes Date: Thu, 11 Jun 2026 10:59:27 +0200 Subject: [PATCH 001/113] Introduce ExpressionResultFactory --- .../ExprHandler/ArrayDimFetchHandler.php | 9 +++++-- src/Analyser/ExprHandler/ArrayHandler.php | 4 +++- .../ExprHandler/ArrowFunctionHandler.php | 4 +++- src/Analyser/ExprHandler/AssignHandler.php | 12 ++++++---- src/Analyser/ExprHandler/AssignOpHandler.php | 8 ++++--- src/Analyser/ExprHandler/BinaryOpHandler.php | 4 +++- .../ExprHandler/BitwiseNotHandler.php | 4 +++- .../ExprHandler/BooleanAndHandler.php | 4 +++- .../ExprHandler/BooleanNotHandler.php | 7 +++++- src/Analyser/ExprHandler/BooleanOrHandler.php | 4 +++- src/Analyser/ExprHandler/CastHandler.php | 4 +++- .../ExprHandler/CastStringHandler.php | 4 +++- .../ExprHandler/ClassConstFetchHandler.php | 4 +++- src/Analyser/ExprHandler/CloneHandler.php | 7 +++++- src/Analyser/ExprHandler/ClosureHandler.php | 4 +++- src/Analyser/ExprHandler/CoalesceHandler.php | 4 +++- .../ExprHandler/ConstFetchHandler.php | 4 +++- src/Analyser/ExprHandler/EmptyHandler.php | 4 +++- .../ExprHandler/ErrorSuppressHandler.php | 7 +++++- src/Analyser/ExprHandler/EvalHandler.php | 7 +++++- src/Analyser/ExprHandler/ExitHandler.php | 7 +++++- src/Analyser/ExprHandler/FuncCallHandler.php | 4 +++- .../Helper/ImplicitToStringCallHelper.php | 6 +++-- src/Analyser/ExprHandler/IncludeHandler.php | 7 +++++- .../ExprHandler/InstanceofHandler.php | 7 +++++- .../ExprHandler/InterpolatedStringHandler.php | 4 +++- src/Analyser/ExprHandler/IssetHandler.php | 4 +++- src/Analyser/ExprHandler/MatchHandler.php | 4 +++- .../ExprHandler/MethodCallHandler.php | 6 +++-- src/Analyser/ExprHandler/NewHandler.php | 4 +++- .../ExprHandler/NullsafeMethodCallHandler.php | 4 +++- .../NullsafePropertyFetchHandler.php | 4 +++- src/Analyser/ExprHandler/PipeHandler.php | 7 +++++- src/Analyser/ExprHandler/PostDecHandler.php | 7 +++++- src/Analyser/ExprHandler/PostIncHandler.php | 7 +++++- src/Analyser/ExprHandler/PreDecHandler.php | 7 +++++- src/Analyser/ExprHandler/PreIncHandler.php | 7 +++++- src/Analyser/ExprHandler/PrintHandler.php | 4 +++- .../ExprHandler/PropertyFetchHandler.php | 4 +++- src/Analyser/ExprHandler/ScalarHandler.php | 4 +++- .../ExprHandler/StaticCallHandler.php | 4 +++- .../StaticPropertyFetchHandler.php | 4 +++- src/Analyser/ExprHandler/TernaryHandler.php | 4 +++- src/Analyser/ExprHandler/ThrowHandler.php | 7 +++++- .../ExprHandler/UnaryMinusHandler.php | 4 +++- src/Analyser/ExprHandler/UnaryPlusHandler.php | 4 +++- src/Analyser/ExprHandler/VariableHandler.php | 7 +++++- .../Virtual/AlwaysRememberedExprHandler.php | 7 +++++- .../Virtual/ExistingArrayDimFetchHandler.php | 7 +++++- .../Virtual/FunctionCallableNodeHandler.php | 7 +++++- .../Virtual/GetIterableKeyTypeExprHandler.php | 7 +++++- .../GetIterableValueTypeExprHandler.php | 7 +++++- .../Virtual/GetOffsetValueTypeExprHandler.php | 7 +++++- .../InstantiationCallableNodeHandler.php | 7 +++++- .../Virtual/MethodCallableNodeHandler.php | 7 +++++- .../Virtual/NativeTypeExprHandler.php | 7 +++++- .../OriginalPropertyTypeExprHandler.php | 4 +++- .../SetExistingOffsetValueTypeExprHandler.php | 7 +++++- .../Virtual/SetOffsetValueTypeExprHandler.php | 7 +++++- .../StaticMethodCallableNodeHandler.php | 7 +++++- .../ExprHandler/Virtual/TypeExprHandler.php | 7 +++++- .../Virtual/UnsetOffsetExprHandler.php | 7 +++++- src/Analyser/ExprHandler/YieldFromHandler.php | 7 +++++- src/Analyser/ExprHandler/YieldHandler.php | 7 +++++- src/Analyser/ExpressionResult.php | 3 +++ src/Analyser/ExpressionResultFactory.php | 24 +++++++++++++++++++ src/Analyser/NodeScopeResolver.php | 11 +++++---- src/Testing/RuleTestCase.php | 2 ++ src/Testing/TypeInferenceTestCase.php | 2 ++ tests/PHPStan/Analyser/AnalyserTest.php | 1 + .../Fiber/FiberNodeScopeResolverRuleTest.php | 2 ++ .../Fiber/FiberNodeScopeResolverTest.php | 2 ++ 72 files changed, 336 insertions(+), 78 deletions(-) create mode 100644 src/Analyser/ExpressionResultFactory.php diff --git a/src/Analyser/ExprHandler/ArrayDimFetchHandler.php b/src/Analyser/ExprHandler/ArrayDimFetchHandler.php index 1c389f9fb9a..f82489c876b 100644 --- a/src/Analyser/ExprHandler/ArrayDimFetchHandler.php +++ b/src/Analyser/ExprHandler/ArrayDimFetchHandler.php @@ -11,6 +11,7 @@ use PhpParser\Node\Stmt; use PHPStan\Analyser\ExpressionContext; use PHPStan\Analyser\ExpressionResult; +use PHPStan\Analyser\ExpressionResultFactory; use PHPStan\Analyser\ExpressionResultStorage; use PHPStan\Analyser\ExprHandler; use PHPStan\Analyser\ExprHandler\Helper\NullsafeShortCircuitingHelper; @@ -35,6 +36,10 @@ final class ArrayDimFetchHandler implements ExprHandler { + public function __construct(private ExpressionResultFactory $expressionResultFactory) + { + } + public function supports(Expr $expr): bool { return $expr instanceof ArrayDimFetch; @@ -80,7 +85,7 @@ public function processExpr(NodeScopeResolver $nodeScopeResolver, Stmt $stmt, Ex $varResult = $nodeScopeResolver->processExprNode($stmt, $expr->var, $scope, $storage, $nodeCallback, $context->enterDeep()); $scope = $varResult->getScope(); - return new ExpressionResult( + return $this->expressionResultFactory->create( $scope, hasYield: $varResult->hasYield(), isAlwaysTerminating: $varResult->isAlwaysTerminating(), @@ -109,7 +114,7 @@ public function processExpr(NodeScopeResolver $nodeScopeResolver, Stmt $stmt, Ex )->getThrowPoints()); } - return new ExpressionResult( + return $this->expressionResultFactory->create( $scope, hasYield: $dimResult->hasYield() || $varResult->hasYield(), isAlwaysTerminating: $dimResult->isAlwaysTerminating() || $varResult->isAlwaysTerminating(), diff --git a/src/Analyser/ExprHandler/ArrayHandler.php b/src/Analyser/ExprHandler/ArrayHandler.php index f64a168e848..580ab998b6c 100644 --- a/src/Analyser/ExprHandler/ArrayHandler.php +++ b/src/Analyser/ExprHandler/ArrayHandler.php @@ -10,6 +10,7 @@ use PhpParser\Node\Stmt; use PHPStan\Analyser\ExpressionContext; use PHPStan\Analyser\ExpressionResult; +use PHPStan\Analyser\ExpressionResultFactory; use PHPStan\Analyser\ExpressionResultStorage; use PHPStan\Analyser\ExprHandler; use PHPStan\Analyser\MutatingScope; @@ -37,6 +38,7 @@ final class ArrayHandler implements ExprHandler public function __construct( private InitializerExprTypeResolver $initializerExprTypeResolver, + private ExpressionResultFactory $expressionResultFactory, ) { } @@ -98,7 +100,7 @@ public function processExpr(NodeScopeResolver $nodeScopeResolver, Stmt $stmt, Ex } $nodeScopeResolver->callNodeCallback($nodeCallback, new LiteralArrayNode($expr, $itemNodes), $scope, $storage); - return new ExpressionResult( + return $this->expressionResultFactory->create( $scope, hasYield: $hasYield, isAlwaysTerminating: $isAlwaysTerminating, diff --git a/src/Analyser/ExprHandler/ArrowFunctionHandler.php b/src/Analyser/ExprHandler/ArrowFunctionHandler.php index 0cdfddf675d..d4306247c81 100644 --- a/src/Analyser/ExprHandler/ArrowFunctionHandler.php +++ b/src/Analyser/ExprHandler/ArrowFunctionHandler.php @@ -7,6 +7,7 @@ use PhpParser\Node\Stmt; use PHPStan\Analyser\ExpressionContext; use PHPStan\Analyser\ExpressionResult; +use PHPStan\Analyser\ExpressionResultFactory; use PHPStan\Analyser\ExpressionResultStorage; use PHPStan\Analyser\ExprHandler; use PHPStan\Analyser\ExprHandler\Helper\ClosureTypeResolver; @@ -28,6 +29,7 @@ final class ArrowFunctionHandler implements ExprHandler public function __construct( private ClosureTypeResolver $closureTypeResolver, + private ExpressionResultFactory $expressionResultFactory, ) { } @@ -41,7 +43,7 @@ public function processExpr(NodeScopeResolver $nodeScopeResolver, Stmt $stmt, Ex { $result = $nodeScopeResolver->processArrowFunctionNode($stmt, $expr, $scope, $storage, $nodeCallback, null); - return new ExpressionResult( + return $this->expressionResultFactory->create( $result->getScope(), hasYield: $result->hasYield(), isAlwaysTerminating: false, diff --git a/src/Analyser/ExprHandler/AssignHandler.php b/src/Analyser/ExprHandler/AssignHandler.php index 540119391e7..5f16804d271 100644 --- a/src/Analyser/ExprHandler/AssignHandler.php +++ b/src/Analyser/ExprHandler/AssignHandler.php @@ -25,6 +25,7 @@ use PHPStan\Analyser\ConditionalExpressionHolder; use PHPStan\Analyser\ExpressionContext; use PHPStan\Analyser\ExpressionResult; +use PHPStan\Analyser\ExpressionResultFactory; use PHPStan\Analyser\ExpressionResultStorage; use PHPStan\Analyser\ExpressionTypeHolder; use PHPStan\Analyser\ExprHandler; @@ -95,6 +96,7 @@ public function __construct( private PhpVersion $phpVersion, private ExprPrinter $exprPrinter, private MatchHandler $matchHandler, + private ExpressionResultFactory $expressionResultFactory, ) { } @@ -298,7 +300,7 @@ public function processExpr(NodeScopeResolver $nodeScopeResolver, Stmt $stmt, Ex $expr->expr, $nodeCallback, $context, - static function (MutatingScope $scope) use ($stmt, $expr, $nodeCallback, $context, $storage, $nodeScopeResolver): ExpressionResult { + function (MutatingScope $scope) use ($stmt, $expr, $nodeCallback, $context, $storage, $nodeScopeResolver): ExpressionResult { $impurePoints = []; if ($expr instanceof AssignRef) { $referencedExpr = $expr->expr; @@ -338,7 +340,7 @@ static function (MutatingScope $scope) use ($stmt, $expr, $nodeCallback, $contex $scope = $scope->exitExpressionAssign($expr->expr); } - return new ExpressionResult($scope, $hasYield, $isAlwaysTerminating, $throwPoints, $impurePoints); + return $this->expressionResultFactory->create($scope, $hasYield, $isAlwaysTerminating, $throwPoints, $impurePoints); }, true, ); @@ -380,7 +382,7 @@ static function (MutatingScope $scope) use ($stmt, $expr, $nodeCallback, $contex } } - return new ExpressionResult( + return $this->expressionResultFactory->create( $scope, hasYield: $result->hasYield(), isAlwaysTerminating: $result->isAlwaysTerminating(), @@ -938,7 +940,7 @@ public function processAssignVar( new GetOffsetValueTypeExpr($assignedExpr, $dimExpr), $nodeCallback, $context, - static fn (MutatingScope $scope): ExpressionResult => new ExpressionResult($scope, hasYield: false, isAlwaysTerminating: false, throwPoints: [], impurePoints: []), + fn (MutatingScope $scope): ExpressionResult => $this->expressionResultFactory->create($scope, hasYield: false, isAlwaysTerminating: false, throwPoints: [], impurePoints: []), $enterExpressionAssign, ); $scope = $result->getScope(); @@ -1030,7 +1032,7 @@ public function processAssignVar( } // stored where processAssignVar is called - return new ExpressionResult($scope, $hasYield, $isAlwaysTerminating, $throwPoints, $impurePoints); + return $this->expressionResultFactory->create($scope, $hasYield, $isAlwaysTerminating, $throwPoints, $impurePoints); } private function createArrayDimFetchConditionalExpressionHolder( diff --git a/src/Analyser/ExprHandler/AssignOpHandler.php b/src/Analyser/ExprHandler/AssignOpHandler.php index 31af0b9c77f..17a35977c29 100644 --- a/src/Analyser/ExprHandler/AssignOpHandler.php +++ b/src/Analyser/ExprHandler/AssignOpHandler.php @@ -11,6 +11,7 @@ use PhpParser\Node\Stmt; use PHPStan\Analyser\ExpressionContext; use PHPStan\Analyser\ExpressionResult; +use PHPStan\Analyser\ExpressionResultFactory; use PHPStan\Analyser\ExpressionResultStorage; use PHPStan\Analyser\ExprHandler; use PHPStan\Analyser\ExprHandler\Helper\ImplicitToStringCallHelper; @@ -42,6 +43,7 @@ public function __construct( private AssignHandler $assignHandler, private InitializerExprTypeResolver $initializerExprTypeResolver, private ImplicitToStringCallHelper $implicitToStringCallHelper, + private ExpressionResultFactory $expressionResultFactory, ) { } @@ -62,7 +64,7 @@ public function processExpr(NodeScopeResolver $nodeScopeResolver, Stmt $stmt, Ex $expr, $nodeCallback, $context, - static function (MutatingScope $scope) use ($stmt, $expr, $nodeCallback, $context, $storage, $nodeScopeResolver): ExpressionResult { + function (MutatingScope $scope) use ($stmt, $expr, $nodeCallback, $context, $storage, $nodeScopeResolver): ExpressionResult { $originalScope = $scope; if ($expr instanceof Expr\AssignOp\Coalesce) { $scope = $scope->filterByFalseyValue( @@ -74,7 +76,7 @@ static function (MutatingScope $scope) use ($stmt, $expr, $nodeCallback, $contex if ($expr instanceof Expr\AssignOp\Coalesce) { $nodeScopeResolver->storeBeforeScope($storage, $expr, $originalScope); $isAlwaysTerminating = $exprResult->isAlwaysTerminating() && $originalScope->getType($expr->var)->isNull()->yes(); - return new ExpressionResult( + return $this->expressionResultFactory->create( $exprResult->getScope()->mergeWith($originalScope), $exprResult->hasYield(), $isAlwaysTerminating, @@ -105,7 +107,7 @@ static function (MutatingScope $scope) use ($stmt, $expr, $nodeCallback, $contex $impurePoints = array_merge($impurePoints, $toStringResult->getImpurePoints()); } - return new ExpressionResult( + return $this->expressionResultFactory->create( $scope, hasYield: $assignResult->hasYield(), isAlwaysTerminating: $assignResult->isAlwaysTerminating(), diff --git a/src/Analyser/ExprHandler/BinaryOpHandler.php b/src/Analyser/ExprHandler/BinaryOpHandler.php index 0f604c86441..4c37485323d 100644 --- a/src/Analyser/ExprHandler/BinaryOpHandler.php +++ b/src/Analyser/ExprHandler/BinaryOpHandler.php @@ -14,6 +14,7 @@ use PhpParser\Node\Stmt; use PHPStan\Analyser\ExpressionContext; use PHPStan\Analyser\ExpressionResult; +use PHPStan\Analyser\ExpressionResultFactory; use PHPStan\Analyser\ExpressionResultStorage; use PHPStan\Analyser\ExprHandler; use PHPStan\Analyser\ExprHandler\Helper\EqualityTypeSpecifyingHelper; @@ -66,6 +67,7 @@ public function __construct( private ImplicitToStringCallHelper $implicitToStringCallHelper, private ExprPrinter $exprPrinter, private EqualityTypeSpecifyingHelper $equalityTypeSpecifyingHelper, + private ExpressionResultFactory $expressionResultFactory, ) { } @@ -101,7 +103,7 @@ public function processExpr(NodeScopeResolver $nodeScopeResolver, Stmt $stmt, Ex } $scope = $rightResult->getScope(); - return new ExpressionResult( + return $this->expressionResultFactory->create( $scope, hasYield: $leftResult->hasYield() || $rightResult->hasYield(), isAlwaysTerminating: $leftResult->isAlwaysTerminating() || $rightResult->isAlwaysTerminating(), diff --git a/src/Analyser/ExprHandler/BitwiseNotHandler.php b/src/Analyser/ExprHandler/BitwiseNotHandler.php index de49fb09887..08ea1a0c1ed 100644 --- a/src/Analyser/ExprHandler/BitwiseNotHandler.php +++ b/src/Analyser/ExprHandler/BitwiseNotHandler.php @@ -7,6 +7,7 @@ use PhpParser\Node\Stmt; use PHPStan\Analyser\ExpressionContext; use PHPStan\Analyser\ExpressionResult; +use PHPStan\Analyser\ExpressionResultFactory; use PHPStan\Analyser\ExpressionResultStorage; use PHPStan\Analyser\ExprHandler; use PHPStan\Analyser\MutatingScope; @@ -28,6 +29,7 @@ final class BitwiseNotHandler implements ExprHandler public function __construct( private InitializerExprTypeResolver $initializerExprTypeResolver, + private ExpressionResultFactory $expressionResultFactory, ) { } @@ -41,7 +43,7 @@ public function processExpr(NodeScopeResolver $nodeScopeResolver, Stmt $stmt, Ex { $exprResult = $nodeScopeResolver->processExprNode($stmt, $expr->expr, $scope, $storage, $nodeCallback, $context->enterDeep()); - return new ExpressionResult( + return $this->expressionResultFactory->create( $exprResult->getScope(), hasYield: $exprResult->hasYield(), isAlwaysTerminating: $exprResult->isAlwaysTerminating(), diff --git a/src/Analyser/ExprHandler/BooleanAndHandler.php b/src/Analyser/ExprHandler/BooleanAndHandler.php index 383b03c7d66..5727c8cfdf5 100644 --- a/src/Analyser/ExprHandler/BooleanAndHandler.php +++ b/src/Analyser/ExprHandler/BooleanAndHandler.php @@ -10,6 +10,7 @@ use PhpParser\Node\Stmt; use PHPStan\Analyser\ExpressionContext; use PHPStan\Analyser\ExpressionResult; +use PHPStan\Analyser\ExpressionResultFactory; use PHPStan\Analyser\ExpressionResultStorage; use PHPStan\Analyser\ExprHandler; use PHPStan\Analyser\ExprHandler\Helper\ConditionalExpressionHolderHelper; @@ -44,6 +45,7 @@ final class BooleanAndHandler implements ExprHandler public function __construct( private NodeScopeResolver $nodeScopeResolver, private ConditionalExpressionHolderHelper $conditionalExpressionHolderHelper, + private ExpressionResultFactory $expressionResultFactory, ) { } @@ -271,7 +273,7 @@ public function processExpr(NodeScopeResolver $nodeScopeResolver, Stmt $stmt, Ex $nodeScopeResolver->callNodeCallbackWithExpression($nodeCallback, new BooleanAndNode($expr, $leftTruthyScope), $scope, $storage, $context); - return new ExpressionResult( + return $this->expressionResultFactory->create( $leftMergedWithRightScope, hasYield: $leftResult->hasYield() || $rightResult->hasYield(), isAlwaysTerminating: $leftResult->isAlwaysTerminating(), diff --git a/src/Analyser/ExprHandler/BooleanNotHandler.php b/src/Analyser/ExprHandler/BooleanNotHandler.php index 6bd2831fb27..082615dc3d5 100644 --- a/src/Analyser/ExprHandler/BooleanNotHandler.php +++ b/src/Analyser/ExprHandler/BooleanNotHandler.php @@ -7,6 +7,7 @@ use PhpParser\Node\Stmt; use PHPStan\Analyser\ExpressionContext; use PHPStan\Analyser\ExpressionResult; +use PHPStan\Analyser\ExpressionResultFactory; use PHPStan\Analyser\ExpressionResultStorage; use PHPStan\Analyser\ExprHandler; use PHPStan\Analyser\MutatingScope; @@ -27,6 +28,10 @@ final class BooleanNotHandler implements ExprHandler { + public function __construct(private ExpressionResultFactory $expressionResultFactory) + { + } + public function supports(Expr $expr): bool { return $expr instanceof BooleanNot; @@ -37,7 +42,7 @@ public function processExpr(NodeScopeResolver $nodeScopeResolver, Stmt $stmt, Ex $exprResult = $nodeScopeResolver->processExprNode($stmt, $expr->expr, $scope, $storage, $nodeCallback, $context->enterDeep()); $scope = $exprResult->getScope(); - return new ExpressionResult( + return $this->expressionResultFactory->create( $scope, hasYield: $exprResult->hasYield(), isAlwaysTerminating: $exprResult->isAlwaysTerminating(), diff --git a/src/Analyser/ExprHandler/BooleanOrHandler.php b/src/Analyser/ExprHandler/BooleanOrHandler.php index d439a2c8082..9f2a00bef4e 100644 --- a/src/Analyser/ExprHandler/BooleanOrHandler.php +++ b/src/Analyser/ExprHandler/BooleanOrHandler.php @@ -8,6 +8,7 @@ use PhpParser\Node\Stmt; use PHPStan\Analyser\ExpressionContext; use PHPStan\Analyser\ExpressionResult; +use PHPStan\Analyser\ExpressionResultFactory; use PHPStan\Analyser\ExpressionResultStorage; use PHPStan\Analyser\ExprHandler; use PHPStan\Analyser\ExprHandler\Helper\ConditionalExpressionHolderHelper; @@ -43,6 +44,7 @@ final class BooleanOrHandler implements ExprHandler public function __construct( private NodeScopeResolver $nodeScopeResolver, private ConditionalExpressionHolderHelper $conditionalExpressionHolderHelper, + private ExpressionResultFactory $expressionResultFactory, ) { } @@ -303,7 +305,7 @@ public function processExpr(NodeScopeResolver $nodeScopeResolver, Stmt $stmt, Ex $nodeScopeResolver->callNodeCallbackWithExpression($nodeCallback, new BooleanOrNode($expr, $leftFalseyScope), $scope, $storage, $context); - return new ExpressionResult( + return $this->expressionResultFactory->create( $leftMergedWithRightScope, hasYield: $leftResult->hasYield() || $rightResult->hasYield(), isAlwaysTerminating: $leftResult->isAlwaysTerminating(), diff --git a/src/Analyser/ExprHandler/CastHandler.php b/src/Analyser/ExprHandler/CastHandler.php index cbc3d70fcb0..f7837085055 100644 --- a/src/Analyser/ExprHandler/CastHandler.php +++ b/src/Analyser/ExprHandler/CastHandler.php @@ -13,6 +13,7 @@ use PhpParser\Node\Stmt; use PHPStan\Analyser\ExpressionContext; use PHPStan\Analyser\ExpressionResult; +use PHPStan\Analyser\ExpressionResultFactory; use PHPStan\Analyser\ExpressionResultStorage; use PHPStan\Analyser\ExprHandler; use PHPStan\Analyser\MutatingScope; @@ -35,6 +36,7 @@ final class CastHandler implements ExprHandler public function __construct( private InitializerExprTypeResolver $initializerExprTypeResolver, + private ExpressionResultFactory $expressionResultFactory, ) { } @@ -49,7 +51,7 @@ public function processExpr(NodeScopeResolver $nodeScopeResolver, Stmt $stmt, Ex $exprResult = $nodeScopeResolver->processExprNode($stmt, $expr->expr, $scope, $storage, $nodeCallback, $context->enterDeep()); $scope = $exprResult->getScope(); - return new ExpressionResult( + return $this->expressionResultFactory->create( $scope, hasYield: $exprResult->hasYield(), isAlwaysTerminating: $exprResult->isAlwaysTerminating(), diff --git a/src/Analyser/ExprHandler/CastStringHandler.php b/src/Analyser/ExprHandler/CastStringHandler.php index 4fdfc9adfc6..e8a4a3c9057 100644 --- a/src/Analyser/ExprHandler/CastStringHandler.php +++ b/src/Analyser/ExprHandler/CastStringHandler.php @@ -9,6 +9,7 @@ use PhpParser\Node\Stmt; use PHPStan\Analyser\ExpressionContext; use PHPStan\Analyser\ExpressionResult; +use PHPStan\Analyser\ExpressionResultFactory; use PHPStan\Analyser\ExpressionResultStorage; use PHPStan\Analyser\ExprHandler; use PHPStan\Analyser\ExprHandler\Helper\ImplicitToStringCallHelper; @@ -33,6 +34,7 @@ final class CastStringHandler implements ExprHandler public function __construct( private InitializerExprTypeResolver $initializerExprTypeResolver, private ImplicitToStringCallHelper $implicitToStringCallHelper, + private ExpressionResultFactory $expressionResultFactory, ) { } @@ -54,7 +56,7 @@ public function processExpr(NodeScopeResolver $nodeScopeResolver, Stmt $stmt, Ex $scope = $exprResult->getScope(); - return new ExpressionResult( + return $this->expressionResultFactory->create( $scope, hasYield: $exprResult->hasYield(), isAlwaysTerminating: $exprResult->isAlwaysTerminating(), diff --git a/src/Analyser/ExprHandler/ClassConstFetchHandler.php b/src/Analyser/ExprHandler/ClassConstFetchHandler.php index 25199ed14f7..61e28d1cac5 100644 --- a/src/Analyser/ExprHandler/ClassConstFetchHandler.php +++ b/src/Analyser/ExprHandler/ClassConstFetchHandler.php @@ -8,6 +8,7 @@ use PhpParser\Node\Stmt; use PHPStan\Analyser\ExpressionContext; use PHPStan\Analyser\ExpressionResult; +use PHPStan\Analyser\ExpressionResultFactory; use PHPStan\Analyser\ExpressionResultStorage; use PHPStan\Analyser\ExprHandler; use PHPStan\Analyser\MutatingScope; @@ -31,6 +32,7 @@ final class ClassConstFetchHandler implements ExprHandler public function __construct( private InitializerExprTypeResolver $initializerExprTypeResolver, + private ExpressionResultFactory $expressionResultFactory, ) { } @@ -83,7 +85,7 @@ public function processExpr(NodeScopeResolver $nodeScopeResolver, Stmt $stmt, Ex $isAlwaysTerminating = $isAlwaysTerminating || $nameResult->isAlwaysTerminating(); } - return new ExpressionResult( + return $this->expressionResultFactory->create( $scope, hasYield: $hasYield, isAlwaysTerminating: $isAlwaysTerminating, diff --git a/src/Analyser/ExprHandler/CloneHandler.php b/src/Analyser/ExprHandler/CloneHandler.php index 9d2f1bf6574..78534ceb1c7 100644 --- a/src/Analyser/ExprHandler/CloneHandler.php +++ b/src/Analyser/ExprHandler/CloneHandler.php @@ -7,6 +7,7 @@ use PhpParser\Node\Stmt; use PHPStan\Analyser\ExpressionContext; use PHPStan\Analyser\ExpressionResult; +use PHPStan\Analyser\ExpressionResultFactory; use PHPStan\Analyser\ExpressionResultStorage; use PHPStan\Analyser\ExprHandler; use PHPStan\Analyser\MutatingScope; @@ -29,6 +30,10 @@ final class CloneHandler implements ExprHandler { + public function __construct(private ExpressionResultFactory $expressionResultFactory) + { + } + public function supports(Expr $expr): bool { return $expr instanceof Clone_; @@ -38,7 +43,7 @@ public function processExpr(NodeScopeResolver $nodeScopeResolver, Stmt $stmt, Ex { $exprResult = $nodeScopeResolver->processExprNode($stmt, $expr->expr, $scope, $storage, $nodeCallback, $context->enterDeep()); - return new ExpressionResult( + return $this->expressionResultFactory->create( $exprResult->getScope(), hasYield: $exprResult->hasYield(), isAlwaysTerminating: $exprResult->isAlwaysTerminating(), diff --git a/src/Analyser/ExprHandler/ClosureHandler.php b/src/Analyser/ExprHandler/ClosureHandler.php index cc70889d379..0961cf29c98 100644 --- a/src/Analyser/ExprHandler/ClosureHandler.php +++ b/src/Analyser/ExprHandler/ClosureHandler.php @@ -7,6 +7,7 @@ use PhpParser\Node\Stmt; use PHPStan\Analyser\ExpressionContext; use PHPStan\Analyser\ExpressionResult; +use PHPStan\Analyser\ExpressionResultFactory; use PHPStan\Analyser\ExpressionResultStorage; use PHPStan\Analyser\ExprHandler; use PHPStan\Analyser\ExprHandler\Helper\ClosureTypeResolver; @@ -28,6 +29,7 @@ final class ClosureHandler implements ExprHandler public function __construct( private ClosureTypeResolver $closureTypeResolver, + private ExpressionResultFactory $expressionResultFactory, ) { } @@ -42,7 +44,7 @@ public function processExpr(NodeScopeResolver $nodeScopeResolver, Stmt $stmt, Ex $processClosureResult = $nodeScopeResolver->processClosureNode($stmt, $expr, $scope, $storage, $nodeCallback, $context, null); $scope = $processClosureResult->applyByRefUseScope($processClosureResult->getScope()); - return new ExpressionResult( + return $this->expressionResultFactory->create( $scope, hasYield: false, isAlwaysTerminating: false, diff --git a/src/Analyser/ExprHandler/CoalesceHandler.php b/src/Analyser/ExprHandler/CoalesceHandler.php index eb566e0166d..2a6fab5a268 100644 --- a/src/Analyser/ExprHandler/CoalesceHandler.php +++ b/src/Analyser/ExprHandler/CoalesceHandler.php @@ -7,6 +7,7 @@ use PhpParser\Node\Stmt; use PHPStan\Analyser\ExpressionContext; use PHPStan\Analyser\ExpressionResult; +use PHPStan\Analyser\ExpressionResultFactory; use PHPStan\Analyser\ExpressionResultStorage; use PHPStan\Analyser\ExprHandler; use PHPStan\Analyser\ExprHandler\Helper\NonNullabilityHelper; @@ -34,6 +35,7 @@ final class CoalesceHandler implements ExprHandler public function __construct( private NonNullabilityHelper $nonNullabilityHelper, + private ExpressionResultFactory $expressionResultFactory, ) { } @@ -129,7 +131,7 @@ public function processExpr(NodeScopeResolver $nodeScopeResolver, Stmt $stmt, Ex $scope = $scope->filterByTruthyValue(new Expr\Isset_([$expr->left]))->mergeWith($rightResult->getScope()); } - return new ExpressionResult( + return $this->expressionResultFactory->create( $scope, hasYield: $condResult->hasYield() || $rightResult->hasYield(), isAlwaysTerminating: $condResult->isAlwaysTerminating(), diff --git a/src/Analyser/ExprHandler/ConstFetchHandler.php b/src/Analyser/ExprHandler/ConstFetchHandler.php index ba5ae2c71e1..6d825348e16 100644 --- a/src/Analyser/ExprHandler/ConstFetchHandler.php +++ b/src/Analyser/ExprHandler/ConstFetchHandler.php @@ -9,6 +9,7 @@ use PHPStan\Analyser\ConstantResolver; use PHPStan\Analyser\ExpressionContext; use PHPStan\Analyser\ExpressionResult; +use PHPStan\Analyser\ExpressionResultFactory; use PHPStan\Analyser\ExpressionResultStorage; use PHPStan\Analyser\ExprHandler; use PHPStan\Analyser\MutatingScope; @@ -33,6 +34,7 @@ final class ConstFetchHandler implements ExprHandler public function __construct( private ConstantResolver $constantResolver, + private ExpressionResultFactory $expressionResultFactory, ) { } @@ -46,7 +48,7 @@ public function processExpr(NodeScopeResolver $nodeScopeResolver, Stmt $stmt, Ex { $nodeScopeResolver->callNodeCallback($nodeCallback, $expr->name, $scope, $storage); - return new ExpressionResult( + return $this->expressionResultFactory->create( $scope, hasYield: false, isAlwaysTerminating: false, diff --git a/src/Analyser/ExprHandler/EmptyHandler.php b/src/Analyser/ExprHandler/EmptyHandler.php index 185850e6e6f..cba551bfd36 100644 --- a/src/Analyser/ExprHandler/EmptyHandler.php +++ b/src/Analyser/ExprHandler/EmptyHandler.php @@ -8,6 +8,7 @@ use PhpParser\Node\Stmt; use PHPStan\Analyser\ExpressionContext; use PHPStan\Analyser\ExpressionResult; +use PHPStan\Analyser\ExpressionResultFactory; use PHPStan\Analyser\ExpressionResultStorage; use PHPStan\Analyser\ExprHandler; use PHPStan\Analyser\ExprHandler\Helper\NonNullabilityHelper; @@ -32,6 +33,7 @@ final class EmptyHandler implements ExprHandler public function __construct( private NonNullabilityHelper $nonNullabilityHelper, + private ExpressionResultFactory $expressionResultFactory, ) { } @@ -92,7 +94,7 @@ public function processExpr(NodeScopeResolver $nodeScopeResolver, Stmt $stmt, Ex $scope = $this->nonNullabilityHelper->revertNonNullability($scope, $nonNullabilityResult->getSpecifiedExpressions()); $scope = $nodeScopeResolver->lookForUnsetAllowedUndefinedExpressions($scope, $expr->expr); - return new ExpressionResult( + return $this->expressionResultFactory->create( $scope, hasYield: $exprResult->hasYield(), isAlwaysTerminating: $exprResult->isAlwaysTerminating(), diff --git a/src/Analyser/ExprHandler/ErrorSuppressHandler.php b/src/Analyser/ExprHandler/ErrorSuppressHandler.php index 0e2e47fee9c..1189f6324a7 100644 --- a/src/Analyser/ExprHandler/ErrorSuppressHandler.php +++ b/src/Analyser/ExprHandler/ErrorSuppressHandler.php @@ -7,6 +7,7 @@ use PhpParser\Node\Stmt; use PHPStan\Analyser\ExpressionContext; use PHPStan\Analyser\ExpressionResult; +use PHPStan\Analyser\ExpressionResultFactory; use PHPStan\Analyser\ExpressionResultStorage; use PHPStan\Analyser\ExprHandler; use PHPStan\Analyser\MutatingScope; @@ -25,6 +26,10 @@ final class ErrorSuppressHandler implements ExprHandler { + public function __construct(private ExpressionResultFactory $expressionResultFactory) + { + } + public function supports(Expr $expr): bool { return $expr instanceof ErrorSuppress; @@ -34,7 +39,7 @@ public function processExpr(NodeScopeResolver $nodeScopeResolver, Stmt $stmt, Ex { $exprResult = $nodeScopeResolver->processExprNode($stmt, $expr->expr, $scope, $storage, $nodeCallback, $context); - return new ExpressionResult( + return $this->expressionResultFactory->create( $exprResult->getScope(), hasYield: $exprResult->hasYield(), isAlwaysTerminating: $exprResult->isAlwaysTerminating(), diff --git a/src/Analyser/ExprHandler/EvalHandler.php b/src/Analyser/ExprHandler/EvalHandler.php index c2e635c4880..62b16484e1a 100644 --- a/src/Analyser/ExprHandler/EvalHandler.php +++ b/src/Analyser/ExprHandler/EvalHandler.php @@ -7,6 +7,7 @@ use PhpParser\Node\Stmt; use PHPStan\Analyser\ExpressionContext; use PHPStan\Analyser\ExpressionResult; +use PHPStan\Analyser\ExpressionResultFactory; use PHPStan\Analyser\ExpressionResultStorage; use PHPStan\Analyser\ExprHandler; use PHPStan\Analyser\ImpurePoint; @@ -29,6 +30,10 @@ final class EvalHandler implements ExprHandler { + public function __construct(private ExpressionResultFactory $expressionResultFactory) + { + } + public function supports(Expr $expr): bool { return $expr instanceof Eval_; @@ -44,7 +49,7 @@ public function processExpr(NodeScopeResolver $nodeScopeResolver, Stmt $stmt, Ex $exprResult = $nodeScopeResolver->processExprNode($stmt, $expr->expr, $scope, $storage, $nodeCallback, $context->enterDeep()); $scope = $exprResult->getScope(); - return new ExpressionResult( + return $this->expressionResultFactory->create( $scope, hasYield: $exprResult->hasYield(), isAlwaysTerminating: $exprResult->isAlwaysTerminating(), diff --git a/src/Analyser/ExprHandler/ExitHandler.php b/src/Analyser/ExprHandler/ExitHandler.php index 7734d8dea34..a2d4a7db273 100644 --- a/src/Analyser/ExprHandler/ExitHandler.php +++ b/src/Analyser/ExprHandler/ExitHandler.php @@ -7,6 +7,7 @@ use PhpParser\Node\Stmt; use PHPStan\Analyser\ExpressionContext; use PHPStan\Analyser\ExpressionResult; +use PHPStan\Analyser\ExpressionResultFactory; use PHPStan\Analyser\ExpressionResultStorage; use PHPStan\Analyser\ExprHandler; use PHPStan\Analyser\ImpurePoint; @@ -28,6 +29,10 @@ final class ExitHandler implements ExprHandler { + public function __construct(private ExpressionResultFactory $expressionResultFactory) + { + } + public function supports(Expr $expr): bool { return $expr instanceof Exit_; @@ -51,7 +56,7 @@ public function processExpr(NodeScopeResolver $nodeScopeResolver, Stmt $stmt, Ex $scope = $exprResult->getScope(); } - return new ExpressionResult( + return $this->expressionResultFactory->create( $scope, hasYield: $hasYield, isAlwaysTerminating: true, diff --git a/src/Analyser/ExprHandler/FuncCallHandler.php b/src/Analyser/ExprHandler/FuncCallHandler.php index 21c8baa561f..3074d340cd1 100644 --- a/src/Analyser/ExprHandler/FuncCallHandler.php +++ b/src/Analyser/ExprHandler/FuncCallHandler.php @@ -16,6 +16,7 @@ use PHPStan\Analyser\ArgumentsNormalizer; use PHPStan\Analyser\ExpressionContext; use PHPStan\Analyser\ExpressionResult; +use PHPStan\Analyser\ExpressionResultFactory; use PHPStan\Analyser\ExpressionResultStorage; use PHPStan\Analyser\ExprHandler; use PHPStan\Analyser\ExprHandler\Helper\VoidToNullTypeTransformer; @@ -95,6 +96,7 @@ public function __construct( private bool $implicitThrows, #[AutowiredParameter] private bool $rememberPossiblyImpureFunctionValues, + private ExpressionResultFactory $expressionResultFactory, ) { } @@ -570,7 +572,7 @@ public function processExpr(NodeScopeResolver $nodeScopeResolver, Stmt $stmt, Ex $scope = $scope->afterOpenSslCall($functionReflection->getName()); } - return new ExpressionResult( + return $this->expressionResultFactory->create( $scope, hasYield: $hasYield, isAlwaysTerminating: $isAlwaysTerminating, diff --git a/src/Analyser/ExprHandler/Helper/ImplicitToStringCallHelper.php b/src/Analyser/ExprHandler/Helper/ImplicitToStringCallHelper.php index eab62846f88..4ef762ad16d 100644 --- a/src/Analyser/ExprHandler/Helper/ImplicitToStringCallHelper.php +++ b/src/Analyser/ExprHandler/Helper/ImplicitToStringCallHelper.php @@ -6,6 +6,7 @@ use PhpParser\Node\Identifier; use PHPStan\Analyser\ExpressionContext; use PHPStan\Analyser\ExpressionResult; +use PHPStan\Analyser\ExpressionResultFactory; use PHPStan\Analyser\ImpurePoint; use PHPStan\Analyser\MutatingScope; use PHPStan\DependencyInjection\AutowiredService; @@ -19,6 +20,7 @@ final class ImplicitToStringCallHelper public function __construct( private PhpVersion $phpVersion, private MethodThrowPointHelper $methodThrowPointHelper, + private ExpressionResultFactory $expressionResultFactory, ) { } @@ -35,7 +37,7 @@ public function processImplicitToStringCall(Expr $expr, MutatingScope $scope): E $toStringMethod = $scope->getMethodReflection($exprType, '__toString'); } if ($toStringMethod === null) { - return new ExpressionResult( + return $this->expressionResultFactory->create( $scope, hasYield: false, isAlwaysTerminating: false, @@ -67,7 +69,7 @@ public function processImplicitToStringCall(Expr $expr, MutatingScope $scope): E } } - return new ExpressionResult( + return $this->expressionResultFactory->create( $scope, hasYield: false, isAlwaysTerminating: false, diff --git a/src/Analyser/ExprHandler/IncludeHandler.php b/src/Analyser/ExprHandler/IncludeHandler.php index 788a7c16522..c8556a88caf 100644 --- a/src/Analyser/ExprHandler/IncludeHandler.php +++ b/src/Analyser/ExprHandler/IncludeHandler.php @@ -7,6 +7,7 @@ use PhpParser\Node\Stmt; use PHPStan\Analyser\ExpressionContext; use PHPStan\Analyser\ExpressionResult; +use PHPStan\Analyser\ExpressionResultFactory; use PHPStan\Analyser\ExpressionResultStorage; use PHPStan\Analyser\ExprHandler; use PHPStan\Analyser\ImpurePoint; @@ -30,6 +31,10 @@ final class IncludeHandler implements ExprHandler { + public function __construct(private ExpressionResultFactory $expressionResultFactory) + { + } + public function supports(Expr $expr): bool { return $expr instanceof Include_; @@ -46,7 +51,7 @@ public function processExpr(NodeScopeResolver $nodeScopeResolver, Stmt $stmt, Ex $identifier = in_array($expr->type, [Include_::TYPE_INCLUDE, Include_::TYPE_INCLUDE_ONCE], true) ? 'include' : 'require'; $scope = $exprResult->getScope()->afterExtractCall(); - return new ExpressionResult( + return $this->expressionResultFactory->create( $scope, hasYield: $exprResult->hasYield(), isAlwaysTerminating: $exprResult->isAlwaysTerminating(), diff --git a/src/Analyser/ExprHandler/InstanceofHandler.php b/src/Analyser/ExprHandler/InstanceofHandler.php index 36933e466b3..0c1007743f8 100644 --- a/src/Analyser/ExprHandler/InstanceofHandler.php +++ b/src/Analyser/ExprHandler/InstanceofHandler.php @@ -8,6 +8,7 @@ use PhpParser\Node\Stmt; use PHPStan\Analyser\ExpressionContext; use PHPStan\Analyser\ExpressionResult; +use PHPStan\Analyser\ExpressionResultFactory; use PHPStan\Analyser\ExpressionResultStorage; use PHPStan\Analyser\ExprHandler; use PHPStan\Analyser\MutatingScope; @@ -38,6 +39,10 @@ final class InstanceofHandler implements ExprHandler { + public function __construct(private ExpressionResultFactory $expressionResultFactory) + { + } + public function supports(Expr $expr): bool { return $expr instanceof Instanceof_; @@ -60,7 +65,7 @@ public function processExpr(NodeScopeResolver $nodeScopeResolver, Stmt $stmt, Ex $isAlwaysTerminating = $isAlwaysTerminating || $classResult->isAlwaysTerminating(); } - return new ExpressionResult( + return $this->expressionResultFactory->create( $scope, hasYield: $hasYield, isAlwaysTerminating: $isAlwaysTerminating, diff --git a/src/Analyser/ExprHandler/InterpolatedStringHandler.php b/src/Analyser/ExprHandler/InterpolatedStringHandler.php index 51de44f579f..3e8672a012f 100644 --- a/src/Analyser/ExprHandler/InterpolatedStringHandler.php +++ b/src/Analyser/ExprHandler/InterpolatedStringHandler.php @@ -8,6 +8,7 @@ use PhpParser\Node\Stmt; use PHPStan\Analyser\ExpressionContext; use PHPStan\Analyser\ExpressionResult; +use PHPStan\Analyser\ExpressionResultFactory; use PHPStan\Analyser\ExpressionResultStorage; use PHPStan\Analyser\ExprHandler; use PHPStan\Analyser\ExprHandler\Helper\ImplicitToStringCallHelper; @@ -33,6 +34,7 @@ final class InterpolatedStringHandler implements ExprHandler public function __construct( private InitializerExprTypeResolver $initializerExprTypeResolver, private ImplicitToStringCallHelper $implicitToStringCallHelper, + private ExpressionResultFactory $expressionResultFactory, ) { } @@ -65,7 +67,7 @@ public function processExpr(NodeScopeResolver $nodeScopeResolver, Stmt $stmt, Ex $scope = $partResult->getScope(); } - return new ExpressionResult( + return $this->expressionResultFactory->create( $scope, hasYield: $hasYield, isAlwaysTerminating: $isAlwaysTerminating, diff --git a/src/Analyser/ExprHandler/IssetHandler.php b/src/Analyser/ExprHandler/IssetHandler.php index da8e48431fd..a2becee723f 100644 --- a/src/Analyser/ExprHandler/IssetHandler.php +++ b/src/Analyser/ExprHandler/IssetHandler.php @@ -15,6 +15,7 @@ use PhpParser\Node\VarLikeIdentifier; use PHPStan\Analyser\ExpressionContext; use PHPStan\Analyser\ExpressionResult; +use PHPStan\Analyser\ExpressionResultFactory; use PHPStan\Analyser\ExpressionResultStorage; use PHPStan\Analyser\ExprHandler; use PHPStan\Analyser\ExprHandler\Helper\NonNullabilityHelper; @@ -59,6 +60,7 @@ final class IssetHandler implements ExprHandler public function __construct( private NonNullabilityHelper $nonNullabilityHelper, + private ExpressionResultFactory $expressionResultFactory, ) { } @@ -385,7 +387,7 @@ public function processExpr(NodeScopeResolver $nodeScopeResolver, Stmt $stmt, Ex $scope = $this->nonNullabilityHelper->revertNonNullability($scope, $nonNullabilityResult->getSpecifiedExpressions()); } - return new ExpressionResult( + return $this->expressionResultFactory->create( $scope, hasYield: $hasYield, isAlwaysTerminating: $isAlwaysTerminating, diff --git a/src/Analyser/ExprHandler/MatchHandler.php b/src/Analyser/ExprHandler/MatchHandler.php index 90d4714a5af..b7a0a4b7696 100644 --- a/src/Analyser/ExprHandler/MatchHandler.php +++ b/src/Analyser/ExprHandler/MatchHandler.php @@ -16,6 +16,7 @@ use PhpParser\Node\Stmt; use PHPStan\Analyser\ExpressionContext; use PHPStan\Analyser\ExpressionResult; +use PHPStan\Analyser\ExpressionResultFactory; use PHPStan\Analyser\ExpressionResultStorage; use PHPStan\Analyser\ExprHandler; use PHPStan\Analyser\InternalThrowPoint; @@ -57,6 +58,7 @@ final class MatchHandler implements ExprHandler public function __construct( #[AutowiredParameter] private bool $treatPhpDocTypesAsCertain, + private ExpressionResultFactory $expressionResultFactory, ) { } @@ -501,7 +503,7 @@ public function processExpr(NodeScopeResolver $nodeScopeResolver, Stmt $stmt, Ex $expr->cond = $expr->cond->getExpr(); } - return new ExpressionResult( + return $this->expressionResultFactory->create( $scope, hasYield: $hasYield, isAlwaysTerminating: $isAlwaysTerminating, diff --git a/src/Analyser/ExprHandler/MethodCallHandler.php b/src/Analyser/ExprHandler/MethodCallHandler.php index 1f661f5a980..310e2483ccb 100644 --- a/src/Analyser/ExprHandler/MethodCallHandler.php +++ b/src/Analyser/ExprHandler/MethodCallHandler.php @@ -11,6 +11,7 @@ use PHPStan\Analyser\ArgumentsNormalizer; use PHPStan\Analyser\ExpressionContext; use PHPStan\Analyser\ExpressionResult; +use PHPStan\Analyser\ExpressionResultFactory; use PHPStan\Analyser\ExpressionResultStorage; use PHPStan\Analyser\ExprHandler; use PHPStan\Analyser\ExprHandler\Helper\MethodCallReturnTypeHelper; @@ -60,6 +61,7 @@ public function __construct( private ReflectionProvider $reflectionProvider, #[AutowiredParameter] private bool $rememberPossiblyImpureFunctionValues, + private ExpressionResultFactory $expressionResultFactory, ) { } @@ -191,7 +193,7 @@ public function processExpr(NodeScopeResolver $nodeScopeResolver, Stmt $stmt, Ex $impurePoints = array_merge($impurePoints, $argsResult->getImpurePoints()); $isAlwaysTerminating = $isAlwaysTerminating || $argsResult->isAlwaysTerminating(); - $result = new ExpressionResult( + $result = $this->expressionResultFactory->create( $scope, hasYield: $hasYield, isAlwaysTerminating: $isAlwaysTerminating, @@ -219,7 +221,7 @@ public function processExpr(NodeScopeResolver $nodeScopeResolver, Stmt $stmt, Ex $calledMethodScope = $nodeScopeResolver->processCalledMethod($methodReflection); if ($calledMethodScope !== null) { $scope = $scope->mergeInitializedProperties($calledMethodScope); - return new ExpressionResult( + return $this->expressionResultFactory->create( $scope, hasYield: $result->hasYield(), isAlwaysTerminating: $result->isAlwaysTerminating(), diff --git a/src/Analyser/ExprHandler/NewHandler.php b/src/Analyser/ExprHandler/NewHandler.php index 2360ccd8076..a59d73ac12a 100644 --- a/src/Analyser/ExprHandler/NewHandler.php +++ b/src/Analyser/ExprHandler/NewHandler.php @@ -11,6 +11,7 @@ use PHPStan\Analyser\ArgumentsNormalizer; use PHPStan\Analyser\ExpressionContext; use PHPStan\Analyser\ExpressionResult; +use PHPStan\Analyser\ExpressionResultFactory; use PHPStan\Analyser\ExpressionResultStorage; use PHPStan\Analyser\ExprHandler; use PHPStan\Analyser\ImpurePoint; @@ -77,6 +78,7 @@ public function __construct( private PropertyReflectionFinder $propertyReflectionFinder, #[AutowiredParameter(ref: '%exceptions.implicitThrows%')] private bool $implicitThrows, + private ExpressionResultFactory $expressionResultFactory, ) { } @@ -215,7 +217,7 @@ public function processExpr(NodeScopeResolver $nodeScopeResolver, Stmt $stmt, Ex $throwPoints[] = InternalThrowPoint::createImplicit($scope, $expr); } - return new ExpressionResult( + return $this->expressionResultFactory->create( $scope, hasYield: $hasYield, isAlwaysTerminating: $isAlwaysTerminating, diff --git a/src/Analyser/ExprHandler/NullsafeMethodCallHandler.php b/src/Analyser/ExprHandler/NullsafeMethodCallHandler.php index 864a4859275..d0ec6d41ca8 100644 --- a/src/Analyser/ExprHandler/NullsafeMethodCallHandler.php +++ b/src/Analyser/ExprHandler/NullsafeMethodCallHandler.php @@ -12,6 +12,7 @@ use PhpParser\Node\Stmt; use PHPStan\Analyser\ExpressionContext; use PHPStan\Analyser\ExpressionResult; +use PHPStan\Analyser\ExpressionResultFactory; use PHPStan\Analyser\ExpressionResultStorage; use PHPStan\Analyser\ExprHandler; use PHPStan\Analyser\ExprHandler\Helper\NonNullabilityHelper; @@ -37,6 +38,7 @@ final class NullsafeMethodCallHandler implements ExprHandler public function __construct( private NonNullabilityHelper $nonNullabilityHelper, + private ExpressionResultFactory $expressionResultFactory, ) { } @@ -115,7 +117,7 @@ public function processExpr(NodeScopeResolver $nodeScopeResolver, Stmt $stmt, Ex $scope = $scope->mergeWith($scopeBeforeNullsafe); } - return new ExpressionResult( + return $this->expressionResultFactory->create( $scope, hasYield: $exprResult->hasYield(), isAlwaysTerminating: false, diff --git a/src/Analyser/ExprHandler/NullsafePropertyFetchHandler.php b/src/Analyser/ExprHandler/NullsafePropertyFetchHandler.php index 0c9e190ff47..8ec6531c115 100644 --- a/src/Analyser/ExprHandler/NullsafePropertyFetchHandler.php +++ b/src/Analyser/ExprHandler/NullsafePropertyFetchHandler.php @@ -12,6 +12,7 @@ use PhpParser\Node\Stmt; use PHPStan\Analyser\ExpressionContext; use PHPStan\Analyser\ExpressionResult; +use PHPStan\Analyser\ExpressionResultFactory; use PHPStan\Analyser\ExpressionResultStorage; use PHPStan\Analyser\ExprHandler; use PHPStan\Analyser\ExprHandler\Helper\NonNullabilityHelper; @@ -37,6 +38,7 @@ final class NullsafePropertyFetchHandler implements ExprHandler public function __construct( private NonNullabilityHelper $nonNullabilityHelper, + private ExpressionResultFactory $expressionResultFactory, ) { } @@ -94,7 +96,7 @@ public function processExpr(NodeScopeResolver $nodeScopeResolver, Stmt $stmt, Ex ), $nonNullabilityResult->getScope(), $storage, $nodeCallback, $context); $scope = $this->nonNullabilityHelper->revertNonNullability($exprResult->getScope(), $nonNullabilityResult->getSpecifiedExpressions()); - return new ExpressionResult( + return $this->expressionResultFactory->create( $scope, hasYield: $exprResult->hasYield(), isAlwaysTerminating: false, diff --git a/src/Analyser/ExprHandler/PipeHandler.php b/src/Analyser/ExprHandler/PipeHandler.php index 93bd3554ef7..4ff195222bd 100644 --- a/src/Analyser/ExprHandler/PipeHandler.php +++ b/src/Analyser/ExprHandler/PipeHandler.php @@ -11,6 +11,7 @@ use PhpParser\Node\Stmt; use PHPStan\Analyser\ExpressionContext; use PHPStan\Analyser\ExpressionResult; +use PHPStan\Analyser\ExpressionResultFactory; use PHPStan\Analyser\ExpressionResultStorage; use PHPStan\Analyser\ExprHandler; use PHPStan\Analyser\MutatingScope; @@ -32,6 +33,10 @@ final class PipeHandler implements ExprHandler { + public function __construct(private ExpressionResultFactory $expressionResultFactory) + { + } + public function supports(Expr $expr): bool { return $expr instanceof Pipe; @@ -84,7 +89,7 @@ public function processExpr(NodeScopeResolver $nodeScopeResolver, Stmt $stmt, Ex $callResult = $nodeScopeResolver->processExprNode($stmt, $callExpr, $scope, $storage, $nodeCallback, $context); - return new ExpressionResult( + return $this->expressionResultFactory->create( $callResult->getScope(), hasYield: $callResult->hasYield(), isAlwaysTerminating: $callResult->isAlwaysTerminating(), diff --git a/src/Analyser/ExprHandler/PostDecHandler.php b/src/Analyser/ExprHandler/PostDecHandler.php index 6c38de4a62e..1714b1d3ff2 100644 --- a/src/Analyser/ExprHandler/PostDecHandler.php +++ b/src/Analyser/ExprHandler/PostDecHandler.php @@ -8,6 +8,7 @@ use PhpParser\Node\Stmt; use PHPStan\Analyser\ExpressionContext; use PHPStan\Analyser\ExpressionResult; +use PHPStan\Analyser\ExpressionResultFactory; use PHPStan\Analyser\ExpressionResultStorage; use PHPStan\Analyser\ExprHandler; use PHPStan\Analyser\MutatingScope; @@ -26,6 +27,10 @@ final class PostDecHandler implements ExprHandler { + public function __construct(private ExpressionResultFactory $expressionResultFactory) + { + } + public function supports(Expr $expr): bool { return $expr instanceof PostDec; @@ -44,7 +49,7 @@ public function processExpr(NodeScopeResolver $nodeScopeResolver, Stmt $stmt, Ex $nodeCallback, )->getScope(); - return new ExpressionResult( + return $this->expressionResultFactory->create( $scope, hasYield: $varResult->hasYield(), isAlwaysTerminating: $varResult->isAlwaysTerminating(), diff --git a/src/Analyser/ExprHandler/PostIncHandler.php b/src/Analyser/ExprHandler/PostIncHandler.php index f26c45c50fe..4f9f304e30c 100644 --- a/src/Analyser/ExprHandler/PostIncHandler.php +++ b/src/Analyser/ExprHandler/PostIncHandler.php @@ -8,6 +8,7 @@ use PhpParser\Node\Stmt; use PHPStan\Analyser\ExpressionContext; use PHPStan\Analyser\ExpressionResult; +use PHPStan\Analyser\ExpressionResultFactory; use PHPStan\Analyser\ExpressionResultStorage; use PHPStan\Analyser\ExprHandler; use PHPStan\Analyser\MutatingScope; @@ -26,6 +27,10 @@ final class PostIncHandler implements ExprHandler { + public function __construct(private ExpressionResultFactory $expressionResultFactory) + { + } + public function supports(Expr $expr): bool { return $expr instanceof PostInc; @@ -44,7 +49,7 @@ public function processExpr(NodeScopeResolver $nodeScopeResolver, Stmt $stmt, Ex $nodeCallback, )->getScope(); - return new ExpressionResult( + return $this->expressionResultFactory->create( $scope, hasYield: $varResult->hasYield(), isAlwaysTerminating: $varResult->isAlwaysTerminating(), diff --git a/src/Analyser/ExprHandler/PreDecHandler.php b/src/Analyser/ExprHandler/PreDecHandler.php index 4abde925a80..f79b754181a 100644 --- a/src/Analyser/ExprHandler/PreDecHandler.php +++ b/src/Analyser/ExprHandler/PreDecHandler.php @@ -9,6 +9,7 @@ use PhpParser\Node\Stmt; use PHPStan\Analyser\ExpressionContext; use PHPStan\Analyser\ExpressionResult; +use PHPStan\Analyser\ExpressionResultFactory; use PHPStan\Analyser\ExpressionResultStorage; use PHPStan\Analyser\ExprHandler; use PHPStan\Analyser\MutatingScope; @@ -40,6 +41,10 @@ final class PreDecHandler implements ExprHandler { + public function __construct(private ExpressionResultFactory $expressionResultFactory) + { + } + public function supports(Expr $expr): bool { return $expr instanceof PreDec; @@ -107,7 +112,7 @@ public function processExpr(NodeScopeResolver $nodeScopeResolver, Stmt $stmt, Ex $nodeCallback, )->getScope(); - return new ExpressionResult( + return $this->expressionResultFactory->create( $scope, hasYield: $varResult->hasYield(), isAlwaysTerminating: $varResult->isAlwaysTerminating(), diff --git a/src/Analyser/ExprHandler/PreIncHandler.php b/src/Analyser/ExprHandler/PreIncHandler.php index 1750b876542..f3504623cca 100644 --- a/src/Analyser/ExprHandler/PreIncHandler.php +++ b/src/Analyser/ExprHandler/PreIncHandler.php @@ -9,6 +9,7 @@ use PhpParser\Node\Stmt; use PHPStan\Analyser\ExpressionContext; use PHPStan\Analyser\ExpressionResult; +use PHPStan\Analyser\ExpressionResultFactory; use PHPStan\Analyser\ExpressionResultStorage; use PHPStan\Analyser\ExprHandler; use PHPStan\Analyser\MutatingScope; @@ -41,6 +42,10 @@ final class PreIncHandler implements ExprHandler { + public function __construct(private ExpressionResultFactory $expressionResultFactory) + { + } + public function supports(Expr $expr): bool { return $expr instanceof PreInc; @@ -108,7 +113,7 @@ public function processExpr(NodeScopeResolver $nodeScopeResolver, Stmt $stmt, Ex $nodeCallback, )->getScope(); - return new ExpressionResult( + return $this->expressionResultFactory->create( $scope, hasYield: $varResult->hasYield(), isAlwaysTerminating: $varResult->isAlwaysTerminating(), diff --git a/src/Analyser/ExprHandler/PrintHandler.php b/src/Analyser/ExprHandler/PrintHandler.php index 9d8b220ccfe..9a3b61a14d3 100644 --- a/src/Analyser/ExprHandler/PrintHandler.php +++ b/src/Analyser/ExprHandler/PrintHandler.php @@ -7,6 +7,7 @@ use PhpParser\Node\Stmt; use PHPStan\Analyser\ExpressionContext; use PHPStan\Analyser\ExpressionResult; +use PHPStan\Analyser\ExpressionResultFactory; use PHPStan\Analyser\ExpressionResultStorage; use PHPStan\Analyser\ExprHandler; use PHPStan\Analyser\ExprHandler\Helper\ImplicitToStringCallHelper; @@ -31,6 +32,7 @@ final class PrintHandler implements ExprHandler public function __construct( private ImplicitToStringCallHelper $implicitToStringCallHelper, + private ExpressionResultFactory $expressionResultFactory, ) { } @@ -57,7 +59,7 @@ public function processExpr(NodeScopeResolver $nodeScopeResolver, Stmt $stmt, Ex $scope = $exprResult->getScope(); - return new ExpressionResult( + return $this->expressionResultFactory->create( $scope, hasYield: $exprResult->hasYield(), isAlwaysTerminating: $exprResult->isAlwaysTerminating(), diff --git a/src/Analyser/ExprHandler/PropertyFetchHandler.php b/src/Analyser/ExprHandler/PropertyFetchHandler.php index 9be0c5c28be..f4ba7070756 100644 --- a/src/Analyser/ExprHandler/PropertyFetchHandler.php +++ b/src/Analyser/ExprHandler/PropertyFetchHandler.php @@ -9,6 +9,7 @@ use PhpParser\Node\Stmt; use PHPStan\Analyser\ExpressionContext; use PHPStan\Analyser\ExpressionResult; +use PHPStan\Analyser\ExpressionResultFactory; use PHPStan\Analyser\ExpressionResultStorage; use PHPStan\Analyser\ExprHandler; use PHPStan\Analyser\ExprHandler\Helper\NullsafeShortCircuitingHelper; @@ -40,6 +41,7 @@ final class PropertyFetchHandler implements ExprHandler public function __construct( private PhpVersion $phpVersion, private PropertyReflectionFinder $propertyReflectionFinder, + private ExpressionResultFactory $expressionResultFactory, ) { } @@ -81,7 +83,7 @@ public function processExpr(NodeScopeResolver $nodeScopeResolver, Stmt $stmt, Ex } } - return new ExpressionResult( + return $this->expressionResultFactory->create( $scope, hasYield: $hasYield, isAlwaysTerminating: $isAlwaysTerminating, diff --git a/src/Analyser/ExprHandler/ScalarHandler.php b/src/Analyser/ExprHandler/ScalarHandler.php index abd0dc63a4f..6f0db0d86f5 100644 --- a/src/Analyser/ExprHandler/ScalarHandler.php +++ b/src/Analyser/ExprHandler/ScalarHandler.php @@ -8,6 +8,7 @@ use PhpParser\Node\Stmt; use PHPStan\Analyser\ExpressionContext; use PHPStan\Analyser\ExpressionResult; +use PHPStan\Analyser\ExpressionResultFactory; use PHPStan\Analyser\ExpressionResultStorage; use PHPStan\Analyser\ExprHandler; use PHPStan\Analyser\MutatingScope; @@ -30,6 +31,7 @@ final class ScalarHandler implements ExprHandler public function __construct( private InitializerExprTypeResolver $initializerExprTypeResolver, + private ExpressionResultFactory $expressionResultFactory, ) { } @@ -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 { - return new ExpressionResult( + return $this->expressionResultFactory->create( $scope, hasYield: false, isAlwaysTerminating: false, diff --git a/src/Analyser/ExprHandler/StaticCallHandler.php b/src/Analyser/ExprHandler/StaticCallHandler.php index 872fac5c790..79e9c1288d4 100644 --- a/src/Analyser/ExprHandler/StaticCallHandler.php +++ b/src/Analyser/ExprHandler/StaticCallHandler.php @@ -14,6 +14,7 @@ use PHPStan\Analyser\ArgumentsNormalizer; use PHPStan\Analyser\ExpressionContext; use PHPStan\Analyser\ExpressionResult; +use PHPStan\Analyser\ExpressionResultFactory; use PHPStan\Analyser\ExpressionResultStorage; use PHPStan\Analyser\ExprHandler; use PHPStan\Analyser\ExprHandler\Helper\MethodCallReturnTypeHelper; @@ -68,6 +69,7 @@ public function __construct( private ReflectionProvider $reflectionProvider, #[AutowiredParameter] private bool $rememberPossiblyImpureFunctionValues, + private ExpressionResultFactory $expressionResultFactory, ) { } @@ -279,7 +281,7 @@ public function processExpr(NodeScopeResolver $nodeScopeResolver, Stmt $stmt, Ex $impurePoints = array_merge($impurePoints, $argsResult->getImpurePoints()); $isAlwaysTerminating = $isAlwaysTerminating || $argsResult->isAlwaysTerminating(); - return new ExpressionResult( + return $this->expressionResultFactory->create( $scope, hasYield: $hasYield, isAlwaysTerminating: $isAlwaysTerminating, diff --git a/src/Analyser/ExprHandler/StaticPropertyFetchHandler.php b/src/Analyser/ExprHandler/StaticPropertyFetchHandler.php index ec36cf64388..dd68417723f 100644 --- a/src/Analyser/ExprHandler/StaticPropertyFetchHandler.php +++ b/src/Analyser/ExprHandler/StaticPropertyFetchHandler.php @@ -11,6 +11,7 @@ use PhpParser\Node\VarLikeIdentifier; use PHPStan\Analyser\ExpressionContext; use PHPStan\Analyser\ExpressionResult; +use PHPStan\Analyser\ExpressionResultFactory; use PHPStan\Analyser\ExpressionResultStorage; use PHPStan\Analyser\ExprHandler; use PHPStan\Analyser\ExprHandler\Helper\NullsafeShortCircuitingHelper; @@ -40,6 +41,7 @@ final class StaticPropertyFetchHandler implements ExprHandler public function __construct( private PropertyReflectionFinder $propertyReflectionFinder, + private ExpressionResultFactory $expressionResultFactory, ) { } @@ -80,7 +82,7 @@ public function processExpr(NodeScopeResolver $nodeScopeResolver, Stmt $stmt, Ex $scope = $nameResult->getScope(); } - return new ExpressionResult( + return $this->expressionResultFactory->create( $scope, hasYield: $hasYield, isAlwaysTerminating: $isAlwaysTerminating, diff --git a/src/Analyser/ExprHandler/TernaryHandler.php b/src/Analyser/ExprHandler/TernaryHandler.php index 3dcc769ad36..b1385b149b6 100644 --- a/src/Analyser/ExprHandler/TernaryHandler.php +++ b/src/Analyser/ExprHandler/TernaryHandler.php @@ -9,6 +9,7 @@ use PhpParser\Node\Stmt; use PHPStan\Analyser\ExpressionContext; use PHPStan\Analyser\ExpressionResult; +use PHPStan\Analyser\ExpressionResultFactory; use PHPStan\Analyser\ExpressionResultStorage; use PHPStan\Analyser\ExprHandler; use PHPStan\Analyser\MutatingScope; @@ -33,6 +34,7 @@ final class TernaryHandler implements ExprHandler public function __construct( private NodeScopeResolver $nodeScopeResolver, + private ExpressionResultFactory $expressionResultFactory, ) { } @@ -144,7 +146,7 @@ public function processExpr(NodeScopeResolver $nodeScopeResolver, Stmt $stmt, Ex } } - return new ExpressionResult( + return $this->expressionResultFactory->create( $finalScope, hasYield: $ternaryCondResult->hasYield(), isAlwaysTerminating: $ternaryCondResult->isAlwaysTerminating(), diff --git a/src/Analyser/ExprHandler/ThrowHandler.php b/src/Analyser/ExprHandler/ThrowHandler.php index 63c9b4720e8..7c283decba4 100644 --- a/src/Analyser/ExprHandler/ThrowHandler.php +++ b/src/Analyser/ExprHandler/ThrowHandler.php @@ -7,6 +7,7 @@ use PhpParser\Node\Stmt; use PHPStan\Analyser\ExpressionContext; use PHPStan\Analyser\ExpressionResult; +use PHPStan\Analyser\ExpressionResultFactory; use PHPStan\Analyser\ExpressionResultStorage; use PHPStan\Analyser\ExprHandler; use PHPStan\Analyser\InternalThrowPoint; @@ -28,6 +29,10 @@ final class ThrowHandler implements ExprHandler { + public function __construct(private ExpressionResultFactory $expressionResultFactory) + { + } + public function supports(Expr $expr): bool { return $expr instanceof Throw_; @@ -37,7 +42,7 @@ public function processExpr(NodeScopeResolver $nodeScopeResolver, Stmt $stmt, Ex { $exprResult = $nodeScopeResolver->processExprNode($stmt, $expr->expr, $scope, $storage, $nodeCallback, ExpressionContext::createDeep()->enterThrow()); - return new ExpressionResult( + return $this->expressionResultFactory->create( $scope, hasYield: false, isAlwaysTerminating: true, diff --git a/src/Analyser/ExprHandler/UnaryMinusHandler.php b/src/Analyser/ExprHandler/UnaryMinusHandler.php index 2bc2d872380..0da8e3d46a4 100644 --- a/src/Analyser/ExprHandler/UnaryMinusHandler.php +++ b/src/Analyser/ExprHandler/UnaryMinusHandler.php @@ -7,6 +7,7 @@ use PhpParser\Node\Stmt; use PHPStan\Analyser\ExpressionContext; use PHPStan\Analyser\ExpressionResult; +use PHPStan\Analyser\ExpressionResultFactory; use PHPStan\Analyser\ExpressionResultStorage; use PHPStan\Analyser\ExprHandler; use PHPStan\Analyser\MutatingScope; @@ -28,6 +29,7 @@ final class UnaryMinusHandler implements ExprHandler public function __construct( private InitializerExprTypeResolver $initializerExprTypeResolver, + private ExpressionResultFactory $expressionResultFactory, ) { } @@ -41,7 +43,7 @@ public function processExpr(NodeScopeResolver $nodeScopeResolver, Stmt $stmt, Ex { $exprResult = $nodeScopeResolver->processExprNode($stmt, $expr->expr, $scope, $storage, $nodeCallback, $context->enterDeep()); - return new ExpressionResult( + return $this->expressionResultFactory->create( $exprResult->getScope(), hasYield: $exprResult->hasYield(), isAlwaysTerminating: $exprResult->isAlwaysTerminating(), diff --git a/src/Analyser/ExprHandler/UnaryPlusHandler.php b/src/Analyser/ExprHandler/UnaryPlusHandler.php index 676110b904f..3f2c6bcbbf9 100644 --- a/src/Analyser/ExprHandler/UnaryPlusHandler.php +++ b/src/Analyser/ExprHandler/UnaryPlusHandler.php @@ -7,6 +7,7 @@ use PhpParser\Node\Stmt; use PHPStan\Analyser\ExpressionContext; use PHPStan\Analyser\ExpressionResult; +use PHPStan\Analyser\ExpressionResultFactory; use PHPStan\Analyser\ExpressionResultStorage; use PHPStan\Analyser\ExprHandler; use PHPStan\Analyser\MutatingScope; @@ -28,6 +29,7 @@ final class UnaryPlusHandler implements ExprHandler public function __construct( private InitializerExprTypeResolver $initializerExprTypeResolver, + private ExpressionResultFactory $expressionResultFactory, ) { } @@ -41,7 +43,7 @@ public function processExpr(NodeScopeResolver $nodeScopeResolver, Stmt $stmt, Ex { $exprResult = $nodeScopeResolver->processExprNode($stmt, $expr->expr, $scope, $storage, $nodeCallback, $context->enterDeep()); - return new ExpressionResult( + return $this->expressionResultFactory->create( $exprResult->getScope(), hasYield: $exprResult->hasYield(), isAlwaysTerminating: $exprResult->isAlwaysTerminating(), diff --git a/src/Analyser/ExprHandler/VariableHandler.php b/src/Analyser/ExprHandler/VariableHandler.php index e104b9fed42..f2c6f414207 100644 --- a/src/Analyser/ExprHandler/VariableHandler.php +++ b/src/Analyser/ExprHandler/VariableHandler.php @@ -9,6 +9,7 @@ use PhpParser\Node\Stmt; use PHPStan\Analyser\ExpressionContext; use PHPStan\Analyser\ExpressionResult; +use PHPStan\Analyser\ExpressionResultFactory; use PHPStan\Analyser\ExpressionResultStorage; use PHPStan\Analyser\ExprHandler; use PHPStan\Analyser\ImpurePoint; @@ -34,6 +35,10 @@ final class VariableHandler implements ExprHandler { + public function __construct(private ExpressionResultFactory $expressionResultFactory) + { + } + public function supports(Expr $expr): bool { return $expr instanceof Variable; @@ -89,7 +94,7 @@ public function processExpr(NodeScopeResolver $nodeScopeResolver, Stmt $stmt, Ex $isAlwaysTerminating = $nameResult->isAlwaysTerminating(); $scope = $nameResult->getScope(); } - return new ExpressionResult( + return $this->expressionResultFactory->create( $scope, $hasYield, $isAlwaysTerminating, diff --git a/src/Analyser/ExprHandler/Virtual/AlwaysRememberedExprHandler.php b/src/Analyser/ExprHandler/Virtual/AlwaysRememberedExprHandler.php index fcf77547e33..8e26801f1e0 100644 --- a/src/Analyser/ExprHandler/Virtual/AlwaysRememberedExprHandler.php +++ b/src/Analyser/ExprHandler/Virtual/AlwaysRememberedExprHandler.php @@ -6,6 +6,7 @@ use PhpParser\Node\Stmt; use PHPStan\Analyser\ExpressionContext; use PHPStan\Analyser\ExpressionResult; +use PHPStan\Analyser\ExpressionResultFactory; use PHPStan\Analyser\ExpressionResultStorage; use PHPStan\Analyser\ExprHandler; use PHPStan\Analyser\MutatingScope; @@ -25,6 +26,10 @@ final class AlwaysRememberedExprHandler implements ExprHandler { + public function __construct(private ExpressionResultFactory $expressionResultFactory) + { + } + public function supports(Expr $expr): bool { return $expr instanceof AlwaysRememberedExpr; @@ -44,7 +49,7 @@ public function processExpr( $innerResult = $nodeScopeResolver->processExprNode($stmt, $innerExpr, $scope, $storage, $nodeCallback, $context); $scope = $innerResult->getScope(); - return new ExpressionResult( + return $this->expressionResultFactory->create( $scope, hasYield: $innerResult->hasYield(), isAlwaysTerminating: $innerResult->isAlwaysTerminating(), diff --git a/src/Analyser/ExprHandler/Virtual/ExistingArrayDimFetchHandler.php b/src/Analyser/ExprHandler/Virtual/ExistingArrayDimFetchHandler.php index c2bf0d37fab..071f56be856 100644 --- a/src/Analyser/ExprHandler/Virtual/ExistingArrayDimFetchHandler.php +++ b/src/Analyser/ExprHandler/Virtual/ExistingArrayDimFetchHandler.php @@ -6,6 +6,7 @@ use PhpParser\Node\Stmt; use PHPStan\Analyser\ExpressionContext; use PHPStan\Analyser\ExpressionResult; +use PHPStan\Analyser\ExpressionResultFactory; use PHPStan\Analyser\ExpressionResultStorage; use PHPStan\Analyser\ExprHandler; use PHPStan\Analyser\MutatingScope; @@ -25,6 +26,10 @@ final class ExistingArrayDimFetchHandler implements ExprHandler { + public function __construct(private ExpressionResultFactory $expressionResultFactory) + { + } + public function supports(Expr $expr): bool { return $expr instanceof ExistingArrayDimFetch; @@ -35,7 +40,7 @@ public function processExpr(NodeScopeResolver $nodeScopeResolver, Stmt $stmt, Ex // because this is a virtual node handler, the caller will only be interested in the type // we don't need to process the inner expr - return new ExpressionResult( + return $this->expressionResultFactory->create( $scope, hasYield: false, isAlwaysTerminating: false, diff --git a/src/Analyser/ExprHandler/Virtual/FunctionCallableNodeHandler.php b/src/Analyser/ExprHandler/Virtual/FunctionCallableNodeHandler.php index b358f70c8df..4b91658cc82 100644 --- a/src/Analyser/ExprHandler/Virtual/FunctionCallableNodeHandler.php +++ b/src/Analyser/ExprHandler/Virtual/FunctionCallableNodeHandler.php @@ -6,6 +6,7 @@ use PhpParser\Node\Stmt; use PHPStan\Analyser\ExpressionContext; use PHPStan\Analyser\ExpressionResult; +use PHPStan\Analyser\ExpressionResultFactory; use PHPStan\Analyser\ExpressionResultStorage; use PHPStan\Analyser\ExprHandler; use PHPStan\Analyser\MutatingScope; @@ -26,6 +27,10 @@ final class FunctionCallableNodeHandler implements ExprHandler { + public function __construct(private ExpressionResultFactory $expressionResultFactory) + { + } + public function supports(Expr $expr): bool { return $expr instanceof FunctionCallableNode; @@ -46,7 +51,7 @@ public function processExpr(NodeScopeResolver $nodeScopeResolver, Stmt $stmt, Ex $isAlwaysTerminating = $nameResult->isAlwaysTerminating(); } - return new ExpressionResult( + return $this->expressionResultFactory->create( $scope, hasYield: $hasYield, isAlwaysTerminating: $isAlwaysTerminating, diff --git a/src/Analyser/ExprHandler/Virtual/GetIterableKeyTypeExprHandler.php b/src/Analyser/ExprHandler/Virtual/GetIterableKeyTypeExprHandler.php index a9de984485e..241122ccf84 100644 --- a/src/Analyser/ExprHandler/Virtual/GetIterableKeyTypeExprHandler.php +++ b/src/Analyser/ExprHandler/Virtual/GetIterableKeyTypeExprHandler.php @@ -6,6 +6,7 @@ use PhpParser\Node\Stmt; use PHPStan\Analyser\ExpressionContext; use PHPStan\Analyser\ExpressionResult; +use PHPStan\Analyser\ExpressionResultFactory; use PHPStan\Analyser\ExpressionResultStorage; use PHPStan\Analyser\ExprHandler; use PHPStan\Analyser\MutatingScope; @@ -25,6 +26,10 @@ final class GetIterableKeyTypeExprHandler implements ExprHandler { + public function __construct(private ExpressionResultFactory $expressionResultFactory) + { + } + public function supports(Expr $expr): bool { return $expr instanceof GetIterableKeyTypeExpr; @@ -35,7 +40,7 @@ public function processExpr(NodeScopeResolver $nodeScopeResolver, Stmt $stmt, Ex // because this is a virtual node handler, the caller will only be interested in the type // we don't need to process the inner expr - return new ExpressionResult( + return $this->expressionResultFactory->create( $scope, hasYield: false, isAlwaysTerminating: false, diff --git a/src/Analyser/ExprHandler/Virtual/GetIterableValueTypeExprHandler.php b/src/Analyser/ExprHandler/Virtual/GetIterableValueTypeExprHandler.php index 261c364ffd3..040732cfac3 100644 --- a/src/Analyser/ExprHandler/Virtual/GetIterableValueTypeExprHandler.php +++ b/src/Analyser/ExprHandler/Virtual/GetIterableValueTypeExprHandler.php @@ -6,6 +6,7 @@ use PhpParser\Node\Stmt; use PHPStan\Analyser\ExpressionContext; use PHPStan\Analyser\ExpressionResult; +use PHPStan\Analyser\ExpressionResultFactory; use PHPStan\Analyser\ExpressionResultStorage; use PHPStan\Analyser\ExprHandler; use PHPStan\Analyser\MutatingScope; @@ -25,6 +26,10 @@ final class GetIterableValueTypeExprHandler implements ExprHandler { + public function __construct(private ExpressionResultFactory $expressionResultFactory) + { + } + public function supports(Expr $expr): bool { return $expr instanceof GetIterableValueTypeExpr; @@ -35,7 +40,7 @@ public function processExpr(NodeScopeResolver $nodeScopeResolver, Stmt $stmt, Ex // because this is a virtual node handler, the caller will only be interested in the type // we don't need to process the inner expr - return new ExpressionResult( + return $this->expressionResultFactory->create( $scope, hasYield: false, isAlwaysTerminating: false, diff --git a/src/Analyser/ExprHandler/Virtual/GetOffsetValueTypeExprHandler.php b/src/Analyser/ExprHandler/Virtual/GetOffsetValueTypeExprHandler.php index 09922c7daa7..539ecc7a3b1 100644 --- a/src/Analyser/ExprHandler/Virtual/GetOffsetValueTypeExprHandler.php +++ b/src/Analyser/ExprHandler/Virtual/GetOffsetValueTypeExprHandler.php @@ -6,6 +6,7 @@ use PhpParser\Node\Stmt; use PHPStan\Analyser\ExpressionContext; use PHPStan\Analyser\ExpressionResult; +use PHPStan\Analyser\ExpressionResultFactory; use PHPStan\Analyser\ExpressionResultStorage; use PHPStan\Analyser\ExprHandler; use PHPStan\Analyser\MutatingScope; @@ -25,6 +26,10 @@ final class GetOffsetValueTypeExprHandler implements ExprHandler { + public function __construct(private ExpressionResultFactory $expressionResultFactory) + { + } + public function supports(Expr $expr): bool { return $expr instanceof GetOffsetValueTypeExpr; @@ -35,7 +40,7 @@ public function processExpr(NodeScopeResolver $nodeScopeResolver, Stmt $stmt, Ex // because this is a virtual node handler, the caller will only be interested in the type // we don't need to process the inner expr - return new ExpressionResult( + return $this->expressionResultFactory->create( $scope, hasYield: false, isAlwaysTerminating: false, diff --git a/src/Analyser/ExprHandler/Virtual/InstantiationCallableNodeHandler.php b/src/Analyser/ExprHandler/Virtual/InstantiationCallableNodeHandler.php index eb552e3a544..3ce468022a9 100644 --- a/src/Analyser/ExprHandler/Virtual/InstantiationCallableNodeHandler.php +++ b/src/Analyser/ExprHandler/Virtual/InstantiationCallableNodeHandler.php @@ -6,6 +6,7 @@ use PhpParser\Node\Stmt; use PHPStan\Analyser\ExpressionContext; use PHPStan\Analyser\ExpressionResult; +use PHPStan\Analyser\ExpressionResultFactory; use PHPStan\Analyser\ExpressionResultStorage; use PHPStan\Analyser\ExprHandler; use PHPStan\Analyser\MutatingScope; @@ -26,6 +27,10 @@ final class InstantiationCallableNodeHandler implements ExprHandler { + public function __construct(private ExpressionResultFactory $expressionResultFactory) + { + } + public function supports(Expr $expr): bool { return $expr instanceof InstantiationCallableNode; @@ -46,7 +51,7 @@ public function processExpr(NodeScopeResolver $nodeScopeResolver, Stmt $stmt, Ex $isAlwaysTerminating = $classResult->isAlwaysTerminating(); } - return new ExpressionResult( + return $this->expressionResultFactory->create( $scope, hasYield: $hasYield, isAlwaysTerminating: $isAlwaysTerminating, diff --git a/src/Analyser/ExprHandler/Virtual/MethodCallableNodeHandler.php b/src/Analyser/ExprHandler/Virtual/MethodCallableNodeHandler.php index f2d224bc91a..c94173c354f 100644 --- a/src/Analyser/ExprHandler/Virtual/MethodCallableNodeHandler.php +++ b/src/Analyser/ExprHandler/Virtual/MethodCallableNodeHandler.php @@ -6,6 +6,7 @@ use PhpParser\Node\Stmt; use PHPStan\Analyser\ExpressionContext; use PHPStan\Analyser\ExpressionResult; +use PHPStan\Analyser\ExpressionResultFactory; use PHPStan\Analyser\ExpressionResultStorage; use PHPStan\Analyser\ExprHandler; use PHPStan\Analyser\MutatingScope; @@ -27,6 +28,10 @@ final class MethodCallableNodeHandler implements ExprHandler { + public function __construct(private ExpressionResultFactory $expressionResultFactory) + { + } + public function supports(Expr $expr): bool { return $expr instanceof MethodCallableNode; @@ -49,7 +54,7 @@ public function processExpr(NodeScopeResolver $nodeScopeResolver, Stmt $stmt, Ex $isAlwaysTerminating = $nameResult->isAlwaysTerminating(); } - return new ExpressionResult( + return $this->expressionResultFactory->create( $scope, hasYield: $hasYield, isAlwaysTerminating: $isAlwaysTerminating, diff --git a/src/Analyser/ExprHandler/Virtual/NativeTypeExprHandler.php b/src/Analyser/ExprHandler/Virtual/NativeTypeExprHandler.php index c952fcc1297..e916eb9ec69 100644 --- a/src/Analyser/ExprHandler/Virtual/NativeTypeExprHandler.php +++ b/src/Analyser/ExprHandler/Virtual/NativeTypeExprHandler.php @@ -6,6 +6,7 @@ use PhpParser\Node\Stmt; use PHPStan\Analyser\ExpressionContext; use PHPStan\Analyser\ExpressionResult; +use PHPStan\Analyser\ExpressionResultFactory; use PHPStan\Analyser\ExpressionResultStorage; use PHPStan\Analyser\ExprHandler; use PHPStan\Analyser\MutatingScope; @@ -25,6 +26,10 @@ final class NativeTypeExprHandler implements ExprHandler { + public function __construct(private ExpressionResultFactory $expressionResultFactory) + { + } + public function supports(Expr $expr): bool { return $expr instanceof NativeTypeExpr; @@ -35,7 +40,7 @@ public function processExpr(NodeScopeResolver $nodeScopeResolver, Stmt $stmt, Ex // because this is a virtual node handler, the caller will only be interested in the type // we don't need to process the inner expr - return new ExpressionResult( + return $this->expressionResultFactory->create( $scope, hasYield: false, isAlwaysTerminating: false, diff --git a/src/Analyser/ExprHandler/Virtual/OriginalPropertyTypeExprHandler.php b/src/Analyser/ExprHandler/Virtual/OriginalPropertyTypeExprHandler.php index 7893990b1a3..ea08496e209 100644 --- a/src/Analyser/ExprHandler/Virtual/OriginalPropertyTypeExprHandler.php +++ b/src/Analyser/ExprHandler/Virtual/OriginalPropertyTypeExprHandler.php @@ -6,6 +6,7 @@ use PhpParser\Node\Stmt; use PHPStan\Analyser\ExpressionContext; use PHPStan\Analyser\ExpressionResult; +use PHPStan\Analyser\ExpressionResultFactory; use PHPStan\Analyser\ExpressionResultStorage; use PHPStan\Analyser\ExprHandler; use PHPStan\Analyser\MutatingScope; @@ -29,6 +30,7 @@ final class OriginalPropertyTypeExprHandler implements ExprHandler public function __construct( private PropertyReflectionFinder $propertyReflectionFinder, + private ExpressionResultFactory $expressionResultFactory, ) { } @@ -43,7 +45,7 @@ public function processExpr(NodeScopeResolver $nodeScopeResolver, Stmt $stmt, Ex // because this is a virtual node handler, the caller will only be interested in the type // we don't need to process the inner expr - return new ExpressionResult( + return $this->expressionResultFactory->create( $scope, hasYield: false, isAlwaysTerminating: false, diff --git a/src/Analyser/ExprHandler/Virtual/SetExistingOffsetValueTypeExprHandler.php b/src/Analyser/ExprHandler/Virtual/SetExistingOffsetValueTypeExprHandler.php index cd764c40dbf..5c986631ba1 100644 --- a/src/Analyser/ExprHandler/Virtual/SetExistingOffsetValueTypeExprHandler.php +++ b/src/Analyser/ExprHandler/Virtual/SetExistingOffsetValueTypeExprHandler.php @@ -6,6 +6,7 @@ use PhpParser\Node\Stmt; use PHPStan\Analyser\ExpressionContext; use PHPStan\Analyser\ExpressionResult; +use PHPStan\Analyser\ExpressionResultFactory; use PHPStan\Analyser\ExpressionResultStorage; use PHPStan\Analyser\ExprHandler; use PHPStan\Analyser\MutatingScope; @@ -27,6 +28,10 @@ final class SetExistingOffsetValueTypeExprHandler implements ExprHandler { + public function __construct(private ExpressionResultFactory $expressionResultFactory) + { + } + public function supports(Expr $expr): bool { return $expr instanceof SetExistingOffsetValueTypeExpr; @@ -37,7 +42,7 @@ public function processExpr(NodeScopeResolver $nodeScopeResolver, Stmt $stmt, Ex // because this is a virtual node handler, the caller will only be interested in the type // we don't need to process the inner expr - return new ExpressionResult( + return $this->expressionResultFactory->create( $scope, hasYield: false, isAlwaysTerminating: false, diff --git a/src/Analyser/ExprHandler/Virtual/SetOffsetValueTypeExprHandler.php b/src/Analyser/ExprHandler/Virtual/SetOffsetValueTypeExprHandler.php index 92c41c6b516..a8ffc98a0bb 100644 --- a/src/Analyser/ExprHandler/Virtual/SetOffsetValueTypeExprHandler.php +++ b/src/Analyser/ExprHandler/Virtual/SetOffsetValueTypeExprHandler.php @@ -6,6 +6,7 @@ use PhpParser\Node\Stmt; use PHPStan\Analyser\ExpressionContext; use PHPStan\Analyser\ExpressionResult; +use PHPStan\Analyser\ExpressionResultFactory; use PHPStan\Analyser\ExpressionResultStorage; use PHPStan\Analyser\ExprHandler; use PHPStan\Analyser\MutatingScope; @@ -27,6 +28,10 @@ final class SetOffsetValueTypeExprHandler implements ExprHandler { + public function __construct(private ExpressionResultFactory $expressionResultFactory) + { + } + public function supports(Expr $expr): bool { return $expr instanceof SetOffsetValueTypeExpr; @@ -37,7 +42,7 @@ public function processExpr(NodeScopeResolver $nodeScopeResolver, Stmt $stmt, Ex // because this is a virtual node handler, the caller will only be interested in the type // we don't need to process the inner expr - return new ExpressionResult( + return $this->expressionResultFactory->create( $scope, hasYield: false, isAlwaysTerminating: false, diff --git a/src/Analyser/ExprHandler/Virtual/StaticMethodCallableNodeHandler.php b/src/Analyser/ExprHandler/Virtual/StaticMethodCallableNodeHandler.php index 10467a171a5..0eef43563b0 100644 --- a/src/Analyser/ExprHandler/Virtual/StaticMethodCallableNodeHandler.php +++ b/src/Analyser/ExprHandler/Virtual/StaticMethodCallableNodeHandler.php @@ -6,6 +6,7 @@ use PhpParser\Node\Stmt; use PHPStan\Analyser\ExpressionContext; use PHPStan\Analyser\ExpressionResult; +use PHPStan\Analyser\ExpressionResultFactory; use PHPStan\Analyser\ExpressionResultStorage; use PHPStan\Analyser\ExprHandler; use PHPStan\Analyser\MutatingScope; @@ -27,6 +28,10 @@ final class StaticMethodCallableNodeHandler implements ExprHandler { + public function __construct(private ExpressionResultFactory $expressionResultFactory) + { + } + public function supports(Expr $expr): bool { return $expr instanceof StaticMethodCallableNode; @@ -55,7 +60,7 @@ public function processExpr(NodeScopeResolver $nodeScopeResolver, Stmt $stmt, Ex $isAlwaysTerminating = $isAlwaysTerminating || $nameResult->isAlwaysTerminating(); } - return new ExpressionResult( + return $this->expressionResultFactory->create( $scope, hasYield: $hasYield, isAlwaysTerminating: $isAlwaysTerminating, diff --git a/src/Analyser/ExprHandler/Virtual/TypeExprHandler.php b/src/Analyser/ExprHandler/Virtual/TypeExprHandler.php index 81c6c9f08f8..53766b68323 100644 --- a/src/Analyser/ExprHandler/Virtual/TypeExprHandler.php +++ b/src/Analyser/ExprHandler/Virtual/TypeExprHandler.php @@ -6,6 +6,7 @@ use PhpParser\Node\Stmt; use PHPStan\Analyser\ExpressionContext; use PHPStan\Analyser\ExpressionResult; +use PHPStan\Analyser\ExpressionResultFactory; use PHPStan\Analyser\ExpressionResultStorage; use PHPStan\Analyser\ExprHandler; use PHPStan\Analyser\MutatingScope; @@ -25,6 +26,10 @@ final class TypeExprHandler implements ExprHandler { + public function __construct(private ExpressionResultFactory $expressionResultFactory) + { + } + public function supports(Expr $expr): bool { return $expr instanceof TypeExpr; @@ -35,7 +40,7 @@ public function processExpr(NodeScopeResolver $nodeScopeResolver, Stmt $stmt, Ex // because this is a virtual node handler, the caller will only be interested in the type // we don't need to process the inner expr - return new ExpressionResult( + return $this->expressionResultFactory->create( $scope, hasYield: false, isAlwaysTerminating: false, diff --git a/src/Analyser/ExprHandler/Virtual/UnsetOffsetExprHandler.php b/src/Analyser/ExprHandler/Virtual/UnsetOffsetExprHandler.php index 0c2c4741831..995219a4f05 100644 --- a/src/Analyser/ExprHandler/Virtual/UnsetOffsetExprHandler.php +++ b/src/Analyser/ExprHandler/Virtual/UnsetOffsetExprHandler.php @@ -6,6 +6,7 @@ use PhpParser\Node\Stmt; use PHPStan\Analyser\ExpressionContext; use PHPStan\Analyser\ExpressionResult; +use PHPStan\Analyser\ExpressionResultFactory; use PHPStan\Analyser\ExpressionResultStorage; use PHPStan\Analyser\ExprHandler; use PHPStan\Analyser\MutatingScope; @@ -25,6 +26,10 @@ final class UnsetOffsetExprHandler implements ExprHandler { + public function __construct(private ExpressionResultFactory $expressionResultFactory) + { + } + public function supports(Expr $expr): bool { return $expr instanceof UnsetOffsetExpr; @@ -35,7 +40,7 @@ public function processExpr(NodeScopeResolver $nodeScopeResolver, Stmt $stmt, Ex // because this is a virtual node handler, the caller will only be interested in the type // we don't need to process the inner expr - return new ExpressionResult( + return $this->expressionResultFactory->create( $scope, hasYield: false, isAlwaysTerminating: false, diff --git a/src/Analyser/ExprHandler/YieldFromHandler.php b/src/Analyser/ExprHandler/YieldFromHandler.php index 7b86f00abb2..9e148f388f7 100644 --- a/src/Analyser/ExprHandler/YieldFromHandler.php +++ b/src/Analyser/ExprHandler/YieldFromHandler.php @@ -8,6 +8,7 @@ use PhpParser\Node\Stmt; use PHPStan\Analyser\ExpressionContext; use PHPStan\Analyser\ExpressionResult; +use PHPStan\Analyser\ExpressionResultFactory; use PHPStan\Analyser\ExpressionResultStorage; use PHPStan\Analyser\ExprHandler; use PHPStan\Analyser\ImpurePoint; @@ -31,6 +32,10 @@ final class YieldFromHandler implements ExprHandler { + public function __construct(private ExpressionResultFactory $expressionResultFactory) + { + } + public function supports(Expr $expr): bool { return $expr instanceof YieldFrom; @@ -52,7 +57,7 @@ public function processExpr(NodeScopeResolver $nodeScopeResolver, Stmt $stmt, Ex $exprResult = $nodeScopeResolver->processExprNode($stmt, $expr->expr, $scope, $storage, $nodeCallback, $context->enterDeep()); $scope = $exprResult->getScope(); - return new ExpressionResult( + return $this->expressionResultFactory->create( $scope, hasYield: true, isAlwaysTerminating: $exprResult->isAlwaysTerminating(), diff --git a/src/Analyser/ExprHandler/YieldHandler.php b/src/Analyser/ExprHandler/YieldHandler.php index 48fb166ee70..583907a17b6 100644 --- a/src/Analyser/ExprHandler/YieldHandler.php +++ b/src/Analyser/ExprHandler/YieldHandler.php @@ -8,6 +8,7 @@ use PhpParser\Node\Stmt; use PHPStan\Analyser\ExpressionContext; use PHPStan\Analyser\ExpressionResult; +use PHPStan\Analyser\ExpressionResultFactory; use PHPStan\Analyser\ExpressionResultStorage; use PHPStan\Analyser\ExprHandler; use PHPStan\Analyser\ImpurePoint; @@ -31,6 +32,10 @@ final class YieldHandler implements ExprHandler { + public function __construct(private ExpressionResultFactory $expressionResultFactory) + { + } + public function supports(Expr $expr): bool { return $expr instanceof Yield_; @@ -82,7 +87,7 @@ public function processExpr(NodeScopeResolver $nodeScopeResolver, Stmt $stmt, Ex $isAlwaysTerminating = $isAlwaysTerminating || $valueResult->isAlwaysTerminating(); } - return new ExpressionResult( + return $this->expressionResultFactory->create( $scope, hasYield: true, isAlwaysTerminating: $isAlwaysTerminating, diff --git a/src/Analyser/ExpressionResult.php b/src/Analyser/ExpressionResult.php index 746c518953d..42e49e004aa 100644 --- a/src/Analyser/ExpressionResult.php +++ b/src/Analyser/ExpressionResult.php @@ -2,6 +2,9 @@ namespace PHPStan\Analyser; +use PHPStan\DependencyInjection\GenerateFactory; + +#[GenerateFactory(interface: ExpressionResultFactory::class)] final class ExpressionResult { diff --git a/src/Analyser/ExpressionResultFactory.php b/src/Analyser/ExpressionResultFactory.php new file mode 100644 index 00000000000..f724e6fdcb4 --- /dev/null +++ b/src/Analyser/ExpressionResultFactory.php @@ -0,0 +1,24 @@ +expressionResultFactory->create($scope, hasYield: false, isAlwaysTerminating: false, throwPoints: [], impurePoints: []); } - return new ExpressionResult( + return $this->expressionResultFactory->create( $scope, hasYield: false, isAlwaysTerminating: false, @@ -3150,7 +3151,7 @@ public function processArrowFunctionNode( $this->callNodeCallback($nodeCallback, new InArrowFunctionNode($arrowFunctionType, $expr), $arrowFunctionScope, $storage); $exprResult = $this->processExprNode($stmt, $expr->expr, $arrowFunctionScope, $storage, $nodeCallback, ExpressionContext::createTopLevel()); - return new ExpressionResult($scope, false, $exprResult->isAlwaysTerminating(), $exprResult->getThrowPoints(), $exprResult->getImpurePoints()); + return $this->expressionResultFactory->create($scope, false, $exprResult->isAlwaysTerminating(), $exprResult->getThrowPoints(), $exprResult->getImpurePoints()); } /** @@ -3853,7 +3854,7 @@ public function processArgs( } // not storing this, it's scope after processing all args - return new ExpressionResult($scope, $hasYield, $isAlwaysTerminating, $throwPoints, $impurePoints); + return $this->expressionResultFactory->create($scope, $hasYield, $isAlwaysTerminating, $throwPoints, $impurePoints); } /** @@ -3990,7 +3991,7 @@ public function processVirtualAssign(MutatingScope $scope, ExpressionResultStora $assignedExpr, new VirtualAssignNodeCallback($nodeCallback), ExpressionContext::createDeep(), - static fn (MutatingScope $scope): ExpressionResult => new ExpressionResult($scope, hasYield: false, isAlwaysTerminating: false, throwPoints: [], impurePoints: []), + fn (MutatingScope $scope): ExpressionResult => $this->expressionResultFactory->create($scope, hasYield: false, isAlwaysTerminating: false, throwPoints: [], impurePoints: []), false, ); } diff --git a/src/Testing/RuleTestCase.php b/src/Testing/RuleTestCase.php index d8d0ba20e1c..59cdbf3eab4 100644 --- a/src/Testing/RuleTestCase.php +++ b/src/Testing/RuleTestCase.php @@ -6,6 +6,7 @@ use PHPStan\Analyser\Analyser; use PHPStan\Analyser\AnalyserResultFinalizer; use PHPStan\Analyser\Error; +use PHPStan\Analyser\ExpressionResultFactory; use PHPStan\Analyser\ExprHandler\Helper\ImplicitToStringCallHelper; use PHPStan\Analyser\Fiber\FiberNodeScopeResolver; use PHPStan\Analyser\FileAnalyser; @@ -118,6 +119,7 @@ protected function createNodeScopeResolver(): NodeScopeResolver self::getContainer()->getParameter('exceptions')['implicitThrows'], $this->shouldTreatPhpDocTypesAsCertain(), self::getContainer()->getByType(ImplicitToStringCallHelper::class), + self::getContainer()->getByType(ExpressionResultFactory::class), ); } diff --git a/src/Testing/TypeInferenceTestCase.php b/src/Testing/TypeInferenceTestCase.php index 7723560fd06..3cda273ecf0 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\ExpressionResultFactory; use PHPStan\Analyser\ExprHandler\Helper\ImplicitToStringCallHelper; use PHPStan\Analyser\Fiber\FiberNodeScopeResolver; use PHPStan\Analyser\MutatingScope; @@ -93,6 +94,7 @@ protected static function createNodeScopeResolver(): NodeScopeResolver $container->getParameter('exceptions')['implicitThrows'], $container->getParameter('treatPhpDocTypesAsCertain'), $container->getByType(ImplicitToStringCallHelper::class), + $container->getByType(ExpressionResultFactory::class), ); } diff --git a/tests/PHPStan/Analyser/AnalyserTest.php b/tests/PHPStan/Analyser/AnalyserTest.php index 39da6a5be9e..ded70fca9e3 100644 --- a/tests/PHPStan/Analyser/AnalyserTest.php +++ b/tests/PHPStan/Analyser/AnalyserTest.php @@ -834,6 +834,7 @@ private function createAnalyser(): Analyser true, $this->shouldTreatPhpDocTypesAsCertain(), $container->getByType(ImplicitToStringCallHelper::class), + $container->getByType(ExpressionResultFactory::class), ); $lexer = new Lexer(); $fileAnalyser = new FileAnalyser( diff --git a/tests/PHPStan/Analyser/Fiber/FiberNodeScopeResolverRuleTest.php b/tests/PHPStan/Analyser/Fiber/FiberNodeScopeResolverRuleTest.php index 0a1804341a6..fae07c69bb5 100644 --- a/tests/PHPStan/Analyser/Fiber/FiberNodeScopeResolverRuleTest.php +++ b/tests/PHPStan/Analyser/Fiber/FiberNodeScopeResolverRuleTest.php @@ -3,6 +3,7 @@ namespace PHPStan\Analyser\Fiber; use PhpParser\Node; +use PHPStan\Analyser\ExpressionResultFactory; use PHPStan\Analyser\ExprHandler\Helper\ImplicitToStringCallHelper; use PHPStan\Analyser\NodeScopeResolver; use PHPStan\Analyser\Scope; @@ -138,6 +139,7 @@ protected function createNodeScopeResolver(): NodeScopeResolver self::getContainer()->getParameter('exceptions')['implicitThrows'], $this->shouldTreatPhpDocTypesAsCertain(), self::getContainer()->getByType(ImplicitToStringCallHelper::class), + self::getContainer()->getByType(ExpressionResultFactory::class), ); } diff --git a/tests/PHPStan/Analyser/Fiber/FiberNodeScopeResolverTest.php b/tests/PHPStan/Analyser/Fiber/FiberNodeScopeResolverTest.php index ae9aa1ec4c1..608bfee99ec 100644 --- a/tests/PHPStan/Analyser/Fiber/FiberNodeScopeResolverTest.php +++ b/tests/PHPStan/Analyser/Fiber/FiberNodeScopeResolverTest.php @@ -2,6 +2,7 @@ namespace PHPStan\Analyser\Fiber; +use PHPStan\Analyser\ExpressionResultFactory; use PHPStan\Analyser\ExprHandler\Helper\ImplicitToStringCallHelper; use PHPStan\Analyser\NodeScopeResolver; use PHPStan\DependencyInjection\Type\ParameterClosureThisExtensionProvider; @@ -71,6 +72,7 @@ protected static function createNodeScopeResolver(): NodeScopeResolver $container->getParameter('exceptions')['implicitThrows'], $container->getParameter('treatPhpDocTypesAsCertain'), $container->getByType(ImplicitToStringCallHelper::class), + $container->getByType(ExpressionResultFactory::class), ); } From f273819c8c552c41dd4eb98a1f03f0807cfc5c8d Mon Sep 17 00:00:00 2001 From: Ondrej Mirtes Date: Thu, 11 Jun 2026 11:28:09 +0200 Subject: [PATCH 002/113] ExpressionResult - add beforeScope --- .../ExprHandler/ArrayDimFetchHandler.php | 3 +++ src/Analyser/ExprHandler/ArrayHandler.php | 2 ++ .../ExprHandler/ArrowFunctionHandler.php | 1 + src/Analyser/ExprHandler/AssignHandler.php | 10 +++++++--- src/Analyser/ExprHandler/AssignOpHandler.php | 3 +++ src/Analyser/ExprHandler/BinaryOpHandler.php | 2 ++ .../ExprHandler/BitwiseNotHandler.php | 1 + .../ExprHandler/BooleanAndHandler.php | 1 + .../ExprHandler/BooleanNotHandler.php | 2 ++ src/Analyser/ExprHandler/BooleanOrHandler.php | 1 + src/Analyser/ExprHandler/CastHandler.php | 2 ++ .../ExprHandler/CastStringHandler.php | 2 ++ .../ExprHandler/ClassConstFetchHandler.php | 2 ++ src/Analyser/ExprHandler/CloneHandler.php | 1 + src/Analyser/ExprHandler/ClosureHandler.php | 4 ++-- src/Analyser/ExprHandler/CoalesceHandler.php | 2 ++ .../ExprHandler/ConstFetchHandler.php | 1 + src/Analyser/ExprHandler/EmptyHandler.php | 2 ++ .../ExprHandler/ErrorSuppressHandler.php | 1 + src/Analyser/ExprHandler/EvalHandler.php | 2 ++ src/Analyser/ExprHandler/ExitHandler.php | 2 ++ src/Analyser/ExprHandler/FuncCallHandler.php | 2 ++ .../Helper/ImplicitToStringCallHelper.php | 2 ++ src/Analyser/ExprHandler/IncludeHandler.php | 2 ++ .../ExprHandler/InstanceofHandler.php | 2 ++ .../ExprHandler/InterpolatedStringHandler.php | 2 ++ src/Analyser/ExprHandler/IssetHandler.php | 2 ++ src/Analyser/ExprHandler/MatchHandler.php | 2 ++ .../ExprHandler/MethodCallHandler.php | 3 +++ src/Analyser/ExprHandler/NewHandler.php | 2 ++ .../ExprHandler/NullsafeMethodCallHandler.php | 2 ++ .../NullsafePropertyFetchHandler.php | 2 ++ src/Analyser/ExprHandler/PipeHandler.php | 1 + src/Analyser/ExprHandler/PostDecHandler.php | 19 +++++++++---------- src/Analyser/ExprHandler/PostIncHandler.php | 19 +++++++++---------- src/Analyser/ExprHandler/PreDecHandler.php | 19 +++++++++---------- src/Analyser/ExprHandler/PreIncHandler.php | 19 +++++++++---------- src/Analyser/ExprHandler/PrintHandler.php | 2 ++ .../ExprHandler/PropertyFetchHandler.php | 2 ++ src/Analyser/ExprHandler/ScalarHandler.php | 1 + .../ExprHandler/StaticCallHandler.php | 2 ++ .../StaticPropertyFetchHandler.php | 2 ++ src/Analyser/ExprHandler/TernaryHandler.php | 1 + src/Analyser/ExprHandler/ThrowHandler.php | 1 + .../ExprHandler/UnaryMinusHandler.php | 1 + src/Analyser/ExprHandler/UnaryPlusHandler.php | 1 + src/Analyser/ExprHandler/VariableHandler.php | 2 ++ .../Virtual/AlwaysRememberedExprHandler.php | 2 ++ .../Virtual/ExistingArrayDimFetchHandler.php | 1 + .../Virtual/FunctionCallableNodeHandler.php | 2 ++ .../Virtual/GetIterableKeyTypeExprHandler.php | 1 + .../GetIterableValueTypeExprHandler.php | 1 + .../Virtual/GetOffsetValueTypeExprHandler.php | 1 + .../InstantiationCallableNodeHandler.php | 2 ++ .../Virtual/MethodCallableNodeHandler.php | 2 ++ .../Virtual/NativeTypeExprHandler.php | 1 + .../OriginalPropertyTypeExprHandler.php | 1 + .../SetExistingOffsetValueTypeExprHandler.php | 1 + .../Virtual/SetOffsetValueTypeExprHandler.php | 1 + .../StaticMethodCallableNodeHandler.php | 2 ++ .../ExprHandler/Virtual/TypeExprHandler.php | 1 + .../Virtual/UnsetOffsetExprHandler.php | 1 + src/Analyser/ExprHandler/YieldFromHandler.php | 2 ++ src/Analyser/ExprHandler/YieldHandler.php | 2 ++ src/Analyser/ExpressionResult.php | 6 ++++++ src/Analyser/ExpressionResultFactory.php | 1 + src/Analyser/NodeScopeResolver.php | 9 +++++---- 67 files changed, 153 insertions(+), 49 deletions(-) diff --git a/src/Analyser/ExprHandler/ArrayDimFetchHandler.php b/src/Analyser/ExprHandler/ArrayDimFetchHandler.php index f82489c876b..59ea71f1e31 100644 --- a/src/Analyser/ExprHandler/ArrayDimFetchHandler.php +++ b/src/Analyser/ExprHandler/ArrayDimFetchHandler.php @@ -81,12 +81,14 @@ public function resolveType(MutatingScope $scope, Expr $expr): Type public function processExpr(NodeScopeResolver $nodeScopeResolver, Stmt $stmt, Expr $expr, MutatingScope $scope, ExpressionResultStorage $storage, callable $nodeCallback, ExpressionContext $context): ExpressionResult { + $beforeScope = $scope; if ($expr->dim === null) { $varResult = $nodeScopeResolver->processExprNode($stmt, $expr->var, $scope, $storage, $nodeCallback, $context->enterDeep()); $scope = $varResult->getScope(); return $this->expressionResultFactory->create( $scope, + beforeScope: $beforeScope, hasYield: $varResult->hasYield(), isAlwaysTerminating: $varResult->isAlwaysTerminating(), throwPoints: $varResult->getThrowPoints(), @@ -116,6 +118,7 @@ public function processExpr(NodeScopeResolver $nodeScopeResolver, Stmt $stmt, Ex return $this->expressionResultFactory->create( $scope, + beforeScope: $beforeScope, hasYield: $dimResult->hasYield() || $varResult->hasYield(), isAlwaysTerminating: $dimResult->isAlwaysTerminating() || $varResult->isAlwaysTerminating(), throwPoints: $throwPoints, diff --git a/src/Analyser/ExprHandler/ArrayHandler.php b/src/Analyser/ExprHandler/ArrayHandler.php index 580ab998b6c..78eb2b5491d 100644 --- a/src/Analyser/ExprHandler/ArrayHandler.php +++ b/src/Analyser/ExprHandler/ArrayHandler.php @@ -74,6 +74,7 @@ public function resolveType(MutatingScope $scope, Expr $expr): Type public function processExpr(NodeScopeResolver $nodeScopeResolver, Stmt $stmt, Expr $expr, MutatingScope $scope, ExpressionResultStorage $storage, callable $nodeCallback, ExpressionContext $context): ExpressionResult { + $beforeScope = $scope; $itemNodes = []; $hasYield = false; $throwPoints = []; @@ -102,6 +103,7 @@ public function processExpr(NodeScopeResolver $nodeScopeResolver, Stmt $stmt, Ex return $this->expressionResultFactory->create( $scope, + beforeScope: $beforeScope, hasYield: $hasYield, isAlwaysTerminating: $isAlwaysTerminating, throwPoints: $throwPoints, diff --git a/src/Analyser/ExprHandler/ArrowFunctionHandler.php b/src/Analyser/ExprHandler/ArrowFunctionHandler.php index d4306247c81..b98fc9c85de 100644 --- a/src/Analyser/ExprHandler/ArrowFunctionHandler.php +++ b/src/Analyser/ExprHandler/ArrowFunctionHandler.php @@ -45,6 +45,7 @@ public function processExpr(NodeScopeResolver $nodeScopeResolver, Stmt $stmt, Ex return $this->expressionResultFactory->create( $result->getScope(), + beforeScope: $scope, hasYield: $result->hasYield(), isAlwaysTerminating: false, throwPoints: [], diff --git a/src/Analyser/ExprHandler/AssignHandler.php b/src/Analyser/ExprHandler/AssignHandler.php index 5f16804d271..a9248dc6d3a 100644 --- a/src/Analyser/ExprHandler/AssignHandler.php +++ b/src/Analyser/ExprHandler/AssignHandler.php @@ -291,6 +291,7 @@ public function specifyTypes(TypeSpecifier $typeSpecifier, Scope $scope, Expr $e public function processExpr(NodeScopeResolver $nodeScopeResolver, Stmt $stmt, Expr $expr, MutatingScope $scope, ExpressionResultStorage $storage, callable $nodeCallback, ExpressionContext $context): ExpressionResult { + $beforeScope = $scope; $result = $this->processAssignVar( $nodeScopeResolver, $scope, @@ -301,6 +302,7 @@ public function processExpr(NodeScopeResolver $nodeScopeResolver, Stmt $stmt, Ex $nodeCallback, $context, function (MutatingScope $scope) use ($stmt, $expr, $nodeCallback, $context, $storage, $nodeScopeResolver): ExpressionResult { + $beforeScope = $scope; $impurePoints = []; if ($expr instanceof AssignRef) { $referencedExpr = $expr->expr; @@ -340,7 +342,7 @@ function (MutatingScope $scope) use ($stmt, $expr, $nodeCallback, $context, $sto $scope = $scope->exitExpressionAssign($expr->expr); } - return $this->expressionResultFactory->create($scope, $hasYield, $isAlwaysTerminating, $throwPoints, $impurePoints); + return $this->expressionResultFactory->create($scope, $beforeScope, $hasYield, $isAlwaysTerminating, $throwPoints, $impurePoints); }, true, ); @@ -384,6 +386,7 @@ function (MutatingScope $scope) use ($stmt, $expr, $nodeCallback, $context, $sto return $this->expressionResultFactory->create( $scope, + beforeScope: $beforeScope, hasYield: $result->hasYield(), isAlwaysTerminating: $result->isAlwaysTerminating(), throwPoints: $result->getThrowPoints(), @@ -410,6 +413,7 @@ public function processAssignVar( bool $enterExpressionAssign, ): ExpressionResult { + $beforeScope = $scope; $nodeScopeResolver->callNodeCallback($nodeCallback, $var, $enterExpressionAssign ? $scope->enterExpressionAssign($var) : $scope, $storage); $hasYield = false; $throwPoints = []; @@ -940,7 +944,7 @@ public function processAssignVar( new GetOffsetValueTypeExpr($assignedExpr, $dimExpr), $nodeCallback, $context, - fn (MutatingScope $scope): ExpressionResult => $this->expressionResultFactory->create($scope, hasYield: false, isAlwaysTerminating: false, throwPoints: [], impurePoints: []), + fn (MutatingScope $scope): ExpressionResult => $this->expressionResultFactory->create($scope, beforeScope: $scope, hasYield: false, isAlwaysTerminating: false, throwPoints: [], impurePoints: []), $enterExpressionAssign, ); $scope = $result->getScope(); @@ -1032,7 +1036,7 @@ public function processAssignVar( } // stored where processAssignVar is called - return $this->expressionResultFactory->create($scope, $hasYield, $isAlwaysTerminating, $throwPoints, $impurePoints); + return $this->expressionResultFactory->create($scope, $beforeScope, $hasYield, $isAlwaysTerminating, $throwPoints, $impurePoints); } private function createArrayDimFetchConditionalExpressionHolder( diff --git a/src/Analyser/ExprHandler/AssignOpHandler.php b/src/Analyser/ExprHandler/AssignOpHandler.php index 17a35977c29..5d0d4c8f842 100644 --- a/src/Analyser/ExprHandler/AssignOpHandler.php +++ b/src/Analyser/ExprHandler/AssignOpHandler.php @@ -55,6 +55,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 { + $beforeScope = $scope; $assignResult = $this->assignHandler->processAssignVar( $nodeScopeResolver, $scope, @@ -78,6 +79,7 @@ function (MutatingScope $scope) use ($stmt, $expr, $nodeCallback, $context, $sto $isAlwaysTerminating = $exprResult->isAlwaysTerminating() && $originalScope->getType($expr->var)->isNull()->yes(); return $this->expressionResultFactory->create( $exprResult->getScope()->mergeWith($originalScope), + $originalScope, $exprResult->hasYield(), $isAlwaysTerminating, $exprResult->getThrowPoints(), @@ -109,6 +111,7 @@ function (MutatingScope $scope) use ($stmt, $expr, $nodeCallback, $context, $sto return $this->expressionResultFactory->create( $scope, + beforeScope: $beforeScope, hasYield: $assignResult->hasYield(), isAlwaysTerminating: $assignResult->isAlwaysTerminating(), throwPoints: $throwPoints, diff --git a/src/Analyser/ExprHandler/BinaryOpHandler.php b/src/Analyser/ExprHandler/BinaryOpHandler.php index 4c37485323d..70d7841012a 100644 --- a/src/Analyser/ExprHandler/BinaryOpHandler.php +++ b/src/Analyser/ExprHandler/BinaryOpHandler.php @@ -85,6 +85,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 { + $beforeScope = $scope; $leftResult = $nodeScopeResolver->processExprNode($stmt, $expr->left, $scope, $storage, $nodeCallback, $context->enterDeep()); $rightResult = $nodeScopeResolver->processExprNode($stmt, $expr->right, $leftResult->getScope(), $storage, $nodeCallback, $context->enterDeep()); $throwPoints = array_merge($leftResult->getThrowPoints(), $rightResult->getThrowPoints()); @@ -105,6 +106,7 @@ public function processExpr(NodeScopeResolver $nodeScopeResolver, Stmt $stmt, Ex return $this->expressionResultFactory->create( $scope, + beforeScope: $beforeScope, hasYield: $leftResult->hasYield() || $rightResult->hasYield(), isAlwaysTerminating: $leftResult->isAlwaysTerminating() || $rightResult->isAlwaysTerminating(), throwPoints: $throwPoints, diff --git a/src/Analyser/ExprHandler/BitwiseNotHandler.php b/src/Analyser/ExprHandler/BitwiseNotHandler.php index 08ea1a0c1ed..ae9ec208bd5 100644 --- a/src/Analyser/ExprHandler/BitwiseNotHandler.php +++ b/src/Analyser/ExprHandler/BitwiseNotHandler.php @@ -45,6 +45,7 @@ public function processExpr(NodeScopeResolver $nodeScopeResolver, Stmt $stmt, Ex return $this->expressionResultFactory->create( $exprResult->getScope(), + beforeScope: $scope, hasYield: $exprResult->hasYield(), isAlwaysTerminating: $exprResult->isAlwaysTerminating(), throwPoints: $exprResult->getThrowPoints(), diff --git a/src/Analyser/ExprHandler/BooleanAndHandler.php b/src/Analyser/ExprHandler/BooleanAndHandler.php index 5727c8cfdf5..d96b8616595 100644 --- a/src/Analyser/ExprHandler/BooleanAndHandler.php +++ b/src/Analyser/ExprHandler/BooleanAndHandler.php @@ -275,6 +275,7 @@ public function processExpr(NodeScopeResolver $nodeScopeResolver, Stmt $stmt, Ex return $this->expressionResultFactory->create( $leftMergedWithRightScope, + beforeScope: $scope, hasYield: $leftResult->hasYield() || $rightResult->hasYield(), isAlwaysTerminating: $leftResult->isAlwaysTerminating(), throwPoints: array_merge($leftResult->getThrowPoints(), $rightResult->getThrowPoints()), diff --git a/src/Analyser/ExprHandler/BooleanNotHandler.php b/src/Analyser/ExprHandler/BooleanNotHandler.php index 082615dc3d5..08091ce1dc0 100644 --- a/src/Analyser/ExprHandler/BooleanNotHandler.php +++ b/src/Analyser/ExprHandler/BooleanNotHandler.php @@ -39,11 +39,13 @@ public function supports(Expr $expr): bool public function processExpr(NodeScopeResolver $nodeScopeResolver, Stmt $stmt, Expr $expr, MutatingScope $scope, ExpressionResultStorage $storage, callable $nodeCallback, ExpressionContext $context): ExpressionResult { + $beforeScope = $scope; $exprResult = $nodeScopeResolver->processExprNode($stmt, $expr->expr, $scope, $storage, $nodeCallback, $context->enterDeep()); $scope = $exprResult->getScope(); return $this->expressionResultFactory->create( $scope, + beforeScope: $beforeScope, hasYield: $exprResult->hasYield(), isAlwaysTerminating: $exprResult->isAlwaysTerminating(), throwPoints: $exprResult->getThrowPoints(), diff --git a/src/Analyser/ExprHandler/BooleanOrHandler.php b/src/Analyser/ExprHandler/BooleanOrHandler.php index 9f2a00bef4e..46e8bc28631 100644 --- a/src/Analyser/ExprHandler/BooleanOrHandler.php +++ b/src/Analyser/ExprHandler/BooleanOrHandler.php @@ -307,6 +307,7 @@ public function processExpr(NodeScopeResolver $nodeScopeResolver, Stmt $stmt, Ex return $this->expressionResultFactory->create( $leftMergedWithRightScope, + beforeScope: $scope, hasYield: $leftResult->hasYield() || $rightResult->hasYield(), isAlwaysTerminating: $leftResult->isAlwaysTerminating(), throwPoints: array_merge($leftResult->getThrowPoints(), $rightResult->getThrowPoints()), diff --git a/src/Analyser/ExprHandler/CastHandler.php b/src/Analyser/ExprHandler/CastHandler.php index f7837085055..99085f9b800 100644 --- a/src/Analyser/ExprHandler/CastHandler.php +++ b/src/Analyser/ExprHandler/CastHandler.php @@ -48,11 +48,13 @@ public function supports(Expr $expr): bool public function processExpr(NodeScopeResolver $nodeScopeResolver, Stmt $stmt, Expr $expr, MutatingScope $scope, ExpressionResultStorage $storage, callable $nodeCallback, ExpressionContext $context): ExpressionResult { + $beforeScope = $scope; $exprResult = $nodeScopeResolver->processExprNode($stmt, $expr->expr, $scope, $storage, $nodeCallback, $context->enterDeep()); $scope = $exprResult->getScope(); return $this->expressionResultFactory->create( $scope, + beforeScope: $beforeScope, hasYield: $exprResult->hasYield(), isAlwaysTerminating: $exprResult->isAlwaysTerminating(), throwPoints: $exprResult->getThrowPoints(), diff --git a/src/Analyser/ExprHandler/CastStringHandler.php b/src/Analyser/ExprHandler/CastStringHandler.php index e8a4a3c9057..32f31ec60a1 100644 --- a/src/Analyser/ExprHandler/CastStringHandler.php +++ b/src/Analyser/ExprHandler/CastStringHandler.php @@ -46,6 +46,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 { + $beforeScope = $scope; $exprResult = $nodeScopeResolver->processExprNode($stmt, $expr->expr, $scope, $storage, $nodeCallback, $context->enterDeep()); $impurePoints = $exprResult->getImpurePoints(); $throwPoints = $exprResult->getThrowPoints(); @@ -58,6 +59,7 @@ public function processExpr(NodeScopeResolver $nodeScopeResolver, Stmt $stmt, Ex return $this->expressionResultFactory->create( $scope, + beforeScope: $beforeScope, hasYield: $exprResult->hasYield(), isAlwaysTerminating: $exprResult->isAlwaysTerminating(), throwPoints: $throwPoints, diff --git a/src/Analyser/ExprHandler/ClassConstFetchHandler.php b/src/Analyser/ExprHandler/ClassConstFetchHandler.php index 61e28d1cac5..3283d8f4153 100644 --- a/src/Analyser/ExprHandler/ClassConstFetchHandler.php +++ b/src/Analyser/ExprHandler/ClassConstFetchHandler.php @@ -58,6 +58,7 @@ public function resolveType(MutatingScope $scope, Expr $expr): Type public function processExpr(NodeScopeResolver $nodeScopeResolver, Stmt $stmt, Expr $expr, MutatingScope $scope, ExpressionResultStorage $storage, callable $nodeCallback, ExpressionContext $context): ExpressionResult { + $beforeScope = $scope; $hasYield = false; $throwPoints = []; $impurePoints = []; @@ -87,6 +88,7 @@ public function processExpr(NodeScopeResolver $nodeScopeResolver, Stmt $stmt, Ex return $this->expressionResultFactory->create( $scope, + beforeScope: $beforeScope, hasYield: $hasYield, isAlwaysTerminating: $isAlwaysTerminating, throwPoints: $throwPoints, diff --git a/src/Analyser/ExprHandler/CloneHandler.php b/src/Analyser/ExprHandler/CloneHandler.php index 78534ceb1c7..ee1951e68d5 100644 --- a/src/Analyser/ExprHandler/CloneHandler.php +++ b/src/Analyser/ExprHandler/CloneHandler.php @@ -45,6 +45,7 @@ public function processExpr(NodeScopeResolver $nodeScopeResolver, Stmt $stmt, Ex return $this->expressionResultFactory->create( $exprResult->getScope(), + beforeScope: $scope, hasYield: $exprResult->hasYield(), isAlwaysTerminating: $exprResult->isAlwaysTerminating(), throwPoints: $exprResult->getThrowPoints(), diff --git a/src/Analyser/ExprHandler/ClosureHandler.php b/src/Analyser/ExprHandler/ClosureHandler.php index 0961cf29c98..e0e742e812d 100644 --- a/src/Analyser/ExprHandler/ClosureHandler.php +++ b/src/Analyser/ExprHandler/ClosureHandler.php @@ -42,10 +42,10 @@ 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, null); - $scope = $processClosureResult->applyByRefUseScope($processClosureResult->getScope()); return $this->expressionResultFactory->create( - $scope, + $processClosureResult->applyByRefUseScope($processClosureResult->getScope()), + beforeScope: $scope, hasYield: false, isAlwaysTerminating: false, throwPoints: [], diff --git a/src/Analyser/ExprHandler/CoalesceHandler.php b/src/Analyser/ExprHandler/CoalesceHandler.php index 2a6fab5a268..562a4f9075c 100644 --- a/src/Analyser/ExprHandler/CoalesceHandler.php +++ b/src/Analyser/ExprHandler/CoalesceHandler.php @@ -116,6 +116,7 @@ public function specifyTypes(TypeSpecifier $typeSpecifier, Scope $scope, Expr $e public function processExpr(NodeScopeResolver $nodeScopeResolver, Stmt $stmt, Expr $expr, MutatingScope $scope, ExpressionResultStorage $storage, callable $nodeCallback, ExpressionContext $context): ExpressionResult { + $beforeScope = $scope; $nonNullabilityResult = $this->nonNullabilityHelper->ensureNonNullability($scope, $expr->left); $condScope = $nodeScopeResolver->lookForSetAllowedUndefinedExpressions($nonNullabilityResult->getScope(), $expr->left); $condResult = $nodeScopeResolver->processExprNode($stmt, $expr->left, $condScope, $storage, $nodeCallback, $context->enterDeep()); @@ -133,6 +134,7 @@ public function processExpr(NodeScopeResolver $nodeScopeResolver, Stmt $stmt, Ex return $this->expressionResultFactory->create( $scope, + beforeScope: $beforeScope, hasYield: $condResult->hasYield() || $rightResult->hasYield(), isAlwaysTerminating: $condResult->isAlwaysTerminating(), throwPoints: array_merge($condResult->getThrowPoints(), $rightResult->getThrowPoints()), diff --git a/src/Analyser/ExprHandler/ConstFetchHandler.php b/src/Analyser/ExprHandler/ConstFetchHandler.php index 6d825348e16..23510dbbaba 100644 --- a/src/Analyser/ExprHandler/ConstFetchHandler.php +++ b/src/Analyser/ExprHandler/ConstFetchHandler.php @@ -50,6 +50,7 @@ public function processExpr(NodeScopeResolver $nodeScopeResolver, Stmt $stmt, Ex return $this->expressionResultFactory->create( $scope, + beforeScope: $scope, hasYield: false, isAlwaysTerminating: false, throwPoints: [], diff --git a/src/Analyser/ExprHandler/EmptyHandler.php b/src/Analyser/ExprHandler/EmptyHandler.php index cba551bfd36..d5f932b2b14 100644 --- a/src/Analyser/ExprHandler/EmptyHandler.php +++ b/src/Analyser/ExprHandler/EmptyHandler.php @@ -87,6 +87,7 @@ public function specifyTypes(TypeSpecifier $typeSpecifier, Scope $scope, Expr $e public function processExpr(NodeScopeResolver $nodeScopeResolver, Stmt $stmt, Expr $expr, MutatingScope $scope, ExpressionResultStorage $storage, callable $nodeCallback, ExpressionContext $context): ExpressionResult { + $beforeScope = $scope; $nonNullabilityResult = $this->nonNullabilityHelper->ensureNonNullability($scope, $expr->expr); $scope = $nodeScopeResolver->lookForSetAllowedUndefinedExpressions($nonNullabilityResult->getScope(), $expr->expr); $exprResult = $nodeScopeResolver->processExprNode($stmt, $expr->expr, $scope, $storage, $nodeCallback, $context->enterDeep()); @@ -96,6 +97,7 @@ public function processExpr(NodeScopeResolver $nodeScopeResolver, Stmt $stmt, Ex return $this->expressionResultFactory->create( $scope, + beforeScope: $beforeScope, hasYield: $exprResult->hasYield(), isAlwaysTerminating: $exprResult->isAlwaysTerminating(), throwPoints: $exprResult->getThrowPoints(), diff --git a/src/Analyser/ExprHandler/ErrorSuppressHandler.php b/src/Analyser/ExprHandler/ErrorSuppressHandler.php index 1189f6324a7..700d260261a 100644 --- a/src/Analyser/ExprHandler/ErrorSuppressHandler.php +++ b/src/Analyser/ExprHandler/ErrorSuppressHandler.php @@ -41,6 +41,7 @@ public function processExpr(NodeScopeResolver $nodeScopeResolver, Stmt $stmt, Ex return $this->expressionResultFactory->create( $exprResult->getScope(), + beforeScope: $scope, hasYield: $exprResult->hasYield(), isAlwaysTerminating: $exprResult->isAlwaysTerminating(), throwPoints: $exprResult->getThrowPoints(), diff --git a/src/Analyser/ExprHandler/EvalHandler.php b/src/Analyser/ExprHandler/EvalHandler.php index 62b16484e1a..d1b7160701f 100644 --- a/src/Analyser/ExprHandler/EvalHandler.php +++ b/src/Analyser/ExprHandler/EvalHandler.php @@ -46,11 +46,13 @@ public function resolveType(MutatingScope $scope, Expr $expr): Type public function processExpr(NodeScopeResolver $nodeScopeResolver, Stmt $stmt, Expr $expr, MutatingScope $scope, ExpressionResultStorage $storage, callable $nodeCallback, ExpressionContext $context): ExpressionResult { + $beforeScope = $scope; $exprResult = $nodeScopeResolver->processExprNode($stmt, $expr->expr, $scope, $storage, $nodeCallback, $context->enterDeep()); $scope = $exprResult->getScope(); return $this->expressionResultFactory->create( $scope, + beforeScope: $beforeScope, hasYield: $exprResult->hasYield(), isAlwaysTerminating: $exprResult->isAlwaysTerminating(), throwPoints: array_merge($exprResult->getThrowPoints(), [InternalThrowPoint::createImplicit($scope, $expr)]), diff --git a/src/Analyser/ExprHandler/ExitHandler.php b/src/Analyser/ExprHandler/ExitHandler.php index a2d4a7db273..0cba81da42c 100644 --- a/src/Analyser/ExprHandler/ExitHandler.php +++ b/src/Analyser/ExprHandler/ExitHandler.php @@ -40,6 +40,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 { + $beforeScope = $scope; $kind = $expr->getAttribute('kind', Exit_::KIND_EXIT); $identifier = $kind === Exit_::KIND_DIE ? 'die' : 'exit'; $impurePoints = [ @@ -58,6 +59,7 @@ public function processExpr(NodeScopeResolver $nodeScopeResolver, Stmt $stmt, Ex return $this->expressionResultFactory->create( $scope, + beforeScope: $beforeScope, hasYield: $hasYield, isAlwaysTerminating: true, throwPoints: $throwPoints, diff --git a/src/Analyser/ExprHandler/FuncCallHandler.php b/src/Analyser/ExprHandler/FuncCallHandler.php index 3074d340cd1..7d985852a05 100644 --- a/src/Analyser/ExprHandler/FuncCallHandler.php +++ b/src/Analyser/ExprHandler/FuncCallHandler.php @@ -108,6 +108,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 { + $beforeScope = $scope; $parametersAcceptor = null; $functionReflection = null; $throwPoints = []; @@ -574,6 +575,7 @@ public function processExpr(NodeScopeResolver $nodeScopeResolver, Stmt $stmt, Ex return $this->expressionResultFactory->create( $scope, + beforeScope: $beforeScope, hasYield: $hasYield, isAlwaysTerminating: $isAlwaysTerminating, throwPoints: $throwPoints, diff --git a/src/Analyser/ExprHandler/Helper/ImplicitToStringCallHelper.php b/src/Analyser/ExprHandler/Helper/ImplicitToStringCallHelper.php index 4ef762ad16d..9303995b032 100644 --- a/src/Analyser/ExprHandler/Helper/ImplicitToStringCallHelper.php +++ b/src/Analyser/ExprHandler/Helper/ImplicitToStringCallHelper.php @@ -39,6 +39,7 @@ public function processImplicitToStringCall(Expr $expr, MutatingScope $scope): E if ($toStringMethod === null) { return $this->expressionResultFactory->create( $scope, + beforeScope: $scope, hasYield: false, isAlwaysTerminating: false, throwPoints: [], @@ -71,6 +72,7 @@ public function processImplicitToStringCall(Expr $expr, MutatingScope $scope): E return $this->expressionResultFactory->create( $scope, + beforeScope: $scope, hasYield: false, isAlwaysTerminating: false, throwPoints: $throwPoints, diff --git a/src/Analyser/ExprHandler/IncludeHandler.php b/src/Analyser/ExprHandler/IncludeHandler.php index c8556a88caf..44df8e73801 100644 --- a/src/Analyser/ExprHandler/IncludeHandler.php +++ b/src/Analyser/ExprHandler/IncludeHandler.php @@ -47,12 +47,14 @@ public function resolveType(MutatingScope $scope, Expr $expr): Type public function processExpr(NodeScopeResolver $nodeScopeResolver, Stmt $stmt, Expr $expr, MutatingScope $scope, ExpressionResultStorage $storage, callable $nodeCallback, ExpressionContext $context): ExpressionResult { + $beforeScope = $scope; $exprResult = $nodeScopeResolver->processExprNode($stmt, $expr->expr, $scope, $storage, $nodeCallback, $context->enterDeep()); $identifier = in_array($expr->type, [Include_::TYPE_INCLUDE, Include_::TYPE_INCLUDE_ONCE], true) ? 'include' : 'require'; $scope = $exprResult->getScope()->afterExtractCall(); return $this->expressionResultFactory->create( $scope, + beforeScope: $beforeScope, hasYield: $exprResult->hasYield(), isAlwaysTerminating: $exprResult->isAlwaysTerminating(), throwPoints: array_merge($exprResult->getThrowPoints(), [InternalThrowPoint::createImplicit($scope, $expr)]), diff --git a/src/Analyser/ExprHandler/InstanceofHandler.php b/src/Analyser/ExprHandler/InstanceofHandler.php index 0c1007743f8..077e965ffdc 100644 --- a/src/Analyser/ExprHandler/InstanceofHandler.php +++ b/src/Analyser/ExprHandler/InstanceofHandler.php @@ -50,6 +50,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 { + $beforeScope = $scope; $exprResult = $nodeScopeResolver->processExprNode($stmt, $expr->expr, $scope, $storage, $nodeCallback, $context->enterDeep()); $hasYield = $exprResult->hasYield(); $throwPoints = $exprResult->getThrowPoints(); @@ -67,6 +68,7 @@ public function processExpr(NodeScopeResolver $nodeScopeResolver, Stmt $stmt, Ex return $this->expressionResultFactory->create( $scope, + beforeScope: $beforeScope, hasYield: $hasYield, isAlwaysTerminating: $isAlwaysTerminating, throwPoints: $throwPoints, diff --git a/src/Analyser/ExprHandler/InterpolatedStringHandler.php b/src/Analyser/ExprHandler/InterpolatedStringHandler.php index 3e8672a012f..df25ce12114 100644 --- a/src/Analyser/ExprHandler/InterpolatedStringHandler.php +++ b/src/Analyser/ExprHandler/InterpolatedStringHandler.php @@ -46,6 +46,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 { + $beforeScope = $scope; $hasYield = false; $throwPoints = []; $impurePoints = []; @@ -69,6 +70,7 @@ public function processExpr(NodeScopeResolver $nodeScopeResolver, Stmt $stmt, Ex return $this->expressionResultFactory->create( $scope, + beforeScope: $beforeScope, hasYield: $hasYield, isAlwaysTerminating: $isAlwaysTerminating, throwPoints: $throwPoints, diff --git a/src/Analyser/ExprHandler/IssetHandler.php b/src/Analyser/ExprHandler/IssetHandler.php index a2becee723f..001ee84f127 100644 --- a/src/Analyser/ExprHandler/IssetHandler.php +++ b/src/Analyser/ExprHandler/IssetHandler.php @@ -346,6 +346,7 @@ public function specifyTypes(TypeSpecifier $typeSpecifier, Scope $scope, Expr $e public function processExpr(NodeScopeResolver $nodeScopeResolver, Stmt $stmt, Expr $expr, MutatingScope $scope, ExpressionResultStorage $storage, callable $nodeCallback, ExpressionContext $context): ExpressionResult { + $beforeScope = $scope; $hasYield = false; $throwPoints = []; $impurePoints = []; @@ -389,6 +390,7 @@ public function processExpr(NodeScopeResolver $nodeScopeResolver, Stmt $stmt, Ex return $this->expressionResultFactory->create( $scope, + beforeScope: $beforeScope, hasYield: $hasYield, isAlwaysTerminating: $isAlwaysTerminating, throwPoints: $throwPoints, diff --git a/src/Analyser/ExprHandler/MatchHandler.php b/src/Analyser/ExprHandler/MatchHandler.php index b7a0a4b7696..6f794d430fe 100644 --- a/src/Analyser/ExprHandler/MatchHandler.php +++ b/src/Analyser/ExprHandler/MatchHandler.php @@ -209,6 +209,7 @@ public function getArmScopesAndTypes(MutatingScope $scope, Match_ $expr): array public function processExpr(NodeScopeResolver $nodeScopeResolver, Stmt $stmt, Expr $expr, MutatingScope $scope, ExpressionResultStorage $storage, callable $nodeCallback, ExpressionContext $context): ExpressionResult { + $beforeScope = $scope; $deepContext = $context->enterDeep(); $condType = $scope->getType($expr->cond); $condNativeType = $scope->getNativeType($expr->cond); @@ -505,6 +506,7 @@ public function processExpr(NodeScopeResolver $nodeScopeResolver, Stmt $stmt, Ex return $this->expressionResultFactory->create( $scope, + beforeScope: $beforeScope, hasYield: $hasYield, isAlwaysTerminating: $isAlwaysTerminating, throwPoints: $throwPoints, diff --git a/src/Analyser/ExprHandler/MethodCallHandler.php b/src/Analyser/ExprHandler/MethodCallHandler.php index 310e2483ccb..c501df4811e 100644 --- a/src/Analyser/ExprHandler/MethodCallHandler.php +++ b/src/Analyser/ExprHandler/MethodCallHandler.php @@ -73,6 +73,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 { + $beforeScope = $scope; $originalScope = $scope; if ( ($expr->var instanceof Expr\Closure || $expr->var instanceof Expr\ArrowFunction) @@ -195,6 +196,7 @@ public function processExpr(NodeScopeResolver $nodeScopeResolver, Stmt $stmt, Ex $result = $this->expressionResultFactory->create( $scope, + beforeScope: $beforeScope, hasYield: $hasYield, isAlwaysTerminating: $isAlwaysTerminating, throwPoints: $throwPoints, @@ -223,6 +225,7 @@ public function processExpr(NodeScopeResolver $nodeScopeResolver, Stmt $stmt, Ex $scope = $scope->mergeInitializedProperties($calledMethodScope); return $this->expressionResultFactory->create( $scope, + beforeScope: $beforeScope, hasYield: $result->hasYield(), isAlwaysTerminating: $result->isAlwaysTerminating(), throwPoints: $result->getThrowPoints(), diff --git a/src/Analyser/ExprHandler/NewHandler.php b/src/Analyser/ExprHandler/NewHandler.php index a59d73ac12a..da9eddc93a9 100644 --- a/src/Analyser/ExprHandler/NewHandler.php +++ b/src/Analyser/ExprHandler/NewHandler.php @@ -90,6 +90,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 { + $beforeScope = $scope; $parametersAcceptor = null; $constructorReflection = null; $classReflection = null; @@ -219,6 +220,7 @@ public function processExpr(NodeScopeResolver $nodeScopeResolver, Stmt $stmt, Ex return $this->expressionResultFactory->create( $scope, + beforeScope: $beforeScope, hasYield: $hasYield, isAlwaysTerminating: $isAlwaysTerminating, throwPoints: $throwPoints, diff --git a/src/Analyser/ExprHandler/NullsafeMethodCallHandler.php b/src/Analyser/ExprHandler/NullsafeMethodCallHandler.php index d0ec6d41ca8..05a94ad2668 100644 --- a/src/Analyser/ExprHandler/NullsafeMethodCallHandler.php +++ b/src/Analyser/ExprHandler/NullsafeMethodCallHandler.php @@ -86,6 +86,7 @@ public function specifyTypes(TypeSpecifier $typeSpecifier, Scope $scope, Expr $e public function processExpr(NodeScopeResolver $nodeScopeResolver, Stmt $stmt, Expr $expr, MutatingScope $scope, ExpressionResultStorage $storage, callable $nodeCallback, ExpressionContext $context): ExpressionResult { + $beforeScope = $scope; $scopeBeforeNullsafe = $scope; $varType = $scope->getType($expr->var); @@ -119,6 +120,7 @@ public function processExpr(NodeScopeResolver $nodeScopeResolver, Stmt $stmt, Ex return $this->expressionResultFactory->create( $scope, + beforeScope: $beforeScope, hasYield: $exprResult->hasYield(), isAlwaysTerminating: false, throwPoints: $exprResult->getThrowPoints(), diff --git a/src/Analyser/ExprHandler/NullsafePropertyFetchHandler.php b/src/Analyser/ExprHandler/NullsafePropertyFetchHandler.php index 8ec6531c115..d9df195fd37 100644 --- a/src/Analyser/ExprHandler/NullsafePropertyFetchHandler.php +++ b/src/Analyser/ExprHandler/NullsafePropertyFetchHandler.php @@ -86,6 +86,7 @@ public function specifyTypes(TypeSpecifier $typeSpecifier, Scope $scope, Expr $e public function processExpr(NodeScopeResolver $nodeScopeResolver, Stmt $stmt, Expr $expr, MutatingScope $scope, ExpressionResultStorage $storage, callable $nodeCallback, ExpressionContext $context): ExpressionResult { + $beforeScope = $scope; $nonNullabilityResult = $this->nonNullabilityHelper->ensureShallowNonNullability($scope, $scope, $expr->var); $attributes = array_merge($expr->getAttributes(), ['virtualNullsafePropertyFetch' => true]); unset($attributes[ExprPrinter::ATTRIBUTE_CACHE_KEY]); @@ -98,6 +99,7 @@ public function processExpr(NodeScopeResolver $nodeScopeResolver, Stmt $stmt, Ex return $this->expressionResultFactory->create( $scope, + beforeScope: $beforeScope, hasYield: $exprResult->hasYield(), isAlwaysTerminating: false, throwPoints: $exprResult->getThrowPoints(), diff --git a/src/Analyser/ExprHandler/PipeHandler.php b/src/Analyser/ExprHandler/PipeHandler.php index 4ff195222bd..e6d010e2bf4 100644 --- a/src/Analyser/ExprHandler/PipeHandler.php +++ b/src/Analyser/ExprHandler/PipeHandler.php @@ -91,6 +91,7 @@ public function processExpr(NodeScopeResolver $nodeScopeResolver, Stmt $stmt, Ex return $this->expressionResultFactory->create( $callResult->getScope(), + beforeScope: $scope, hasYield: $callResult->hasYield(), isAlwaysTerminating: $callResult->isAlwaysTerminating(), throwPoints: $callResult->getThrowPoints(), diff --git a/src/Analyser/ExprHandler/PostDecHandler.php b/src/Analyser/ExprHandler/PostDecHandler.php index 1714b1d3ff2..c28a4a0175e 100644 --- a/src/Analyser/ExprHandler/PostDecHandler.php +++ b/src/Analyser/ExprHandler/PostDecHandler.php @@ -40,17 +40,16 @@ public function processExpr(NodeScopeResolver $nodeScopeResolver, Stmt $stmt, Ex { $varResult = $nodeScopeResolver->processExprNode($stmt, $expr->var, $scope, $storage, $nodeCallback, $context->enterDeep()); - $scope = $nodeScopeResolver->processVirtualAssign( - $varResult->getScope(), - $storage, - $stmt, - $expr->var, - new PreDec($expr->var), - $nodeCallback, - )->getScope(); - return $this->expressionResultFactory->create( - $scope, + $nodeScopeResolver->processVirtualAssign( + $varResult->getScope(), + $storage, + $stmt, + $expr->var, + new PreDec($expr->var), + $nodeCallback, + )->getScope(), + beforeScope: $scope, hasYield: $varResult->hasYield(), isAlwaysTerminating: $varResult->isAlwaysTerminating(), throwPoints: $varResult->getThrowPoints(), diff --git a/src/Analyser/ExprHandler/PostIncHandler.php b/src/Analyser/ExprHandler/PostIncHandler.php index 4f9f304e30c..f54fa7d4e0a 100644 --- a/src/Analyser/ExprHandler/PostIncHandler.php +++ b/src/Analyser/ExprHandler/PostIncHandler.php @@ -40,17 +40,16 @@ public function processExpr(NodeScopeResolver $nodeScopeResolver, Stmt $stmt, Ex { $varResult = $nodeScopeResolver->processExprNode($stmt, $expr->var, $scope, $storage, $nodeCallback, $context->enterDeep()); - $scope = $nodeScopeResolver->processVirtualAssign( - $varResult->getScope(), - $storage, - $stmt, - $expr->var, - new PreInc($expr->var), - $nodeCallback, - )->getScope(); - return $this->expressionResultFactory->create( - $scope, + $nodeScopeResolver->processVirtualAssign( + $varResult->getScope(), + $storage, + $stmt, + $expr->var, + new PreInc($expr->var), + $nodeCallback, + )->getScope(), + beforeScope: $scope, hasYield: $varResult->hasYield(), isAlwaysTerminating: $varResult->isAlwaysTerminating(), throwPoints: $varResult->getThrowPoints(), diff --git a/src/Analyser/ExprHandler/PreDecHandler.php b/src/Analyser/ExprHandler/PreDecHandler.php index f79b754181a..344dde7a6ad 100644 --- a/src/Analyser/ExprHandler/PreDecHandler.php +++ b/src/Analyser/ExprHandler/PreDecHandler.php @@ -103,17 +103,16 @@ public function processExpr(NodeScopeResolver $nodeScopeResolver, Stmt $stmt, Ex { $varResult = $nodeScopeResolver->processExprNode($stmt, $expr->var, $scope, $storage, $nodeCallback, $context->enterDeep()); - $scope = $nodeScopeResolver->processVirtualAssign( - $varResult->getScope(), - $storage, - $stmt, - $expr->var, - $expr, - $nodeCallback, - )->getScope(); - return $this->expressionResultFactory->create( - $scope, + $nodeScopeResolver->processVirtualAssign( + $varResult->getScope(), + $storage, + $stmt, + $expr->var, + $expr, + $nodeCallback, + )->getScope(), + beforeScope: $scope, hasYield: $varResult->hasYield(), isAlwaysTerminating: $varResult->isAlwaysTerminating(), throwPoints: $varResult->getThrowPoints(), diff --git a/src/Analyser/ExprHandler/PreIncHandler.php b/src/Analyser/ExprHandler/PreIncHandler.php index f3504623cca..9e6d026f101 100644 --- a/src/Analyser/ExprHandler/PreIncHandler.php +++ b/src/Analyser/ExprHandler/PreIncHandler.php @@ -104,17 +104,16 @@ public function processExpr(NodeScopeResolver $nodeScopeResolver, Stmt $stmt, Ex { $varResult = $nodeScopeResolver->processExprNode($stmt, $expr->var, $scope, $storage, $nodeCallback, $context->enterDeep()); - $scope = $nodeScopeResolver->processVirtualAssign( - $varResult->getScope(), - $storage, - $stmt, - $expr->var, - $expr, - $nodeCallback, - )->getScope(); - return $this->expressionResultFactory->create( - $scope, + $nodeScopeResolver->processVirtualAssign( + $varResult->getScope(), + $storage, + $stmt, + $expr->var, + $expr, + $nodeCallback, + )->getScope(), + beforeScope: $scope, hasYield: $varResult->hasYield(), isAlwaysTerminating: $varResult->isAlwaysTerminating(), throwPoints: $varResult->getThrowPoints(), diff --git a/src/Analyser/ExprHandler/PrintHandler.php b/src/Analyser/ExprHandler/PrintHandler.php index 9a3b61a14d3..d495f8b63ca 100644 --- a/src/Analyser/ExprHandler/PrintHandler.php +++ b/src/Analyser/ExprHandler/PrintHandler.php @@ -49,6 +49,7 @@ public function resolveType(MutatingScope $scope, Expr $expr): Type public function processExpr(NodeScopeResolver $nodeScopeResolver, Stmt $stmt, Expr $expr, MutatingScope $scope, ExpressionResultStorage $storage, callable $nodeCallback, ExpressionContext $context): ExpressionResult { + $beforeScope = $scope; $exprResult = $nodeScopeResolver->processExprNode($stmt, $expr->expr, $scope, $storage, $nodeCallback, $context->enterDeep()); $throwPoints = $exprResult->getThrowPoints(); $impurePoints = $exprResult->getImpurePoints(); @@ -61,6 +62,7 @@ public function processExpr(NodeScopeResolver $nodeScopeResolver, Stmt $stmt, Ex return $this->expressionResultFactory->create( $scope, + beforeScope: $beforeScope, hasYield: $exprResult->hasYield(), isAlwaysTerminating: $exprResult->isAlwaysTerminating(), throwPoints: $throwPoints, diff --git a/src/Analyser/ExprHandler/PropertyFetchHandler.php b/src/Analyser/ExprHandler/PropertyFetchHandler.php index f4ba7070756..06d78067198 100644 --- a/src/Analyser/ExprHandler/PropertyFetchHandler.php +++ b/src/Analyser/ExprHandler/PropertyFetchHandler.php @@ -53,6 +53,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 { + $beforeScope = $scope; $scopeBeforeVar = $scope; $varResult = $nodeScopeResolver->processExprNode($stmt, $expr->var, $scope, $storage, $nodeCallback, $context->enterDeep()); $hasYield = $varResult->hasYield(); @@ -85,6 +86,7 @@ public function processExpr(NodeScopeResolver $nodeScopeResolver, Stmt $stmt, Ex return $this->expressionResultFactory->create( $scope, + beforeScope: $beforeScope, hasYield: $hasYield, isAlwaysTerminating: $isAlwaysTerminating, throwPoints: $throwPoints, diff --git a/src/Analyser/ExprHandler/ScalarHandler.php b/src/Analyser/ExprHandler/ScalarHandler.php index 6f0db0d86f5..ade6dc84023 100644 --- a/src/Analyser/ExprHandler/ScalarHandler.php +++ b/src/Analyser/ExprHandler/ScalarHandler.php @@ -45,6 +45,7 @@ public function processExpr(NodeScopeResolver $nodeScopeResolver, Stmt $stmt, Ex { return $this->expressionResultFactory->create( $scope, + beforeScope: $scope, hasYield: false, isAlwaysTerminating: false, throwPoints: [], diff --git a/src/Analyser/ExprHandler/StaticCallHandler.php b/src/Analyser/ExprHandler/StaticCallHandler.php index 79e9c1288d4..d08ba830276 100644 --- a/src/Analyser/ExprHandler/StaticCallHandler.php +++ b/src/Analyser/ExprHandler/StaticCallHandler.php @@ -81,6 +81,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 { + $beforeScope = $scope; $hasYield = false; $throwPoints = []; $impurePoints = []; @@ -283,6 +284,7 @@ public function processExpr(NodeScopeResolver $nodeScopeResolver, Stmt $stmt, Ex return $this->expressionResultFactory->create( $scope, + beforeScope: $beforeScope, hasYield: $hasYield, isAlwaysTerminating: $isAlwaysTerminating, throwPoints: $throwPoints, diff --git a/src/Analyser/ExprHandler/StaticPropertyFetchHandler.php b/src/Analyser/ExprHandler/StaticPropertyFetchHandler.php index dd68417723f..13a6279a5a1 100644 --- a/src/Analyser/ExprHandler/StaticPropertyFetchHandler.php +++ b/src/Analyser/ExprHandler/StaticPropertyFetchHandler.php @@ -53,6 +53,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 { + $beforeScope = $scope; $hasYield = false; $throwPoints = []; $impurePoints = [ @@ -84,6 +85,7 @@ public function processExpr(NodeScopeResolver $nodeScopeResolver, Stmt $stmt, Ex return $this->expressionResultFactory->create( $scope, + beforeScope: $beforeScope, hasYield: $hasYield, isAlwaysTerminating: $isAlwaysTerminating, throwPoints: $throwPoints, diff --git a/src/Analyser/ExprHandler/TernaryHandler.php b/src/Analyser/ExprHandler/TernaryHandler.php index b1385b149b6..b26c09378e9 100644 --- a/src/Analyser/ExprHandler/TernaryHandler.php +++ b/src/Analyser/ExprHandler/TernaryHandler.php @@ -148,6 +148,7 @@ public function processExpr(NodeScopeResolver $nodeScopeResolver, Stmt $stmt, Ex return $this->expressionResultFactory->create( $finalScope, + beforeScope: $scope, hasYield: $ternaryCondResult->hasYield(), isAlwaysTerminating: $ternaryCondResult->isAlwaysTerminating(), throwPoints: $throwPoints, diff --git a/src/Analyser/ExprHandler/ThrowHandler.php b/src/Analyser/ExprHandler/ThrowHandler.php index 7c283decba4..2cf4666b6bd 100644 --- a/src/Analyser/ExprHandler/ThrowHandler.php +++ b/src/Analyser/ExprHandler/ThrowHandler.php @@ -44,6 +44,7 @@ public function processExpr(NodeScopeResolver $nodeScopeResolver, Stmt $stmt, Ex return $this->expressionResultFactory->create( $scope, + beforeScope: $scope, hasYield: false, isAlwaysTerminating: true, throwPoints: array_merge($exprResult->getThrowPoints(), [InternalThrowPoint::createExplicit($scope, $scope->getType($expr->expr), $expr, false)]), diff --git a/src/Analyser/ExprHandler/UnaryMinusHandler.php b/src/Analyser/ExprHandler/UnaryMinusHandler.php index 0da8e3d46a4..d97068c3acb 100644 --- a/src/Analyser/ExprHandler/UnaryMinusHandler.php +++ b/src/Analyser/ExprHandler/UnaryMinusHandler.php @@ -45,6 +45,7 @@ public function processExpr(NodeScopeResolver $nodeScopeResolver, Stmt $stmt, Ex return $this->expressionResultFactory->create( $exprResult->getScope(), + beforeScope: $scope, hasYield: $exprResult->hasYield(), isAlwaysTerminating: $exprResult->isAlwaysTerminating(), throwPoints: $exprResult->getThrowPoints(), diff --git a/src/Analyser/ExprHandler/UnaryPlusHandler.php b/src/Analyser/ExprHandler/UnaryPlusHandler.php index 3f2c6bcbbf9..e2450452e31 100644 --- a/src/Analyser/ExprHandler/UnaryPlusHandler.php +++ b/src/Analyser/ExprHandler/UnaryPlusHandler.php @@ -45,6 +45,7 @@ public function processExpr(NodeScopeResolver $nodeScopeResolver, Stmt $stmt, Ex return $this->expressionResultFactory->create( $exprResult->getScope(), + beforeScope: $scope, hasYield: $exprResult->hasYield(), isAlwaysTerminating: $exprResult->isAlwaysTerminating(), throwPoints: $exprResult->getThrowPoints(), diff --git a/src/Analyser/ExprHandler/VariableHandler.php b/src/Analyser/ExprHandler/VariableHandler.php index f2c6f414207..3fdbdd0544a 100644 --- a/src/Analyser/ExprHandler/VariableHandler.php +++ b/src/Analyser/ExprHandler/VariableHandler.php @@ -78,6 +78,7 @@ public function resolveType(MutatingScope $scope, Expr $expr): Type public function processExpr(NodeScopeResolver $nodeScopeResolver, Stmt $stmt, Expr $expr, MutatingScope $scope, ExpressionResultStorage $storage, callable $nodeCallback, ExpressionContext $context): ExpressionResult { + $beforeScope = $scope; $hasYield = false; $throwPoints = []; $impurePoints = []; @@ -96,6 +97,7 @@ public function processExpr(NodeScopeResolver $nodeScopeResolver, Stmt $stmt, Ex } return $this->expressionResultFactory->create( $scope, + $beforeScope, $hasYield, $isAlwaysTerminating, $throwPoints, diff --git a/src/Analyser/ExprHandler/Virtual/AlwaysRememberedExprHandler.php b/src/Analyser/ExprHandler/Virtual/AlwaysRememberedExprHandler.php index 8e26801f1e0..7a75e944ebc 100644 --- a/src/Analyser/ExprHandler/Virtual/AlwaysRememberedExprHandler.php +++ b/src/Analyser/ExprHandler/Virtual/AlwaysRememberedExprHandler.php @@ -45,12 +45,14 @@ public function processExpr( ExpressionContext $context, ): ExpressionResult { + $beforeScope = $scope; $innerExpr = $expr->getExpr(); $innerResult = $nodeScopeResolver->processExprNode($stmt, $innerExpr, $scope, $storage, $nodeCallback, $context); $scope = $innerResult->getScope(); return $this->expressionResultFactory->create( $scope, + beforeScope: $beforeScope, hasYield: $innerResult->hasYield(), isAlwaysTerminating: $innerResult->isAlwaysTerminating(), throwPoints: $innerResult->getThrowPoints(), diff --git a/src/Analyser/ExprHandler/Virtual/ExistingArrayDimFetchHandler.php b/src/Analyser/ExprHandler/Virtual/ExistingArrayDimFetchHandler.php index 071f56be856..02d15c413b8 100644 --- a/src/Analyser/ExprHandler/Virtual/ExistingArrayDimFetchHandler.php +++ b/src/Analyser/ExprHandler/Virtual/ExistingArrayDimFetchHandler.php @@ -42,6 +42,7 @@ public function processExpr(NodeScopeResolver $nodeScopeResolver, Stmt $stmt, Ex return $this->expressionResultFactory->create( $scope, + beforeScope: $scope, hasYield: false, isAlwaysTerminating: false, throwPoints: [], diff --git a/src/Analyser/ExprHandler/Virtual/FunctionCallableNodeHandler.php b/src/Analyser/ExprHandler/Virtual/FunctionCallableNodeHandler.php index 4b91658cc82..8f3aaf827c2 100644 --- a/src/Analyser/ExprHandler/Virtual/FunctionCallableNodeHandler.php +++ b/src/Analyser/ExprHandler/Virtual/FunctionCallableNodeHandler.php @@ -38,6 +38,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 { + $beforeScope = $scope; $throwPoints = []; $impurePoints = []; $hasYield = false; @@ -53,6 +54,7 @@ public function processExpr(NodeScopeResolver $nodeScopeResolver, Stmt $stmt, Ex return $this->expressionResultFactory->create( $scope, + beforeScope: $beforeScope, hasYield: $hasYield, isAlwaysTerminating: $isAlwaysTerminating, throwPoints: $throwPoints, diff --git a/src/Analyser/ExprHandler/Virtual/GetIterableKeyTypeExprHandler.php b/src/Analyser/ExprHandler/Virtual/GetIterableKeyTypeExprHandler.php index 241122ccf84..4d5f0e54a82 100644 --- a/src/Analyser/ExprHandler/Virtual/GetIterableKeyTypeExprHandler.php +++ b/src/Analyser/ExprHandler/Virtual/GetIterableKeyTypeExprHandler.php @@ -42,6 +42,7 @@ public function processExpr(NodeScopeResolver $nodeScopeResolver, Stmt $stmt, Ex return $this->expressionResultFactory->create( $scope, + beforeScope: $scope, hasYield: false, isAlwaysTerminating: false, throwPoints: [], diff --git a/src/Analyser/ExprHandler/Virtual/GetIterableValueTypeExprHandler.php b/src/Analyser/ExprHandler/Virtual/GetIterableValueTypeExprHandler.php index 040732cfac3..5b5b59ee994 100644 --- a/src/Analyser/ExprHandler/Virtual/GetIterableValueTypeExprHandler.php +++ b/src/Analyser/ExprHandler/Virtual/GetIterableValueTypeExprHandler.php @@ -42,6 +42,7 @@ public function processExpr(NodeScopeResolver $nodeScopeResolver, Stmt $stmt, Ex return $this->expressionResultFactory->create( $scope, + beforeScope: $scope, hasYield: false, isAlwaysTerminating: false, throwPoints: [], diff --git a/src/Analyser/ExprHandler/Virtual/GetOffsetValueTypeExprHandler.php b/src/Analyser/ExprHandler/Virtual/GetOffsetValueTypeExprHandler.php index 539ecc7a3b1..03f846910c4 100644 --- a/src/Analyser/ExprHandler/Virtual/GetOffsetValueTypeExprHandler.php +++ b/src/Analyser/ExprHandler/Virtual/GetOffsetValueTypeExprHandler.php @@ -42,6 +42,7 @@ public function processExpr(NodeScopeResolver $nodeScopeResolver, Stmt $stmt, Ex return $this->expressionResultFactory->create( $scope, + beforeScope: $scope, hasYield: false, isAlwaysTerminating: false, throwPoints: [], diff --git a/src/Analyser/ExprHandler/Virtual/InstantiationCallableNodeHandler.php b/src/Analyser/ExprHandler/Virtual/InstantiationCallableNodeHandler.php index 3ce468022a9..dddc6e216c9 100644 --- a/src/Analyser/ExprHandler/Virtual/InstantiationCallableNodeHandler.php +++ b/src/Analyser/ExprHandler/Virtual/InstantiationCallableNodeHandler.php @@ -38,6 +38,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 { + $beforeScope = $scope; $throwPoints = []; $impurePoints = []; $hasYield = false; @@ -53,6 +54,7 @@ public function processExpr(NodeScopeResolver $nodeScopeResolver, Stmt $stmt, Ex return $this->expressionResultFactory->create( $scope, + beforeScope: $beforeScope, hasYield: $hasYield, isAlwaysTerminating: $isAlwaysTerminating, throwPoints: $throwPoints, diff --git a/src/Analyser/ExprHandler/Virtual/MethodCallableNodeHandler.php b/src/Analyser/ExprHandler/Virtual/MethodCallableNodeHandler.php index c94173c354f..39b370ed581 100644 --- a/src/Analyser/ExprHandler/Virtual/MethodCallableNodeHandler.php +++ b/src/Analyser/ExprHandler/Virtual/MethodCallableNodeHandler.php @@ -39,6 +39,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 { + $beforeScope = $scope; $varResult = $nodeScopeResolver->processExprNode($stmt, $expr->getVar(), $scope, $storage, $nodeCallback, ExpressionContext::createDeep()); $scope = $varResult->getScope(); $hasYield = $varResult->hasYield(); @@ -56,6 +57,7 @@ public function processExpr(NodeScopeResolver $nodeScopeResolver, Stmt $stmt, Ex return $this->expressionResultFactory->create( $scope, + beforeScope: $beforeScope, hasYield: $hasYield, isAlwaysTerminating: $isAlwaysTerminating, throwPoints: $throwPoints, diff --git a/src/Analyser/ExprHandler/Virtual/NativeTypeExprHandler.php b/src/Analyser/ExprHandler/Virtual/NativeTypeExprHandler.php index e916eb9ec69..0b7999acb50 100644 --- a/src/Analyser/ExprHandler/Virtual/NativeTypeExprHandler.php +++ b/src/Analyser/ExprHandler/Virtual/NativeTypeExprHandler.php @@ -42,6 +42,7 @@ public function processExpr(NodeScopeResolver $nodeScopeResolver, Stmt $stmt, Ex return $this->expressionResultFactory->create( $scope, + beforeScope: $scope, hasYield: false, isAlwaysTerminating: false, throwPoints: [], diff --git a/src/Analyser/ExprHandler/Virtual/OriginalPropertyTypeExprHandler.php b/src/Analyser/ExprHandler/Virtual/OriginalPropertyTypeExprHandler.php index ea08496e209..9f36aee736c 100644 --- a/src/Analyser/ExprHandler/Virtual/OriginalPropertyTypeExprHandler.php +++ b/src/Analyser/ExprHandler/Virtual/OriginalPropertyTypeExprHandler.php @@ -47,6 +47,7 @@ public function processExpr(NodeScopeResolver $nodeScopeResolver, Stmt $stmt, Ex return $this->expressionResultFactory->create( $scope, + beforeScope: $scope, hasYield: false, isAlwaysTerminating: false, throwPoints: [], diff --git a/src/Analyser/ExprHandler/Virtual/SetExistingOffsetValueTypeExprHandler.php b/src/Analyser/ExprHandler/Virtual/SetExistingOffsetValueTypeExprHandler.php index 5c986631ba1..7c7f2847d29 100644 --- a/src/Analyser/ExprHandler/Virtual/SetExistingOffsetValueTypeExprHandler.php +++ b/src/Analyser/ExprHandler/Virtual/SetExistingOffsetValueTypeExprHandler.php @@ -44,6 +44,7 @@ public function processExpr(NodeScopeResolver $nodeScopeResolver, Stmt $stmt, Ex return $this->expressionResultFactory->create( $scope, + beforeScope: $scope, hasYield: false, isAlwaysTerminating: false, throwPoints: [], diff --git a/src/Analyser/ExprHandler/Virtual/SetOffsetValueTypeExprHandler.php b/src/Analyser/ExprHandler/Virtual/SetOffsetValueTypeExprHandler.php index a8ffc98a0bb..e604dece482 100644 --- a/src/Analyser/ExprHandler/Virtual/SetOffsetValueTypeExprHandler.php +++ b/src/Analyser/ExprHandler/Virtual/SetOffsetValueTypeExprHandler.php @@ -44,6 +44,7 @@ public function processExpr(NodeScopeResolver $nodeScopeResolver, Stmt $stmt, Ex return $this->expressionResultFactory->create( $scope, + beforeScope: $scope, hasYield: false, isAlwaysTerminating: false, throwPoints: [], diff --git a/src/Analyser/ExprHandler/Virtual/StaticMethodCallableNodeHandler.php b/src/Analyser/ExprHandler/Virtual/StaticMethodCallableNodeHandler.php index 0eef43563b0..34e7dbf25b7 100644 --- a/src/Analyser/ExprHandler/Virtual/StaticMethodCallableNodeHandler.php +++ b/src/Analyser/ExprHandler/Virtual/StaticMethodCallableNodeHandler.php @@ -39,6 +39,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 { + $beforeScope = $scope; $throwPoints = []; $impurePoints = []; $hasYield = false; @@ -62,6 +63,7 @@ public function processExpr(NodeScopeResolver $nodeScopeResolver, Stmt $stmt, Ex return $this->expressionResultFactory->create( $scope, + beforeScope: $beforeScope, hasYield: $hasYield, isAlwaysTerminating: $isAlwaysTerminating, throwPoints: $throwPoints, diff --git a/src/Analyser/ExprHandler/Virtual/TypeExprHandler.php b/src/Analyser/ExprHandler/Virtual/TypeExprHandler.php index 53766b68323..26b05213a50 100644 --- a/src/Analyser/ExprHandler/Virtual/TypeExprHandler.php +++ b/src/Analyser/ExprHandler/Virtual/TypeExprHandler.php @@ -42,6 +42,7 @@ public function processExpr(NodeScopeResolver $nodeScopeResolver, Stmt $stmt, Ex return $this->expressionResultFactory->create( $scope, + beforeScope: $scope, hasYield: false, isAlwaysTerminating: false, throwPoints: [], diff --git a/src/Analyser/ExprHandler/Virtual/UnsetOffsetExprHandler.php b/src/Analyser/ExprHandler/Virtual/UnsetOffsetExprHandler.php index 995219a4f05..1246378b936 100644 --- a/src/Analyser/ExprHandler/Virtual/UnsetOffsetExprHandler.php +++ b/src/Analyser/ExprHandler/Virtual/UnsetOffsetExprHandler.php @@ -42,6 +42,7 @@ public function processExpr(NodeScopeResolver $nodeScopeResolver, Stmt $stmt, Ex return $this->expressionResultFactory->create( $scope, + beforeScope: $scope, hasYield: false, isAlwaysTerminating: false, throwPoints: [], diff --git a/src/Analyser/ExprHandler/YieldFromHandler.php b/src/Analyser/ExprHandler/YieldFromHandler.php index 9e148f388f7..089ce51b630 100644 --- a/src/Analyser/ExprHandler/YieldFromHandler.php +++ b/src/Analyser/ExprHandler/YieldFromHandler.php @@ -54,11 +54,13 @@ public function resolveType(MutatingScope $scope, Expr $expr): Type public function processExpr(NodeScopeResolver $nodeScopeResolver, Stmt $stmt, Expr $expr, MutatingScope $scope, ExpressionResultStorage $storage, callable $nodeCallback, ExpressionContext $context): ExpressionResult { + $beforeScope = $scope; $exprResult = $nodeScopeResolver->processExprNode($stmt, $expr->expr, $scope, $storage, $nodeCallback, $context->enterDeep()); $scope = $exprResult->getScope(); return $this->expressionResultFactory->create( $scope, + beforeScope: $beforeScope, hasYield: true, isAlwaysTerminating: $exprResult->isAlwaysTerminating(), throwPoints: array_merge($exprResult->getThrowPoints(), [InternalThrowPoint::createImplicit($scope, $expr)]), diff --git a/src/Analyser/ExprHandler/YieldHandler.php b/src/Analyser/ExprHandler/YieldHandler.php index 583907a17b6..76356bfbf7b 100644 --- a/src/Analyser/ExprHandler/YieldHandler.php +++ b/src/Analyser/ExprHandler/YieldHandler.php @@ -59,6 +59,7 @@ public function resolveType(MutatingScope $scope, Expr $expr): Type public function processExpr(NodeScopeResolver $nodeScopeResolver, Stmt $stmt, Expr $expr, MutatingScope $scope, ExpressionResultStorage $storage, callable $nodeCallback, ExpressionContext $context): ExpressionResult { + $beforeScope = $scope; $throwPoints = [ InternalThrowPoint::createImplicit($scope, $expr), ]; @@ -89,6 +90,7 @@ public function processExpr(NodeScopeResolver $nodeScopeResolver, Stmt $stmt, Ex return $this->expressionResultFactory->create( $scope, + beforeScope: $beforeScope, hasYield: true, isAlwaysTerminating: $isAlwaysTerminating, throwPoints: $throwPoints, diff --git a/src/Analyser/ExpressionResult.php b/src/Analyser/ExpressionResult.php index 42e49e004aa..c75c3b3ac33 100644 --- a/src/Analyser/ExpressionResult.php +++ b/src/Analyser/ExpressionResult.php @@ -26,6 +26,7 @@ final class ExpressionResult */ public function __construct( private MutatingScope $scope, + private MutatingScope $beforeScope, private bool $hasYield, private bool $isAlwaysTerminating, private array $throwPoints, @@ -43,6 +44,11 @@ public function getScope(): MutatingScope return $this->scope; } + public function getBeforeScope(): MutatingScope + { + return $this->beforeScope; + } + public function hasYield(): bool { return $this->hasYield; diff --git a/src/Analyser/ExpressionResultFactory.php b/src/Analyser/ExpressionResultFactory.php index f724e6fdcb4..e065862b20a 100644 --- a/src/Analyser/ExpressionResultFactory.php +++ b/src/Analyser/ExpressionResultFactory.php @@ -13,6 +13,7 @@ interface ExpressionResultFactory */ public function create( MutatingScope $scope, + MutatingScope $beforeScope, bool $hasYield, bool $isAlwaysTerminating, array $throwPoints, diff --git a/src/Analyser/NodeScopeResolver.php b/src/Analyser/NodeScopeResolver.php index dc019d75472..011abbbac38 100644 --- a/src/Analyser/NodeScopeResolver.php +++ b/src/Analyser/NodeScopeResolver.php @@ -2780,11 +2780,12 @@ public function processExprNode( if ($expr instanceof List_) { // only in assign and foreach, processed elsewhere - return $this->expressionResultFactory->create($scope, hasYield: false, isAlwaysTerminating: false, throwPoints: [], impurePoints: []); + return $this->expressionResultFactory->create($scope, beforeScope: $scope, hasYield: false, isAlwaysTerminating: false, throwPoints: [], impurePoints: []); } return $this->expressionResultFactory->create( $scope, + beforeScope: $scope, hasYield: false, isAlwaysTerminating: false, throwPoints: [], @@ -3151,7 +3152,7 @@ public function processArrowFunctionNode( $this->callNodeCallback($nodeCallback, new InArrowFunctionNode($arrowFunctionType, $expr), $arrowFunctionScope, $storage); $exprResult = $this->processExprNode($stmt, $expr->expr, $arrowFunctionScope, $storage, $nodeCallback, ExpressionContext::createTopLevel()); - return $this->expressionResultFactory->create($scope, false, $exprResult->isAlwaysTerminating(), $exprResult->getThrowPoints(), $exprResult->getImpurePoints()); + return $this->expressionResultFactory->create($scope, beforeScope: $scope, hasYield: false, isAlwaysTerminating: $exprResult->isAlwaysTerminating(), throwPoints: $exprResult->getThrowPoints(), impurePoints: $exprResult->getImpurePoints()); } /** @@ -3854,7 +3855,7 @@ public function processArgs( } // not storing this, it's scope after processing all args - return $this->expressionResultFactory->create($scope, $hasYield, $isAlwaysTerminating, $throwPoints, $impurePoints); + return $this->expressionResultFactory->create($scope, $scope, $hasYield, $isAlwaysTerminating, $throwPoints, $impurePoints); } /** @@ -3991,7 +3992,7 @@ public function processVirtualAssign(MutatingScope $scope, ExpressionResultStora $assignedExpr, new VirtualAssignNodeCallback($nodeCallback), ExpressionContext::createDeep(), - fn (MutatingScope $scope): ExpressionResult => $this->expressionResultFactory->create($scope, hasYield: false, isAlwaysTerminating: false, throwPoints: [], impurePoints: []), + fn (MutatingScope $scope): ExpressionResult => $this->expressionResultFactory->create($scope, beforeScope: $scope, hasYield: false, isAlwaysTerminating: false, throwPoints: [], impurePoints: []), false, ); } From 59996fe24828e4781e624274799320d51805980f Mon Sep 17 00:00:00 2001 From: Ondrej Mirtes Date: Thu, 11 Jun 2026 12:36:11 +0200 Subject: [PATCH 003/113] ExpressionResult - add Expr --- .../ExprHandler/ArrayDimFetchHandler.php | 6 +-- src/Analyser/ExprHandler/ArrayHandler.php | 1 + .../ExprHandler/ArrowFunctionHandler.php | 1 + src/Analyser/ExprHandler/AssignHandler.php | 12 +++--- src/Analyser/ExprHandler/AssignOpHandler.php | 4 +- src/Analyser/ExprHandler/BinaryOpHandler.php | 3 +- .../ExprHandler/BitwiseNotHandler.php | 1 + .../ExprHandler/BooleanAndHandler.php | 1 + .../ExprHandler/BooleanNotHandler.php | 3 +- src/Analyser/ExprHandler/BooleanOrHandler.php | 1 + src/Analyser/ExprHandler/CastHandler.php | 3 +- .../ExprHandler/CastStringHandler.php | 3 +- .../ExprHandler/ClassConstFetchHandler.php | 3 +- src/Analyser/ExprHandler/CloneHandler.php | 1 + src/Analyser/ExprHandler/ClosureHandler.php | 1 + src/Analyser/ExprHandler/CoalesceHandler.php | 3 +- .../ExprHandler/ConstFetchHandler.php | 3 +- src/Analyser/ExprHandler/EmptyHandler.php | 3 +- .../ExprHandler/ErrorSuppressHandler.php | 1 + src/Analyser/ExprHandler/EvalHandler.php | 1 + src/Analyser/ExprHandler/ExitHandler.php | 1 + src/Analyser/ExprHandler/FuncCallHandler.php | 3 +- .../Helper/ImplicitToStringCallHelper.php | 2 + src/Analyser/ExprHandler/IncludeHandler.php | 1 + .../ExprHandler/InstanceofHandler.php | 3 +- .../ExprHandler/InterpolatedStringHandler.php | 1 + src/Analyser/ExprHandler/IssetHandler.php | 3 +- src/Analyser/ExprHandler/MatchHandler.php | 1 + .../ExprHandler/MethodCallHandler.php | 6 +-- src/Analyser/ExprHandler/NewHandler.php | 1 + .../ExprHandler/NullsafeMethodCallHandler.php | 3 +- .../NullsafePropertyFetchHandler.php | 3 +- src/Analyser/ExprHandler/PipeHandler.php | 1 + src/Analyser/ExprHandler/PostDecHandler.php | 1 + src/Analyser/ExprHandler/PostIncHandler.php | 1 + src/Analyser/ExprHandler/PreDecHandler.php | 1 + src/Analyser/ExprHandler/PreIncHandler.php | 1 + src/Analyser/ExprHandler/PrintHandler.php | 1 + .../ExprHandler/PropertyFetchHandler.php | 3 +- src/Analyser/ExprHandler/ScalarHandler.php | 1 + .../ExprHandler/StaticCallHandler.php | 3 +- .../StaticPropertyFetchHandler.php | 3 +- src/Analyser/ExprHandler/TernaryHandler.php | 3 +- src/Analyser/ExprHandler/ThrowHandler.php | 1 + .../ExprHandler/UnaryMinusHandler.php | 1 + src/Analyser/ExprHandler/UnaryPlusHandler.php | 1 + src/Analyser/ExprHandler/VariableHandler.php | 1 + .../Virtual/AlwaysRememberedExprHandler.php | 3 +- .../Virtual/ExistingArrayDimFetchHandler.php | 1 + .../Virtual/FunctionCallableNodeHandler.php | 1 + .../Virtual/GetIterableKeyTypeExprHandler.php | 1 + .../GetIterableValueTypeExprHandler.php | 1 + .../Virtual/GetOffsetValueTypeExprHandler.php | 1 + .../InstantiationCallableNodeHandler.php | 1 + .../Virtual/MethodCallableNodeHandler.php | 1 + .../Virtual/NativeTypeExprHandler.php | 1 + .../OriginalPropertyTypeExprHandler.php | 1 + .../SetExistingOffsetValueTypeExprHandler.php | 1 + .../Virtual/SetOffsetValueTypeExprHandler.php | 1 + .../StaticMethodCallableNodeHandler.php | 1 + .../ExprHandler/Virtual/TypeExprHandler.php | 1 + .../Virtual/UnsetOffsetExprHandler.php | 1 + src/Analyser/ExprHandler/YieldFromHandler.php | 1 + src/Analyser/ExprHandler/YieldHandler.php | 1 + src/Analyser/ExpressionResult.php | 40 +++++++++++-------- src/Analyser/ExpressionResultFactory.php | 3 ++ src/Analyser/NodeScopeResolver.php | 15 ++++--- 67 files changed, 106 insertions(+), 77 deletions(-) diff --git a/src/Analyser/ExprHandler/ArrayDimFetchHandler.php b/src/Analyser/ExprHandler/ArrayDimFetchHandler.php index 59ea71f1e31..0a0198cf3c2 100644 --- a/src/Analyser/ExprHandler/ArrayDimFetchHandler.php +++ b/src/Analyser/ExprHandler/ArrayDimFetchHandler.php @@ -89,12 +89,11 @@ public function processExpr(NodeScopeResolver $nodeScopeResolver, Stmt $stmt, Ex return $this->expressionResultFactory->create( $scope, beforeScope: $beforeScope, + expr: $expr, hasYield: $varResult->hasYield(), isAlwaysTerminating: $varResult->isAlwaysTerminating(), throwPoints: $varResult->getThrowPoints(), impurePoints: $varResult->getImpurePoints(), - truthyScopeCallback: static fn (): MutatingScope => $scope->filterByTruthyValue($expr), - falseyScopeCallback: static fn (): MutatingScope => $scope->filterByFalseyValue($expr), ); } @@ -119,12 +118,11 @@ public function processExpr(NodeScopeResolver $nodeScopeResolver, Stmt $stmt, Ex return $this->expressionResultFactory->create( $scope, beforeScope: $beforeScope, + expr: $expr, hasYield: $dimResult->hasYield() || $varResult->hasYield(), isAlwaysTerminating: $dimResult->isAlwaysTerminating() || $varResult->isAlwaysTerminating(), throwPoints: $throwPoints, impurePoints: $impurePoints, - truthyScopeCallback: static fn (): MutatingScope => $scope->filterByTruthyValue($expr), - falseyScopeCallback: static fn (): MutatingScope => $scope->filterByFalseyValue($expr), ); } diff --git a/src/Analyser/ExprHandler/ArrayHandler.php b/src/Analyser/ExprHandler/ArrayHandler.php index 78eb2b5491d..a8e58b932a4 100644 --- a/src/Analyser/ExprHandler/ArrayHandler.php +++ b/src/Analyser/ExprHandler/ArrayHandler.php @@ -104,6 +104,7 @@ public function processExpr(NodeScopeResolver $nodeScopeResolver, Stmt $stmt, Ex return $this->expressionResultFactory->create( $scope, beforeScope: $beforeScope, + expr: $expr, hasYield: $hasYield, isAlwaysTerminating: $isAlwaysTerminating, throwPoints: $throwPoints, diff --git a/src/Analyser/ExprHandler/ArrowFunctionHandler.php b/src/Analyser/ExprHandler/ArrowFunctionHandler.php index b98fc9c85de..5390ee8e212 100644 --- a/src/Analyser/ExprHandler/ArrowFunctionHandler.php +++ b/src/Analyser/ExprHandler/ArrowFunctionHandler.php @@ -46,6 +46,7 @@ public function processExpr(NodeScopeResolver $nodeScopeResolver, Stmt $stmt, Ex return $this->expressionResultFactory->create( $result->getScope(), beforeScope: $scope, + expr: $expr, hasYield: $result->hasYield(), isAlwaysTerminating: false, throwPoints: [], diff --git a/src/Analyser/ExprHandler/AssignHandler.php b/src/Analyser/ExprHandler/AssignHandler.php index a9248dc6d3a..70caac5134b 100644 --- a/src/Analyser/ExprHandler/AssignHandler.php +++ b/src/Analyser/ExprHandler/AssignHandler.php @@ -342,7 +342,7 @@ function (MutatingScope $scope) use ($stmt, $expr, $nodeCallback, $context, $sto $scope = $scope->exitExpressionAssign($expr->expr); } - return $this->expressionResultFactory->create($scope, $beforeScope, $hasYield, $isAlwaysTerminating, $throwPoints, $impurePoints); + return $this->expressionResultFactory->create($scope, $beforeScope, $expr->expr, $hasYield, $isAlwaysTerminating, $throwPoints, $impurePoints); }, true, ); @@ -387,12 +387,11 @@ function (MutatingScope $scope) use ($stmt, $expr, $nodeCallback, $context, $sto return $this->expressionResultFactory->create( $scope, beforeScope: $beforeScope, + expr: $expr, hasYield: $result->hasYield(), isAlwaysTerminating: $result->isAlwaysTerminating(), throwPoints: $result->getThrowPoints(), impurePoints: $result->getImpurePoints(), - truthyScopeCallback: static fn (): MutatingScope => $scope->filterByTruthyValue($expr), - falseyScopeCallback: static fn (): MutatingScope => $scope->filterByFalseyValue($expr), ); } @@ -935,16 +934,17 @@ public function processAssignVar( } else { $dimExpr = $arrayItem->key; } + $getOffsetValueTypeExpr = new GetOffsetValueTypeExpr($assignedExpr, $dimExpr); $result = $this->processAssignVar( $nodeScopeResolver, $scope, $storage, $stmt, $arrayItem->value, - new GetOffsetValueTypeExpr($assignedExpr, $dimExpr), + $getOffsetValueTypeExpr, $nodeCallback, $context, - fn (MutatingScope $scope): ExpressionResult => $this->expressionResultFactory->create($scope, beforeScope: $scope, hasYield: false, isAlwaysTerminating: false, throwPoints: [], impurePoints: []), + fn (MutatingScope $scope): ExpressionResult => $this->expressionResultFactory->create($scope, beforeScope: $scope, expr: $getOffsetValueTypeExpr, hasYield: false, isAlwaysTerminating: false, throwPoints: [], impurePoints: []), $enterExpressionAssign, ); $scope = $result->getScope(); @@ -1036,7 +1036,7 @@ public function processAssignVar( } // stored where processAssignVar is called - return $this->expressionResultFactory->create($scope, $beforeScope, $hasYield, $isAlwaysTerminating, $throwPoints, $impurePoints); + return $this->expressionResultFactory->create($scope, $beforeScope, $var, $hasYield, $isAlwaysTerminating, $throwPoints, $impurePoints); } private function createArrayDimFetchConditionalExpressionHolder( diff --git a/src/Analyser/ExprHandler/AssignOpHandler.php b/src/Analyser/ExprHandler/AssignOpHandler.php index 5d0d4c8f842..ff70d04dbe5 100644 --- a/src/Analyser/ExprHandler/AssignOpHandler.php +++ b/src/Analyser/ExprHandler/AssignOpHandler.php @@ -80,6 +80,7 @@ function (MutatingScope $scope) use ($stmt, $expr, $nodeCallback, $context, $sto return $this->expressionResultFactory->create( $exprResult->getScope()->mergeWith($originalScope), $originalScope, + $expr->expr, $exprResult->hasYield(), $isAlwaysTerminating, $exprResult->getThrowPoints(), @@ -112,12 +113,11 @@ function (MutatingScope $scope) use ($stmt, $expr, $nodeCallback, $context, $sto return $this->expressionResultFactory->create( $scope, beforeScope: $beforeScope, + expr: $expr, hasYield: $assignResult->hasYield(), isAlwaysTerminating: $assignResult->isAlwaysTerminating(), throwPoints: $throwPoints, impurePoints: $impurePoints, - truthyScopeCallback: static fn (): MutatingScope => $scope->filterByTruthyValue($expr), - falseyScopeCallback: static fn (): MutatingScope => $scope->filterByFalseyValue($expr), ); } diff --git a/src/Analyser/ExprHandler/BinaryOpHandler.php b/src/Analyser/ExprHandler/BinaryOpHandler.php index 70d7841012a..c3d686ba134 100644 --- a/src/Analyser/ExprHandler/BinaryOpHandler.php +++ b/src/Analyser/ExprHandler/BinaryOpHandler.php @@ -107,12 +107,11 @@ public function processExpr(NodeScopeResolver $nodeScopeResolver, Stmt $stmt, Ex return $this->expressionResultFactory->create( $scope, beforeScope: $beforeScope, + expr: $expr, hasYield: $leftResult->hasYield() || $rightResult->hasYield(), isAlwaysTerminating: $leftResult->isAlwaysTerminating() || $rightResult->isAlwaysTerminating(), throwPoints: $throwPoints, impurePoints: $impurePoints, - truthyScopeCallback: static fn (): MutatingScope => $scope->filterByTruthyValue($expr), - falseyScopeCallback: static fn (): MutatingScope => $scope->filterByFalseyValue($expr), ); } diff --git a/src/Analyser/ExprHandler/BitwiseNotHandler.php b/src/Analyser/ExprHandler/BitwiseNotHandler.php index ae9ec208bd5..4b6b6667823 100644 --- a/src/Analyser/ExprHandler/BitwiseNotHandler.php +++ b/src/Analyser/ExprHandler/BitwiseNotHandler.php @@ -46,6 +46,7 @@ public function processExpr(NodeScopeResolver $nodeScopeResolver, Stmt $stmt, Ex return $this->expressionResultFactory->create( $exprResult->getScope(), beforeScope: $scope, + expr: $expr, hasYield: $exprResult->hasYield(), isAlwaysTerminating: $exprResult->isAlwaysTerminating(), throwPoints: $exprResult->getThrowPoints(), diff --git a/src/Analyser/ExprHandler/BooleanAndHandler.php b/src/Analyser/ExprHandler/BooleanAndHandler.php index d96b8616595..5263207b8cb 100644 --- a/src/Analyser/ExprHandler/BooleanAndHandler.php +++ b/src/Analyser/ExprHandler/BooleanAndHandler.php @@ -276,6 +276,7 @@ public function processExpr(NodeScopeResolver $nodeScopeResolver, Stmt $stmt, Ex return $this->expressionResultFactory->create( $leftMergedWithRightScope, beforeScope: $scope, + expr: $expr, hasYield: $leftResult->hasYield() || $rightResult->hasYield(), isAlwaysTerminating: $leftResult->isAlwaysTerminating(), throwPoints: array_merge($leftResult->getThrowPoints(), $rightResult->getThrowPoints()), diff --git a/src/Analyser/ExprHandler/BooleanNotHandler.php b/src/Analyser/ExprHandler/BooleanNotHandler.php index 08091ce1dc0..59cb5a986c1 100644 --- a/src/Analyser/ExprHandler/BooleanNotHandler.php +++ b/src/Analyser/ExprHandler/BooleanNotHandler.php @@ -46,12 +46,11 @@ public function processExpr(NodeScopeResolver $nodeScopeResolver, Stmt $stmt, Ex return $this->expressionResultFactory->create( $scope, beforeScope: $beforeScope, + expr: $expr, hasYield: $exprResult->hasYield(), isAlwaysTerminating: $exprResult->isAlwaysTerminating(), throwPoints: $exprResult->getThrowPoints(), impurePoints: $exprResult->getImpurePoints(), - truthyScopeCallback: static fn (): MutatingScope => $scope->filterByTruthyValue($expr), - falseyScopeCallback: static fn (): MutatingScope => $scope->filterByFalseyValue($expr), ); } diff --git a/src/Analyser/ExprHandler/BooleanOrHandler.php b/src/Analyser/ExprHandler/BooleanOrHandler.php index 46e8bc28631..9b1d34d1cc2 100644 --- a/src/Analyser/ExprHandler/BooleanOrHandler.php +++ b/src/Analyser/ExprHandler/BooleanOrHandler.php @@ -308,6 +308,7 @@ public function processExpr(NodeScopeResolver $nodeScopeResolver, Stmt $stmt, Ex return $this->expressionResultFactory->create( $leftMergedWithRightScope, beforeScope: $scope, + expr: $expr, hasYield: $leftResult->hasYield() || $rightResult->hasYield(), isAlwaysTerminating: $leftResult->isAlwaysTerminating(), throwPoints: array_merge($leftResult->getThrowPoints(), $rightResult->getThrowPoints()), diff --git a/src/Analyser/ExprHandler/CastHandler.php b/src/Analyser/ExprHandler/CastHandler.php index 99085f9b800..6877fdd5b3e 100644 --- a/src/Analyser/ExprHandler/CastHandler.php +++ b/src/Analyser/ExprHandler/CastHandler.php @@ -55,12 +55,11 @@ public function processExpr(NodeScopeResolver $nodeScopeResolver, Stmt $stmt, Ex return $this->expressionResultFactory->create( $scope, beforeScope: $beforeScope, + expr: $expr, hasYield: $exprResult->hasYield(), isAlwaysTerminating: $exprResult->isAlwaysTerminating(), throwPoints: $exprResult->getThrowPoints(), impurePoints: $exprResult->getImpurePoints(), - truthyScopeCallback: static fn (): MutatingScope => $scope->filterByTruthyValue($expr), - falseyScopeCallback: static fn (): MutatingScope => $scope->filterByFalseyValue($expr), ); } diff --git a/src/Analyser/ExprHandler/CastStringHandler.php b/src/Analyser/ExprHandler/CastStringHandler.php index 32f31ec60a1..bb13a0e2817 100644 --- a/src/Analyser/ExprHandler/CastStringHandler.php +++ b/src/Analyser/ExprHandler/CastStringHandler.php @@ -60,12 +60,11 @@ public function processExpr(NodeScopeResolver $nodeScopeResolver, Stmt $stmt, Ex return $this->expressionResultFactory->create( $scope, beforeScope: $beforeScope, + expr: $expr, hasYield: $exprResult->hasYield(), isAlwaysTerminating: $exprResult->isAlwaysTerminating(), throwPoints: $throwPoints, impurePoints: $impurePoints, - truthyScopeCallback: static fn (): MutatingScope => $scope->filterByTruthyValue($expr), - falseyScopeCallback: static fn (): MutatingScope => $scope->filterByFalseyValue($expr), ); } diff --git a/src/Analyser/ExprHandler/ClassConstFetchHandler.php b/src/Analyser/ExprHandler/ClassConstFetchHandler.php index 3283d8f4153..fe989f332c9 100644 --- a/src/Analyser/ExprHandler/ClassConstFetchHandler.php +++ b/src/Analyser/ExprHandler/ClassConstFetchHandler.php @@ -89,12 +89,11 @@ public function processExpr(NodeScopeResolver $nodeScopeResolver, Stmt $stmt, Ex return $this->expressionResultFactory->create( $scope, beforeScope: $beforeScope, + expr: $expr, hasYield: $hasYield, isAlwaysTerminating: $isAlwaysTerminating, throwPoints: $throwPoints, impurePoints: $impurePoints, - truthyScopeCallback: static fn (): MutatingScope => $scope->filterByTruthyValue($expr), - falseyScopeCallback: static fn (): MutatingScope => $scope->filterByFalseyValue($expr), ); } diff --git a/src/Analyser/ExprHandler/CloneHandler.php b/src/Analyser/ExprHandler/CloneHandler.php index ee1951e68d5..bc707347411 100644 --- a/src/Analyser/ExprHandler/CloneHandler.php +++ b/src/Analyser/ExprHandler/CloneHandler.php @@ -46,6 +46,7 @@ public function processExpr(NodeScopeResolver $nodeScopeResolver, Stmt $stmt, Ex return $this->expressionResultFactory->create( $exprResult->getScope(), beforeScope: $scope, + expr: $expr, hasYield: $exprResult->hasYield(), isAlwaysTerminating: $exprResult->isAlwaysTerminating(), throwPoints: $exprResult->getThrowPoints(), diff --git a/src/Analyser/ExprHandler/ClosureHandler.php b/src/Analyser/ExprHandler/ClosureHandler.php index e0e742e812d..b107c5843c6 100644 --- a/src/Analyser/ExprHandler/ClosureHandler.php +++ b/src/Analyser/ExprHandler/ClosureHandler.php @@ -46,6 +46,7 @@ public function processExpr(NodeScopeResolver $nodeScopeResolver, Stmt $stmt, Ex return $this->expressionResultFactory->create( $processClosureResult->applyByRefUseScope($processClosureResult->getScope()), beforeScope: $scope, + expr: $expr, hasYield: false, isAlwaysTerminating: false, throwPoints: [], diff --git a/src/Analyser/ExprHandler/CoalesceHandler.php b/src/Analyser/ExprHandler/CoalesceHandler.php index 562a4f9075c..f47554f2811 100644 --- a/src/Analyser/ExprHandler/CoalesceHandler.php +++ b/src/Analyser/ExprHandler/CoalesceHandler.php @@ -135,12 +135,11 @@ public function processExpr(NodeScopeResolver $nodeScopeResolver, Stmt $stmt, Ex return $this->expressionResultFactory->create( $scope, beforeScope: $beforeScope, + expr: $expr, hasYield: $condResult->hasYield() || $rightResult->hasYield(), isAlwaysTerminating: $condResult->isAlwaysTerminating(), throwPoints: array_merge($condResult->getThrowPoints(), $rightResult->getThrowPoints()), impurePoints: array_merge($condResult->getImpurePoints(), $rightResult->getImpurePoints()), - truthyScopeCallback: static fn (): MutatingScope => $scope->filterByTruthyValue($expr), - falseyScopeCallback: static fn (): MutatingScope => $scope->filterByFalseyValue($expr), ); } diff --git a/src/Analyser/ExprHandler/ConstFetchHandler.php b/src/Analyser/ExprHandler/ConstFetchHandler.php index 23510dbbaba..17f429322e0 100644 --- a/src/Analyser/ExprHandler/ConstFetchHandler.php +++ b/src/Analyser/ExprHandler/ConstFetchHandler.php @@ -51,12 +51,11 @@ public function processExpr(NodeScopeResolver $nodeScopeResolver, Stmt $stmt, Ex return $this->expressionResultFactory->create( $scope, beforeScope: $scope, + expr: $expr, hasYield: false, isAlwaysTerminating: false, throwPoints: [], impurePoints: [], - truthyScopeCallback: static fn (): MutatingScope => $scope->filterByTruthyValue($expr), - falseyScopeCallback: static fn (): MutatingScope => $scope->filterByFalseyValue($expr), ); } diff --git a/src/Analyser/ExprHandler/EmptyHandler.php b/src/Analyser/ExprHandler/EmptyHandler.php index d5f932b2b14..54e9c397fa1 100644 --- a/src/Analyser/ExprHandler/EmptyHandler.php +++ b/src/Analyser/ExprHandler/EmptyHandler.php @@ -98,12 +98,11 @@ public function processExpr(NodeScopeResolver $nodeScopeResolver, Stmt $stmt, Ex return $this->expressionResultFactory->create( $scope, beforeScope: $beforeScope, + expr: $expr, hasYield: $exprResult->hasYield(), isAlwaysTerminating: $exprResult->isAlwaysTerminating(), throwPoints: $exprResult->getThrowPoints(), impurePoints: $exprResult->getImpurePoints(), - truthyScopeCallback: static fn (): MutatingScope => $scope->filterByTruthyValue($expr), - falseyScopeCallback: static fn (): MutatingScope => $scope->filterByFalseyValue($expr), ); } diff --git a/src/Analyser/ExprHandler/ErrorSuppressHandler.php b/src/Analyser/ExprHandler/ErrorSuppressHandler.php index 700d260261a..ca006ebcedb 100644 --- a/src/Analyser/ExprHandler/ErrorSuppressHandler.php +++ b/src/Analyser/ExprHandler/ErrorSuppressHandler.php @@ -42,6 +42,7 @@ public function processExpr(NodeScopeResolver $nodeScopeResolver, Stmt $stmt, Ex return $this->expressionResultFactory->create( $exprResult->getScope(), beforeScope: $scope, + expr: $expr, hasYield: $exprResult->hasYield(), isAlwaysTerminating: $exprResult->isAlwaysTerminating(), throwPoints: $exprResult->getThrowPoints(), diff --git a/src/Analyser/ExprHandler/EvalHandler.php b/src/Analyser/ExprHandler/EvalHandler.php index d1b7160701f..9bbbbe9fbd2 100644 --- a/src/Analyser/ExprHandler/EvalHandler.php +++ b/src/Analyser/ExprHandler/EvalHandler.php @@ -53,6 +53,7 @@ public function processExpr(NodeScopeResolver $nodeScopeResolver, Stmt $stmt, Ex return $this->expressionResultFactory->create( $scope, beforeScope: $beforeScope, + expr: $expr, hasYield: $exprResult->hasYield(), isAlwaysTerminating: $exprResult->isAlwaysTerminating(), throwPoints: array_merge($exprResult->getThrowPoints(), [InternalThrowPoint::createImplicit($scope, $expr)]), diff --git a/src/Analyser/ExprHandler/ExitHandler.php b/src/Analyser/ExprHandler/ExitHandler.php index 0cba81da42c..7c1029c14e2 100644 --- a/src/Analyser/ExprHandler/ExitHandler.php +++ b/src/Analyser/ExprHandler/ExitHandler.php @@ -60,6 +60,7 @@ public function processExpr(NodeScopeResolver $nodeScopeResolver, Stmt $stmt, Ex return $this->expressionResultFactory->create( $scope, beforeScope: $beforeScope, + expr: $expr, hasYield: $hasYield, isAlwaysTerminating: true, throwPoints: $throwPoints, diff --git a/src/Analyser/ExprHandler/FuncCallHandler.php b/src/Analyser/ExprHandler/FuncCallHandler.php index 7d985852a05..ae3d6fc94a6 100644 --- a/src/Analyser/ExprHandler/FuncCallHandler.php +++ b/src/Analyser/ExprHandler/FuncCallHandler.php @@ -576,12 +576,11 @@ public function processExpr(NodeScopeResolver $nodeScopeResolver, Stmt $stmt, Ex return $this->expressionResultFactory->create( $scope, beforeScope: $beforeScope, + expr: $expr, hasYield: $hasYield, isAlwaysTerminating: $isAlwaysTerminating, throwPoints: $throwPoints, impurePoints: $impurePoints, - truthyScopeCallback: static fn (): MutatingScope => $scope->filterByTruthyValue($expr), - falseyScopeCallback: static fn (): MutatingScope => $scope->filterByFalseyValue($expr), ); } diff --git a/src/Analyser/ExprHandler/Helper/ImplicitToStringCallHelper.php b/src/Analyser/ExprHandler/Helper/ImplicitToStringCallHelper.php index 9303995b032..58ed6c39ac6 100644 --- a/src/Analyser/ExprHandler/Helper/ImplicitToStringCallHelper.php +++ b/src/Analyser/ExprHandler/Helper/ImplicitToStringCallHelper.php @@ -40,6 +40,7 @@ public function processImplicitToStringCall(Expr $expr, MutatingScope $scope): E return $this->expressionResultFactory->create( $scope, beforeScope: $scope, + expr: $expr, hasYield: false, isAlwaysTerminating: false, throwPoints: [], @@ -73,6 +74,7 @@ public function processImplicitToStringCall(Expr $expr, MutatingScope $scope): E return $this->expressionResultFactory->create( $scope, beforeScope: $scope, + expr: $expr, hasYield: false, isAlwaysTerminating: false, throwPoints: $throwPoints, diff --git a/src/Analyser/ExprHandler/IncludeHandler.php b/src/Analyser/ExprHandler/IncludeHandler.php index 44df8e73801..2a0dd18b37f 100644 --- a/src/Analyser/ExprHandler/IncludeHandler.php +++ b/src/Analyser/ExprHandler/IncludeHandler.php @@ -55,6 +55,7 @@ public function processExpr(NodeScopeResolver $nodeScopeResolver, Stmt $stmt, Ex return $this->expressionResultFactory->create( $scope, beforeScope: $beforeScope, + expr: $expr, hasYield: $exprResult->hasYield(), isAlwaysTerminating: $exprResult->isAlwaysTerminating(), throwPoints: array_merge($exprResult->getThrowPoints(), [InternalThrowPoint::createImplicit($scope, $expr)]), diff --git a/src/Analyser/ExprHandler/InstanceofHandler.php b/src/Analyser/ExprHandler/InstanceofHandler.php index 077e965ffdc..b5288696912 100644 --- a/src/Analyser/ExprHandler/InstanceofHandler.php +++ b/src/Analyser/ExprHandler/InstanceofHandler.php @@ -69,12 +69,11 @@ public function processExpr(NodeScopeResolver $nodeScopeResolver, Stmt $stmt, Ex return $this->expressionResultFactory->create( $scope, beforeScope: $beforeScope, + expr: $expr, hasYield: $hasYield, isAlwaysTerminating: $isAlwaysTerminating, throwPoints: $throwPoints, impurePoints: $impurePoints, - truthyScopeCallback: static fn (): MutatingScope => $scope->filterByTruthyValue($expr), - falseyScopeCallback: static fn (): MutatingScope => $scope->filterByFalseyValue($expr), ); } diff --git a/src/Analyser/ExprHandler/InterpolatedStringHandler.php b/src/Analyser/ExprHandler/InterpolatedStringHandler.php index df25ce12114..cdf2bdba301 100644 --- a/src/Analyser/ExprHandler/InterpolatedStringHandler.php +++ b/src/Analyser/ExprHandler/InterpolatedStringHandler.php @@ -71,6 +71,7 @@ public function processExpr(NodeScopeResolver $nodeScopeResolver, Stmt $stmt, Ex return $this->expressionResultFactory->create( $scope, beforeScope: $beforeScope, + expr: $expr, hasYield: $hasYield, isAlwaysTerminating: $isAlwaysTerminating, throwPoints: $throwPoints, diff --git a/src/Analyser/ExprHandler/IssetHandler.php b/src/Analyser/ExprHandler/IssetHandler.php index 001ee84f127..9fc6c135a1c 100644 --- a/src/Analyser/ExprHandler/IssetHandler.php +++ b/src/Analyser/ExprHandler/IssetHandler.php @@ -391,12 +391,11 @@ public function processExpr(NodeScopeResolver $nodeScopeResolver, Stmt $stmt, Ex return $this->expressionResultFactory->create( $scope, beforeScope: $beforeScope, + expr: $expr, hasYield: $hasYield, isAlwaysTerminating: $isAlwaysTerminating, throwPoints: $throwPoints, impurePoints: $impurePoints, - truthyScopeCallback: static fn (): MutatingScope => $scope->filterByTruthyValue($expr), - falseyScopeCallback: static fn (): MutatingScope => $scope->filterByFalseyValue($expr), ); } diff --git a/src/Analyser/ExprHandler/MatchHandler.php b/src/Analyser/ExprHandler/MatchHandler.php index 6f794d430fe..66fd854eef4 100644 --- a/src/Analyser/ExprHandler/MatchHandler.php +++ b/src/Analyser/ExprHandler/MatchHandler.php @@ -507,6 +507,7 @@ public function processExpr(NodeScopeResolver $nodeScopeResolver, Stmt $stmt, Ex return $this->expressionResultFactory->create( $scope, beforeScope: $beforeScope, + expr: $expr, hasYield: $hasYield, isAlwaysTerminating: $isAlwaysTerminating, throwPoints: $throwPoints, diff --git a/src/Analyser/ExprHandler/MethodCallHandler.php b/src/Analyser/ExprHandler/MethodCallHandler.php index c501df4811e..7de23d146cb 100644 --- a/src/Analyser/ExprHandler/MethodCallHandler.php +++ b/src/Analyser/ExprHandler/MethodCallHandler.php @@ -197,12 +197,11 @@ public function processExpr(NodeScopeResolver $nodeScopeResolver, Stmt $stmt, Ex $result = $this->expressionResultFactory->create( $scope, beforeScope: $beforeScope, + expr: $expr, hasYield: $hasYield, isAlwaysTerminating: $isAlwaysTerminating, throwPoints: $throwPoints, impurePoints: $impurePoints, - truthyScopeCallback: static fn (): MutatingScope => $scope->filterByTruthyValue($expr), - falseyScopeCallback: static fn (): MutatingScope => $scope->filterByFalseyValue($expr), ); $calledOnType = $originalScope->getType($expr->var); @@ -226,12 +225,11 @@ public function processExpr(NodeScopeResolver $nodeScopeResolver, Stmt $stmt, Ex return $this->expressionResultFactory->create( $scope, beforeScope: $beforeScope, + expr: $expr, hasYield: $result->hasYield(), isAlwaysTerminating: $result->isAlwaysTerminating(), throwPoints: $result->getThrowPoints(), impurePoints: $result->getImpurePoints(), - truthyScopeCallback: static fn (): MutatingScope => $scope->filterByTruthyValue($expr), - falseyScopeCallback: static fn (): MutatingScope => $scope->filterByFalseyValue($expr), ); } } diff --git a/src/Analyser/ExprHandler/NewHandler.php b/src/Analyser/ExprHandler/NewHandler.php index da9eddc93a9..a70924f2118 100644 --- a/src/Analyser/ExprHandler/NewHandler.php +++ b/src/Analyser/ExprHandler/NewHandler.php @@ -221,6 +221,7 @@ public function processExpr(NodeScopeResolver $nodeScopeResolver, Stmt $stmt, Ex return $this->expressionResultFactory->create( $scope, beforeScope: $beforeScope, + expr: $expr, hasYield: $hasYield, isAlwaysTerminating: $isAlwaysTerminating, throwPoints: $throwPoints, diff --git a/src/Analyser/ExprHandler/NullsafeMethodCallHandler.php b/src/Analyser/ExprHandler/NullsafeMethodCallHandler.php index 05a94ad2668..215c1d6b497 100644 --- a/src/Analyser/ExprHandler/NullsafeMethodCallHandler.php +++ b/src/Analyser/ExprHandler/NullsafeMethodCallHandler.php @@ -121,12 +121,11 @@ public function processExpr(NodeScopeResolver $nodeScopeResolver, Stmt $stmt, Ex return $this->expressionResultFactory->create( $scope, beforeScope: $beforeScope, + expr: $expr, hasYield: $exprResult->hasYield(), isAlwaysTerminating: false, throwPoints: $exprResult->getThrowPoints(), impurePoints: $exprResult->getImpurePoints(), - truthyScopeCallback: static fn (): MutatingScope => $scope->filterByTruthyValue($expr), - falseyScopeCallback: static fn (): MutatingScope => $scope->filterByFalseyValue($expr), ); } diff --git a/src/Analyser/ExprHandler/NullsafePropertyFetchHandler.php b/src/Analyser/ExprHandler/NullsafePropertyFetchHandler.php index d9df195fd37..b5d253da2b0 100644 --- a/src/Analyser/ExprHandler/NullsafePropertyFetchHandler.php +++ b/src/Analyser/ExprHandler/NullsafePropertyFetchHandler.php @@ -100,12 +100,11 @@ public function processExpr(NodeScopeResolver $nodeScopeResolver, Stmt $stmt, Ex return $this->expressionResultFactory->create( $scope, beforeScope: $beforeScope, + expr: $expr, hasYield: $exprResult->hasYield(), isAlwaysTerminating: false, throwPoints: $exprResult->getThrowPoints(), impurePoints: $exprResult->getImpurePoints(), - truthyScopeCallback: static fn (): MutatingScope => $scope->filterByTruthyValue($expr), - falseyScopeCallback: static fn (): MutatingScope => $scope->filterByFalseyValue($expr), ); } diff --git a/src/Analyser/ExprHandler/PipeHandler.php b/src/Analyser/ExprHandler/PipeHandler.php index e6d010e2bf4..d3bbbc90413 100644 --- a/src/Analyser/ExprHandler/PipeHandler.php +++ b/src/Analyser/ExprHandler/PipeHandler.php @@ -92,6 +92,7 @@ public function processExpr(NodeScopeResolver $nodeScopeResolver, Stmt $stmt, Ex return $this->expressionResultFactory->create( $callResult->getScope(), beforeScope: $scope, + expr: $expr, hasYield: $callResult->hasYield(), isAlwaysTerminating: $callResult->isAlwaysTerminating(), throwPoints: $callResult->getThrowPoints(), diff --git a/src/Analyser/ExprHandler/PostDecHandler.php b/src/Analyser/ExprHandler/PostDecHandler.php index c28a4a0175e..ecdf3bd84d8 100644 --- a/src/Analyser/ExprHandler/PostDecHandler.php +++ b/src/Analyser/ExprHandler/PostDecHandler.php @@ -50,6 +50,7 @@ public function processExpr(NodeScopeResolver $nodeScopeResolver, Stmt $stmt, Ex $nodeCallback, )->getScope(), beforeScope: $scope, + expr: $expr, hasYield: $varResult->hasYield(), isAlwaysTerminating: $varResult->isAlwaysTerminating(), throwPoints: $varResult->getThrowPoints(), diff --git a/src/Analyser/ExprHandler/PostIncHandler.php b/src/Analyser/ExprHandler/PostIncHandler.php index f54fa7d4e0a..9a68af90336 100644 --- a/src/Analyser/ExprHandler/PostIncHandler.php +++ b/src/Analyser/ExprHandler/PostIncHandler.php @@ -50,6 +50,7 @@ public function processExpr(NodeScopeResolver $nodeScopeResolver, Stmt $stmt, Ex $nodeCallback, )->getScope(), beforeScope: $scope, + expr: $expr, hasYield: $varResult->hasYield(), isAlwaysTerminating: $varResult->isAlwaysTerminating(), throwPoints: $varResult->getThrowPoints(), diff --git a/src/Analyser/ExprHandler/PreDecHandler.php b/src/Analyser/ExprHandler/PreDecHandler.php index 344dde7a6ad..6569fde8c10 100644 --- a/src/Analyser/ExprHandler/PreDecHandler.php +++ b/src/Analyser/ExprHandler/PreDecHandler.php @@ -113,6 +113,7 @@ public function processExpr(NodeScopeResolver $nodeScopeResolver, Stmt $stmt, Ex $nodeCallback, )->getScope(), beforeScope: $scope, + expr: $expr, hasYield: $varResult->hasYield(), isAlwaysTerminating: $varResult->isAlwaysTerminating(), throwPoints: $varResult->getThrowPoints(), diff --git a/src/Analyser/ExprHandler/PreIncHandler.php b/src/Analyser/ExprHandler/PreIncHandler.php index 9e6d026f101..7d4be597076 100644 --- a/src/Analyser/ExprHandler/PreIncHandler.php +++ b/src/Analyser/ExprHandler/PreIncHandler.php @@ -114,6 +114,7 @@ public function processExpr(NodeScopeResolver $nodeScopeResolver, Stmt $stmt, Ex $nodeCallback, )->getScope(), beforeScope: $scope, + expr: $expr, hasYield: $varResult->hasYield(), isAlwaysTerminating: $varResult->isAlwaysTerminating(), throwPoints: $varResult->getThrowPoints(), diff --git a/src/Analyser/ExprHandler/PrintHandler.php b/src/Analyser/ExprHandler/PrintHandler.php index d495f8b63ca..cd6a90aee17 100644 --- a/src/Analyser/ExprHandler/PrintHandler.php +++ b/src/Analyser/ExprHandler/PrintHandler.php @@ -63,6 +63,7 @@ public function processExpr(NodeScopeResolver $nodeScopeResolver, Stmt $stmt, Ex return $this->expressionResultFactory->create( $scope, beforeScope: $beforeScope, + expr: $expr, hasYield: $exprResult->hasYield(), isAlwaysTerminating: $exprResult->isAlwaysTerminating(), throwPoints: $throwPoints, diff --git a/src/Analyser/ExprHandler/PropertyFetchHandler.php b/src/Analyser/ExprHandler/PropertyFetchHandler.php index 06d78067198..61152971d82 100644 --- a/src/Analyser/ExprHandler/PropertyFetchHandler.php +++ b/src/Analyser/ExprHandler/PropertyFetchHandler.php @@ -87,12 +87,11 @@ public function processExpr(NodeScopeResolver $nodeScopeResolver, Stmt $stmt, Ex return $this->expressionResultFactory->create( $scope, beforeScope: $beforeScope, + expr: $expr, hasYield: $hasYield, isAlwaysTerminating: $isAlwaysTerminating, throwPoints: $throwPoints, impurePoints: $impurePoints, - truthyScopeCallback: static fn (): MutatingScope => $scope->filterByTruthyValue($expr), - falseyScopeCallback: static fn (): MutatingScope => $scope->filterByFalseyValue($expr), ); } diff --git a/src/Analyser/ExprHandler/ScalarHandler.php b/src/Analyser/ExprHandler/ScalarHandler.php index ade6dc84023..9b4de986801 100644 --- a/src/Analyser/ExprHandler/ScalarHandler.php +++ b/src/Analyser/ExprHandler/ScalarHandler.php @@ -46,6 +46,7 @@ public function processExpr(NodeScopeResolver $nodeScopeResolver, Stmt $stmt, Ex return $this->expressionResultFactory->create( $scope, beforeScope: $scope, + expr: $expr, hasYield: false, isAlwaysTerminating: false, throwPoints: [], diff --git a/src/Analyser/ExprHandler/StaticCallHandler.php b/src/Analyser/ExprHandler/StaticCallHandler.php index d08ba830276..7c219787fe2 100644 --- a/src/Analyser/ExprHandler/StaticCallHandler.php +++ b/src/Analyser/ExprHandler/StaticCallHandler.php @@ -285,12 +285,11 @@ public function processExpr(NodeScopeResolver $nodeScopeResolver, Stmt $stmt, Ex return $this->expressionResultFactory->create( $scope, beforeScope: $beforeScope, + expr: $expr, hasYield: $hasYield, isAlwaysTerminating: $isAlwaysTerminating, throwPoints: $throwPoints, impurePoints: $impurePoints, - truthyScopeCallback: static fn (): MutatingScope => $scope->filterByTruthyValue($expr), - falseyScopeCallback: static fn (): MutatingScope => $scope->filterByFalseyValue($expr), ); } diff --git a/src/Analyser/ExprHandler/StaticPropertyFetchHandler.php b/src/Analyser/ExprHandler/StaticPropertyFetchHandler.php index 13a6279a5a1..c6d61ddd866 100644 --- a/src/Analyser/ExprHandler/StaticPropertyFetchHandler.php +++ b/src/Analyser/ExprHandler/StaticPropertyFetchHandler.php @@ -86,12 +86,11 @@ public function processExpr(NodeScopeResolver $nodeScopeResolver, Stmt $stmt, Ex return $this->expressionResultFactory->create( $scope, beforeScope: $beforeScope, + expr: $expr, hasYield: $hasYield, isAlwaysTerminating: $isAlwaysTerminating, throwPoints: $throwPoints, impurePoints: $impurePoints, - truthyScopeCallback: static fn (): MutatingScope => $scope->filterByTruthyValue($expr), - falseyScopeCallback: static fn (): MutatingScope => $scope->filterByFalseyValue($expr), ); } diff --git a/src/Analyser/ExprHandler/TernaryHandler.php b/src/Analyser/ExprHandler/TernaryHandler.php index b26c09378e9..101ffe45330 100644 --- a/src/Analyser/ExprHandler/TernaryHandler.php +++ b/src/Analyser/ExprHandler/TernaryHandler.php @@ -149,12 +149,11 @@ public function processExpr(NodeScopeResolver $nodeScopeResolver, Stmt $stmt, Ex return $this->expressionResultFactory->create( $finalScope, beforeScope: $scope, + expr: $expr, hasYield: $ternaryCondResult->hasYield(), isAlwaysTerminating: $ternaryCondResult->isAlwaysTerminating(), throwPoints: $throwPoints, impurePoints: $impurePoints, - truthyScopeCallback: static fn (): MutatingScope => $finalScope->filterByTruthyValue($expr), - falseyScopeCallback: static fn (): MutatingScope => $finalScope->filterByFalseyValue($expr), ); } diff --git a/src/Analyser/ExprHandler/ThrowHandler.php b/src/Analyser/ExprHandler/ThrowHandler.php index 2cf4666b6bd..05bda29ec2d 100644 --- a/src/Analyser/ExprHandler/ThrowHandler.php +++ b/src/Analyser/ExprHandler/ThrowHandler.php @@ -45,6 +45,7 @@ public function processExpr(NodeScopeResolver $nodeScopeResolver, Stmt $stmt, Ex return $this->expressionResultFactory->create( $scope, beforeScope: $scope, + expr: $expr, hasYield: false, isAlwaysTerminating: true, throwPoints: array_merge($exprResult->getThrowPoints(), [InternalThrowPoint::createExplicit($scope, $scope->getType($expr->expr), $expr, false)]), diff --git a/src/Analyser/ExprHandler/UnaryMinusHandler.php b/src/Analyser/ExprHandler/UnaryMinusHandler.php index d97068c3acb..ea67e7dabc4 100644 --- a/src/Analyser/ExprHandler/UnaryMinusHandler.php +++ b/src/Analyser/ExprHandler/UnaryMinusHandler.php @@ -46,6 +46,7 @@ public function processExpr(NodeScopeResolver $nodeScopeResolver, Stmt $stmt, Ex return $this->expressionResultFactory->create( $exprResult->getScope(), beforeScope: $scope, + expr: $expr, hasYield: $exprResult->hasYield(), isAlwaysTerminating: $exprResult->isAlwaysTerminating(), throwPoints: $exprResult->getThrowPoints(), diff --git a/src/Analyser/ExprHandler/UnaryPlusHandler.php b/src/Analyser/ExprHandler/UnaryPlusHandler.php index e2450452e31..6ec1abe38fc 100644 --- a/src/Analyser/ExprHandler/UnaryPlusHandler.php +++ b/src/Analyser/ExprHandler/UnaryPlusHandler.php @@ -46,6 +46,7 @@ public function processExpr(NodeScopeResolver $nodeScopeResolver, Stmt $stmt, Ex return $this->expressionResultFactory->create( $exprResult->getScope(), beforeScope: $scope, + expr: $expr, hasYield: $exprResult->hasYield(), isAlwaysTerminating: $exprResult->isAlwaysTerminating(), throwPoints: $exprResult->getThrowPoints(), diff --git a/src/Analyser/ExprHandler/VariableHandler.php b/src/Analyser/ExprHandler/VariableHandler.php index 3fdbdd0544a..6082dea55f2 100644 --- a/src/Analyser/ExprHandler/VariableHandler.php +++ b/src/Analyser/ExprHandler/VariableHandler.php @@ -98,6 +98,7 @@ public function processExpr(NodeScopeResolver $nodeScopeResolver, Stmt $stmt, Ex return $this->expressionResultFactory->create( $scope, $beforeScope, + $expr, $hasYield, $isAlwaysTerminating, $throwPoints, diff --git a/src/Analyser/ExprHandler/Virtual/AlwaysRememberedExprHandler.php b/src/Analyser/ExprHandler/Virtual/AlwaysRememberedExprHandler.php index 7a75e944ebc..99d6d9925e8 100644 --- a/src/Analyser/ExprHandler/Virtual/AlwaysRememberedExprHandler.php +++ b/src/Analyser/ExprHandler/Virtual/AlwaysRememberedExprHandler.php @@ -53,12 +53,11 @@ public function processExpr( return $this->expressionResultFactory->create( $scope, beforeScope: $beforeScope, + expr: $expr, hasYield: $innerResult->hasYield(), isAlwaysTerminating: $innerResult->isAlwaysTerminating(), throwPoints: $innerResult->getThrowPoints(), impurePoints: $innerResult->getImpurePoints(), - truthyScopeCallback: static fn (): MutatingScope => $scope->filterByTruthyValue($innerExpr), - falseyScopeCallback: static fn (): MutatingScope => $scope->filterByFalseyValue($innerExpr), ); } diff --git a/src/Analyser/ExprHandler/Virtual/ExistingArrayDimFetchHandler.php b/src/Analyser/ExprHandler/Virtual/ExistingArrayDimFetchHandler.php index 02d15c413b8..411d2ee8d65 100644 --- a/src/Analyser/ExprHandler/Virtual/ExistingArrayDimFetchHandler.php +++ b/src/Analyser/ExprHandler/Virtual/ExistingArrayDimFetchHandler.php @@ -43,6 +43,7 @@ public function processExpr(NodeScopeResolver $nodeScopeResolver, Stmt $stmt, Ex return $this->expressionResultFactory->create( $scope, beforeScope: $scope, + expr: $expr, hasYield: false, isAlwaysTerminating: false, throwPoints: [], diff --git a/src/Analyser/ExprHandler/Virtual/FunctionCallableNodeHandler.php b/src/Analyser/ExprHandler/Virtual/FunctionCallableNodeHandler.php index 8f3aaf827c2..e56509e627e 100644 --- a/src/Analyser/ExprHandler/Virtual/FunctionCallableNodeHandler.php +++ b/src/Analyser/ExprHandler/Virtual/FunctionCallableNodeHandler.php @@ -55,6 +55,7 @@ public function processExpr(NodeScopeResolver $nodeScopeResolver, Stmt $stmt, Ex return $this->expressionResultFactory->create( $scope, beforeScope: $beforeScope, + expr: $expr, hasYield: $hasYield, isAlwaysTerminating: $isAlwaysTerminating, throwPoints: $throwPoints, diff --git a/src/Analyser/ExprHandler/Virtual/GetIterableKeyTypeExprHandler.php b/src/Analyser/ExprHandler/Virtual/GetIterableKeyTypeExprHandler.php index 4d5f0e54a82..127ef939539 100644 --- a/src/Analyser/ExprHandler/Virtual/GetIterableKeyTypeExprHandler.php +++ b/src/Analyser/ExprHandler/Virtual/GetIterableKeyTypeExprHandler.php @@ -43,6 +43,7 @@ public function processExpr(NodeScopeResolver $nodeScopeResolver, Stmt $stmt, Ex return $this->expressionResultFactory->create( $scope, beforeScope: $scope, + expr: $expr, hasYield: false, isAlwaysTerminating: false, throwPoints: [], diff --git a/src/Analyser/ExprHandler/Virtual/GetIterableValueTypeExprHandler.php b/src/Analyser/ExprHandler/Virtual/GetIterableValueTypeExprHandler.php index 5b5b59ee994..56b7b9be00e 100644 --- a/src/Analyser/ExprHandler/Virtual/GetIterableValueTypeExprHandler.php +++ b/src/Analyser/ExprHandler/Virtual/GetIterableValueTypeExprHandler.php @@ -43,6 +43,7 @@ public function processExpr(NodeScopeResolver $nodeScopeResolver, Stmt $stmt, Ex return $this->expressionResultFactory->create( $scope, beforeScope: $scope, + expr: $expr, hasYield: false, isAlwaysTerminating: false, throwPoints: [], diff --git a/src/Analyser/ExprHandler/Virtual/GetOffsetValueTypeExprHandler.php b/src/Analyser/ExprHandler/Virtual/GetOffsetValueTypeExprHandler.php index 03f846910c4..ed0b5da45c5 100644 --- a/src/Analyser/ExprHandler/Virtual/GetOffsetValueTypeExprHandler.php +++ b/src/Analyser/ExprHandler/Virtual/GetOffsetValueTypeExprHandler.php @@ -43,6 +43,7 @@ public function processExpr(NodeScopeResolver $nodeScopeResolver, Stmt $stmt, Ex return $this->expressionResultFactory->create( $scope, beforeScope: $scope, + expr: $expr, hasYield: false, isAlwaysTerminating: false, throwPoints: [], diff --git a/src/Analyser/ExprHandler/Virtual/InstantiationCallableNodeHandler.php b/src/Analyser/ExprHandler/Virtual/InstantiationCallableNodeHandler.php index dddc6e216c9..937b6618d85 100644 --- a/src/Analyser/ExprHandler/Virtual/InstantiationCallableNodeHandler.php +++ b/src/Analyser/ExprHandler/Virtual/InstantiationCallableNodeHandler.php @@ -55,6 +55,7 @@ public function processExpr(NodeScopeResolver $nodeScopeResolver, Stmt $stmt, Ex return $this->expressionResultFactory->create( $scope, beforeScope: $beforeScope, + expr: $expr, hasYield: $hasYield, isAlwaysTerminating: $isAlwaysTerminating, throwPoints: $throwPoints, diff --git a/src/Analyser/ExprHandler/Virtual/MethodCallableNodeHandler.php b/src/Analyser/ExprHandler/Virtual/MethodCallableNodeHandler.php index 39b370ed581..28492541bce 100644 --- a/src/Analyser/ExprHandler/Virtual/MethodCallableNodeHandler.php +++ b/src/Analyser/ExprHandler/Virtual/MethodCallableNodeHandler.php @@ -58,6 +58,7 @@ public function processExpr(NodeScopeResolver $nodeScopeResolver, Stmt $stmt, Ex return $this->expressionResultFactory->create( $scope, beforeScope: $beforeScope, + expr: $expr, hasYield: $hasYield, isAlwaysTerminating: $isAlwaysTerminating, throwPoints: $throwPoints, diff --git a/src/Analyser/ExprHandler/Virtual/NativeTypeExprHandler.php b/src/Analyser/ExprHandler/Virtual/NativeTypeExprHandler.php index 0b7999acb50..852d00b14cd 100644 --- a/src/Analyser/ExprHandler/Virtual/NativeTypeExprHandler.php +++ b/src/Analyser/ExprHandler/Virtual/NativeTypeExprHandler.php @@ -43,6 +43,7 @@ public function processExpr(NodeScopeResolver $nodeScopeResolver, Stmt $stmt, Ex return $this->expressionResultFactory->create( $scope, beforeScope: $scope, + expr: $expr, hasYield: false, isAlwaysTerminating: false, throwPoints: [], diff --git a/src/Analyser/ExprHandler/Virtual/OriginalPropertyTypeExprHandler.php b/src/Analyser/ExprHandler/Virtual/OriginalPropertyTypeExprHandler.php index 9f36aee736c..2621d242cc7 100644 --- a/src/Analyser/ExprHandler/Virtual/OriginalPropertyTypeExprHandler.php +++ b/src/Analyser/ExprHandler/Virtual/OriginalPropertyTypeExprHandler.php @@ -48,6 +48,7 @@ public function processExpr(NodeScopeResolver $nodeScopeResolver, Stmt $stmt, Ex return $this->expressionResultFactory->create( $scope, beforeScope: $scope, + expr: $expr, hasYield: false, isAlwaysTerminating: false, throwPoints: [], diff --git a/src/Analyser/ExprHandler/Virtual/SetExistingOffsetValueTypeExprHandler.php b/src/Analyser/ExprHandler/Virtual/SetExistingOffsetValueTypeExprHandler.php index 7c7f2847d29..11487e514f2 100644 --- a/src/Analyser/ExprHandler/Virtual/SetExistingOffsetValueTypeExprHandler.php +++ b/src/Analyser/ExprHandler/Virtual/SetExistingOffsetValueTypeExprHandler.php @@ -45,6 +45,7 @@ public function processExpr(NodeScopeResolver $nodeScopeResolver, Stmt $stmt, Ex return $this->expressionResultFactory->create( $scope, beforeScope: $scope, + expr: $expr, hasYield: false, isAlwaysTerminating: false, throwPoints: [], diff --git a/src/Analyser/ExprHandler/Virtual/SetOffsetValueTypeExprHandler.php b/src/Analyser/ExprHandler/Virtual/SetOffsetValueTypeExprHandler.php index e604dece482..c85440094b8 100644 --- a/src/Analyser/ExprHandler/Virtual/SetOffsetValueTypeExprHandler.php +++ b/src/Analyser/ExprHandler/Virtual/SetOffsetValueTypeExprHandler.php @@ -45,6 +45,7 @@ public function processExpr(NodeScopeResolver $nodeScopeResolver, Stmt $stmt, Ex return $this->expressionResultFactory->create( $scope, beforeScope: $scope, + expr: $expr, hasYield: false, isAlwaysTerminating: false, throwPoints: [], diff --git a/src/Analyser/ExprHandler/Virtual/StaticMethodCallableNodeHandler.php b/src/Analyser/ExprHandler/Virtual/StaticMethodCallableNodeHandler.php index 34e7dbf25b7..b12d7e120e5 100644 --- a/src/Analyser/ExprHandler/Virtual/StaticMethodCallableNodeHandler.php +++ b/src/Analyser/ExprHandler/Virtual/StaticMethodCallableNodeHandler.php @@ -64,6 +64,7 @@ public function processExpr(NodeScopeResolver $nodeScopeResolver, Stmt $stmt, Ex return $this->expressionResultFactory->create( $scope, beforeScope: $beforeScope, + expr: $expr, hasYield: $hasYield, isAlwaysTerminating: $isAlwaysTerminating, throwPoints: $throwPoints, diff --git a/src/Analyser/ExprHandler/Virtual/TypeExprHandler.php b/src/Analyser/ExprHandler/Virtual/TypeExprHandler.php index 26b05213a50..6ca636fe081 100644 --- a/src/Analyser/ExprHandler/Virtual/TypeExprHandler.php +++ b/src/Analyser/ExprHandler/Virtual/TypeExprHandler.php @@ -43,6 +43,7 @@ public function processExpr(NodeScopeResolver $nodeScopeResolver, Stmt $stmt, Ex return $this->expressionResultFactory->create( $scope, beforeScope: $scope, + expr: $expr, hasYield: false, isAlwaysTerminating: false, throwPoints: [], diff --git a/src/Analyser/ExprHandler/Virtual/UnsetOffsetExprHandler.php b/src/Analyser/ExprHandler/Virtual/UnsetOffsetExprHandler.php index 1246378b936..d414e648fd2 100644 --- a/src/Analyser/ExprHandler/Virtual/UnsetOffsetExprHandler.php +++ b/src/Analyser/ExprHandler/Virtual/UnsetOffsetExprHandler.php @@ -43,6 +43,7 @@ public function processExpr(NodeScopeResolver $nodeScopeResolver, Stmt $stmt, Ex return $this->expressionResultFactory->create( $scope, beforeScope: $scope, + expr: $expr, hasYield: false, isAlwaysTerminating: false, throwPoints: [], diff --git a/src/Analyser/ExprHandler/YieldFromHandler.php b/src/Analyser/ExprHandler/YieldFromHandler.php index 089ce51b630..1aac8244af7 100644 --- a/src/Analyser/ExprHandler/YieldFromHandler.php +++ b/src/Analyser/ExprHandler/YieldFromHandler.php @@ -61,6 +61,7 @@ public function processExpr(NodeScopeResolver $nodeScopeResolver, Stmt $stmt, Ex return $this->expressionResultFactory->create( $scope, beforeScope: $beforeScope, + expr: $expr, hasYield: true, isAlwaysTerminating: $exprResult->isAlwaysTerminating(), throwPoints: array_merge($exprResult->getThrowPoints(), [InternalThrowPoint::createImplicit($scope, $expr)]), diff --git a/src/Analyser/ExprHandler/YieldHandler.php b/src/Analyser/ExprHandler/YieldHandler.php index 76356bfbf7b..07abbc7e6ee 100644 --- a/src/Analyser/ExprHandler/YieldHandler.php +++ b/src/Analyser/ExprHandler/YieldHandler.php @@ -91,6 +91,7 @@ public function processExpr(NodeScopeResolver $nodeScopeResolver, Stmt $stmt, Ex return $this->expressionResultFactory->create( $scope, beforeScope: $beforeScope, + expr: $expr, hasYield: true, isAlwaysTerminating: $isAlwaysTerminating, throwPoints: $throwPoints, diff --git a/src/Analyser/ExpressionResult.php b/src/Analyser/ExpressionResult.php index c75c3b3ac33..1e833cfcc0d 100644 --- a/src/Analyser/ExpressionResult.php +++ b/src/Analyser/ExpressionResult.php @@ -2,7 +2,9 @@ namespace PHPStan\Analyser; +use PhpParser\Node\Expr; use PHPStan\DependencyInjection\GenerateFactory; +use PHPStan\Type\Type; #[GenerateFactory(interface: ExpressionResultFactory::class)] final class ExpressionResult @@ -27,6 +29,7 @@ final class ExpressionResult public function __construct( private MutatingScope $scope, private MutatingScope $beforeScope, + private Expr $expr, private bool $hasYield, private bool $isAlwaysTerminating, private array $throwPoints, @@ -44,11 +47,6 @@ public function getScope(): MutatingScope return $this->scope; } - public function getBeforeScope(): MutatingScope - { - return $this->beforeScope; - } - public function hasYield(): bool { return $this->hasYield; @@ -72,32 +70,30 @@ public function getImpurePoints(): array public function getTruthyScope(): MutatingScope { - if ($this->truthyScopeCallback === null) { - return $this->scope; - } - if ($this->truthyScope !== null) { return $this->truthyScope; } + if ($this->truthyScopeCallback === null) { + return $this->truthyScope = $this->scope->filterByTruthyValue($this->expr); + } + $callback = $this->truthyScopeCallback; - $this->truthyScope = $callback(); - return $this->truthyScope; + return $this->truthyScope = $callback(); } public function getFalseyScope(): MutatingScope { - if ($this->falseyScopeCallback === null) { - return $this->scope; - } - if ($this->falseyScope !== null) { return $this->falseyScope; } + if ($this->falseyScopeCallback === null) { + return $this->falseyScope = $this->scope->filterByFalseyValue($this->expr); + } + $callback = $this->falseyScopeCallback; - $this->falseyScope = $callback(); - return $this->falseyScope; + return $this->falseyScope = $callback(); } public function isAlwaysTerminating(): bool @@ -105,4 +101,14 @@ public function isAlwaysTerminating(): bool return $this->isAlwaysTerminating; } + public function getType(): Type + { + return $this->beforeScope->getType($this->expr); + } + + public function getNativeType(): Type + { + return $this->beforeScope->getNativeType($this->expr); + } + } diff --git a/src/Analyser/ExpressionResultFactory.php b/src/Analyser/ExpressionResultFactory.php index e065862b20a..255d6258fba 100644 --- a/src/Analyser/ExpressionResultFactory.php +++ b/src/Analyser/ExpressionResultFactory.php @@ -2,6 +2,8 @@ namespace PHPStan\Analyser; +use PhpParser\Node\Expr; + interface ExpressionResultFactory { @@ -14,6 +16,7 @@ interface ExpressionResultFactory public function create( MutatingScope $scope, MutatingScope $beforeScope, + Expr $expr, bool $hasYield, bool $isAlwaysTerminating, array $throwPoints, diff --git a/src/Analyser/NodeScopeResolver.php b/src/Analyser/NodeScopeResolver.php index 011abbbac38..55b8d663853 100644 --- a/src/Analyser/NodeScopeResolver.php +++ b/src/Analyser/NodeScopeResolver.php @@ -1301,9 +1301,9 @@ public function processStmtNode( $this->callNodeCallback($nodeCallback, $stmt->type, $scope, $storage); } } elseif ($stmt instanceof If_) { - $conditionType = ($this->treatPhpDocTypesAsCertain ? $scope->getType($stmt->cond) : $scope->getNativeType($stmt->cond))->toBoolean(); - $ifAlwaysTrue = $conditionType->isTrue()->yes(); $condResult = $this->processExprNode($stmt, $stmt->cond, $scope, $storage, $nodeCallback, ExpressionContext::createDeep()); + $conditionType = ($this->treatPhpDocTypesAsCertain ? $condResult->getType() : $condResult->getNativeType())->toBoolean(); + $ifAlwaysTrue = $conditionType->isTrue()->yes(); $exitPoints = []; $throwPoints = $overridingThrowPoints ?? $condResult->getThrowPoints(); $impurePoints = $condResult->getImpurePoints(); @@ -2780,18 +2780,17 @@ public function processExprNode( if ($expr instanceof List_) { // only in assign and foreach, processed elsewhere - return $this->expressionResultFactory->create($scope, beforeScope: $scope, hasYield: false, isAlwaysTerminating: false, throwPoints: [], impurePoints: []); + return $this->expressionResultFactory->create($scope, beforeScope: $scope, expr: $expr, hasYield: false, isAlwaysTerminating: false, throwPoints: [], impurePoints: []); } return $this->expressionResultFactory->create( $scope, beforeScope: $scope, + expr: $expr, hasYield: false, isAlwaysTerminating: false, throwPoints: [], impurePoints: [], - truthyScopeCallback: static fn (): MutatingScope => $scope->filterByTruthyValue($expr), - falseyScopeCallback: static fn (): MutatingScope => $scope->filterByFalseyValue($expr), ); } @@ -3152,7 +3151,7 @@ public function processArrowFunctionNode( $this->callNodeCallback($nodeCallback, new InArrowFunctionNode($arrowFunctionType, $expr), $arrowFunctionScope, $storage); $exprResult = $this->processExprNode($stmt, $expr->expr, $arrowFunctionScope, $storage, $nodeCallback, ExpressionContext::createTopLevel()); - return $this->expressionResultFactory->create($scope, beforeScope: $scope, hasYield: false, isAlwaysTerminating: $exprResult->isAlwaysTerminating(), throwPoints: $exprResult->getThrowPoints(), impurePoints: $exprResult->getImpurePoints()); + return $this->expressionResultFactory->create($scope, beforeScope: $scope, expr: $expr, hasYield: false, isAlwaysTerminating: $exprResult->isAlwaysTerminating(), throwPoints: $exprResult->getThrowPoints(), impurePoints: $exprResult->getImpurePoints()); } /** @@ -3855,7 +3854,7 @@ public function processArgs( } // not storing this, it's scope after processing all args - return $this->expressionResultFactory->create($scope, $scope, $hasYield, $isAlwaysTerminating, $throwPoints, $impurePoints); + return $this->expressionResultFactory->create($scope, $scope, $callLike, $hasYield, $isAlwaysTerminating, $throwPoints, $impurePoints); } /** @@ -3992,7 +3991,7 @@ public function processVirtualAssign(MutatingScope $scope, ExpressionResultStora $assignedExpr, new VirtualAssignNodeCallback($nodeCallback), ExpressionContext::createDeep(), - fn (MutatingScope $scope): ExpressionResult => $this->expressionResultFactory->create($scope, beforeScope: $scope, hasYield: false, isAlwaysTerminating: false, throwPoints: [], impurePoints: []), + fn (MutatingScope $scope): ExpressionResult => $this->expressionResultFactory->create($scope, beforeScope: $scope, expr: $assignedExpr, hasYield: false, isAlwaysTerminating: false, throwPoints: [], impurePoints: []), false, ); } From 21bf8e9719b7195f4dc79a8b7d6453753d36f6b7 Mon Sep 17 00:00:00 2001 From: Ondrej Mirtes Date: Thu, 11 Jun 2026 20:16:10 +0200 Subject: [PATCH 004/113] Store ExpressionResult instead of before-Scope --- src/Analyser/ExprHandler/AssignHandler.php | 34 +++++++++++--- src/Analyser/ExprHandler/AssignOpHandler.php | 4 -- src/Analyser/ExpressionResult.php | 5 +++ src/Analyser/ExpressionResultStorage.php | 22 +++++----- ...equest.php => ExpressionResultRequest.php} | 5 +-- src/Analyser/Fiber/FiberNodeScopeResolver.php | 44 +++++++++++++------ src/Analyser/Fiber/FiberScope.php | 42 +++++++++++++----- src/Analyser/NodeScopeResolver.php | 42 +++++++++++++----- 8 files changed, 135 insertions(+), 63 deletions(-) rename src/Analyser/Fiber/{BeforeScopeForExprRequest.php => ExpressionResultRequest.php} (61%) diff --git a/src/Analyser/ExprHandler/AssignHandler.php b/src/Analyser/ExprHandler/AssignHandler.php index 70caac5134b..143c52da656 100644 --- a/src/Analyser/ExprHandler/AssignHandler.php +++ b/src/Analyser/ExprHandler/AssignHandler.php @@ -330,7 +330,6 @@ function (MutatingScope $scope) use ($stmt, $expr, $nodeCallback, $context, $sto ); } - $nodeScopeResolver->storeBeforeScope($storage, $expr, $scope); $result = $nodeScopeResolver->processExprNode($stmt, $expr->expr, $scope, $storage, $nodeCallback, $context->enterDeep()); $hasYield = $result->hasYield(); $throwPoints = $result->getThrowPoints(); @@ -413,6 +412,15 @@ public function processAssignVar( ): ExpressionResult { $beforeScope = $scope; + $nodeScopeResolver->storeExpressionResult($storage, $var, $this->expressionResultFactory->create( + $scope, + beforeScope: $scope, + expr: $var, + hasYield: false, + isAlwaysTerminating: false, + throwPoints: [], + impurePoints: [], + )); $nodeScopeResolver->callNodeCallback($nodeCallback, $var, $enterExpressionAssign ? $scope->enterExpressionAssign($var) : $scope, $storage); $hasYield = false; $throwPoints = []; @@ -420,7 +428,6 @@ public function processAssignVar( $isAlwaysTerminating = false; $isAssignOp = $assignedExpr instanceof Expr\AssignOp && !$enterExpressionAssign; if ($var instanceof Variable) { - $nodeScopeResolver->storeBeforeScope($storage, $var, $scope); $result = $processExprCallback($scope); $hasYield = $result->hasYield(); $throwPoints = $result->getThrowPoints(); @@ -592,7 +599,15 @@ public function processAssignVar( if ($dimExpr === null) { $offsetTypes[] = [null, $dimFetch]; $offsetNativeTypes[] = [null, $dimFetch]; - $nodeScopeResolver->storeBeforeScope($storage, $dimFetch, $scope); + $nodeScopeResolver->storeExpressionResult($storage, $dimFetch, $this->expressionResultFactory->create( + $scope, + beforeScope: $scope, + expr: $dimFetch, + hasYield: false, + isAlwaysTerminating: false, + throwPoints: [], + impurePoints: [], + )); } else { $offsetTypes[] = [$scope->getType($dimExpr), $dimFetch]; @@ -601,7 +616,15 @@ public function processAssignVar( if ($enterExpressionAssign) { $scope->enterExpressionAssign($dimExpr); } - $nodeScopeResolver->storeBeforeScope($storage, $dimFetch, $scope); + $nodeScopeResolver->storeExpressionResult($storage, $dimFetch, $this->expressionResultFactory->create( + $scope, + beforeScope: $scope, + expr: $dimFetch, + hasYield: false, + isAlwaysTerminating: false, + throwPoints: [], + impurePoints: [], + )); $result = $nodeScopeResolver->processExprNode($stmt, $dimExpr, $scope, $storage, $nodeCallback, $context->enterDeep()); $hasYield = $hasYield || $result->hasYield(); $throwPoints = array_merge($throwPoints, $result->getThrowPoints()); @@ -723,7 +746,6 @@ public function processAssignVar( )->getThrowPoints()); } } elseif ($var instanceof PropertyFetch) { - $nodeScopeResolver->storeBeforeScope($storage, $var, $scope); $objectResult = $nodeScopeResolver->processExprNode($stmt, $var->var, $scope, $storage, $nodeCallback, $context); $hasYield = $objectResult->hasYield(); $throwPoints = $objectResult->getThrowPoints(); @@ -836,7 +858,6 @@ public function processAssignVar( } } elseif ($var instanceof Expr\StaticPropertyFetch) { - $nodeScopeResolver->storeBeforeScope($storage, $var, $scope); if ($var->class instanceof Node\Name) { $propertyHolderType = $scope->resolveTypeByName($var->class); } else { @@ -902,7 +923,6 @@ public function processAssignVar( $scope = $scope->assignExpression($var, $assignedExprType, $scope->getNativeType($assignedExpr)); } } elseif ($var instanceof List_) { - $nodeScopeResolver->storeBeforeScope($storage, $var, $scope); $result = $processExprCallback($scope); $hasYield = $result->hasYield(); $throwPoints = array_merge($throwPoints, $result->getThrowPoints()); diff --git a/src/Analyser/ExprHandler/AssignOpHandler.php b/src/Analyser/ExprHandler/AssignOpHandler.php index ff70d04dbe5..5ebcff7b679 100644 --- a/src/Analyser/ExprHandler/AssignOpHandler.php +++ b/src/Analyser/ExprHandler/AssignOpHandler.php @@ -75,7 +75,6 @@ function (MutatingScope $scope) use ($stmt, $expr, $nodeCallback, $context, $sto $exprResult = $nodeScopeResolver->processExprNode($stmt, $expr->expr, $scope, $storage, $nodeCallback, $context->enterDeep()); if ($expr instanceof Expr\AssignOp\Coalesce) { - $nodeScopeResolver->storeBeforeScope($storage, $expr, $originalScope); $isAlwaysTerminating = $exprResult->isAlwaysTerminating() && $originalScope->getType($expr->var)->isNull()->yes(); return $this->expressionResultFactory->create( $exprResult->getScope()->mergeWith($originalScope), @@ -92,9 +91,6 @@ function (MutatingScope $scope) use ($stmt, $expr, $nodeCallback, $context, $sto }, $expr instanceof Expr\AssignOp\Coalesce, ); - if (!$expr instanceof Expr\AssignOp\Coalesce) { - $nodeScopeResolver->storeBeforeScope($storage, $expr, $scope); - } $scope = $assignResult->getScope(); $throwPoints = $assignResult->getThrowPoints(); $impurePoints = $assignResult->getImpurePoints(); diff --git a/src/Analyser/ExpressionResult.php b/src/Analyser/ExpressionResult.php index 1e833cfcc0d..7669a67a2c8 100644 --- a/src/Analyser/ExpressionResult.php +++ b/src/Analyser/ExpressionResult.php @@ -47,6 +47,11 @@ public function getScope(): MutatingScope return $this->scope; } + public function getBeforeScope(): MutatingScope + { + return $this->beforeScope; + } + public function hasYield(): bool { return $this->hasYield; diff --git a/src/Analyser/ExpressionResultStorage.php b/src/Analyser/ExpressionResultStorage.php index d14923866c9..9fe2d2d14bd 100644 --- a/src/Analyser/ExpressionResultStorage.php +++ b/src/Analyser/ExpressionResultStorage.php @@ -5,42 +5,42 @@ use Fiber; use PhpParser\Node; use PhpParser\Node\Expr; -use PHPStan\Analyser\Fiber\BeforeScopeForExprRequest; +use PHPStan\Analyser\Fiber\ExpressionResultRequest; use PHPStan\Analyser\Fiber\ParkFiberRequest; use SplObjectStorage; final class ExpressionResultStorage { - /** @var SplObjectStorage */ - private SplObjectStorage $scopes; + /** @var SplObjectStorage */ + private SplObjectStorage $exprResults; - /** @var array, request: BeforeScopeForExprRequest}> */ + /** @var array, request: ExpressionResultRequest}> */ public array $pendingFibers = []; - /** @var list> */ + /** @var list> */ public array $parkedFibers = []; public function __construct() { - $this->scopes = new SplObjectStorage(); + $this->exprResults = new SplObjectStorage(); } public function duplicate(): self { $new = new self(); - $new->scopes->addAll($this->scopes); + $new->exprResults->addAll($this->exprResults); return $new; } - public function storeBeforeScope(Expr $expr, Scope $scope): void + public function storeExpressionResult(Expr $expr, ExpressionResult $expressionResult): void { - $this->scopes[$expr] = $scope; + $this->exprResults[$expr] = $expressionResult; } - public function findBeforeScope(Expr $expr): ?Scope + public function findExpressionResult(Expr $expr): ?ExpressionResult { - return $this->scopes[$expr] ?? null; + return $this->exprResults[$expr] ?? null; } } diff --git a/src/Analyser/Fiber/BeforeScopeForExprRequest.php b/src/Analyser/Fiber/ExpressionResultRequest.php similarity index 61% rename from src/Analyser/Fiber/BeforeScopeForExprRequest.php rename to src/Analyser/Fiber/ExpressionResultRequest.php index 0fc6ecd35cd..4f3ecabdf97 100644 --- a/src/Analyser/Fiber/BeforeScopeForExprRequest.php +++ b/src/Analyser/Fiber/ExpressionResultRequest.php @@ -3,12 +3,11 @@ namespace PHPStan\Analyser\Fiber; use PhpParser\Node\Expr; -use PHPStan\Analyser\MutatingScope; -final class BeforeScopeForExprRequest +final class ExpressionResultRequest { - public function __construct(public readonly Expr $expr, public readonly MutatingScope $scope) + public function __construct(public readonly Expr $expr, public readonly FiberScope $scope) { } diff --git a/src/Analyser/Fiber/FiberNodeScopeResolver.php b/src/Analyser/Fiber/FiberNodeScopeResolver.php index e8d160f6a1d..a0340a3781b 100644 --- a/src/Analyser/Fiber/FiberNodeScopeResolver.php +++ b/src/Analyser/Fiber/FiberNodeScopeResolver.php @@ -5,9 +5,12 @@ use Fiber; use PhpParser\Node; use PhpParser\Node\Expr; +use PHPStan\Analyser\ExpressionContext; +use PHPStan\Analyser\ExpressionResult; use PHPStan\Analyser\ExpressionResultStorage; use PHPStan\Analyser\MutatingScope; use PHPStan\Analyser\NodeScopeResolver; +use PHPStan\Analyser\NoopNodeCallback; use PHPStan\Analyser\Scope; use PHPStan\DependencyInjection\AutowiredService; use PHPStan\ShouldNotHappenException; @@ -48,26 +51,26 @@ public function callNodeCallback( $this->runFiberForNodeCallback($storage, $fiber, $request); } - public function storeBeforeScope(ExpressionResultStorage $storage, Expr $expr, Scope $beforeScope): void + public function storeExpressionResult(ExpressionResultStorage $storage, Expr $expr, ExpressionResult $expressionResult): void { - $storage->storeBeforeScope($expr, $beforeScope); - $this->processPendingFibersForRequestedExpr($storage, $expr, $beforeScope); + $storage->storeExpressionResult($expr, $expressionResult); + $this->processPendingFibersForRequestedExpr($storage, $expr, $expressionResult); } /** - * @param Fiber $fiber + * @param Fiber $fiber */ private function runFiberForNodeCallback( ExpressionResultStorage $storage, Fiber $fiber, - BeforeScopeForExprRequest|ParkFiberRequest|null $request, + ExpressionResultRequest|ParkFiberRequest|null $request, ): void { while (!$fiber->isTerminated()) { - if ($request instanceof BeforeScopeForExprRequest) { - $beforeScope = $storage->findBeforeScope($request->expr); - if ($beforeScope !== null) { - $request = $fiber->resume($beforeScope); + if ($request instanceof ExpressionResultRequest) { + $expressionResult = $storage->findExpressionResult($request->expr); + if ($expressionResult !== null) { + $request = $fiber->resume($expressionResult); continue; } @@ -100,16 +103,29 @@ protected function processPendingFibers(ExpressionResultStorage $storage): void foreach ($storage->pendingFibers as $key => $pending) { $request = $pending['request']; - $beforeScope = $storage->findBeforeScope($request->expr); + $expressionResult = $storage->findExpressionResult($request->expr); - if ($beforeScope !== null) { + if ($expressionResult !== null) { throw new ShouldNotHappenException('Pending fibers at the end should be about synthetic nodes'); } unset($storage->pendingFibers[$key]); $fiber = $pending['fiber']; - $request = $fiber->resume($request->scope); + + // Process the expression with a duplicated storage so that the result + // computed from the asker's scope does not poison the real storage. + // The expression might still be processed naturally later (e.g. a loop + // condition asked about by a rule before the loop converges) and other + // fibers need to wait for that result instead of this one. + $request = $fiber->resume($this->processExprNode( + new Node\Stmt\Expression($request->expr), + $request->expr, + $request->scope->toMutatingScope(), + $storage->duplicate(), + new NoopNodeCallback(), + ExpressionContext::createTopLevel(), + )); $this->runFiberForNodeCallback($storage, $fiber, $request); // Break and restart the loop since the array may have been modified @@ -117,7 +133,7 @@ protected function processPendingFibers(ExpressionResultStorage $storage): void } } - private function processPendingFibersForRequestedExpr(ExpressionResultStorage $storage, Expr $expr, Scope $result): void + private function processPendingFibersForRequestedExpr(ExpressionResultStorage $storage, Expr $expr, ExpressionResult $expressionResult): void { start: @@ -130,7 +146,7 @@ private function processPendingFibersForRequestedExpr(ExpressionResultStorage $s unset($storage->pendingFibers[$key]); $fiber = $pending['fiber']; - $request = $fiber->resume($result); + $request = $fiber->resume($expressionResult); $this->runFiberForNodeCallback($storage, $fiber, $request); // Break and restart the loop since the array may have been modified diff --git a/src/Analyser/Fiber/FiberScope.php b/src/Analyser/Fiber/FiberScope.php index b02f322c358..698cf7fa1d0 100644 --- a/src/Analyser/Fiber/FiberScope.php +++ b/src/Analyser/Fiber/FiberScope.php @@ -4,6 +4,7 @@ use Fiber; use PhpParser\Node\Expr; +use PHPStan\Analyser\ExpressionResult; use PHPStan\Analyser\MutatingScope; use PHPStan\Analyser\Scope; use PHPStan\Reflection\FunctionReflection; @@ -11,6 +12,7 @@ use PHPStan\Reflection\ParameterReflection; use PHPStan\Type\Type; use function array_pop; +use function count; final class FiberScope extends MutatingScope { @@ -57,12 +59,20 @@ public function toMutatingScope(): MutatingScope /** @api */ public function getType(Expr $node): Type { - /** @var Scope $beforeScope */ - $beforeScope = Fiber::suspend( - new BeforeScopeForExprRequest($node, $this), + /** @var ExpressionResult $expressionResult */ + $expressionResult = Fiber::suspend( + new ExpressionResultRequest($node, $this), ); - $scope = $this->preprocessScope($beforeScope->toMutatingScope()); + if ( + !$this->nativeTypesPromoted + && count($this->truthyValueExprs) === 0 + && count($this->falseyValueExprs) === 0 + ) { + return $expressionResult->getType(); + } + + $scope = $this->preprocessScope($expressionResult->getBeforeScope()); return $scope->getType($node); } @@ -79,23 +89,31 @@ public function getScopeNativeType(Expr $expr): Type /** @api */ public function getNativeType(Expr $expr): Type { - /** @var Scope $beforeScope */ - $beforeScope = Fiber::suspend( - new BeforeScopeForExprRequest($expr, $this), + /** @var ExpressionResult $expressionResult */ + $expressionResult = Fiber::suspend( + new ExpressionResultRequest($expr, $this), ); - $scope = $this->preprocessScope($beforeScope->toMutatingScope()); + if ( + !$this->nativeTypesPromoted + && count($this->truthyValueExprs) === 0 + && count($this->falseyValueExprs) === 0 + ) { + return $expressionResult->getNativeType(); + } + + $scope = $this->preprocessScope($expressionResult->getBeforeScope()); return $scope->getNativeType($expr); } public function getKeepVoidType(Expr $node): Type { - /** @var Scope $beforeScope */ - $beforeScope = Fiber::suspend( - new BeforeScopeForExprRequest($node, $this), + /** @var ExpressionResult $expressionResult */ + $expressionResult = Fiber::suspend( + new ExpressionResultRequest($node, $this), ); - $scope = $this->preprocessScope($beforeScope->toMutatingScope()); + $scope = $this->preprocessScope($expressionResult->getBeforeScope()); return $scope->getKeepVoidType($node); } diff --git a/src/Analyser/NodeScopeResolver.php b/src/Analyser/NodeScopeResolver.php index 55b8d663853..e480674d2c2 100644 --- a/src/Analyser/NodeScopeResolver.php +++ b/src/Analyser/NodeScopeResolver.php @@ -354,7 +354,7 @@ public function processNodes( $this->processPendingFibers($expressionResultStorage); } - public function storeBeforeScope(ExpressionResultStorage $storage, Expr $expr, Scope $beforeScope): void + public function storeExpressionResult(ExpressionResultStorage $storage, Expr $expr, ExpressionResult $expressionResult): void { } @@ -2750,7 +2750,6 @@ public function processExprNode( ExpressionContext $context, ): ExpressionResult { - $this->storeBeforeScope($storage, $expr, $scope); if ($expr instanceof Expr\CallLike && $expr->isFirstClassCallable()) { if ($expr instanceof FuncCall) { $newExpr = new FunctionCallableNode($expr->name, $expr); @@ -2764,7 +2763,18 @@ public function processExprNode( throw new ShouldNotHappenException(); } - return $this->processExprNode($stmt, $newExpr, $scope, $storage, $nodeCallback, $context); + $newExprResult = $this->processExprNode($stmt, $newExpr, $scope, $storage, $nodeCallback, $context); + $expressionResult = $this->expressionResultFactory->create( + $newExprResult->getScope(), + beforeScope: $scope, + expr: $expr, + hasYield: $newExprResult->hasYield(), + isAlwaysTerminating: $newExprResult->isAlwaysTerminating(), + throwPoints: $newExprResult->getThrowPoints(), + impurePoints: $newExprResult->getImpurePoints(), + ); + $this->storeExpressionResult($storage, $expr, $expressionResult); + return $expressionResult; } $this->callNodeCallbackWithExpression($nodeCallback, $expr, $scope, $storage, $context); @@ -2775,15 +2785,12 @@ public function processExprNode( continue; } - return $exprHandler->processExpr($this, $stmt, $expr, $scope, $storage, $nodeCallback, $context); - } - - if ($expr instanceof List_) { - // only in assign and foreach, processed elsewhere - return $this->expressionResultFactory->create($scope, beforeScope: $scope, expr: $expr, hasYield: false, isAlwaysTerminating: false, throwPoints: [], impurePoints: []); + $expressionResult = $exprHandler->processExpr($this, $stmt, $expr, $scope, $storage, $nodeCallback, $context); + $this->storeExpressionResult($storage, $expr, $expressionResult); + return $expressionResult; } - return $this->expressionResultFactory->create( + $expressionResult = $this->expressionResultFactory->create( $scope, beforeScope: $scope, expr: $expr, @@ -2792,6 +2799,9 @@ public function processExprNode( throwPoints: [], impurePoints: [], ); + $this->storeExpressionResult($storage, $expr, $expressionResult); + + return $expressionResult; } /** @@ -3657,7 +3667,15 @@ public function processArgs( $impurePoints = array_merge($impurePoints, $closureResult->getImpurePoints()); } - $this->storeBeforeScope($storage, $arg->value, $scopeToPass); + $this->storeExpressionResult($storage, $arg->value, new ExpressionResult( + $closureResult->getScope(), + $scopeToPass, + $arg->value, + hasYield: false, + isAlwaysTerminating: false, + throwPoints: [], + impurePoints: [], + )); $uses = []; foreach ($arg->value->uses as $use) { @@ -3715,7 +3733,7 @@ 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), $arrowFunctionResult->getThrowPoints())); $impurePoints = array_merge($impurePoints, $arrowFunctionResult->getImpurePoints()); } - $this->storeBeforeScope($storage, $arg->value, $scopeToPass); + $this->storeExpressionResult($storage, $arg->value, $arrowFunctionResult); } else { $exprType = $scope->getType($arg->value); $enterExpressionAssignForByRef = $assignByReference && $arg->value instanceof ArrayDimFetch && $arg->value->dim === null; From 8ae08393b58ca808732e4a48a1f2b0803fc59642 Mon Sep 17 00:00:00 2001 From: Ondrej Mirtes Date: Fri, 12 Jun 2026 11:23:57 +0200 Subject: [PATCH 005/113] Fill the missing gaps in expr processing --- src/Analyser/ExprHandler/AssignHandler.php | 6 ++++ src/Analyser/ExprHandler/PipeHandler.php | 19 ++++++++++++ src/Analyser/ExpressionResultStorage.php | 5 ++++ src/Analyser/Fiber/FiberNodeScopeResolver.php | 29 +++++++++++-------- src/Analyser/NodeScopeResolver.php | 29 +++++++++++++++++-- 5 files changed, 73 insertions(+), 15 deletions(-) diff --git a/src/Analyser/ExprHandler/AssignHandler.php b/src/Analyser/ExprHandler/AssignHandler.php index 143c52da656..1cf5c4cc1c8 100644 --- a/src/Analyser/ExprHandler/AssignHandler.php +++ b/src/Analyser/ExprHandler/AssignHandler.php @@ -990,10 +990,16 @@ public function processAssignVar( $var = $var->getVar(); } + // the chain is usually a clone of AST nodes already processed elsewhere + // (see Unset_ handling) - process it with a noop callback so that + // results for its nodes are stored without invoking rules twice + $nodeScopeResolver->processExprNode($stmt, $var, $scope, $storage, new NoopNodeCallback(), $context->enterDeep()); + $offsetTypes = []; $offsetNativeTypes = []; foreach (array_reverse($dimFetchStack) as $dimFetch) { $dimExpr = $dimFetch->getDim(); + $nodeScopeResolver->processExprNode($stmt, $dimExpr, $scope, $storage, new NoopNodeCallback(), $context->enterDeep()); $offsetTypes[] = [$scope->getType($dimExpr), $dimFetch]; $offsetNativeTypes[] = [$scope->getNativeType($dimExpr), $dimFetch]; } diff --git a/src/Analyser/ExprHandler/PipeHandler.php b/src/Analyser/ExprHandler/PipeHandler.php index d3bbbc90413..a10009c2d56 100644 --- a/src/Analyser/ExprHandler/PipeHandler.php +++ b/src/Analyser/ExprHandler/PipeHandler.php @@ -69,24 +69,43 @@ public function processExpr(NodeScopeResolver $nodeScopeResolver, Stmt $stmt, Ex unset($rightAttributes[ExprPrinter::ATTRIBUTE_CACHE_KEY]); $argAttributes = $expr->getAttribute(ReversePipeTransformerVisitor::ARG_ATTRIBUTES_NAME, []); + $isRightFirstClassCallable = false; if ($expr->right instanceof FuncCall && $expr->right->isFirstClassCallable()) { $callExpr = new FuncCall($expr->right->name, [ new Arg($expr->left, attributes: $argAttributes), ], $rightAttributes); + $isRightFirstClassCallable = true; } elseif ($expr->right instanceof MethodCall && $expr->right->isFirstClassCallable()) { $callExpr = new MethodCall($expr->right->var, $expr->right->name, [ new Arg($expr->left, attributes: $argAttributes), ], $rightAttributes); + $isRightFirstClassCallable = true; } elseif ($expr->right instanceof StaticCall && $expr->right->isFirstClassCallable()) { $callExpr = new StaticCall($expr->right->class, $expr->right->name, [ new Arg($expr->left, attributes: $argAttributes), ], $rightAttributes); + $isRightFirstClassCallable = true; } else { $callExpr = new FuncCall($expr->right, [ new Arg($expr->left, attributes: $argAttributes), ], $rightAttributes); } + if ($isRightFirstClassCallable) { + // the original first-class callable node is not processed through + // processExprNode - store its result so that node callbacks asking + // about its type can be resumed + $nodeScopeResolver->storeExpressionResult($storage, $expr->right, $this->expressionResultFactory->create( + $scope, + beforeScope: $scope, + expr: $expr->right, + hasYield: false, + isAlwaysTerminating: false, + throwPoints: [], + impurePoints: [], + )); + } + $callResult = $nodeScopeResolver->processExprNode($stmt, $callExpr, $scope, $storage, $nodeCallback, $context); return $this->expressionResultFactory->create( diff --git a/src/Analyser/ExpressionResultStorage.php b/src/Analyser/ExpressionResultStorage.php index 9fe2d2d14bd..daea8cf3c93 100644 --- a/src/Analyser/ExpressionResultStorage.php +++ b/src/Analyser/ExpressionResultStorage.php @@ -33,6 +33,11 @@ public function duplicate(): self return $new; } + public function mergeResults(self $other): void + { + $this->exprResults->addAll($other->exprResults); + } + public function storeExpressionResult(Expr $expr, ExpressionResult $expressionResult): void { $this->exprResults[$expr] = $expressionResult; diff --git a/src/Analyser/Fiber/FiberNodeScopeResolver.php b/src/Analyser/Fiber/FiberNodeScopeResolver.php index a0340a3781b..2beceacb7d0 100644 --- a/src/Analyser/Fiber/FiberNodeScopeResolver.php +++ b/src/Analyser/Fiber/FiberNodeScopeResolver.php @@ -113,19 +113,24 @@ protected function processPendingFibers(ExpressionResultStorage $storage): void $fiber = $pending['fiber']; - // Process the expression with a duplicated storage so that the result + // Process the synthetic node with a duplicated storage so that the result // computed from the asker's scope does not poison the real storage. - // The expression might still be processed naturally later (e.g. a loop - // condition asked about by a rule before the loop converges) and other - // fibers need to wait for that result instead of this one. - $request = $fiber->resume($this->processExprNode( - new Node\Stmt\Expression($request->expr), - $request->expr, - $request->scope->toMutatingScope(), - $storage->duplicate(), - new NoopNodeCallback(), - ExpressionContext::createTopLevel(), - )); + // Real AST nodes contained in the synthetic node already have their + // results stored and are not processed again. + $this->returnStoredExpressionResults = true; + try { + $expressionResult = $this->processExprNode( + new Node\Stmt\Expression($request->expr), + $request->expr, + $request->scope->toMutatingScope(), + $storage->duplicate(), + new NoopNodeCallback(), + ExpressionContext::createTopLevel(), + ); + } finally { + $this->returnStoredExpressionResults = false; + } + $request = $fiber->resume($expressionResult); $this->runFiberForNodeCallback($storage, $fiber, $request); // Break and restart the loop since the array may have been modified diff --git a/src/Analyser/NodeScopeResolver.php b/src/Analyser/NodeScopeResolver.php index e480674d2c2..97babd039fe 100644 --- a/src/Analyser/NodeScopeResolver.php +++ b/src/Analyser/NodeScopeResolver.php @@ -206,6 +206,12 @@ class NodeScopeResolver /** @var array */ private array $calledMethodResults = []; + /** + * When processing a synthetic node on demand (for a Fiber request), real AST + * nodes contained in it were already processed and must not be processed again. + */ + protected bool $returnStoredExpressionResults = false; + /** * @param string[][] $earlyTerminatingMethodCalls className(string) => methods(string[]) * @param array $earlyTerminatingFunctionCalls @@ -1415,10 +1421,15 @@ public function processStmtNode( $throwPoints = []; $impurePoints = []; - $traitStorage = $storage->duplicate(); - $traitStorage->pendingFibers = []; + // fresh storage - the same trait node objects are processed once per + // using class and fibers must not see results from a previous pass + $traitStorage = new ExpressionResultStorage(); $this->processTraitUse($stmt, $scope, $traitStorage, $nodeCallback); $this->processPendingFibers($traitStorage); + + // class-level node callbacks (like ClassMethodsNode) are invoked with + // the outer storage but ask about expressions inside the used trait + $storage->mergeResults($traitStorage); } elseif ($stmt instanceof Foreach_) { if ($stmt->expr instanceof Variable && is_string($stmt->expr->name)) { $scope = $this->processVarAnnotation($scope, [$stmt->expr->name], $stmt); @@ -2750,6 +2761,13 @@ public function processExprNode( ExpressionContext $context, ): ExpressionResult { + if ($this->returnStoredExpressionResults) { + $storedResult = $storage->findExpressionResult($expr); + if ($storedResult !== null) { + return $storedResult; + } + } + if ($expr instanceof Expr\CallLike && $expr->isFirstClassCallable()) { if ($expr instanceof FuncCall) { $newExpr = new FunctionCallableNode($expr->name, $expr); @@ -3667,7 +3685,7 @@ public function processArgs( $impurePoints = array_merge($impurePoints, $closureResult->getImpurePoints()); } - $this->storeExpressionResult($storage, $arg->value, new ExpressionResult( + $this->storeExpressionResult($storage, $arg->value, $this->expressionResultFactory->create( $closureResult->getScope(), $scopeToPass, $arg->value, @@ -4708,6 +4726,11 @@ private function processNodesForTraitUse($node, ClassReflection $traitReflection throw new ShouldNotHappenException(); } $traitScope = $scope->enterTrait($traitReflection); + + // attribute args are not processed as part of the trait statements + // but rules like TraitAttributesRule ask about their types + $this->processAttributeGroups($node, $node->attrGroups, $traitScope, $storage, new NoopNodeCallback()); + $this->callNodeCallback($nodeCallback, new InTraitNode($node, $traitReflection, $scope->getClassReflection()), $traitScope, $storage); $this->processStmtNodesInternal($node, $stmts, $traitScope, $storage, $nodeCallback, StatementContext::createTopLevel()); return; From 83535c973f14cc2f33b0e5b03423fe79ba71abaa Mon Sep 17 00:00:00 2001 From: Ondrej Mirtes Date: Fri, 12 Jun 2026 11:52:11 +0200 Subject: [PATCH 006/113] Divide ExprHandler into TypeResolvingExprHandler Not all ExprHandlers will be TypeResolvingExprHandler coming into the future. Instead of resolveType+specifyTypes, they will pass callbacks into ExpressionResult doing similar job. --- src/Analyser/ExprHandler.php | 16 ---------- .../ExprHandler/ArrayDimFetchHandler.php | 6 ++-- src/Analyser/ExprHandler/ArrayHandler.php | 6 ++-- .../ExprHandler/ArrowFunctionHandler.php | 6 ++-- src/Analyser/ExprHandler/AssignHandler.php | 6 ++-- src/Analyser/ExprHandler/AssignOpHandler.php | 6 ++-- src/Analyser/ExprHandler/BinaryOpHandler.php | 6 ++-- .../ExprHandler/BitwiseNotHandler.php | 6 ++-- .../ExprHandler/BooleanAndHandler.php | 6 ++-- .../ExprHandler/BooleanNotHandler.php | 6 ++-- src/Analyser/ExprHandler/BooleanOrHandler.php | 6 ++-- src/Analyser/ExprHandler/CastHandler.php | 6 ++-- .../ExprHandler/CastStringHandler.php | 6 ++-- .../ExprHandler/ClassConstFetchHandler.php | 6 ++-- src/Analyser/ExprHandler/CloneHandler.php | 6 ++-- src/Analyser/ExprHandler/ClosureHandler.php | 6 ++-- src/Analyser/ExprHandler/CoalesceHandler.php | 6 ++-- .../ExprHandler/ConstFetchHandler.php | 6 ++-- src/Analyser/ExprHandler/EmptyHandler.php | 6 ++-- .../ExprHandler/ErrorSuppressHandler.php | 6 ++-- src/Analyser/ExprHandler/EvalHandler.php | 6 ++-- src/Analyser/ExprHandler/ExitHandler.php | 6 ++-- .../FirstClassCallableFuncCallHandler.php | 6 ++-- .../FirstClassCallableMethodCallHandler.php | 6 ++-- .../FirstClassCallableNewHandler.php | 6 ++-- .../FirstClassCallableStaticCallHandler.php | 6 ++-- src/Analyser/ExprHandler/FuncCallHandler.php | 6 ++-- src/Analyser/ExprHandler/IncludeHandler.php | 6 ++-- .../ExprHandler/InstanceofHandler.php | 6 ++-- .../ExprHandler/InterpolatedStringHandler.php | 6 ++-- src/Analyser/ExprHandler/IssetHandler.php | 6 ++-- src/Analyser/ExprHandler/MatchHandler.php | 6 ++-- .../ExprHandler/MethodCallHandler.php | 6 ++-- src/Analyser/ExprHandler/NewHandler.php | 6 ++-- .../ExprHandler/NullsafeMethodCallHandler.php | 6 ++-- .../NullsafePropertyFetchHandler.php | 6 ++-- src/Analyser/ExprHandler/PipeHandler.php | 6 ++-- src/Analyser/ExprHandler/PostDecHandler.php | 6 ++-- src/Analyser/ExprHandler/PostIncHandler.php | 6 ++-- src/Analyser/ExprHandler/PreDecHandler.php | 6 ++-- src/Analyser/ExprHandler/PreIncHandler.php | 6 ++-- src/Analyser/ExprHandler/PrintHandler.php | 6 ++-- .../ExprHandler/PropertyFetchHandler.php | 6 ++-- src/Analyser/ExprHandler/ScalarHandler.php | 6 ++-- .../ExprHandler/StaticCallHandler.php | 6 ++-- .../StaticPropertyFetchHandler.php | 6 ++-- src/Analyser/ExprHandler/TernaryHandler.php | 6 ++-- src/Analyser/ExprHandler/ThrowHandler.php | 6 ++-- .../ExprHandler/UnaryMinusHandler.php | 6 ++-- src/Analyser/ExprHandler/UnaryPlusHandler.php | 6 ++-- src/Analyser/ExprHandler/VariableHandler.php | 6 ++-- .../Virtual/AlwaysRememberedExprHandler.php | 6 ++-- .../Virtual/ExistingArrayDimFetchHandler.php | 6 ++-- .../Virtual/FunctionCallableNodeHandler.php | 6 ++-- .../Virtual/GetIterableKeyTypeExprHandler.php | 6 ++-- .../GetIterableValueTypeExprHandler.php | 6 ++-- .../Virtual/GetOffsetValueTypeExprHandler.php | 6 ++-- .../InstantiationCallableNodeHandler.php | 6 ++-- .../Virtual/MethodCallableNodeHandler.php | 6 ++-- .../Virtual/NativeTypeExprHandler.php | 6 ++-- .../OriginalPropertyTypeExprHandler.php | 6 ++-- .../SetExistingOffsetValueTypeExprHandler.php | 6 ++-- .../Virtual/SetOffsetValueTypeExprHandler.php | 6 ++-- .../StaticMethodCallableNodeHandler.php | 6 ++-- .../ExprHandler/Virtual/TypeExprHandler.php | 6 ++-- .../Virtual/UnsetOffsetExprHandler.php | 6 ++-- src/Analyser/ExprHandler/YieldFromHandler.php | 6 ++-- src/Analyser/ExprHandler/YieldHandler.php | 6 ++-- src/Analyser/MutatingScope.php | 3 ++ src/Analyser/TypeResolvingExprHandler.php | 30 +++++++++++++++++++ src/Analyser/TypeSpecifier.php | 3 ++ 71 files changed, 237 insertions(+), 217 deletions(-) create mode 100644 src/Analyser/TypeResolvingExprHandler.php diff --git a/src/Analyser/ExprHandler.php b/src/Analyser/ExprHandler.php index 98b8728ca40..7e93df1a6cf 100644 --- a/src/Analyser/ExprHandler.php +++ b/src/Analyser/ExprHandler.php @@ -5,7 +5,6 @@ use PhpParser\Node; use PhpParser\Node\Expr; use PhpParser\Node\Stmt; -use PHPStan\Type\Type; /** * @template T of Expr @@ -32,19 +31,4 @@ public function processExpr( ExpressionContext $context, ): ExpressionResult; - /** - * @param T $expr - */ - public function resolveType(MutatingScope $scope, Expr $expr): Type; - - /** - * @param T $expr - */ - public function specifyTypes( - TypeSpecifier $typeSpecifier, - Scope $scope, - Expr $expr, - TypeSpecifierContext $context, - ): SpecifiedTypes; - } diff --git a/src/Analyser/ExprHandler/ArrayDimFetchHandler.php b/src/Analyser/ExprHandler/ArrayDimFetchHandler.php index 0a0198cf3c2..14dec18a003 100644 --- a/src/Analyser/ExprHandler/ArrayDimFetchHandler.php +++ b/src/Analyser/ExprHandler/ArrayDimFetchHandler.php @@ -13,13 +13,13 @@ use PHPStan\Analyser\ExpressionResult; use PHPStan\Analyser\ExpressionResultFactory; use PHPStan\Analyser\ExpressionResultStorage; -use PHPStan\Analyser\ExprHandler; use PHPStan\Analyser\ExprHandler\Helper\NullsafeShortCircuitingHelper; use PHPStan\Analyser\MutatingScope; use PHPStan\Analyser\NodeScopeResolver; use PHPStan\Analyser\NoopNodeCallback; use PHPStan\Analyser\Scope; use PHPStan\Analyser\SpecifiedTypes; +use PHPStan\Analyser\TypeResolvingExprHandler; use PHPStan\Analyser\TypeSpecifier; use PHPStan\Analyser\TypeSpecifierContext; use PHPStan\DependencyInjection\AutowiredService; @@ -30,10 +30,10 @@ use function array_merge; /** - * @implements ExprHandler + * @implements TypeResolvingExprHandler */ #[AutowiredService] -final class ArrayDimFetchHandler implements ExprHandler +final class ArrayDimFetchHandler implements TypeResolvingExprHandler { public function __construct(private ExpressionResultFactory $expressionResultFactory) diff --git a/src/Analyser/ExprHandler/ArrayHandler.php b/src/Analyser/ExprHandler/ArrayHandler.php index a8e58b932a4..6fe74abef78 100644 --- a/src/Analyser/ExprHandler/ArrayHandler.php +++ b/src/Analyser/ExprHandler/ArrayHandler.php @@ -12,11 +12,11 @@ use PHPStan\Analyser\ExpressionResult; use PHPStan\Analyser\ExpressionResultFactory; use PHPStan\Analyser\ExpressionResultStorage; -use PHPStan\Analyser\ExprHandler; use PHPStan\Analyser\MutatingScope; use PHPStan\Analyser\NodeScopeResolver; use PHPStan\Analyser\Scope; use PHPStan\Analyser\SpecifiedTypes; +use PHPStan\Analyser\TypeResolvingExprHandler; use PHPStan\Analyser\TypeSpecifier; use PHPStan\Analyser\TypeSpecifierContext; use PHPStan\DependencyInjection\AutowiredService; @@ -30,10 +30,10 @@ use function count; /** - * @implements ExprHandler + * @implements TypeResolvingExprHandler */ #[AutowiredService] -final class ArrayHandler implements ExprHandler +final class ArrayHandler implements TypeResolvingExprHandler { public function __construct( diff --git a/src/Analyser/ExprHandler/ArrowFunctionHandler.php b/src/Analyser/ExprHandler/ArrowFunctionHandler.php index 5390ee8e212..da01eb8e39f 100644 --- a/src/Analyser/ExprHandler/ArrowFunctionHandler.php +++ b/src/Analyser/ExprHandler/ArrowFunctionHandler.php @@ -9,22 +9,22 @@ use PHPStan\Analyser\ExpressionResult; use PHPStan\Analyser\ExpressionResultFactory; use PHPStan\Analyser\ExpressionResultStorage; -use PHPStan\Analyser\ExprHandler; use PHPStan\Analyser\ExprHandler\Helper\ClosureTypeResolver; use PHPStan\Analyser\MutatingScope; use PHPStan\Analyser\NodeScopeResolver; use PHPStan\Analyser\Scope; use PHPStan\Analyser\SpecifiedTypes; +use PHPStan\Analyser\TypeResolvingExprHandler; use PHPStan\Analyser\TypeSpecifier; use PHPStan\Analyser\TypeSpecifierContext; use PHPStan\DependencyInjection\AutowiredService; use PHPStan\Type\Type; /** - * @implements ExprHandler + * @implements TypeResolvingExprHandler */ #[AutowiredService] -final class ArrowFunctionHandler implements ExprHandler +final class ArrowFunctionHandler implements TypeResolvingExprHandler { public function __construct( diff --git a/src/Analyser/ExprHandler/AssignHandler.php b/src/Analyser/ExprHandler/AssignHandler.php index 1cf5c4cc1c8..e0db4837ddc 100644 --- a/src/Analyser/ExprHandler/AssignHandler.php +++ b/src/Analyser/ExprHandler/AssignHandler.php @@ -28,7 +28,6 @@ use PHPStan\Analyser\ExpressionResultFactory; use PHPStan\Analyser\ExpressionResultStorage; use PHPStan\Analyser\ExpressionTypeHolder; -use PHPStan\Analyser\ExprHandler; use PHPStan\Analyser\ImpurePoint; use PHPStan\Analyser\InternalThrowPoint; use PHPStan\Analyser\MutatingScope; @@ -36,6 +35,7 @@ use PHPStan\Analyser\NoopNodeCallback; use PHPStan\Analyser\Scope; use PHPStan\Analyser\SpecifiedTypes; +use PHPStan\Analyser\TypeResolvingExprHandler; use PHPStan\Analyser\TypeSpecifier; use PHPStan\Analyser\TypeSpecifierContext; use PHPStan\DependencyInjection\AutowiredService; @@ -85,10 +85,10 @@ use function is_string; /** - * @implements ExprHandler + * @implements TypeResolvingExprHandler */ #[AutowiredService] -final class AssignHandler implements ExprHandler +final class AssignHandler implements TypeResolvingExprHandler { public function __construct( diff --git a/src/Analyser/ExprHandler/AssignOpHandler.php b/src/Analyser/ExprHandler/AssignOpHandler.php index 5ebcff7b679..26d17c102be 100644 --- a/src/Analyser/ExprHandler/AssignOpHandler.php +++ b/src/Analyser/ExprHandler/AssignOpHandler.php @@ -13,13 +13,13 @@ use PHPStan\Analyser\ExpressionResult; use PHPStan\Analyser\ExpressionResultFactory; use PHPStan\Analyser\ExpressionResultStorage; -use PHPStan\Analyser\ExprHandler; use PHPStan\Analyser\ExprHandler\Helper\ImplicitToStringCallHelper; use PHPStan\Analyser\InternalThrowPoint; use PHPStan\Analyser\MutatingScope; use PHPStan\Analyser\NodeScopeResolver; use PHPStan\Analyser\Scope; use PHPStan\Analyser\SpecifiedTypes; +use PHPStan\Analyser\TypeResolvingExprHandler; use PHPStan\Analyser\TypeSpecifier; use PHPStan\Analyser\TypeSpecifierContext; use PHPStan\DependencyInjection\AutowiredService; @@ -33,10 +33,10 @@ use function sprintf; /** - * @implements ExprHandler + * @implements TypeResolvingExprHandler */ #[AutowiredService] -final class AssignOpHandler implements ExprHandler +final class AssignOpHandler implements TypeResolvingExprHandler { public function __construct( diff --git a/src/Analyser/ExprHandler/BinaryOpHandler.php b/src/Analyser/ExprHandler/BinaryOpHandler.php index c3d686ba134..516f54780a0 100644 --- a/src/Analyser/ExprHandler/BinaryOpHandler.php +++ b/src/Analyser/ExprHandler/BinaryOpHandler.php @@ -16,7 +16,6 @@ use PHPStan\Analyser\ExpressionResult; use PHPStan\Analyser\ExpressionResultFactory; use PHPStan\Analyser\ExpressionResultStorage; -use PHPStan\Analyser\ExprHandler; use PHPStan\Analyser\ExprHandler\Helper\EqualityTypeSpecifyingHelper; use PHPStan\Analyser\ExprHandler\Helper\ImplicitToStringCallHelper; use PHPStan\Analyser\InternalThrowPoint; @@ -25,6 +24,7 @@ use PHPStan\Analyser\RicherScopeGetTypeHelper; use PHPStan\Analyser\Scope; use PHPStan\Analyser\SpecifiedTypes; +use PHPStan\Analyser\TypeResolvingExprHandler; use PHPStan\Analyser\TypeSpecifier; use PHPStan\Analyser\TypeSpecifierContext; use PHPStan\DependencyInjection\AutowiredService; @@ -54,10 +54,10 @@ use function strtolower; /** - * @implements ExprHandler + * @implements TypeResolvingExprHandler */ #[AutowiredService] -final class BinaryOpHandler implements ExprHandler +final class BinaryOpHandler implements TypeResolvingExprHandler { public function __construct( diff --git a/src/Analyser/ExprHandler/BitwiseNotHandler.php b/src/Analyser/ExprHandler/BitwiseNotHandler.php index 4b6b6667823..5efa2fdef6d 100644 --- a/src/Analyser/ExprHandler/BitwiseNotHandler.php +++ b/src/Analyser/ExprHandler/BitwiseNotHandler.php @@ -9,11 +9,11 @@ use PHPStan\Analyser\ExpressionResult; use PHPStan\Analyser\ExpressionResultFactory; use PHPStan\Analyser\ExpressionResultStorage; -use PHPStan\Analyser\ExprHandler; use PHPStan\Analyser\MutatingScope; use PHPStan\Analyser\NodeScopeResolver; use PHPStan\Analyser\Scope; use PHPStan\Analyser\SpecifiedTypes; +use PHPStan\Analyser\TypeResolvingExprHandler; use PHPStan\Analyser\TypeSpecifier; use PHPStan\Analyser\TypeSpecifierContext; use PHPStan\DependencyInjection\AutowiredService; @@ -21,10 +21,10 @@ use PHPStan\Type\Type; /** - * @implements ExprHandler + * @implements TypeResolvingExprHandler */ #[AutowiredService] -final class BitwiseNotHandler implements ExprHandler +final class BitwiseNotHandler implements TypeResolvingExprHandler { public function __construct( diff --git a/src/Analyser/ExprHandler/BooleanAndHandler.php b/src/Analyser/ExprHandler/BooleanAndHandler.php index 5263207b8cb..7ceb06af1bd 100644 --- a/src/Analyser/ExprHandler/BooleanAndHandler.php +++ b/src/Analyser/ExprHandler/BooleanAndHandler.php @@ -12,13 +12,13 @@ use PHPStan\Analyser\ExpressionResult; use PHPStan\Analyser\ExpressionResultFactory; use PHPStan\Analyser\ExpressionResultStorage; -use PHPStan\Analyser\ExprHandler; use PHPStan\Analyser\ExprHandler\Helper\ConditionalExpressionHolderHelper; use PHPStan\Analyser\MutatingScope; use PHPStan\Analyser\NodeScopeResolver; use PHPStan\Analyser\NoopNodeCallback; use PHPStan\Analyser\Scope; use PHPStan\Analyser\SpecifiedTypes; +use PHPStan\Analyser\TypeResolvingExprHandler; use PHPStan\Analyser\TypeSpecifier; use PHPStan\Analyser\TypeSpecifierContext; use PHPStan\DependencyInjection\AutowiredService; @@ -34,10 +34,10 @@ use function is_string; /** - * @implements ExprHandler + * @implements TypeResolvingExprHandler */ #[AutowiredService] -final class BooleanAndHandler implements ExprHandler +final class BooleanAndHandler implements TypeResolvingExprHandler { private const BOOLEAN_EXPRESSION_MAX_PROCESS_DEPTH = 4; diff --git a/src/Analyser/ExprHandler/BooleanNotHandler.php b/src/Analyser/ExprHandler/BooleanNotHandler.php index 59cb5a986c1..fc4f263faee 100644 --- a/src/Analyser/ExprHandler/BooleanNotHandler.php +++ b/src/Analyser/ExprHandler/BooleanNotHandler.php @@ -9,11 +9,11 @@ use PHPStan\Analyser\ExpressionResult; use PHPStan\Analyser\ExpressionResultFactory; use PHPStan\Analyser\ExpressionResultStorage; -use PHPStan\Analyser\ExprHandler; use PHPStan\Analyser\MutatingScope; use PHPStan\Analyser\NodeScopeResolver; use PHPStan\Analyser\Scope; use PHPStan\Analyser\SpecifiedTypes; +use PHPStan\Analyser\TypeResolvingExprHandler; use PHPStan\Analyser\TypeSpecifier; use PHPStan\Analyser\TypeSpecifierContext; use PHPStan\DependencyInjection\AutowiredService; @@ -22,10 +22,10 @@ use PHPStan\Type\Type; /** - * @implements ExprHandler + * @implements TypeResolvingExprHandler */ #[AutowiredService] -final class BooleanNotHandler implements ExprHandler +final class BooleanNotHandler implements TypeResolvingExprHandler { public function __construct(private ExpressionResultFactory $expressionResultFactory) diff --git a/src/Analyser/ExprHandler/BooleanOrHandler.php b/src/Analyser/ExprHandler/BooleanOrHandler.php index 9b1d34d1cc2..c0ffac43f45 100644 --- a/src/Analyser/ExprHandler/BooleanOrHandler.php +++ b/src/Analyser/ExprHandler/BooleanOrHandler.php @@ -10,13 +10,13 @@ use PHPStan\Analyser\ExpressionResult; use PHPStan\Analyser\ExpressionResultFactory; use PHPStan\Analyser\ExpressionResultStorage; -use PHPStan\Analyser\ExprHandler; use PHPStan\Analyser\ExprHandler\Helper\ConditionalExpressionHolderHelper; use PHPStan\Analyser\MutatingScope; use PHPStan\Analyser\NodeScopeResolver; use PHPStan\Analyser\NoopNodeCallback; use PHPStan\Analyser\Scope; use PHPStan\Analyser\SpecifiedTypes; +use PHPStan\Analyser\TypeResolvingExprHandler; use PHPStan\Analyser\TypeSpecifier; use PHPStan\Analyser\TypeSpecifierContext; use PHPStan\DependencyInjection\AutowiredService; @@ -33,10 +33,10 @@ use function count; /** - * @implements ExprHandler + * @implements TypeResolvingExprHandler */ #[AutowiredService] -final class BooleanOrHandler implements ExprHandler +final class BooleanOrHandler implements TypeResolvingExprHandler { private const BOOLEAN_EXPRESSION_MAX_PROCESS_DEPTH = 4; diff --git a/src/Analyser/ExprHandler/CastHandler.php b/src/Analyser/ExprHandler/CastHandler.php index 6877fdd5b3e..af928d21ed1 100644 --- a/src/Analyser/ExprHandler/CastHandler.php +++ b/src/Analyser/ExprHandler/CastHandler.php @@ -15,11 +15,11 @@ use PHPStan\Analyser\ExpressionResult; use PHPStan\Analyser\ExpressionResultFactory; use PHPStan\Analyser\ExpressionResultStorage; -use PHPStan\Analyser\ExprHandler; use PHPStan\Analyser\MutatingScope; use PHPStan\Analyser\NodeScopeResolver; use PHPStan\Analyser\Scope; use PHPStan\Analyser\SpecifiedTypes; +use PHPStan\Analyser\TypeResolvingExprHandler; use PHPStan\Analyser\TypeSpecifier; use PHPStan\Analyser\TypeSpecifierContext; use PHPStan\DependencyInjection\AutowiredService; @@ -28,10 +28,10 @@ use PHPStan\Type\Type; /** - * @implements ExprHandler + * @implements TypeResolvingExprHandler */ #[AutowiredService] -final class CastHandler implements ExprHandler +final class CastHandler implements TypeResolvingExprHandler { public function __construct( diff --git a/src/Analyser/ExprHandler/CastStringHandler.php b/src/Analyser/ExprHandler/CastStringHandler.php index bb13a0e2817..daa2c148be6 100644 --- a/src/Analyser/ExprHandler/CastStringHandler.php +++ b/src/Analyser/ExprHandler/CastStringHandler.php @@ -11,12 +11,12 @@ use PHPStan\Analyser\ExpressionResult; use PHPStan\Analyser\ExpressionResultFactory; use PHPStan\Analyser\ExpressionResultStorage; -use PHPStan\Analyser\ExprHandler; use PHPStan\Analyser\ExprHandler\Helper\ImplicitToStringCallHelper; use PHPStan\Analyser\MutatingScope; use PHPStan\Analyser\NodeScopeResolver; use PHPStan\Analyser\Scope; use PHPStan\Analyser\SpecifiedTypes; +use PHPStan\Analyser\TypeResolvingExprHandler; use PHPStan\Analyser\TypeSpecifier; use PHPStan\Analyser\TypeSpecifierContext; use PHPStan\DependencyInjection\AutowiredService; @@ -25,10 +25,10 @@ use function array_merge; /** - * @implements ExprHandler + * @implements TypeResolvingExprHandler */ #[AutowiredService] -final class CastStringHandler implements ExprHandler +final class CastStringHandler implements TypeResolvingExprHandler { public function __construct( diff --git a/src/Analyser/ExprHandler/ClassConstFetchHandler.php b/src/Analyser/ExprHandler/ClassConstFetchHandler.php index fe989f332c9..c00160c7c50 100644 --- a/src/Analyser/ExprHandler/ClassConstFetchHandler.php +++ b/src/Analyser/ExprHandler/ClassConstFetchHandler.php @@ -10,11 +10,11 @@ use PHPStan\Analyser\ExpressionResult; use PHPStan\Analyser\ExpressionResultFactory; use PHPStan\Analyser\ExpressionResultStorage; -use PHPStan\Analyser\ExprHandler; use PHPStan\Analyser\MutatingScope; use PHPStan\Analyser\NodeScopeResolver; use PHPStan\Analyser\Scope; use PHPStan\Analyser\SpecifiedTypes; +use PHPStan\Analyser\TypeResolvingExprHandler; use PHPStan\Analyser\TypeSpecifier; use PHPStan\Analyser\TypeSpecifierContext; use PHPStan\DependencyInjection\AutowiredService; @@ -24,10 +24,10 @@ use function array_merge; /** - * @implements ExprHandler + * @implements TypeResolvingExprHandler */ #[AutowiredService] -final class ClassConstFetchHandler implements ExprHandler +final class ClassConstFetchHandler implements TypeResolvingExprHandler { public function __construct( diff --git a/src/Analyser/ExprHandler/CloneHandler.php b/src/Analyser/ExprHandler/CloneHandler.php index bc707347411..caddb7d0889 100644 --- a/src/Analyser/ExprHandler/CloneHandler.php +++ b/src/Analyser/ExprHandler/CloneHandler.php @@ -9,12 +9,12 @@ use PHPStan\Analyser\ExpressionResult; use PHPStan\Analyser\ExpressionResultFactory; use PHPStan\Analyser\ExpressionResultStorage; -use PHPStan\Analyser\ExprHandler; use PHPStan\Analyser\MutatingScope; use PHPStan\Analyser\NodeScopeResolver; use PHPStan\Analyser\Scope; use PHPStan\Analyser\SpecifiedTypes; use PHPStan\Analyser\Traverser\CloneTypeTraverser; +use PHPStan\Analyser\TypeResolvingExprHandler; use PHPStan\Analyser\TypeSpecifier; use PHPStan\Analyser\TypeSpecifierContext; use PHPStan\DependencyInjection\AutowiredService; @@ -24,10 +24,10 @@ use PHPStan\Type\TypeTraverser; /** - * @implements ExprHandler + * @implements TypeResolvingExprHandler */ #[AutowiredService] -final class CloneHandler implements ExprHandler +final class CloneHandler implements TypeResolvingExprHandler { public function __construct(private ExpressionResultFactory $expressionResultFactory) diff --git a/src/Analyser/ExprHandler/ClosureHandler.php b/src/Analyser/ExprHandler/ClosureHandler.php index b107c5843c6..65e801d6350 100644 --- a/src/Analyser/ExprHandler/ClosureHandler.php +++ b/src/Analyser/ExprHandler/ClosureHandler.php @@ -9,22 +9,22 @@ use PHPStan\Analyser\ExpressionResult; use PHPStan\Analyser\ExpressionResultFactory; use PHPStan\Analyser\ExpressionResultStorage; -use PHPStan\Analyser\ExprHandler; use PHPStan\Analyser\ExprHandler\Helper\ClosureTypeResolver; use PHPStan\Analyser\MutatingScope; use PHPStan\Analyser\NodeScopeResolver; use PHPStan\Analyser\Scope; use PHPStan\Analyser\SpecifiedTypes; +use PHPStan\Analyser\TypeResolvingExprHandler; use PHPStan\Analyser\TypeSpecifier; use PHPStan\Analyser\TypeSpecifierContext; use PHPStan\DependencyInjection\AutowiredService; use PHPStan\Type\Type; /** - * @implements ExprHandler + * @implements TypeResolvingExprHandler */ #[AutowiredService] -final class ClosureHandler implements ExprHandler +final class ClosureHandler implements TypeResolvingExprHandler { public function __construct( diff --git a/src/Analyser/ExprHandler/CoalesceHandler.php b/src/Analyser/ExprHandler/CoalesceHandler.php index f47554f2811..fa8cc9fcbc8 100644 --- a/src/Analyser/ExprHandler/CoalesceHandler.php +++ b/src/Analyser/ExprHandler/CoalesceHandler.php @@ -9,12 +9,12 @@ use PHPStan\Analyser\ExpressionResult; use PHPStan\Analyser\ExpressionResultFactory; use PHPStan\Analyser\ExpressionResultStorage; -use PHPStan\Analyser\ExprHandler; use PHPStan\Analyser\ExprHandler\Helper\NonNullabilityHelper; use PHPStan\Analyser\MutatingScope; use PHPStan\Analyser\NodeScopeResolver; use PHPStan\Analyser\Scope; use PHPStan\Analyser\SpecifiedTypes; +use PHPStan\Analyser\TypeResolvingExprHandler; use PHPStan\Analyser\TypeSpecifier; use PHPStan\Analyser\TypeSpecifierContext; use PHPStan\DependencyInjection\AutowiredService; @@ -27,10 +27,10 @@ use function array_merge; /** - * @implements ExprHandler + * @implements TypeResolvingExprHandler */ #[AutowiredService] -final class CoalesceHandler implements ExprHandler +final class CoalesceHandler implements TypeResolvingExprHandler { public function __construct( diff --git a/src/Analyser/ExprHandler/ConstFetchHandler.php b/src/Analyser/ExprHandler/ConstFetchHandler.php index 17f429322e0..6f1518f163c 100644 --- a/src/Analyser/ExprHandler/ConstFetchHandler.php +++ b/src/Analyser/ExprHandler/ConstFetchHandler.php @@ -11,11 +11,11 @@ use PHPStan\Analyser\ExpressionResult; use PHPStan\Analyser\ExpressionResultFactory; use PHPStan\Analyser\ExpressionResultStorage; -use PHPStan\Analyser\ExprHandler; use PHPStan\Analyser\MutatingScope; use PHPStan\Analyser\NodeScopeResolver; use PHPStan\Analyser\Scope; use PHPStan\Analyser\SpecifiedTypes; +use PHPStan\Analyser\TypeResolvingExprHandler; use PHPStan\Analyser\TypeSpecifier; use PHPStan\Analyser\TypeSpecifierContext; use PHPStan\DependencyInjection\AutowiredService; @@ -26,10 +26,10 @@ use function strtolower; /** - * @implements ExprHandler + * @implements TypeResolvingExprHandler */ #[AutowiredService] -final class ConstFetchHandler implements ExprHandler +final class ConstFetchHandler implements TypeResolvingExprHandler { public function __construct( diff --git a/src/Analyser/ExprHandler/EmptyHandler.php b/src/Analyser/ExprHandler/EmptyHandler.php index 54e9c397fa1..1939315de1b 100644 --- a/src/Analyser/ExprHandler/EmptyHandler.php +++ b/src/Analyser/ExprHandler/EmptyHandler.php @@ -10,12 +10,12 @@ use PHPStan\Analyser\ExpressionResult; use PHPStan\Analyser\ExpressionResultFactory; use PHPStan\Analyser\ExpressionResultStorage; -use PHPStan\Analyser\ExprHandler; use PHPStan\Analyser\ExprHandler\Helper\NonNullabilityHelper; use PHPStan\Analyser\MutatingScope; use PHPStan\Analyser\NodeScopeResolver; use PHPStan\Analyser\Scope; use PHPStan\Analyser\SpecifiedTypes; +use PHPStan\Analyser\TypeResolvingExprHandler; use PHPStan\Analyser\TypeSpecifier; use PHPStan\Analyser\TypeSpecifierContext; use PHPStan\DependencyInjection\AutowiredService; @@ -25,10 +25,10 @@ use PHPStan\Type\Type; /** - * @implements ExprHandler + * @implements TypeResolvingExprHandler */ #[AutowiredService] -final class EmptyHandler implements ExprHandler +final class EmptyHandler implements TypeResolvingExprHandler { public function __construct( diff --git a/src/Analyser/ExprHandler/ErrorSuppressHandler.php b/src/Analyser/ExprHandler/ErrorSuppressHandler.php index ca006ebcedb..25bfa07bd36 100644 --- a/src/Analyser/ExprHandler/ErrorSuppressHandler.php +++ b/src/Analyser/ExprHandler/ErrorSuppressHandler.php @@ -9,21 +9,21 @@ use PHPStan\Analyser\ExpressionResult; use PHPStan\Analyser\ExpressionResultFactory; use PHPStan\Analyser\ExpressionResultStorage; -use PHPStan\Analyser\ExprHandler; use PHPStan\Analyser\MutatingScope; use PHPStan\Analyser\NodeScopeResolver; use PHPStan\Analyser\Scope; use PHPStan\Analyser\SpecifiedTypes; +use PHPStan\Analyser\TypeResolvingExprHandler; use PHPStan\Analyser\TypeSpecifier; use PHPStan\Analyser\TypeSpecifierContext; use PHPStan\DependencyInjection\AutowiredService; use PHPStan\Type\Type; /** - * @implements ExprHandler + * @implements TypeResolvingExprHandler */ #[AutowiredService] -final class ErrorSuppressHandler implements ExprHandler +final class ErrorSuppressHandler implements TypeResolvingExprHandler { public function __construct(private ExpressionResultFactory $expressionResultFactory) diff --git a/src/Analyser/ExprHandler/EvalHandler.php b/src/Analyser/ExprHandler/EvalHandler.php index 9bbbbe9fbd2..d4425781435 100644 --- a/src/Analyser/ExprHandler/EvalHandler.php +++ b/src/Analyser/ExprHandler/EvalHandler.php @@ -9,13 +9,13 @@ use PHPStan\Analyser\ExpressionResult; use PHPStan\Analyser\ExpressionResultFactory; use PHPStan\Analyser\ExpressionResultStorage; -use PHPStan\Analyser\ExprHandler; use PHPStan\Analyser\ImpurePoint; use PHPStan\Analyser\InternalThrowPoint; use PHPStan\Analyser\MutatingScope; use PHPStan\Analyser\NodeScopeResolver; use PHPStan\Analyser\Scope; use PHPStan\Analyser\SpecifiedTypes; +use PHPStan\Analyser\TypeResolvingExprHandler; use PHPStan\Analyser\TypeSpecifier; use PHPStan\Analyser\TypeSpecifierContext; use PHPStan\DependencyInjection\AutowiredService; @@ -24,10 +24,10 @@ use function array_merge; /** - * @implements ExprHandler + * @implements TypeResolvingExprHandler */ #[AutowiredService] -final class EvalHandler implements ExprHandler +final class EvalHandler implements TypeResolvingExprHandler { public function __construct(private ExpressionResultFactory $expressionResultFactory) diff --git a/src/Analyser/ExprHandler/ExitHandler.php b/src/Analyser/ExprHandler/ExitHandler.php index 7c1029c14e2..c459f38dc11 100644 --- a/src/Analyser/ExprHandler/ExitHandler.php +++ b/src/Analyser/ExprHandler/ExitHandler.php @@ -9,12 +9,12 @@ use PHPStan\Analyser\ExpressionResult; use PHPStan\Analyser\ExpressionResultFactory; use PHPStan\Analyser\ExpressionResultStorage; -use PHPStan\Analyser\ExprHandler; use PHPStan\Analyser\ImpurePoint; use PHPStan\Analyser\MutatingScope; use PHPStan\Analyser\NodeScopeResolver; use PHPStan\Analyser\Scope; use PHPStan\Analyser\SpecifiedTypes; +use PHPStan\Analyser\TypeResolvingExprHandler; use PHPStan\Analyser\TypeSpecifier; use PHPStan\Analyser\TypeSpecifierContext; use PHPStan\DependencyInjection\AutowiredService; @@ -23,10 +23,10 @@ use function array_merge; /** - * @implements ExprHandler + * @implements TypeResolvingExprHandler */ #[AutowiredService] -final class ExitHandler implements ExprHandler +final class ExitHandler implements TypeResolvingExprHandler { public function __construct(private ExpressionResultFactory $expressionResultFactory) diff --git a/src/Analyser/ExprHandler/FirstClassCallableFuncCallHandler.php b/src/Analyser/ExprHandler/FirstClassCallableFuncCallHandler.php index 266996eaeb2..2fafbee4dbe 100644 --- a/src/Analyser/ExprHandler/FirstClassCallableFuncCallHandler.php +++ b/src/Analyser/ExprHandler/FirstClassCallableFuncCallHandler.php @@ -9,11 +9,11 @@ use PHPStan\Analyser\ExpressionContext; use PHPStan\Analyser\ExpressionResult; use PHPStan\Analyser\ExpressionResultStorage; -use PHPStan\Analyser\ExprHandler; use PHPStan\Analyser\MutatingScope; use PHPStan\Analyser\NodeScopeResolver; use PHPStan\Analyser\Scope; use PHPStan\Analyser\SpecifiedTypes; +use PHPStan\Analyser\TypeResolvingExprHandler; use PHPStan\Analyser\TypeSpecifier; use PHPStan\Analyser\TypeSpecifierContext; use PHPStan\DependencyInjection\AutowiredService; @@ -24,10 +24,10 @@ use PHPStan\Type\Type; /** - * @implements ExprHandler + * @implements TypeResolvingExprHandler */ #[AutowiredService] -final class FirstClassCallableFuncCallHandler implements ExprHandler +final class FirstClassCallableFuncCallHandler implements TypeResolvingExprHandler { public function __construct( diff --git a/src/Analyser/ExprHandler/FirstClassCallableMethodCallHandler.php b/src/Analyser/ExprHandler/FirstClassCallableMethodCallHandler.php index 1cafdd5b120..e487de41fb6 100644 --- a/src/Analyser/ExprHandler/FirstClassCallableMethodCallHandler.php +++ b/src/Analyser/ExprHandler/FirstClassCallableMethodCallHandler.php @@ -10,11 +10,11 @@ use PHPStan\Analyser\ExpressionContext; use PHPStan\Analyser\ExpressionResult; use PHPStan\Analyser\ExpressionResultStorage; -use PHPStan\Analyser\ExprHandler; use PHPStan\Analyser\MutatingScope; use PHPStan\Analyser\NodeScopeResolver; use PHPStan\Analyser\Scope; use PHPStan\Analyser\SpecifiedTypes; +use PHPStan\Analyser\TypeResolvingExprHandler; use PHPStan\Analyser\TypeSpecifier; use PHPStan\Analyser\TypeSpecifierContext; use PHPStan\DependencyInjection\AutowiredService; @@ -24,10 +24,10 @@ use PHPStan\Type\Type; /** - * @implements ExprHandler + * @implements TypeResolvingExprHandler */ #[AutowiredService] -final class FirstClassCallableMethodCallHandler implements ExprHandler +final class FirstClassCallableMethodCallHandler implements TypeResolvingExprHandler { public function __construct( diff --git a/src/Analyser/ExprHandler/FirstClassCallableNewHandler.php b/src/Analyser/ExprHandler/FirstClassCallableNewHandler.php index e158a8cc7b8..9f5e05c198d 100644 --- a/src/Analyser/ExprHandler/FirstClassCallableNewHandler.php +++ b/src/Analyser/ExprHandler/FirstClassCallableNewHandler.php @@ -9,11 +9,11 @@ use PHPStan\Analyser\ExpressionContext; use PHPStan\Analyser\ExpressionResult; use PHPStan\Analyser\ExpressionResultStorage; -use PHPStan\Analyser\ExprHandler; use PHPStan\Analyser\MutatingScope; use PHPStan\Analyser\NodeScopeResolver; use PHPStan\Analyser\Scope; use PHPStan\Analyser\SpecifiedTypes; +use PHPStan\Analyser\TypeResolvingExprHandler; use PHPStan\Analyser\TypeSpecifier; use PHPStan\Analyser\TypeSpecifierContext; use PHPStan\DependencyInjection\AutowiredService; @@ -23,10 +23,10 @@ use PHPStan\Type\Type; /** - * @implements ExprHandler + * @implements TypeResolvingExprHandler */ #[AutowiredService] -final class FirstClassCallableNewHandler implements ExprHandler +final class FirstClassCallableNewHandler implements TypeResolvingExprHandler { public function __construct( diff --git a/src/Analyser/ExprHandler/FirstClassCallableStaticCallHandler.php b/src/Analyser/ExprHandler/FirstClassCallableStaticCallHandler.php index 4d3519cf944..4a5c3cd0645 100644 --- a/src/Analyser/ExprHandler/FirstClassCallableStaticCallHandler.php +++ b/src/Analyser/ExprHandler/FirstClassCallableStaticCallHandler.php @@ -8,11 +8,11 @@ use PHPStan\Analyser\ExpressionContext; use PHPStan\Analyser\ExpressionResult; use PHPStan\Analyser\ExpressionResultStorage; -use PHPStan\Analyser\ExprHandler; use PHPStan\Analyser\MutatingScope; use PHPStan\Analyser\NodeScopeResolver; use PHPStan\Analyser\Scope; use PHPStan\Analyser\SpecifiedTypes; +use PHPStan\Analyser\TypeResolvingExprHandler; use PHPStan\Analyser\TypeSpecifier; use PHPStan\Analyser\TypeSpecifierContext; use PHPStan\DependencyInjection\AutowiredService; @@ -22,10 +22,10 @@ use PHPStan\Type\Type; /** - * @implements ExprHandler + * @implements TypeResolvingExprHandler */ #[AutowiredService] -final class FirstClassCallableStaticCallHandler implements ExprHandler +final class FirstClassCallableStaticCallHandler implements TypeResolvingExprHandler { public function __construct( diff --git a/src/Analyser/ExprHandler/FuncCallHandler.php b/src/Analyser/ExprHandler/FuncCallHandler.php index ae3d6fc94a6..dbc496274ed 100644 --- a/src/Analyser/ExprHandler/FuncCallHandler.php +++ b/src/Analyser/ExprHandler/FuncCallHandler.php @@ -18,7 +18,6 @@ use PHPStan\Analyser\ExpressionResult; use PHPStan\Analyser\ExpressionResultFactory; use PHPStan\Analyser\ExpressionResultStorage; -use PHPStan\Analyser\ExprHandler; use PHPStan\Analyser\ExprHandler\Helper\VoidToNullTypeTransformer; use PHPStan\Analyser\ImpurePoint; use PHPStan\Analyser\InternalThrowPoint; @@ -27,6 +26,7 @@ use PHPStan\Analyser\NoopNodeCallback; use PHPStan\Analyser\Scope; use PHPStan\Analyser\SpecifiedTypes; +use PHPStan\Analyser\TypeResolvingExprHandler; use PHPStan\Analyser\TypeSpecifier; use PHPStan\Analyser\TypeSpecifierContext; use PHPStan\DependencyInjection\AutowiredParameter; @@ -82,10 +82,10 @@ use function str_starts_with; /** - * @implements ExprHandler + * @implements TypeResolvingExprHandler */ #[AutowiredService] -final class FuncCallHandler implements ExprHandler +final class FuncCallHandler implements TypeResolvingExprHandler { public function __construct( diff --git a/src/Analyser/ExprHandler/IncludeHandler.php b/src/Analyser/ExprHandler/IncludeHandler.php index 2a0dd18b37f..528d2f0bd1d 100644 --- a/src/Analyser/ExprHandler/IncludeHandler.php +++ b/src/Analyser/ExprHandler/IncludeHandler.php @@ -9,13 +9,13 @@ use PHPStan\Analyser\ExpressionResult; use PHPStan\Analyser\ExpressionResultFactory; use PHPStan\Analyser\ExpressionResultStorage; -use PHPStan\Analyser\ExprHandler; use PHPStan\Analyser\ImpurePoint; use PHPStan\Analyser\InternalThrowPoint; use PHPStan\Analyser\MutatingScope; use PHPStan\Analyser\NodeScopeResolver; use PHPStan\Analyser\Scope; use PHPStan\Analyser\SpecifiedTypes; +use PHPStan\Analyser\TypeResolvingExprHandler; use PHPStan\Analyser\TypeSpecifier; use PHPStan\Analyser\TypeSpecifierContext; use PHPStan\DependencyInjection\AutowiredService; @@ -25,10 +25,10 @@ use function in_array; /** - * @implements ExprHandler + * @implements TypeResolvingExprHandler */ #[AutowiredService] -final class IncludeHandler implements ExprHandler +final class IncludeHandler implements TypeResolvingExprHandler { public function __construct(private ExpressionResultFactory $expressionResultFactory) diff --git a/src/Analyser/ExprHandler/InstanceofHandler.php b/src/Analyser/ExprHandler/InstanceofHandler.php index b5288696912..885a418f846 100644 --- a/src/Analyser/ExprHandler/InstanceofHandler.php +++ b/src/Analyser/ExprHandler/InstanceofHandler.php @@ -10,11 +10,11 @@ use PHPStan\Analyser\ExpressionResult; use PHPStan\Analyser\ExpressionResultFactory; use PHPStan\Analyser\ExpressionResultStorage; -use PHPStan\Analyser\ExprHandler; use PHPStan\Analyser\MutatingScope; use PHPStan\Analyser\NodeScopeResolver; use PHPStan\Analyser\Scope; use PHPStan\Analyser\SpecifiedTypes; +use PHPStan\Analyser\TypeResolvingExprHandler; use PHPStan\Analyser\TypeSpecifier; use PHPStan\Analyser\TypeSpecifierContext; use PHPStan\DependencyInjection\AutowiredService; @@ -33,10 +33,10 @@ use function strtolower; /** - * @implements ExprHandler + * @implements TypeResolvingExprHandler */ #[AutowiredService] -final class InstanceofHandler implements ExprHandler +final class InstanceofHandler implements TypeResolvingExprHandler { public function __construct(private ExpressionResultFactory $expressionResultFactory) diff --git a/src/Analyser/ExprHandler/InterpolatedStringHandler.php b/src/Analyser/ExprHandler/InterpolatedStringHandler.php index cdf2bdba301..75547f573bb 100644 --- a/src/Analyser/ExprHandler/InterpolatedStringHandler.php +++ b/src/Analyser/ExprHandler/InterpolatedStringHandler.php @@ -10,12 +10,12 @@ use PHPStan\Analyser\ExpressionResult; use PHPStan\Analyser\ExpressionResultFactory; use PHPStan\Analyser\ExpressionResultStorage; -use PHPStan\Analyser\ExprHandler; use PHPStan\Analyser\ExprHandler\Helper\ImplicitToStringCallHelper; use PHPStan\Analyser\MutatingScope; use PHPStan\Analyser\NodeScopeResolver; use PHPStan\Analyser\Scope; use PHPStan\Analyser\SpecifiedTypes; +use PHPStan\Analyser\TypeResolvingExprHandler; use PHPStan\Analyser\TypeSpecifier; use PHPStan\Analyser\TypeSpecifierContext; use PHPStan\DependencyInjection\AutowiredService; @@ -25,10 +25,10 @@ use function array_merge; /** - * @implements ExprHandler + * @implements TypeResolvingExprHandler */ #[AutowiredService] -final class InterpolatedStringHandler implements ExprHandler +final class InterpolatedStringHandler implements TypeResolvingExprHandler { public function __construct( diff --git a/src/Analyser/ExprHandler/IssetHandler.php b/src/Analyser/ExprHandler/IssetHandler.php index 9fc6c135a1c..7d379b16556 100644 --- a/src/Analyser/ExprHandler/IssetHandler.php +++ b/src/Analyser/ExprHandler/IssetHandler.php @@ -17,13 +17,13 @@ use PHPStan\Analyser\ExpressionResult; use PHPStan\Analyser\ExpressionResultFactory; use PHPStan\Analyser\ExpressionResultStorage; -use PHPStan\Analyser\ExprHandler; use PHPStan\Analyser\ExprHandler\Helper\NonNullabilityHelper; use PHPStan\Analyser\MutatingScope; use PHPStan\Analyser\NodeScopeResolver; use PHPStan\Analyser\NoopNodeCallback; use PHPStan\Analyser\Scope; use PHPStan\Analyser\SpecifiedTypes; +use PHPStan\Analyser\TypeResolvingExprHandler; use PHPStan\Analyser\TypeSpecifier; use PHPStan\Analyser\TypeSpecifierContext; use PHPStan\DependencyInjection\AutowiredService; @@ -52,10 +52,10 @@ use function is_string; /** - * @implements ExprHandler + * @implements TypeResolvingExprHandler */ #[AutowiredService] -final class IssetHandler implements ExprHandler +final class IssetHandler implements TypeResolvingExprHandler { public function __construct( diff --git a/src/Analyser/ExprHandler/MatchHandler.php b/src/Analyser/ExprHandler/MatchHandler.php index 66fd854eef4..88e80239efa 100644 --- a/src/Analyser/ExprHandler/MatchHandler.php +++ b/src/Analyser/ExprHandler/MatchHandler.php @@ -18,12 +18,12 @@ use PHPStan\Analyser\ExpressionResult; use PHPStan\Analyser\ExpressionResultFactory; use PHPStan\Analyser\ExpressionResultStorage; -use PHPStan\Analyser\ExprHandler; use PHPStan\Analyser\InternalThrowPoint; use PHPStan\Analyser\MutatingScope; use PHPStan\Analyser\NodeScopeResolver; use PHPStan\Analyser\Scope; use PHPStan\Analyser\SpecifiedTypes; +use PHPStan\Analyser\TypeResolvingExprHandler; use PHPStan\Analyser\TypeSpecifier; use PHPStan\Analyser\TypeSpecifierContext; use PHPStan\DependencyInjection\AutowiredParameter; @@ -49,10 +49,10 @@ use const SORT_NUMERIC; /** - * @implements ExprHandler + * @implements TypeResolvingExprHandler */ #[AutowiredService] -final class MatchHandler implements ExprHandler +final class MatchHandler implements TypeResolvingExprHandler { public function __construct( diff --git a/src/Analyser/ExprHandler/MethodCallHandler.php b/src/Analyser/ExprHandler/MethodCallHandler.php index 7de23d146cb..2fdc2afb877 100644 --- a/src/Analyser/ExprHandler/MethodCallHandler.php +++ b/src/Analyser/ExprHandler/MethodCallHandler.php @@ -13,7 +13,6 @@ use PHPStan\Analyser\ExpressionResult; use PHPStan\Analyser\ExpressionResultFactory; use PHPStan\Analyser\ExpressionResultStorage; -use PHPStan\Analyser\ExprHandler; use PHPStan\Analyser\ExprHandler\Helper\MethodCallReturnTypeHelper; use PHPStan\Analyser\ExprHandler\Helper\MethodThrowPointHelper; use PHPStan\Analyser\ExprHandler\Helper\NullsafeShortCircuitingHelper; @@ -23,6 +22,7 @@ use PHPStan\Analyser\NodeScopeResolver; use PHPStan\Analyser\Scope; use PHPStan\Analyser\SpecifiedTypes; +use PHPStan\Analyser\TypeResolvingExprHandler; use PHPStan\Analyser\TypeSpecifier; use PHPStan\Analyser\TypeSpecifierContext; use PHPStan\DependencyInjection\AutowiredParameter; @@ -49,10 +49,10 @@ use function strtolower; /** - * @implements ExprHandler + * @implements TypeResolvingExprHandler */ #[AutowiredService] -final class MethodCallHandler implements ExprHandler +final class MethodCallHandler implements TypeResolvingExprHandler { public function __construct( diff --git a/src/Analyser/ExprHandler/NewHandler.php b/src/Analyser/ExprHandler/NewHandler.php index a70924f2118..d96d297d4f6 100644 --- a/src/Analyser/ExprHandler/NewHandler.php +++ b/src/Analyser/ExprHandler/NewHandler.php @@ -13,7 +13,6 @@ use PHPStan\Analyser\ExpressionResult; use PHPStan\Analyser\ExpressionResultFactory; use PHPStan\Analyser\ExpressionResultStorage; -use PHPStan\Analyser\ExprHandler; use PHPStan\Analyser\ImpurePoint; use PHPStan\Analyser\InternalThrowPoint; use PHPStan\Analyser\MutatingScope; @@ -25,6 +24,7 @@ use PHPStan\Analyser\ThrowPoint; use PHPStan\Analyser\Traverser\ConstructorClassTemplateTraverser; use PHPStan\Analyser\Traverser\GenericTypeTemplateTraverser; +use PHPStan\Analyser\TypeResolvingExprHandler; use PHPStan\Analyser\TypeSpecifier; use PHPStan\Analyser\TypeSpecifierContext; use PHPStan\DependencyInjection\AutowiredParameter; @@ -65,10 +65,10 @@ use function sprintf; /** - * @implements ExprHandler + * @implements TypeResolvingExprHandler */ #[AutowiredService] -final class NewHandler implements ExprHandler +final class NewHandler implements TypeResolvingExprHandler { public function __construct( diff --git a/src/Analyser/ExprHandler/NullsafeMethodCallHandler.php b/src/Analyser/ExprHandler/NullsafeMethodCallHandler.php index 215c1d6b497..b5db844ed26 100644 --- a/src/Analyser/ExprHandler/NullsafeMethodCallHandler.php +++ b/src/Analyser/ExprHandler/NullsafeMethodCallHandler.php @@ -14,12 +14,12 @@ use PHPStan\Analyser\ExpressionResult; use PHPStan\Analyser\ExpressionResultFactory; use PHPStan\Analyser\ExpressionResultStorage; -use PHPStan\Analyser\ExprHandler; use PHPStan\Analyser\ExprHandler\Helper\NonNullabilityHelper; use PHPStan\Analyser\MutatingScope; use PHPStan\Analyser\NodeScopeResolver; use PHPStan\Analyser\Scope; use PHPStan\Analyser\SpecifiedTypes; +use PHPStan\Analyser\TypeResolvingExprHandler; use PHPStan\Analyser\TypeSpecifier; use PHPStan\Analyser\TypeSpecifierContext; use PHPStan\DependencyInjection\AutowiredService; @@ -30,10 +30,10 @@ use function array_merge; /** - * @implements ExprHandler + * @implements TypeResolvingExprHandler */ #[AutowiredService] -final class NullsafeMethodCallHandler implements ExprHandler +final class NullsafeMethodCallHandler implements TypeResolvingExprHandler { public function __construct( diff --git a/src/Analyser/ExprHandler/NullsafePropertyFetchHandler.php b/src/Analyser/ExprHandler/NullsafePropertyFetchHandler.php index b5d253da2b0..017a57b723d 100644 --- a/src/Analyser/ExprHandler/NullsafePropertyFetchHandler.php +++ b/src/Analyser/ExprHandler/NullsafePropertyFetchHandler.php @@ -14,12 +14,12 @@ use PHPStan\Analyser\ExpressionResult; use PHPStan\Analyser\ExpressionResultFactory; use PHPStan\Analyser\ExpressionResultStorage; -use PHPStan\Analyser\ExprHandler; use PHPStan\Analyser\ExprHandler\Helper\NonNullabilityHelper; use PHPStan\Analyser\MutatingScope; use PHPStan\Analyser\NodeScopeResolver; use PHPStan\Analyser\Scope; use PHPStan\Analyser\SpecifiedTypes; +use PHPStan\Analyser\TypeResolvingExprHandler; use PHPStan\Analyser\TypeSpecifier; use PHPStan\Analyser\TypeSpecifierContext; use PHPStan\DependencyInjection\AutowiredService; @@ -30,10 +30,10 @@ use function array_merge; /** - * @implements ExprHandler + * @implements TypeResolvingExprHandler */ #[AutowiredService] -final class NullsafePropertyFetchHandler implements ExprHandler +final class NullsafePropertyFetchHandler implements TypeResolvingExprHandler { public function __construct( diff --git a/src/Analyser/ExprHandler/PipeHandler.php b/src/Analyser/ExprHandler/PipeHandler.php index a10009c2d56..45a5e23322d 100644 --- a/src/Analyser/ExprHandler/PipeHandler.php +++ b/src/Analyser/ExprHandler/PipeHandler.php @@ -13,11 +13,11 @@ use PHPStan\Analyser\ExpressionResult; use PHPStan\Analyser\ExpressionResultFactory; use PHPStan\Analyser\ExpressionResultStorage; -use PHPStan\Analyser\ExprHandler; use PHPStan\Analyser\MutatingScope; use PHPStan\Analyser\NodeScopeResolver; use PHPStan\Analyser\Scope; use PHPStan\Analyser\SpecifiedTypes; +use PHPStan\Analyser\TypeResolvingExprHandler; use PHPStan\Analyser\TypeSpecifier; use PHPStan\Analyser\TypeSpecifierContext; use PHPStan\DependencyInjection\AutowiredService; @@ -27,10 +27,10 @@ use function array_merge; /** - * @implements ExprHandler + * @implements TypeResolvingExprHandler */ #[AutowiredService] -final class PipeHandler implements ExprHandler +final class PipeHandler implements TypeResolvingExprHandler { public function __construct(private ExpressionResultFactory $expressionResultFactory) diff --git a/src/Analyser/ExprHandler/PostDecHandler.php b/src/Analyser/ExprHandler/PostDecHandler.php index ecdf3bd84d8..0cff682498c 100644 --- a/src/Analyser/ExprHandler/PostDecHandler.php +++ b/src/Analyser/ExprHandler/PostDecHandler.php @@ -10,21 +10,21 @@ use PHPStan\Analyser\ExpressionResult; use PHPStan\Analyser\ExpressionResultFactory; use PHPStan\Analyser\ExpressionResultStorage; -use PHPStan\Analyser\ExprHandler; use PHPStan\Analyser\MutatingScope; use PHPStan\Analyser\NodeScopeResolver; use PHPStan\Analyser\Scope; use PHPStan\Analyser\SpecifiedTypes; +use PHPStan\Analyser\TypeResolvingExprHandler; use PHPStan\Analyser\TypeSpecifier; use PHPStan\Analyser\TypeSpecifierContext; use PHPStan\DependencyInjection\AutowiredService; use PHPStan\Type\Type; /** - * @implements ExprHandler + * @implements TypeResolvingExprHandler */ #[AutowiredService] -final class PostDecHandler implements ExprHandler +final class PostDecHandler implements TypeResolvingExprHandler { public function __construct(private ExpressionResultFactory $expressionResultFactory) diff --git a/src/Analyser/ExprHandler/PostIncHandler.php b/src/Analyser/ExprHandler/PostIncHandler.php index 9a68af90336..a04b0cfea75 100644 --- a/src/Analyser/ExprHandler/PostIncHandler.php +++ b/src/Analyser/ExprHandler/PostIncHandler.php @@ -10,21 +10,21 @@ use PHPStan\Analyser\ExpressionResult; use PHPStan\Analyser\ExpressionResultFactory; use PHPStan\Analyser\ExpressionResultStorage; -use PHPStan\Analyser\ExprHandler; use PHPStan\Analyser\MutatingScope; use PHPStan\Analyser\NodeScopeResolver; use PHPStan\Analyser\Scope; use PHPStan\Analyser\SpecifiedTypes; +use PHPStan\Analyser\TypeResolvingExprHandler; use PHPStan\Analyser\TypeSpecifier; use PHPStan\Analyser\TypeSpecifierContext; use PHPStan\DependencyInjection\AutowiredService; use PHPStan\Type\Type; /** - * @implements ExprHandler + * @implements TypeResolvingExprHandler */ #[AutowiredService] -final class PostIncHandler implements ExprHandler +final class PostIncHandler implements TypeResolvingExprHandler { public function __construct(private ExpressionResultFactory $expressionResultFactory) diff --git a/src/Analyser/ExprHandler/PreDecHandler.php b/src/Analyser/ExprHandler/PreDecHandler.php index 6569fde8c10..221376fd209 100644 --- a/src/Analyser/ExprHandler/PreDecHandler.php +++ b/src/Analyser/ExprHandler/PreDecHandler.php @@ -11,11 +11,11 @@ use PHPStan\Analyser\ExpressionResult; use PHPStan\Analyser\ExpressionResultFactory; use PHPStan\Analyser\ExpressionResultStorage; -use PHPStan\Analyser\ExprHandler; use PHPStan\Analyser\MutatingScope; use PHPStan\Analyser\NodeScopeResolver; use PHPStan\Analyser\Scope; use PHPStan\Analyser\SpecifiedTypes; +use PHPStan\Analyser\TypeResolvingExprHandler; use PHPStan\Analyser\TypeSpecifier; use PHPStan\Analyser\TypeSpecifierContext; use PHPStan\DependencyInjection\AutowiredService; @@ -35,10 +35,10 @@ use function str_decrement; /** - * @implements ExprHandler + * @implements TypeResolvingExprHandler */ #[AutowiredService] -final class PreDecHandler implements ExprHandler +final class PreDecHandler implements TypeResolvingExprHandler { public function __construct(private ExpressionResultFactory $expressionResultFactory) diff --git a/src/Analyser/ExprHandler/PreIncHandler.php b/src/Analyser/ExprHandler/PreIncHandler.php index 7d4be597076..73fd74c7dc5 100644 --- a/src/Analyser/ExprHandler/PreIncHandler.php +++ b/src/Analyser/ExprHandler/PreIncHandler.php @@ -11,11 +11,11 @@ use PHPStan\Analyser\ExpressionResult; use PHPStan\Analyser\ExpressionResultFactory; use PHPStan\Analyser\ExpressionResultStorage; -use PHPStan\Analyser\ExprHandler; use PHPStan\Analyser\MutatingScope; use PHPStan\Analyser\NodeScopeResolver; use PHPStan\Analyser\Scope; use PHPStan\Analyser\SpecifiedTypes; +use PHPStan\Analyser\TypeResolvingExprHandler; use PHPStan\Analyser\TypeSpecifier; use PHPStan\Analyser\TypeSpecifierContext; use PHPStan\DependencyInjection\AutowiredService; @@ -36,10 +36,10 @@ use function str_increment; /** - * @implements ExprHandler + * @implements TypeResolvingExprHandler */ #[AutowiredService] -final class PreIncHandler implements ExprHandler +final class PreIncHandler implements TypeResolvingExprHandler { public function __construct(private ExpressionResultFactory $expressionResultFactory) diff --git a/src/Analyser/ExprHandler/PrintHandler.php b/src/Analyser/ExprHandler/PrintHandler.php index cd6a90aee17..2b21db38ec4 100644 --- a/src/Analyser/ExprHandler/PrintHandler.php +++ b/src/Analyser/ExprHandler/PrintHandler.php @@ -9,13 +9,13 @@ use PHPStan\Analyser\ExpressionResult; use PHPStan\Analyser\ExpressionResultFactory; use PHPStan\Analyser\ExpressionResultStorage; -use PHPStan\Analyser\ExprHandler; use PHPStan\Analyser\ExprHandler\Helper\ImplicitToStringCallHelper; use PHPStan\Analyser\ImpurePoint; use PHPStan\Analyser\MutatingScope; use PHPStan\Analyser\NodeScopeResolver; use PHPStan\Analyser\Scope; use PHPStan\Analyser\SpecifiedTypes; +use PHPStan\Analyser\TypeResolvingExprHandler; use PHPStan\Analyser\TypeSpecifier; use PHPStan\Analyser\TypeSpecifierContext; use PHPStan\DependencyInjection\AutowiredService; @@ -24,10 +24,10 @@ use function array_merge; /** - * @implements ExprHandler + * @implements TypeResolvingExprHandler */ #[AutowiredService] -final class PrintHandler implements ExprHandler +final class PrintHandler implements TypeResolvingExprHandler { public function __construct( diff --git a/src/Analyser/ExprHandler/PropertyFetchHandler.php b/src/Analyser/ExprHandler/PropertyFetchHandler.php index 61152971d82..f8a4ab70259 100644 --- a/src/Analyser/ExprHandler/PropertyFetchHandler.php +++ b/src/Analyser/ExprHandler/PropertyFetchHandler.php @@ -11,13 +11,13 @@ use PHPStan\Analyser\ExpressionResult; use PHPStan\Analyser\ExpressionResultFactory; use PHPStan\Analyser\ExpressionResultStorage; -use PHPStan\Analyser\ExprHandler; use PHPStan\Analyser\ExprHandler\Helper\NullsafeShortCircuitingHelper; use PHPStan\Analyser\InternalThrowPoint; use PHPStan\Analyser\MutatingScope; use PHPStan\Analyser\NodeScopeResolver; use PHPStan\Analyser\Scope; use PHPStan\Analyser\SpecifiedTypes; +use PHPStan\Analyser\TypeResolvingExprHandler; use PHPStan\Analyser\TypeSpecifier; use PHPStan\Analyser\TypeSpecifierContext; use PHPStan\DependencyInjection\AutowiredService; @@ -32,10 +32,10 @@ use function count; /** - * @implements ExprHandler + * @implements TypeResolvingExprHandler */ #[AutowiredService] -final class PropertyFetchHandler implements ExprHandler +final class PropertyFetchHandler implements TypeResolvingExprHandler { public function __construct( diff --git a/src/Analyser/ExprHandler/ScalarHandler.php b/src/Analyser/ExprHandler/ScalarHandler.php index 9b4de986801..dba4a0669da 100644 --- a/src/Analyser/ExprHandler/ScalarHandler.php +++ b/src/Analyser/ExprHandler/ScalarHandler.php @@ -10,11 +10,11 @@ use PHPStan\Analyser\ExpressionResult; use PHPStan\Analyser\ExpressionResultFactory; use PHPStan\Analyser\ExpressionResultStorage; -use PHPStan\Analyser\ExprHandler; use PHPStan\Analyser\MutatingScope; use PHPStan\Analyser\NodeScopeResolver; use PHPStan\Analyser\Scope; use PHPStan\Analyser\SpecifiedTypes; +use PHPStan\Analyser\TypeResolvingExprHandler; use PHPStan\Analyser\TypeSpecifier; use PHPStan\Analyser\TypeSpecifierContext; use PHPStan\DependencyInjection\AutowiredService; @@ -23,10 +23,10 @@ use PHPStan\Type\Type; /** - * @implements ExprHandler + * @implements TypeResolvingExprHandler */ #[AutowiredService] -final class ScalarHandler implements ExprHandler +final class ScalarHandler implements TypeResolvingExprHandler { public function __construct( diff --git a/src/Analyser/ExprHandler/StaticCallHandler.php b/src/Analyser/ExprHandler/StaticCallHandler.php index 7c219787fe2..e3a96facf05 100644 --- a/src/Analyser/ExprHandler/StaticCallHandler.php +++ b/src/Analyser/ExprHandler/StaticCallHandler.php @@ -16,7 +16,6 @@ use PHPStan\Analyser\ExpressionResult; use PHPStan\Analyser\ExpressionResultFactory; use PHPStan\Analyser\ExpressionResultStorage; -use PHPStan\Analyser\ExprHandler; use PHPStan\Analyser\ExprHandler\Helper\MethodCallReturnTypeHelper; use PHPStan\Analyser\ExprHandler\Helper\MethodThrowPointHelper; use PHPStan\Analyser\ExprHandler\Helper\NullsafeShortCircuitingHelper; @@ -27,6 +26,7 @@ use PHPStan\Analyser\NoopNodeCallback; use PHPStan\Analyser\Scope; use PHPStan\Analyser\SpecifiedTypes; +use PHPStan\Analyser\TypeResolvingExprHandler; use PHPStan\Analyser\TypeSpecifier; use PHPStan\Analyser\TypeSpecifierContext; use PHPStan\DependencyInjection\AutowiredParameter; @@ -57,10 +57,10 @@ use function strtolower; /** - * @implements ExprHandler + * @implements TypeResolvingExprHandler */ #[AutowiredService] -final class StaticCallHandler implements ExprHandler +final class StaticCallHandler implements TypeResolvingExprHandler { public function __construct( diff --git a/src/Analyser/ExprHandler/StaticPropertyFetchHandler.php b/src/Analyser/ExprHandler/StaticPropertyFetchHandler.php index c6d61ddd866..4a1388c50e4 100644 --- a/src/Analyser/ExprHandler/StaticPropertyFetchHandler.php +++ b/src/Analyser/ExprHandler/StaticPropertyFetchHandler.php @@ -13,13 +13,13 @@ use PHPStan\Analyser\ExpressionResult; use PHPStan\Analyser\ExpressionResultFactory; use PHPStan\Analyser\ExpressionResultStorage; -use PHPStan\Analyser\ExprHandler; use PHPStan\Analyser\ExprHandler\Helper\NullsafeShortCircuitingHelper; use PHPStan\Analyser\ImpurePoint; use PHPStan\Analyser\MutatingScope; use PHPStan\Analyser\NodeScopeResolver; use PHPStan\Analyser\Scope; use PHPStan\Analyser\SpecifiedTypes; +use PHPStan\Analyser\TypeResolvingExprHandler; use PHPStan\Analyser\TypeSpecifier; use PHPStan\Analyser\TypeSpecifierContext; use PHPStan\DependencyInjection\AutowiredService; @@ -33,10 +33,10 @@ use function count; /** - * @implements ExprHandler + * @implements TypeResolvingExprHandler */ #[AutowiredService] -final class StaticPropertyFetchHandler implements ExprHandler +final class StaticPropertyFetchHandler implements TypeResolvingExprHandler { public function __construct( diff --git a/src/Analyser/ExprHandler/TernaryHandler.php b/src/Analyser/ExprHandler/TernaryHandler.php index 101ffe45330..bccabd76e0b 100644 --- a/src/Analyser/ExprHandler/TernaryHandler.php +++ b/src/Analyser/ExprHandler/TernaryHandler.php @@ -11,12 +11,12 @@ use PHPStan\Analyser\ExpressionResult; use PHPStan\Analyser\ExpressionResultFactory; use PHPStan\Analyser\ExpressionResultStorage; -use PHPStan\Analyser\ExprHandler; use PHPStan\Analyser\MutatingScope; use PHPStan\Analyser\NodeScopeResolver; use PHPStan\Analyser\NoopNodeCallback; use PHPStan\Analyser\Scope; use PHPStan\Analyser\SpecifiedTypes; +use PHPStan\Analyser\TypeResolvingExprHandler; use PHPStan\Analyser\TypeSpecifier; use PHPStan\Analyser\TypeSpecifierContext; use PHPStan\DependencyInjection\AutowiredService; @@ -26,10 +26,10 @@ use function array_merge; /** - * @implements ExprHandler + * @implements TypeResolvingExprHandler */ #[AutowiredService] -final class TernaryHandler implements ExprHandler +final class TernaryHandler implements TypeResolvingExprHandler { public function __construct( diff --git a/src/Analyser/ExprHandler/ThrowHandler.php b/src/Analyser/ExprHandler/ThrowHandler.php index 05bda29ec2d..8231e9b6726 100644 --- a/src/Analyser/ExprHandler/ThrowHandler.php +++ b/src/Analyser/ExprHandler/ThrowHandler.php @@ -9,12 +9,12 @@ use PHPStan\Analyser\ExpressionResult; use PHPStan\Analyser\ExpressionResultFactory; use PHPStan\Analyser\ExpressionResultStorage; -use PHPStan\Analyser\ExprHandler; use PHPStan\Analyser\InternalThrowPoint; use PHPStan\Analyser\MutatingScope; use PHPStan\Analyser\NodeScopeResolver; use PHPStan\Analyser\Scope; use PHPStan\Analyser\SpecifiedTypes; +use PHPStan\Analyser\TypeResolvingExprHandler; use PHPStan\Analyser\TypeSpecifier; use PHPStan\Analyser\TypeSpecifierContext; use PHPStan\DependencyInjection\AutowiredService; @@ -23,10 +23,10 @@ use function array_merge; /** - * @implements ExprHandler + * @implements TypeResolvingExprHandler */ #[AutowiredService] -final class ThrowHandler implements ExprHandler +final class ThrowHandler implements TypeResolvingExprHandler { public function __construct(private ExpressionResultFactory $expressionResultFactory) diff --git a/src/Analyser/ExprHandler/UnaryMinusHandler.php b/src/Analyser/ExprHandler/UnaryMinusHandler.php index ea67e7dabc4..ad84baeee02 100644 --- a/src/Analyser/ExprHandler/UnaryMinusHandler.php +++ b/src/Analyser/ExprHandler/UnaryMinusHandler.php @@ -9,11 +9,11 @@ use PHPStan\Analyser\ExpressionResult; use PHPStan\Analyser\ExpressionResultFactory; use PHPStan\Analyser\ExpressionResultStorage; -use PHPStan\Analyser\ExprHandler; use PHPStan\Analyser\MutatingScope; use PHPStan\Analyser\NodeScopeResolver; use PHPStan\Analyser\Scope; use PHPStan\Analyser\SpecifiedTypes; +use PHPStan\Analyser\TypeResolvingExprHandler; use PHPStan\Analyser\TypeSpecifier; use PHPStan\Analyser\TypeSpecifierContext; use PHPStan\DependencyInjection\AutowiredService; @@ -21,10 +21,10 @@ use PHPStan\Type\Type; /** - * @implements ExprHandler + * @implements TypeResolvingExprHandler */ #[AutowiredService] -final class UnaryMinusHandler implements ExprHandler +final class UnaryMinusHandler implements TypeResolvingExprHandler { public function __construct( diff --git a/src/Analyser/ExprHandler/UnaryPlusHandler.php b/src/Analyser/ExprHandler/UnaryPlusHandler.php index 6ec1abe38fc..c3f38936c05 100644 --- a/src/Analyser/ExprHandler/UnaryPlusHandler.php +++ b/src/Analyser/ExprHandler/UnaryPlusHandler.php @@ -9,11 +9,11 @@ use PHPStan\Analyser\ExpressionResult; use PHPStan\Analyser\ExpressionResultFactory; use PHPStan\Analyser\ExpressionResultStorage; -use PHPStan\Analyser\ExprHandler; use PHPStan\Analyser\MutatingScope; use PHPStan\Analyser\NodeScopeResolver; use PHPStan\Analyser\Scope; use PHPStan\Analyser\SpecifiedTypes; +use PHPStan\Analyser\TypeResolvingExprHandler; use PHPStan\Analyser\TypeSpecifier; use PHPStan\Analyser\TypeSpecifierContext; use PHPStan\DependencyInjection\AutowiredService; @@ -21,10 +21,10 @@ use PHPStan\Type\Type; /** - * @implements ExprHandler + * @implements TypeResolvingExprHandler */ #[AutowiredService] -final class UnaryPlusHandler implements ExprHandler +final class UnaryPlusHandler implements TypeResolvingExprHandler { public function __construct( diff --git a/src/Analyser/ExprHandler/VariableHandler.php b/src/Analyser/ExprHandler/VariableHandler.php index 6082dea55f2..70ce8439856 100644 --- a/src/Analyser/ExprHandler/VariableHandler.php +++ b/src/Analyser/ExprHandler/VariableHandler.php @@ -11,12 +11,12 @@ use PHPStan\Analyser\ExpressionResult; use PHPStan\Analyser\ExpressionResultFactory; use PHPStan\Analyser\ExpressionResultStorage; -use PHPStan\Analyser\ExprHandler; use PHPStan\Analyser\ImpurePoint; use PHPStan\Analyser\MutatingScope; use PHPStan\Analyser\NodeScopeResolver; use PHPStan\Analyser\Scope; use PHPStan\Analyser\SpecifiedTypes; +use PHPStan\Analyser\TypeResolvingExprHandler; use PHPStan\Analyser\TypeSpecifier; use PHPStan\Analyser\TypeSpecifierContext; use PHPStan\DependencyInjection\AutowiredService; @@ -29,10 +29,10 @@ use function is_string; /** - * @implements ExprHandler + * @implements TypeResolvingExprHandler */ #[AutowiredService] -final class VariableHandler implements ExprHandler +final class VariableHandler implements TypeResolvingExprHandler { public function __construct(private ExpressionResultFactory $expressionResultFactory) diff --git a/src/Analyser/ExprHandler/Virtual/AlwaysRememberedExprHandler.php b/src/Analyser/ExprHandler/Virtual/AlwaysRememberedExprHandler.php index 99d6d9925e8..d4700f6e048 100644 --- a/src/Analyser/ExprHandler/Virtual/AlwaysRememberedExprHandler.php +++ b/src/Analyser/ExprHandler/Virtual/AlwaysRememberedExprHandler.php @@ -8,11 +8,11 @@ use PHPStan\Analyser\ExpressionResult; use PHPStan\Analyser\ExpressionResultFactory; use PHPStan\Analyser\ExpressionResultStorage; -use PHPStan\Analyser\ExprHandler; use PHPStan\Analyser\MutatingScope; use PHPStan\Analyser\NodeScopeResolver; use PHPStan\Analyser\Scope; use PHPStan\Analyser\SpecifiedTypes; +use PHPStan\Analyser\TypeResolvingExprHandler; use PHPStan\Analyser\TypeSpecifier; use PHPStan\Analyser\TypeSpecifierContext; use PHPStan\DependencyInjection\AutowiredService; @@ -20,10 +20,10 @@ use PHPStan\Type\Type; /** - * @implements ExprHandler + * @implements TypeResolvingExprHandler */ #[AutowiredService] -final class AlwaysRememberedExprHandler implements ExprHandler +final class AlwaysRememberedExprHandler implements TypeResolvingExprHandler { public function __construct(private ExpressionResultFactory $expressionResultFactory) diff --git a/src/Analyser/ExprHandler/Virtual/ExistingArrayDimFetchHandler.php b/src/Analyser/ExprHandler/Virtual/ExistingArrayDimFetchHandler.php index 411d2ee8d65..61b7884270d 100644 --- a/src/Analyser/ExprHandler/Virtual/ExistingArrayDimFetchHandler.php +++ b/src/Analyser/ExprHandler/Virtual/ExistingArrayDimFetchHandler.php @@ -8,11 +8,11 @@ use PHPStan\Analyser\ExpressionResult; use PHPStan\Analyser\ExpressionResultFactory; use PHPStan\Analyser\ExpressionResultStorage; -use PHPStan\Analyser\ExprHandler; use PHPStan\Analyser\MutatingScope; use PHPStan\Analyser\NodeScopeResolver; use PHPStan\Analyser\Scope; use PHPStan\Analyser\SpecifiedTypes; +use PHPStan\Analyser\TypeResolvingExprHandler; use PHPStan\Analyser\TypeSpecifier; use PHPStan\Analyser\TypeSpecifierContext; use PHPStan\DependencyInjection\AutowiredService; @@ -20,10 +20,10 @@ use PHPStan\Type\Type; /** - * @implements ExprHandler + * @implements TypeResolvingExprHandler */ #[AutowiredService] -final class ExistingArrayDimFetchHandler implements ExprHandler +final class ExistingArrayDimFetchHandler implements TypeResolvingExprHandler { public function __construct(private ExpressionResultFactory $expressionResultFactory) diff --git a/src/Analyser/ExprHandler/Virtual/FunctionCallableNodeHandler.php b/src/Analyser/ExprHandler/Virtual/FunctionCallableNodeHandler.php index e56509e627e..b27515f40fd 100644 --- a/src/Analyser/ExprHandler/Virtual/FunctionCallableNodeHandler.php +++ b/src/Analyser/ExprHandler/Virtual/FunctionCallableNodeHandler.php @@ -8,11 +8,11 @@ use PHPStan\Analyser\ExpressionResult; use PHPStan\Analyser\ExpressionResultFactory; use PHPStan\Analyser\ExpressionResultStorage; -use PHPStan\Analyser\ExprHandler; use PHPStan\Analyser\MutatingScope; use PHPStan\Analyser\NodeScopeResolver; use PHPStan\Analyser\Scope; use PHPStan\Analyser\SpecifiedTypes; +use PHPStan\Analyser\TypeResolvingExprHandler; use PHPStan\Analyser\TypeSpecifier; use PHPStan\Analyser\TypeSpecifierContext; use PHPStan\DependencyInjection\AutowiredService; @@ -21,10 +21,10 @@ use PHPStan\Type\Type; /** - * @implements ExprHandler + * @implements TypeResolvingExprHandler */ #[AutowiredService] -final class FunctionCallableNodeHandler implements ExprHandler +final class FunctionCallableNodeHandler implements TypeResolvingExprHandler { public function __construct(private ExpressionResultFactory $expressionResultFactory) diff --git a/src/Analyser/ExprHandler/Virtual/GetIterableKeyTypeExprHandler.php b/src/Analyser/ExprHandler/Virtual/GetIterableKeyTypeExprHandler.php index 127ef939539..6f32dd6c6ec 100644 --- a/src/Analyser/ExprHandler/Virtual/GetIterableKeyTypeExprHandler.php +++ b/src/Analyser/ExprHandler/Virtual/GetIterableKeyTypeExprHandler.php @@ -8,11 +8,11 @@ use PHPStan\Analyser\ExpressionResult; use PHPStan\Analyser\ExpressionResultFactory; use PHPStan\Analyser\ExpressionResultStorage; -use PHPStan\Analyser\ExprHandler; use PHPStan\Analyser\MutatingScope; use PHPStan\Analyser\NodeScopeResolver; use PHPStan\Analyser\Scope; use PHPStan\Analyser\SpecifiedTypes; +use PHPStan\Analyser\TypeResolvingExprHandler; use PHPStan\Analyser\TypeSpecifier; use PHPStan\Analyser\TypeSpecifierContext; use PHPStan\DependencyInjection\AutowiredService; @@ -20,10 +20,10 @@ use PHPStan\Type\Type; /** - * @implements ExprHandler + * @implements TypeResolvingExprHandler */ #[AutowiredService] -final class GetIterableKeyTypeExprHandler implements ExprHandler +final class GetIterableKeyTypeExprHandler implements TypeResolvingExprHandler { public function __construct(private ExpressionResultFactory $expressionResultFactory) diff --git a/src/Analyser/ExprHandler/Virtual/GetIterableValueTypeExprHandler.php b/src/Analyser/ExprHandler/Virtual/GetIterableValueTypeExprHandler.php index 56b7b9be00e..950d1025266 100644 --- a/src/Analyser/ExprHandler/Virtual/GetIterableValueTypeExprHandler.php +++ b/src/Analyser/ExprHandler/Virtual/GetIterableValueTypeExprHandler.php @@ -8,11 +8,11 @@ use PHPStan\Analyser\ExpressionResult; use PHPStan\Analyser\ExpressionResultFactory; use PHPStan\Analyser\ExpressionResultStorage; -use PHPStan\Analyser\ExprHandler; use PHPStan\Analyser\MutatingScope; use PHPStan\Analyser\NodeScopeResolver; use PHPStan\Analyser\Scope; use PHPStan\Analyser\SpecifiedTypes; +use PHPStan\Analyser\TypeResolvingExprHandler; use PHPStan\Analyser\TypeSpecifier; use PHPStan\Analyser\TypeSpecifierContext; use PHPStan\DependencyInjection\AutowiredService; @@ -20,10 +20,10 @@ use PHPStan\Type\Type; /** - * @implements ExprHandler + * @implements TypeResolvingExprHandler */ #[AutowiredService] -final class GetIterableValueTypeExprHandler implements ExprHandler +final class GetIterableValueTypeExprHandler implements TypeResolvingExprHandler { public function __construct(private ExpressionResultFactory $expressionResultFactory) diff --git a/src/Analyser/ExprHandler/Virtual/GetOffsetValueTypeExprHandler.php b/src/Analyser/ExprHandler/Virtual/GetOffsetValueTypeExprHandler.php index ed0b5da45c5..ad70185384f 100644 --- a/src/Analyser/ExprHandler/Virtual/GetOffsetValueTypeExprHandler.php +++ b/src/Analyser/ExprHandler/Virtual/GetOffsetValueTypeExprHandler.php @@ -8,11 +8,11 @@ use PHPStan\Analyser\ExpressionResult; use PHPStan\Analyser\ExpressionResultFactory; use PHPStan\Analyser\ExpressionResultStorage; -use PHPStan\Analyser\ExprHandler; use PHPStan\Analyser\MutatingScope; use PHPStan\Analyser\NodeScopeResolver; use PHPStan\Analyser\Scope; use PHPStan\Analyser\SpecifiedTypes; +use PHPStan\Analyser\TypeResolvingExprHandler; use PHPStan\Analyser\TypeSpecifier; use PHPStan\Analyser\TypeSpecifierContext; use PHPStan\DependencyInjection\AutowiredService; @@ -20,10 +20,10 @@ use PHPStan\Type\Type; /** - * @implements ExprHandler + * @implements TypeResolvingExprHandler */ #[AutowiredService] -final class GetOffsetValueTypeExprHandler implements ExprHandler +final class GetOffsetValueTypeExprHandler implements TypeResolvingExprHandler { public function __construct(private ExpressionResultFactory $expressionResultFactory) diff --git a/src/Analyser/ExprHandler/Virtual/InstantiationCallableNodeHandler.php b/src/Analyser/ExprHandler/Virtual/InstantiationCallableNodeHandler.php index 937b6618d85..304aedb86ad 100644 --- a/src/Analyser/ExprHandler/Virtual/InstantiationCallableNodeHandler.php +++ b/src/Analyser/ExprHandler/Virtual/InstantiationCallableNodeHandler.php @@ -8,11 +8,11 @@ use PHPStan\Analyser\ExpressionResult; use PHPStan\Analyser\ExpressionResultFactory; use PHPStan\Analyser\ExpressionResultStorage; -use PHPStan\Analyser\ExprHandler; use PHPStan\Analyser\MutatingScope; use PHPStan\Analyser\NodeScopeResolver; use PHPStan\Analyser\Scope; use PHPStan\Analyser\SpecifiedTypes; +use PHPStan\Analyser\TypeResolvingExprHandler; use PHPStan\Analyser\TypeSpecifier; use PHPStan\Analyser\TypeSpecifierContext; use PHPStan\DependencyInjection\AutowiredService; @@ -21,10 +21,10 @@ use PHPStan\Type\Type; /** - * @implements ExprHandler + * @implements TypeResolvingExprHandler */ #[AutowiredService] -final class InstantiationCallableNodeHandler implements ExprHandler +final class InstantiationCallableNodeHandler implements TypeResolvingExprHandler { public function __construct(private ExpressionResultFactory $expressionResultFactory) diff --git a/src/Analyser/ExprHandler/Virtual/MethodCallableNodeHandler.php b/src/Analyser/ExprHandler/Virtual/MethodCallableNodeHandler.php index 28492541bce..25ca81bbcd0 100644 --- a/src/Analyser/ExprHandler/Virtual/MethodCallableNodeHandler.php +++ b/src/Analyser/ExprHandler/Virtual/MethodCallableNodeHandler.php @@ -8,11 +8,11 @@ use PHPStan\Analyser\ExpressionResult; use PHPStan\Analyser\ExpressionResultFactory; use PHPStan\Analyser\ExpressionResultStorage; -use PHPStan\Analyser\ExprHandler; use PHPStan\Analyser\MutatingScope; use PHPStan\Analyser\NodeScopeResolver; use PHPStan\Analyser\Scope; use PHPStan\Analyser\SpecifiedTypes; +use PHPStan\Analyser\TypeResolvingExprHandler; use PHPStan\Analyser\TypeSpecifier; use PHPStan\Analyser\TypeSpecifierContext; use PHPStan\DependencyInjection\AutowiredService; @@ -22,10 +22,10 @@ use function array_merge; /** - * @implements ExprHandler + * @implements TypeResolvingExprHandler */ #[AutowiredService] -final class MethodCallableNodeHandler implements ExprHandler +final class MethodCallableNodeHandler implements TypeResolvingExprHandler { public function __construct(private ExpressionResultFactory $expressionResultFactory) diff --git a/src/Analyser/ExprHandler/Virtual/NativeTypeExprHandler.php b/src/Analyser/ExprHandler/Virtual/NativeTypeExprHandler.php index 852d00b14cd..08715ad31cc 100644 --- a/src/Analyser/ExprHandler/Virtual/NativeTypeExprHandler.php +++ b/src/Analyser/ExprHandler/Virtual/NativeTypeExprHandler.php @@ -8,11 +8,11 @@ use PHPStan\Analyser\ExpressionResult; use PHPStan\Analyser\ExpressionResultFactory; use PHPStan\Analyser\ExpressionResultStorage; -use PHPStan\Analyser\ExprHandler; use PHPStan\Analyser\MutatingScope; use PHPStan\Analyser\NodeScopeResolver; use PHPStan\Analyser\Scope; use PHPStan\Analyser\SpecifiedTypes; +use PHPStan\Analyser\TypeResolvingExprHandler; use PHPStan\Analyser\TypeSpecifier; use PHPStan\Analyser\TypeSpecifierContext; use PHPStan\DependencyInjection\AutowiredService; @@ -20,10 +20,10 @@ use PHPStan\Type\Type; /** - * @implements ExprHandler + * @implements TypeResolvingExprHandler */ #[AutowiredService] -final class NativeTypeExprHandler implements ExprHandler +final class NativeTypeExprHandler implements TypeResolvingExprHandler { public function __construct(private ExpressionResultFactory $expressionResultFactory) diff --git a/src/Analyser/ExprHandler/Virtual/OriginalPropertyTypeExprHandler.php b/src/Analyser/ExprHandler/Virtual/OriginalPropertyTypeExprHandler.php index 2621d242cc7..0e346c921c5 100644 --- a/src/Analyser/ExprHandler/Virtual/OriginalPropertyTypeExprHandler.php +++ b/src/Analyser/ExprHandler/Virtual/OriginalPropertyTypeExprHandler.php @@ -8,11 +8,11 @@ use PHPStan\Analyser\ExpressionResult; use PHPStan\Analyser\ExpressionResultFactory; use PHPStan\Analyser\ExpressionResultStorage; -use PHPStan\Analyser\ExprHandler; use PHPStan\Analyser\MutatingScope; use PHPStan\Analyser\NodeScopeResolver; use PHPStan\Analyser\Scope; use PHPStan\Analyser\SpecifiedTypes; +use PHPStan\Analyser\TypeResolvingExprHandler; use PHPStan\Analyser\TypeSpecifier; use PHPStan\Analyser\TypeSpecifierContext; use PHPStan\DependencyInjection\AutowiredService; @@ -22,10 +22,10 @@ use PHPStan\Type\Type; /** - * @implements ExprHandler + * @implements TypeResolvingExprHandler */ #[AutowiredService] -final class OriginalPropertyTypeExprHandler implements ExprHandler +final class OriginalPropertyTypeExprHandler implements TypeResolvingExprHandler { public function __construct( diff --git a/src/Analyser/ExprHandler/Virtual/SetExistingOffsetValueTypeExprHandler.php b/src/Analyser/ExprHandler/Virtual/SetExistingOffsetValueTypeExprHandler.php index 11487e514f2..56a154af6f8 100644 --- a/src/Analyser/ExprHandler/Virtual/SetExistingOffsetValueTypeExprHandler.php +++ b/src/Analyser/ExprHandler/Virtual/SetExistingOffsetValueTypeExprHandler.php @@ -8,11 +8,11 @@ use PHPStan\Analyser\ExpressionResult; use PHPStan\Analyser\ExpressionResultFactory; use PHPStan\Analyser\ExpressionResultStorage; -use PHPStan\Analyser\ExprHandler; use PHPStan\Analyser\MutatingScope; use PHPStan\Analyser\NodeScopeResolver; use PHPStan\Analyser\Scope; use PHPStan\Analyser\SpecifiedTypes; +use PHPStan\Analyser\TypeResolvingExprHandler; use PHPStan\Analyser\TypeSpecifier; use PHPStan\Analyser\TypeSpecifierContext; use PHPStan\DependencyInjection\AutowiredService; @@ -22,10 +22,10 @@ use PHPStan\Type\UnionType; /** - * @implements ExprHandler + * @implements TypeResolvingExprHandler */ #[AutowiredService] -final class SetExistingOffsetValueTypeExprHandler implements ExprHandler +final class SetExistingOffsetValueTypeExprHandler implements TypeResolvingExprHandler { public function __construct(private ExpressionResultFactory $expressionResultFactory) diff --git a/src/Analyser/ExprHandler/Virtual/SetOffsetValueTypeExprHandler.php b/src/Analyser/ExprHandler/Virtual/SetOffsetValueTypeExprHandler.php index c85440094b8..08659b35101 100644 --- a/src/Analyser/ExprHandler/Virtual/SetOffsetValueTypeExprHandler.php +++ b/src/Analyser/ExprHandler/Virtual/SetOffsetValueTypeExprHandler.php @@ -8,11 +8,11 @@ use PHPStan\Analyser\ExpressionResult; use PHPStan\Analyser\ExpressionResultFactory; use PHPStan\Analyser\ExpressionResultStorage; -use PHPStan\Analyser\ExprHandler; use PHPStan\Analyser\MutatingScope; use PHPStan\Analyser\NodeScopeResolver; use PHPStan\Analyser\Scope; use PHPStan\Analyser\SpecifiedTypes; +use PHPStan\Analyser\TypeResolvingExprHandler; use PHPStan\Analyser\TypeSpecifier; use PHPStan\Analyser\TypeSpecifierContext; use PHPStan\DependencyInjection\AutowiredService; @@ -22,10 +22,10 @@ use PHPStan\Type\UnionType; /** - * @implements ExprHandler + * @implements TypeResolvingExprHandler */ #[AutowiredService] -final class SetOffsetValueTypeExprHandler implements ExprHandler +final class SetOffsetValueTypeExprHandler implements TypeResolvingExprHandler { public function __construct(private ExpressionResultFactory $expressionResultFactory) diff --git a/src/Analyser/ExprHandler/Virtual/StaticMethodCallableNodeHandler.php b/src/Analyser/ExprHandler/Virtual/StaticMethodCallableNodeHandler.php index b12d7e120e5..8f9570ee5ec 100644 --- a/src/Analyser/ExprHandler/Virtual/StaticMethodCallableNodeHandler.php +++ b/src/Analyser/ExprHandler/Virtual/StaticMethodCallableNodeHandler.php @@ -8,11 +8,11 @@ use PHPStan\Analyser\ExpressionResult; use PHPStan\Analyser\ExpressionResultFactory; use PHPStan\Analyser\ExpressionResultStorage; -use PHPStan\Analyser\ExprHandler; use PHPStan\Analyser\MutatingScope; use PHPStan\Analyser\NodeScopeResolver; use PHPStan\Analyser\Scope; use PHPStan\Analyser\SpecifiedTypes; +use PHPStan\Analyser\TypeResolvingExprHandler; use PHPStan\Analyser\TypeSpecifier; use PHPStan\Analyser\TypeSpecifierContext; use PHPStan\DependencyInjection\AutowiredService; @@ -22,10 +22,10 @@ use function array_merge; /** - * @implements ExprHandler + * @implements TypeResolvingExprHandler */ #[AutowiredService] -final class StaticMethodCallableNodeHandler implements ExprHandler +final class StaticMethodCallableNodeHandler implements TypeResolvingExprHandler { public function __construct(private ExpressionResultFactory $expressionResultFactory) diff --git a/src/Analyser/ExprHandler/Virtual/TypeExprHandler.php b/src/Analyser/ExprHandler/Virtual/TypeExprHandler.php index 6ca636fe081..32208c6a569 100644 --- a/src/Analyser/ExprHandler/Virtual/TypeExprHandler.php +++ b/src/Analyser/ExprHandler/Virtual/TypeExprHandler.php @@ -8,11 +8,11 @@ use PHPStan\Analyser\ExpressionResult; use PHPStan\Analyser\ExpressionResultFactory; use PHPStan\Analyser\ExpressionResultStorage; -use PHPStan\Analyser\ExprHandler; use PHPStan\Analyser\MutatingScope; use PHPStan\Analyser\NodeScopeResolver; use PHPStan\Analyser\Scope; use PHPStan\Analyser\SpecifiedTypes; +use PHPStan\Analyser\TypeResolvingExprHandler; use PHPStan\Analyser\TypeSpecifier; use PHPStan\Analyser\TypeSpecifierContext; use PHPStan\DependencyInjection\AutowiredService; @@ -20,10 +20,10 @@ use PHPStan\Type\Type; /** - * @implements ExprHandler + * @implements TypeResolvingExprHandler */ #[AutowiredService] -final class TypeExprHandler implements ExprHandler +final class TypeExprHandler implements TypeResolvingExprHandler { public function __construct(private ExpressionResultFactory $expressionResultFactory) diff --git a/src/Analyser/ExprHandler/Virtual/UnsetOffsetExprHandler.php b/src/Analyser/ExprHandler/Virtual/UnsetOffsetExprHandler.php index d414e648fd2..9f5e4368b27 100644 --- a/src/Analyser/ExprHandler/Virtual/UnsetOffsetExprHandler.php +++ b/src/Analyser/ExprHandler/Virtual/UnsetOffsetExprHandler.php @@ -8,11 +8,11 @@ use PHPStan\Analyser\ExpressionResult; use PHPStan\Analyser\ExpressionResultFactory; use PHPStan\Analyser\ExpressionResultStorage; -use PHPStan\Analyser\ExprHandler; use PHPStan\Analyser\MutatingScope; use PHPStan\Analyser\NodeScopeResolver; use PHPStan\Analyser\Scope; use PHPStan\Analyser\SpecifiedTypes; +use PHPStan\Analyser\TypeResolvingExprHandler; use PHPStan\Analyser\TypeSpecifier; use PHPStan\Analyser\TypeSpecifierContext; use PHPStan\DependencyInjection\AutowiredService; @@ -20,10 +20,10 @@ use PHPStan\Type\Type; /** - * @implements ExprHandler + * @implements TypeResolvingExprHandler */ #[AutowiredService] -final class UnsetOffsetExprHandler implements ExprHandler +final class UnsetOffsetExprHandler implements TypeResolvingExprHandler { public function __construct(private ExpressionResultFactory $expressionResultFactory) diff --git a/src/Analyser/ExprHandler/YieldFromHandler.php b/src/Analyser/ExprHandler/YieldFromHandler.php index 1aac8244af7..ca086700866 100644 --- a/src/Analyser/ExprHandler/YieldFromHandler.php +++ b/src/Analyser/ExprHandler/YieldFromHandler.php @@ -10,13 +10,13 @@ use PHPStan\Analyser\ExpressionResult; use PHPStan\Analyser\ExpressionResultFactory; use PHPStan\Analyser\ExpressionResultStorage; -use PHPStan\Analyser\ExprHandler; use PHPStan\Analyser\ImpurePoint; use PHPStan\Analyser\InternalThrowPoint; use PHPStan\Analyser\MutatingScope; use PHPStan\Analyser\NodeScopeResolver; use PHPStan\Analyser\Scope; use PHPStan\Analyser\SpecifiedTypes; +use PHPStan\Analyser\TypeResolvingExprHandler; use PHPStan\Analyser\TypeSpecifier; use PHPStan\Analyser\TypeSpecifierContext; use PHPStan\DependencyInjection\AutowiredService; @@ -26,10 +26,10 @@ use function array_merge; /** - * @implements ExprHandler + * @implements TypeResolvingExprHandler */ #[AutowiredService] -final class YieldFromHandler implements ExprHandler +final class YieldFromHandler implements TypeResolvingExprHandler { public function __construct(private ExpressionResultFactory $expressionResultFactory) diff --git a/src/Analyser/ExprHandler/YieldHandler.php b/src/Analyser/ExprHandler/YieldHandler.php index 07abbc7e6ee..dd8a5478fc0 100644 --- a/src/Analyser/ExprHandler/YieldHandler.php +++ b/src/Analyser/ExprHandler/YieldHandler.php @@ -10,13 +10,13 @@ use PHPStan\Analyser\ExpressionResult; use PHPStan\Analyser\ExpressionResultFactory; use PHPStan\Analyser\ExpressionResultStorage; -use PHPStan\Analyser\ExprHandler; use PHPStan\Analyser\ImpurePoint; use PHPStan\Analyser\InternalThrowPoint; use PHPStan\Analyser\MutatingScope; use PHPStan\Analyser\NodeScopeResolver; use PHPStan\Analyser\Scope; use PHPStan\Analyser\SpecifiedTypes; +use PHPStan\Analyser\TypeResolvingExprHandler; use PHPStan\Analyser\TypeSpecifier; use PHPStan\Analyser\TypeSpecifierContext; use PHPStan\DependencyInjection\AutowiredService; @@ -26,10 +26,10 @@ use function array_merge; /** - * @implements ExprHandler + * @implements TypeResolvingExprHandler */ #[AutowiredService] -final class YieldHandler implements ExprHandler +final class YieldHandler implements TypeResolvingExprHandler { public function __construct(private ExpressionResultFactory $expressionResultFactory) diff --git a/src/Analyser/MutatingScope.php b/src/Analyser/MutatingScope.php index e02c18b0da3..640bfc481b1 100644 --- a/src/Analyser/MutatingScope.php +++ b/src/Analyser/MutatingScope.php @@ -986,6 +986,9 @@ private function resolveType(string $exprString, Expr $node): Type /** @var ExprHandler $exprHandler */ foreach ($this->container->getServicesByTag(ExprHandler::EXTENSION_TAG) as $exprHandler) { + if (!$exprHandler instanceof TypeResolvingExprHandler) { + continue; + } if (!$exprHandler->supports($node)) { continue; } diff --git a/src/Analyser/TypeResolvingExprHandler.php b/src/Analyser/TypeResolvingExprHandler.php new file mode 100644 index 00000000000..030a2dfb8a3 --- /dev/null +++ b/src/Analyser/TypeResolvingExprHandler.php @@ -0,0 +1,30 @@ + + */ +interface TypeResolvingExprHandler extends ExprHandler +{ + + /** + * @param T $expr + */ + public function resolveType(MutatingScope $scope, Expr $expr): Type; + + /** + * @param T $expr + */ + public function specifyTypes( + TypeSpecifier $typeSpecifier, + Scope $scope, + Expr $expr, + TypeSpecifierContext $context, + ): SpecifiedTypes; + +} diff --git a/src/Analyser/TypeSpecifier.php b/src/Analyser/TypeSpecifier.php index 27156a8b3f0..dd1982e6694 100644 --- a/src/Analyser/TypeSpecifier.php +++ b/src/Analyser/TypeSpecifier.php @@ -91,6 +91,9 @@ public function specifyTypesInCondition( /** @var ExprHandler $exprHandler */ foreach ($this->container->getServicesByTag(ExprHandler::EXTENSION_TAG) as $exprHandler) { + if (!$exprHandler instanceof TypeResolvingExprHandler) { + continue; + } if (!$exprHandler->supports($expr)) { continue; } From 30424b44d5f970ea1386af01750fbe6e5c270431 Mon Sep 17 00:00:00 2001 From: Ondrej Mirtes Date: Fri, 12 Jun 2026 12:21:16 +0200 Subject: [PATCH 007/113] ScalarHandler stops implementing TypeResolvingExprHandler --- src/Analyser/ExprHandler/ScalarHandler.php | 22 +++--------- src/Analyser/ExpressionResult.php | 40 ++++++++++++++++++++-- src/Analyser/ExpressionResultFactory.php | 3 ++ src/Analyser/MutatingScope.php | 2 +- 4 files changed, 47 insertions(+), 20 deletions(-) diff --git a/src/Analyser/ExprHandler/ScalarHandler.php b/src/Analyser/ExprHandler/ScalarHandler.php index dba4a0669da..690ec8b8dd6 100644 --- a/src/Analyser/ExprHandler/ScalarHandler.php +++ b/src/Analyser/ExprHandler/ScalarHandler.php @@ -10,23 +10,19 @@ use PHPStan\Analyser\ExpressionResult; use PHPStan\Analyser\ExpressionResultFactory; use PHPStan\Analyser\ExpressionResultStorage; +use PHPStan\Analyser\ExprHandler; use PHPStan\Analyser\MutatingScope; use PHPStan\Analyser\NodeScopeResolver; use PHPStan\Analyser\Scope; -use PHPStan\Analyser\SpecifiedTypes; -use PHPStan\Analyser\TypeResolvingExprHandler; -use PHPStan\Analyser\TypeSpecifier; -use PHPStan\Analyser\TypeSpecifierContext; use PHPStan\DependencyInjection\AutowiredService; use PHPStan\Reflection\InitializerExprContext; use PHPStan\Reflection\InitializerExprTypeResolver; -use PHPStan\Type\Type; /** - * @implements TypeResolvingExprHandler + * @implements ExprHandler */ #[AutowiredService] -final class ScalarHandler implements TypeResolvingExprHandler +final class ScalarHandler implements ExprHandler { public function __construct( @@ -43,6 +39,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 { + // TODO $typeSpecifier->specifyDefaultTypes($scope, $expr, $context) OR noop return $this->expressionResultFactory->create( $scope, beforeScope: $scope, @@ -51,17 +48,8 @@ public function processExpr(NodeScopeResolver $nodeScopeResolver, Stmt $stmt, Ex isAlwaysTerminating: false, throwPoints: [], impurePoints: [], + typeCallback: fn (Scope $scope) => $this->initializerExprTypeResolver->getType($expr, InitializerExprContext::fromScope($scope)), ); } - public function resolveType(MutatingScope $scope, Expr $expr): Type - { - return $this->initializerExprTypeResolver->getType($expr, InitializerExprContext::fromScope($scope)); - } - - public function specifyTypes(TypeSpecifier $typeSpecifier, Scope $scope, Expr $expr, TypeSpecifierContext $context): SpecifiedTypes - { - return $typeSpecifier->specifyDefaultTypes($scope, $expr, $context); - } - } diff --git a/src/Analyser/ExpressionResult.php b/src/Analyser/ExpressionResult.php index 7669a67a2c8..4bc44318c57 100644 --- a/src/Analyser/ExpressionResult.php +++ b/src/Analyser/ExpressionResult.php @@ -4,12 +4,17 @@ use PhpParser\Node\Expr; use PHPStan\DependencyInjection\GenerateFactory; +use PHPStan\DependencyInjection\Type\ExpressionTypeResolverExtensionRegistryProvider; use PHPStan\Type\Type; +use PHPStan\Type\TypeUtils; #[GenerateFactory(interface: ExpressionResultFactory::class)] final class ExpressionResult { + /** @var (callable(MutatingScope, Expr): Type)|null */ + private $typeCallback; + /** @var (callable(): MutatingScope)|null */ private $truthyScopeCallback; @@ -20,13 +25,19 @@ final class ExpressionResult private ?MutatingScope $falseyScope = null; + private ?Type $cachedType = null; + + private ?Type $cachedNativeType = null; + /** * @param InternalThrowPoint[] $throwPoints * @param ImpurePoint[] $impurePoints + * @param (callable(MutatingScope, Expr): Type)|null $typeCallback * @param (callable(): MutatingScope)|null $truthyScopeCallback * @param (callable(): MutatingScope)|null $falseyScopeCallback */ public function __construct( + private ExpressionTypeResolverExtensionRegistryProvider $expressionTypeResolverExtensionRegistryProvider, private MutatingScope $scope, private MutatingScope $beforeScope, private Expr $expr, @@ -36,10 +47,12 @@ public function __construct( private array $impurePoints, ?callable $truthyScopeCallback = null, ?callable $falseyScopeCallback = null, + ?callable $typeCallback = null, ) { $this->truthyScopeCallback = $truthyScopeCallback; $this->falseyScopeCallback = $falseyScopeCallback; + $this->typeCallback = $typeCallback; } public function getScope(): MutatingScope @@ -108,12 +121,35 @@ public function isAlwaysTerminating(): bool public function getType(): Type { - return $this->beforeScope->getType($this->expr); + if ($this->cachedType !== null) { + return $this->cachedType; + } + + foreach ($this->expressionTypeResolverExtensionRegistryProvider->getRegistry()->getExtensions() as $extension) { + $type = $extension->getType($this->expr, $this->beforeScope); + if ($type !== null) { + return $this->cachedType = $type; + } + } + + if ($this->typeCallback !== null) { + return $this->cachedType = TypeUtils::resolveLateResolvableTypes(($this->typeCallback)($this->beforeScope, $this->expr)); + } + + return $this->cachedType = $this->beforeScope->getType($this->expr); } public function getNativeType(): Type { - return $this->beforeScope->getNativeType($this->expr); + if ($this->cachedNativeType !== null) { + return $this->cachedNativeType; + } + + if ($this->typeCallback !== null) { + return $this->cachedNativeType = TypeUtils::resolveLateResolvableTypes(($this->typeCallback)($this->beforeScope->doNotTreatPhpDocTypesAsCertain(), $this->expr)); + } + + return $this->cachedNativeType = $this->beforeScope->getNativeType($this->expr); } } diff --git a/src/Analyser/ExpressionResultFactory.php b/src/Analyser/ExpressionResultFactory.php index 255d6258fba..d7c5955f4c1 100644 --- a/src/Analyser/ExpressionResultFactory.php +++ b/src/Analyser/ExpressionResultFactory.php @@ -3,6 +3,7 @@ namespace PHPStan\Analyser; use PhpParser\Node\Expr; +use PHPStan\Type\Type; interface ExpressionResultFactory { @@ -12,6 +13,7 @@ interface ExpressionResultFactory * @param ImpurePoint[] $impurePoints * @param (callable(): MutatingScope)|null $truthyScopeCallback * @param (callable(): MutatingScope)|null $falseyScopeCallback + * @param (callable(MutatingScope, Expr): Type)|null $typeCallback */ public function create( MutatingScope $scope, @@ -23,6 +25,7 @@ public function create( array $impurePoints, ?callable $truthyScopeCallback = null, ?callable $falseyScopeCallback = null, + ?callable $typeCallback = null, ): ExpressionResult; } diff --git a/src/Analyser/MutatingScope.php b/src/Analyser/MutatingScope.php index 640bfc481b1..bd16d76e2f5 100644 --- a/src/Analyser/MutatingScope.php +++ b/src/Analyser/MutatingScope.php @@ -1198,7 +1198,7 @@ public function getKeepVoidType(Expr $node): Type return $this->getType($clonedNode); } - public function doNotTreatPhpDocTypesAsCertain(): Scope + public function doNotTreatPhpDocTypesAsCertain(): self { return $this->promoteNativeTypes(); } From 5c4ea041293cf725b0fa1d64d01e172bf8a0243f Mon Sep 17 00:00:00 2001 From: Ondrej Mirtes Date: Fri, 12 Jun 2026 18:46:29 +0200 Subject: [PATCH 008/113] ExpressionResultStorageStack - answer type questions from ExpressionResults Old-world consumers (TypeSpecifier dispatcher, extensions, rules below PHP 8.1, unconverted handlers' resolveType) keep working for nodes whose handler no longer implements TypeResolvingExprHandler: every scope shares the ExpressionResultStorageStack created by its internal scope factory, NodeScopeResolver pushes the storage of the analysis in progress through MutatingScope::pushExpressionResultStorage() (always popped in finally), and MutatingScope resolves such nodes from the stored result - or by processing a synthetic node on demand. Also adds MutatingScope::applySpecifiedTypes (filterBySpecifiedTypes without Scope::getType()) and the specifyTypesCallback slot on ExpressionResult consulted by getTruthyScope()/getFalseyScope(). The cycle collector is disabled in bin/phpstan - scopes deliberately never reference a storage directly, only the stack. Popping severs the stack->storage edge when an analysis ends, so retained scopes do not pin the whole result graph. Co-Authored-By: Claude Fable 5 --- phpstan-baseline.neon | 2 +- src/Analyser/DirectInternalScopeFactory.php | 30 +-- src/Analyser/ExpressionResult.php | 36 +++ src/Analyser/ExpressionResultFactory.php | 2 + src/Analyser/ExpressionResultStorage.php | 10 +- src/Analyser/ExpressionResultStorageStack.php | 52 ++++ src/Analyser/Fiber/FiberNodeScopeResolver.php | 27 +- src/Analyser/LazyInternalScopeFactory.php | 9 +- src/Analyser/MutatingScope.php | 241 ++++++++++++++++-- src/Analyser/NodeScopeResolver.php | 74 +++++- 10 files changed, 412 insertions(+), 71 deletions(-) create mode 100644 src/Analyser/ExpressionResultStorageStack.php diff --git a/phpstan-baseline.neon b/phpstan-baseline.neon index 2c3e0d16c3c..9b17fcbb52e 100644 --- a/phpstan-baseline.neon +++ b/phpstan-baseline.neon @@ -69,7 +69,7 @@ parameters: - rawMessage: Casting to string something that's already string. identifier: cast.useless - count: 3 + count: 5 path: src/Analyser/MutatingScope.php - diff --git a/src/Analyser/DirectInternalScopeFactory.php b/src/Analyser/DirectInternalScopeFactory.php index 533d37c5f92..02afcab90e5 100644 --- a/src/Analyser/DirectInternalScopeFactory.php +++ b/src/Analyser/DirectInternalScopeFactory.php @@ -19,6 +19,8 @@ final class DirectInternalScopeFactory implements InternalScopeFactory { + private ExpressionResultStorageStack $expressionResultStorageStack; + /** * @param int|array{min: int, max: int}|null $configPhpVersion * @param callable(Node $node, Scope $scope): void|null $nodeCallback @@ -38,8 +40,10 @@ public function __construct( private $nodeCallback, private ConstantResolver $constantResolver, private bool $fiber = false, + ?ExpressionResultStorageStack $expressionResultStorageStack = null, ) { + $this->expressionResultStorageStack = $expressionResultStorageStack ?? new ExpressionResultStorageStack(); } public function create( @@ -77,6 +81,7 @@ public function create( $this->propertyReflectionFinder, $this->parser, $this->constantResolver, + $this->expressionResultStorageStack, $context, $this->phpVersion, $this->attributeReflectionFactory, @@ -102,25 +107,15 @@ public function create( public function toFiberFactory(): InternalScopeFactory { - return new self( - $this->container, - $this->reflectionProvider, - $this->initializerExprTypeResolver, - $this->expressionTypeResolverExtensionRegistryProvider, - $this->exprPrinter, - $this->typeSpecifier, - $this->propertyReflectionFinder, - $this->parser, - $this->phpVersion, - $this->attributeReflectionFactory, - $this->configPhpVersion, - $this->nodeCallback, - $this->constantResolver, - true, - ); + return $this->withFlavor(true); } public function toMutatingFactory(): InternalScopeFactory + { + return $this->withFlavor(false); + } + + private function withFlavor(bool $fiber): self { return new self( $this->container, @@ -136,7 +131,8 @@ public function toMutatingFactory(): InternalScopeFactory $this->configPhpVersion, $this->nodeCallback, $this->constantResolver, - false, + $fiber, + $this->expressionResultStorageStack, ); } diff --git a/src/Analyser/ExpressionResult.php b/src/Analyser/ExpressionResult.php index 4bc44318c57..9128ea6d2e0 100644 --- a/src/Analyser/ExpressionResult.php +++ b/src/Analyser/ExpressionResult.php @@ -15,6 +15,9 @@ final class ExpressionResult /** @var (callable(MutatingScope, Expr): Type)|null */ private $typeCallback; + /** @var (callable(MutatingScope, TypeSpecifierContext): SpecifiedTypes)|null */ + private $specifyTypesCallback; + /** @var (callable(): MutatingScope)|null */ private $truthyScopeCallback; @@ -33,6 +36,7 @@ final class ExpressionResult * @param InternalThrowPoint[] $throwPoints * @param ImpurePoint[] $impurePoints * @param (callable(MutatingScope, Expr): Type)|null $typeCallback + * @param (callable(MutatingScope, TypeSpecifierContext): SpecifiedTypes)|null $specifyTypesCallback * @param (callable(): MutatingScope)|null $truthyScopeCallback * @param (callable(): MutatingScope)|null $falseyScopeCallback */ @@ -48,11 +52,13 @@ public function __construct( ?callable $truthyScopeCallback = null, ?callable $falseyScopeCallback = null, ?callable $typeCallback = null, + ?callable $specifyTypesCallback = null, ) { $this->truthyScopeCallback = $truthyScopeCallback; $this->falseyScopeCallback = $falseyScopeCallback; $this->typeCallback = $typeCallback; + $this->specifyTypesCallback = $specifyTypesCallback; } public function getScope(): MutatingScope @@ -93,6 +99,12 @@ public function getTruthyScope(): MutatingScope } if ($this->truthyScopeCallback === null) { + if ($this->specifyTypesCallback !== null) { + return $this->truthyScope = $this->scope->applySpecifiedTypes( + ($this->specifyTypesCallback)($this->scope, TypeSpecifierContext::createTruthy()), + ); + } + return $this->truthyScope = $this->scope->filterByTruthyValue($this->expr); } @@ -107,6 +119,12 @@ public function getFalseyScope(): MutatingScope } if ($this->falseyScopeCallback === null) { + if ($this->specifyTypesCallback !== null) { + return $this->falseyScope = $this->scope->applySpecifiedTypes( + ($this->specifyTypesCallback)($this->scope, TypeSpecifierContext::createFalsey()), + ); + } + return $this->falseyScope = $this->scope->filterByFalseyValue($this->expr); } @@ -152,4 +170,22 @@ public function getNativeType(): Type return $this->cachedNativeType = $this->beforeScope->getNativeType($this->expr); } + public function hasTypeCallback(): bool + { + return $this->typeCallback !== null; + } + + /** + * Re-evaluates the expression type on a different scope (e.g. a narrowed one). + * Unlike getType(), the result is not cached. + */ + public function getTypeForScope(MutatingScope $scope): Type + { + if ($this->typeCallback !== null) { + return TypeUtils::resolveLateResolvableTypes(($this->typeCallback)($scope, $this->expr)); + } + + return $scope->getType($this->expr); + } + } diff --git a/src/Analyser/ExpressionResultFactory.php b/src/Analyser/ExpressionResultFactory.php index d7c5955f4c1..43926896f5f 100644 --- a/src/Analyser/ExpressionResultFactory.php +++ b/src/Analyser/ExpressionResultFactory.php @@ -14,6 +14,7 @@ interface ExpressionResultFactory * @param (callable(): MutatingScope)|null $truthyScopeCallback * @param (callable(): MutatingScope)|null $falseyScopeCallback * @param (callable(MutatingScope, Expr): Type)|null $typeCallback + * @param (callable(MutatingScope, TypeSpecifierContext): SpecifiedTypes)|null $specifyTypesCallback */ public function create( MutatingScope $scope, @@ -26,6 +27,7 @@ public function create( ?callable $truthyScopeCallback = null, ?callable $falseyScopeCallback = null, ?callable $typeCallback = null, + ?callable $specifyTypesCallback = null, ): ExpressionResult; } diff --git a/src/Analyser/ExpressionResultStorage.php b/src/Analyser/ExpressionResultStorage.php index daea8cf3c93..b04bfb507e2 100644 --- a/src/Analyser/ExpressionResultStorage.php +++ b/src/Analyser/ExpressionResultStorage.php @@ -15,6 +15,12 @@ final class ExpressionResultStorage /** @var SplObjectStorage */ private SplObjectStorage $exprResults; + /** + * Read-only fallback - writes never reach it. Makes duplicate() O(1) + * instead of copying all stored results. + */ + private ?self $fallback = null; + /** @var array, request: ExpressionResultRequest}> */ public array $pendingFibers = []; @@ -29,7 +35,7 @@ public function __construct() public function duplicate(): self { $new = new self(); - $new->exprResults->addAll($this->exprResults); + $new->fallback = $this; return $new; } @@ -45,7 +51,7 @@ public function storeExpressionResult(Expr $expr, ExpressionResult $expressionRe public function findExpressionResult(Expr $expr): ?ExpressionResult { - return $this->exprResults[$expr] ?? null; + return $this->exprResults[$expr] ?? $this->fallback?->findExpressionResult($expr); } } diff --git a/src/Analyser/ExpressionResultStorageStack.php b/src/Analyser/ExpressionResultStorageStack.php new file mode 100644 index 00000000000..3bf644f7965 --- /dev/null +++ b/src/Analyser/ExpressionResultStorageStack.php @@ -0,0 +1,52 @@ + results -> scopes -> storage) + * that never gets collected because the cycle collector is disabled + * in bin/phpstan. + * + * NodeScopeResolver pushes a storage for the duration of an analysis (file, + * statement list, trait pass, on-demand expression) through + * MutatingScope::pushExpressionResultStorage() and must always pop it + * in a finally block. Old-world type questions about expressions whose + * handler no longer implements TypeResolvingExprHandler are answered from + * the current storage (see MutatingScope::resolveTypeOfNewWorldHandlerNode()). + * A scope used outside any running analysis simply misses here and resolves + * on demand with a throwaway storage. + */ +final class ExpressionResultStorageStack +{ + + /** @var list */ + private array $stack = []; + + public function push(ExpressionResultStorage $storage): void + { + $this->stack[] = $storage; + } + + public function pop(): void + { + array_pop($this->stack); + } + + public function getCurrent(): ?ExpressionResultStorage + { + if (count($this->stack) === 0) { + return null; + } + + return $this->stack[count($this->stack) - 1]; + } + +} diff --git a/src/Analyser/Fiber/FiberNodeScopeResolver.php b/src/Analyser/Fiber/FiberNodeScopeResolver.php index 2beceacb7d0..3d74df8e1d6 100644 --- a/src/Analyser/Fiber/FiberNodeScopeResolver.php +++ b/src/Analyser/Fiber/FiberNodeScopeResolver.php @@ -5,7 +5,6 @@ use Fiber; use PhpParser\Node; use PhpParser\Node\Expr; -use PHPStan\Analyser\ExpressionContext; use PHPStan\Analyser\ExpressionResult; use PHPStan\Analyser\ExpressionResultStorage; use PHPStan\Analyser\MutatingScope; @@ -32,6 +31,12 @@ public function callNodeCallback( ExpressionResultStorage $storage, ): void { + if ($nodeCallback instanceof NoopNodeCallback) { + // fibers exist solely to let node callbacks ask about types, + // a noop callback does not need one + return; + } + if (Fiber::getCurrent() !== null) { $nodeCallback($node, $scope->toFiberScope()); return; @@ -115,21 +120,11 @@ protected function processPendingFibers(ExpressionResultStorage $storage): void // Process the synthetic node with a duplicated storage so that the result // computed from the asker's scope does not poison the real storage. - // Real AST nodes contained in the synthetic node already have their - // results stored and are not processed again. - $this->returnStoredExpressionResults = true; - try { - $expressionResult = $this->processExprNode( - new Node\Stmt\Expression($request->expr), - $request->expr, - $request->scope->toMutatingScope(), - $storage->duplicate(), - new NoopNodeCallback(), - ExpressionContext::createTopLevel(), - ); - } finally { - $this->returnStoredExpressionResults = false; - } + $expressionResult = $this->processExprOnDemand( + $request->expr, + $request->scope->toMutatingScope(), + $storage->duplicate(), + ); $request = $fiber->resume($expressionResult); $this->runFiberForNodeCallback($storage, $fiber, $request); diff --git a/src/Analyser/LazyInternalScopeFactory.php b/src/Analyser/LazyInternalScopeFactory.php index 30bad59a9a4..b554cc1a3dd 100644 --- a/src/Analyser/LazyInternalScopeFactory.php +++ b/src/Analyser/LazyInternalScopeFactory.php @@ -41,6 +41,8 @@ final class LazyInternalScopeFactory implements InternalScopeFactory private ?ConstantResolver $constantResolver = null; + private ExpressionResultStorageStack $expressionResultStorageStack; + private ?PhpVersion $phpVersionType = null; private ?AttributeReflectionFactory $attributeReflectionFactory = null; @@ -52,10 +54,12 @@ public function __construct( private Container $container, private $nodeCallback, private bool $fiber = false, + ?ExpressionResultStorageStack $expressionResultStorageStack = null, ) { $this->phpVersion = $this->container->getParameter('phpVersion'); $this->currentSimpleVersionParser = $this->container->getService('currentPhpVersionSimpleParser'); + $this->expressionResultStorageStack = $expressionResultStorageStack ?? new ExpressionResultStorageStack(); } public function create( @@ -105,6 +109,7 @@ public function create( $this->propertyReflectionFinder, $this->currentSimpleVersionParser, $this->constantResolver, + $this->expressionResultStorageStack, $context, $this->phpVersionType, $this->attributeReflectionFactory, @@ -130,12 +135,12 @@ public function create( public function toFiberFactory(): InternalScopeFactory { - return new self($this->container, $this->nodeCallback, true); + return new self($this->container, $this->nodeCallback, true, $this->expressionResultStorageStack); } public function toMutatingFactory(): InternalScopeFactory { - return new self($this->container, $this->nodeCallback, false); + return new self($this->container, $this->nodeCallback, false, $this->expressionResultStorageStack); } } diff --git a/src/Analyser/MutatingScope.php b/src/Analyser/MutatingScope.php index bd16d76e2f5..eedd593d888 100644 --- a/src/Analyser/MutatingScope.php +++ b/src/Analyser/MutatingScope.php @@ -186,6 +186,7 @@ public function __construct( private PropertyReflectionFinder $propertyReflectionFinder, private Parser $parser, private ConstantResolver $constantResolver, + private ExpressionResultStorageStack $expressionResultStorageStack, protected ScopeContext $context, private PhpVersion $phpVersion, private AttributeReflectionFactory $attributeReflectionFactory, @@ -986,19 +987,71 @@ private function resolveType(string $exprString, Expr $node): Type /** @var ExprHandler $exprHandler */ foreach ($this->container->getServicesByTag(ExprHandler::EXTENSION_TAG) as $exprHandler) { - if (!$exprHandler instanceof TypeResolvingExprHandler) { - continue; - } if (!$exprHandler->supports($node)) { continue; } - return $exprHandler->resolveType($this, $node); + if ($exprHandler instanceof TypeResolvingExprHandler) { + return $exprHandler->resolveType($this, $node); + } + + return $this->resolveTypeOfNewWorldHandlerNode($node); } return new MixedType(); } + /** + * The handler of the node no longer implements TypeResolvingExprHandler. + * The answer comes from the ExpressionResult stored during the analysis + * currently in progress, or from processing the node on demand (synthetic + * nodes, or no analysis in progress at all). + * + * The scope deliberately does not reference the storage - that would create + * a reference cycle that never gets collected (see ExpressionResultStorageStack). + */ + private function resolveTypeOfNewWorldHandlerNode(Expr $node): Type + { + $storage = $this->expressionResultStorageStack->getCurrent(); + if ($storage !== null) { + $result = $storage->findExpressionResult($node); + if ($result !== null) { + if (!$result->hasTypeCallback()) { + throw new ShouldNotHappenException(sprintf( + 'ExprHandler for %s does not implement TypeResolvingExprHandler but its ExpressionResult is missing a typeCallback.', + get_class($node), + )); + } + + return $result->getTypeForScope($this); + } + } + + // a synthetic node, or no analysis in progress + $onDemandResult = $this->container->getByType(NodeScopeResolver::class)->processExprOnDemand( + $node, + $this, + $storage !== null ? $storage->duplicate() : new ExpressionResultStorage(), + ); + + return $onDemandResult->getTypeForScope($this); + } + + /** + * Makes the storage answer type questions asked on this scope (and every + * scope sharing its ExpressionResultStorageStack) for the duration of an + * analysis. The caller must pop in a finally block. + */ + public function pushExpressionResultStorage(ExpressionResultStorage $storage): void + { + $this->expressionResultStorageStack->push($storage); + } + + public function popExpressionResultStorage(): void + { + $this->expressionResultStorageStack->pop(); + } + /** * @param callable(Type): ?bool $typeCallback */ @@ -3400,6 +3453,166 @@ public function filterBySpecifiedTypes(SpecifiedTypes $specifiedTypes): self $specifiedExpressions[$typeSpecification['exprString']] = ExpressionTypeHolder::createYes($expr, $scope->getScopeType($expr)); } + $scope = $scope->processConditionalExpressionsAfterSpecifying($specifiedExpressions); + + /** @var static */ + return $scope->scopeFactory->create( + $scope->context, + $scope->isDeclareStrictTypes(), + $scope->getFunction(), + $scope->getNamespace(), + $scope->expressionTypes, + $scope->nativeExpressionTypes, + $this->mergeConditionalExpressions($specifiedTypes->getNewConditionalExpressionHolders(), $scope->conditionalExpressions), + $scope->inClosureBindScopeClasses, + $scope->anonymousFunctionReflection, + $scope->inFirstLevelStatement, + $scope->currentlyAssignedExpressions, + $scope->currentlyAllowedUndefinedExpressions, + $scope->inFunctionCallsStack, + $scope->afterExtractCall, + $scope->parentScope, + $scope->nativeTypesPromoted, + ); + } + + /** + * New-world counterpart of filterBySpecifiedTypes. + * + * The types inside SpecifiedTypes were already computed from ExpressionResults + * by the specifyTypesCallback of an ExprHandler. This method must never call + * Scope::getType() - it only combines the given types with already-tracked + * expression type holders. + */ + public function applySpecifiedTypes(SpecifiedTypes $specifiedTypes): self + { + $typeSpecifications = []; + foreach ($specifiedTypes->getSureTypes() as $exprString => [$expr, $type]) { + if ($expr instanceof Node\Scalar || $expr instanceof Array_ || $expr instanceof Expr\UnaryMinus && $expr->expr instanceof Node\Scalar) { + continue; + } + $typeSpecifications[] = [ + 'sure' => true, + 'exprString' => (string) $exprString, + 'expr' => $expr, + 'type' => $type, + ]; + } + foreach ($specifiedTypes->getSureNotTypes() as $exprString => [$expr, $type]) { + if ($expr instanceof Node\Scalar || $expr instanceof Array_ || $expr instanceof Expr\UnaryMinus && $expr->expr instanceof Node\Scalar) { + continue; + } + $typeSpecifications[] = [ + 'sure' => false, + 'exprString' => (string) $exprString, + 'expr' => $expr, + 'type' => $type, + ]; + } + + usort($typeSpecifications, static function (array $a, array $b): int { + $length = strlen($a['exprString']) - strlen($b['exprString']); + if ($length !== 0) { + return $length; + } + + return $b['sure'] - $a['sure']; // @phpstan-ignore minus.leftNonNumeric, minus.rightNonNumeric + }); + + $scope = $this; + $specifiedExpressions = []; + foreach ($typeSpecifications as $typeSpecification) { + $expr = $typeSpecification['expr']; + $type = $typeSpecification['type']; + $exprString = $typeSpecification['exprString']; + + if ($expr instanceof IssetExpr) { + $issetExpr = $expr; + $expr = $issetExpr->getExpr(); + + if ($typeSpecification['sure']) { + $scope = $scope->setExpressionCertainty( + $expr, + TrinaryLogic::createMaybe(), + ); + } else { + $scope = $scope->unsetExpression($expr); + } + + continue; + } + + $trackedType = null; + $trackedNativeType = null; + if (array_key_exists($exprString, $scope->expressionTypes)) { + $trackedType = $scope->expressionTypes[$exprString]->getType(); + } + if (array_key_exists($exprString, $scope->nativeExpressionTypes)) { + $trackedNativeType = $scope->nativeExpressionTypes[$exprString]->getType(); + } + + if ($typeSpecification['sure']) { + if ($specifiedTypes->shouldOverwrite()) { + $scope = $scope->assignExpression($expr, $type, $type); + } else { + $newType = $trackedType !== null ? TypeCombinator::intersect($type, $trackedType) : $type; + $newNativeType = $trackedNativeType !== null ? TypeCombinator::intersect($type, $trackedNativeType) : $type; + $scope = $scope->specifyExpressionType($expr, $newType, $newNativeType, TrinaryLogic::createYes()); + } + } else { + if ($type instanceof NeverType || $trackedType instanceof NeverType) { + continue; + } + $newType = $trackedType !== null ? TypeCombinator::remove($trackedType, $type) : null; + if ($newType === null) { + // the expression is not tracked - there is nothing to subtract from + continue; + } + $newNativeType = $trackedNativeType !== null ? TypeCombinator::remove($trackedNativeType, $type) : $newType; + $scope = $scope->specifyExpressionType($expr, $newType, $newNativeType, TrinaryLogic::createYes()); + } + + $holderType = array_key_exists($exprString, $scope->expressionTypes) + ? $scope->expressionTypes[$exprString]->getType() + : $type; + $specifiedExpressions[$exprString] = ExpressionTypeHolder::createYes($expr, $holderType); + } + + $scope = $scope->processConditionalExpressionsAfterSpecifying($specifiedExpressions); + + /** @var static */ + return $scope->scopeFactory->create( + $scope->context, + $scope->isDeclareStrictTypes(), + $scope->getFunction(), + $scope->getNamespace(), + $scope->expressionTypes, + $scope->nativeExpressionTypes, + $this->mergeConditionalExpressions($specifiedTypes->getNewConditionalExpressionHolders(), $scope->conditionalExpressions), + $scope->inClosureBindScopeClasses, + $scope->anonymousFunctionReflection, + $scope->inFirstLevelStatement, + $scope->currentlyAssignedExpressions, + $scope->currentlyAllowedUndefinedExpressions, + $scope->inFunctionCallsStack, + $scope->afterExtractCall, + $scope->parentScope, + $scope->nativeTypesPromoted, + ); + } + + /** + * Matches already-registered conditional expressions against the just-specified + * expression type holders and applies the matching consequences. + * + * Mutates and returns $this - only to be called on an intermediate scope + * that is about to be rebuilt through the scope factory. + * + * @param array $specifiedExpressions + */ + private function processConditionalExpressionsAfterSpecifying(array $specifiedExpressions): self + { + $scope = $this; $conditions = []; $originallySpecifiedExprStrings = $specifiedExpressions; $prevSpecifiedCount = -1; @@ -3478,25 +3691,7 @@ public function filterBySpecifiedTypes(SpecifiedTypes $specifiedTypes): self } } - /** @var static */ - return $scope->scopeFactory->create( - $scope->context, - $scope->isDeclareStrictTypes(), - $scope->getFunction(), - $scope->getNamespace(), - $scope->expressionTypes, - $scope->nativeExpressionTypes, - $this->mergeConditionalExpressions($specifiedTypes->getNewConditionalExpressionHolders(), $scope->conditionalExpressions), - $scope->inClosureBindScopeClasses, - $scope->anonymousFunctionReflection, - $scope->inFirstLevelStatement, - $scope->currentlyAssignedExpressions, - $scope->currentlyAllowedUndefinedExpressions, - $scope->inFunctionCallsStack, - $scope->afterExtractCall, - $scope->parentScope, - $scope->nativeTypesPromoted, - ); + return $scope; } /** diff --git a/src/Analyser/NodeScopeResolver.php b/src/Analyser/NodeScopeResolver.php index 97babd039fe..a90327178bb 100644 --- a/src/Analyser/NodeScopeResolver.php +++ b/src/Analyser/NodeScopeResolver.php @@ -283,6 +283,25 @@ public function processNodes( ): void { $expressionResultStorage = new ExpressionResultStorage(); + $scope->pushExpressionResultStorage($expressionResultStorage); + try { + $this->processNodesWithStorage($nodes, $scope, $expressionResultStorage, $nodeCallback); + } finally { + $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 = []; @@ -498,14 +517,19 @@ public function processStmtNodes( ): StatementResult { $storage = new ExpressionResultStorage(); - return $this->processStmtNodesInternal( - $parentNode, - $stmts, - $scope, - $storage, - $nodeCallback, - $context, - )->toPublic(); + $scope->pushExpressionResultStorage($storage); + try { + return $this->processStmtNodesInternal( + $parentNode, + $stmts, + $scope, + $storage, + $nodeCallback, + $context, + )->toPublic(); + } finally { + $scope->popExpressionResultStorage(); + } } /** @@ -1424,8 +1448,13 @@ public function processStmtNode( // fresh storage - the same trait node objects are processed once per // using class and fibers must not see results from a previous pass $traitStorage = new ExpressionResultStorage(); - $this->processTraitUse($stmt, $scope, $traitStorage, $nodeCallback); - $this->processPendingFibers($traitStorage); + $scope->pushExpressionResultStorage($traitStorage); + try { + $this->processTraitUse($stmt, $scope, $traitStorage, $nodeCallback); + $this->processPendingFibers($traitStorage); + } finally { + $scope->popExpressionResultStorage(); + } // class-level node callbacks (like ClassMethodsNode) are invoked with // the outer storage but ask about expressions inside the used trait @@ -2749,6 +2778,31 @@ private function findEarlyTerminatingExpr(Expr $expr, Scope $scope): ?Expr return null; } + /** + * Processes an expression outside the normal AST traversal - e.g. a synthetic + * node a rule or extension asks about. Real AST nodes contained in it return + * their already-stored results instead of being processed again. New results + * are stored into the given storage - pass a duplicate to keep them isolated. + */ + public function processExprOnDemand(Expr $expr, MutatingScope $scope, ExpressionResultStorage $storage): ExpressionResult + { + $this->returnStoredExpressionResults = true; + $scope->pushExpressionResultStorage($storage); + try { + return $this->processExprNode( + new Node\Stmt\Expression($expr), + $expr, + $scope, + $storage, + new NoopNodeCallback(), + ExpressionContext::createTopLevel(), + ); + } finally { + $scope->popExpressionResultStorage(); + $this->returnStoredExpressionResults = false; + } + } + /** * @param callable(Node $node, Scope $scope): void $nodeCallback */ From 55f3c7b4ddb3421212003811b6904ac1de6dcd71 Mon Sep 17 00:00:00 2001 From: Ondrej Mirtes Date: Fri, 12 Jun 2026 18:46:29 +0200 Subject: [PATCH 009/113] Migrate ArrayHandler - per-item types from ExpressionResults Each item type is captured at its own evaluation point in the sequence, so [$b = 1, $b + 1, $c = $b, $c + 2, $c++, $c] infers array{1, 2, 1, 3, 1, 2} - the old world resolves all items on a single scope and cannot do this. Until the item handlers (BinaryOp, inc/dec, Assign) migrate themselves, the items resolve as their own results' before-scope evaluation instead of cascading getTypeForScope(). Narrowing stays on the fallback path - it is identical to the removed specifyDefaultTypes body. Co-Authored-By: Claude Fable 5 --- src/Analyser/ExprHandler/ArrayHandler.php | 80 ++++++++++--------- .../PHPStan/Analyser/nsrt/assign-in-array.php | 23 ++++++ 2 files changed, 67 insertions(+), 36 deletions(-) create mode 100644 tests/PHPStan/Analyser/nsrt/assign-in-array.php diff --git a/src/Analyser/ExprHandler/ArrayHandler.php b/src/Analyser/ExprHandler/ArrayHandler.php index 6fe74abef78..501fd5074de 100644 --- a/src/Analyser/ExprHandler/ArrayHandler.php +++ b/src/Analyser/ExprHandler/ArrayHandler.php @@ -12,13 +12,9 @@ use PHPStan\Analyser\ExpressionResult; use PHPStan\Analyser\ExpressionResultFactory; use PHPStan\Analyser\ExpressionResultStorage; +use PHPStan\Analyser\ExprHandler; use PHPStan\Analyser\MutatingScope; use PHPStan\Analyser\NodeScopeResolver; -use PHPStan\Analyser\Scope; -use PHPStan\Analyser\SpecifiedTypes; -use PHPStan\Analyser\TypeResolvingExprHandler; -use PHPStan\Analyser\TypeSpecifier; -use PHPStan\Analyser\TypeSpecifierContext; use PHPStan\DependencyInjection\AutowiredService; use PHPStan\Node\LiteralArrayItem; use PHPStan\Node\LiteralArrayNode; @@ -26,14 +22,16 @@ use PHPStan\Type\CallableType; use PHPStan\Type\Type; use PHPStan\Type\TypeCombinator; +use function array_key_exists; use function array_merge; use function count; +use function spl_object_id; /** - * @implements TypeResolvingExprHandler + * @implements ExprHandler */ #[AutowiredService] -final class ArrayHandler implements TypeResolvingExprHandler +final class ArrayHandler implements ExprHandler { public function __construct( @@ -48,34 +46,11 @@ public function supports(Expr $expr): bool return $expr instanceof Array_; } - public function resolveType(MutatingScope $scope, Expr $expr): Type - { - $type = $this->initializerExprTypeResolver->getArrayType($expr, static fn (Expr $expr): Type => $scope->getType($expr)); - - if ( - count($expr->items) === 2 - && isset($expr->items[0], $expr->items[1]) - && $type->isCallable()->maybe() - ) { - $isCallableCall = new FuncCall( - new FullyQualified('is_callable'), - [new Arg($expr)], - ); - if ( - $scope->hasExpressionType($isCallableCall)->yes() - && $scope->getType($isCallableCall)->isTrue()->yes() - ) { - $type = TypeCombinator::intersect($type, new CallableType()); - } - } - - return $type; - } - public function processExpr(NodeScopeResolver $nodeScopeResolver, Stmt $stmt, Expr $expr, MutatingScope $scope, ExpressionResultStorage $storage, callable $nodeCallback, ExpressionContext $context): ExpressionResult { $beforeScope = $scope; $itemNodes = []; + $itemResults = []; $hasYield = false; $throwPoints = []; $impurePoints = []; @@ -85,6 +60,7 @@ public function processExpr(NodeScopeResolver $nodeScopeResolver, Stmt $stmt, Ex $nodeScopeResolver->callNodeCallback($nodeCallback, $arrayItem, $scope, $storage); if ($arrayItem->key !== null) { $keyResult = $nodeScopeResolver->processExprNode($stmt, $arrayItem->key, $scope, $storage, $nodeCallback, $context->enterDeep()); + $itemResults[spl_object_id($arrayItem->key)] = $keyResult; $hasYield = $hasYield || $keyResult->hasYield(); $throwPoints = array_merge($throwPoints, $keyResult->getThrowPoints()); $impurePoints = array_merge($impurePoints, $keyResult->getImpurePoints()); @@ -93,6 +69,7 @@ public function processExpr(NodeScopeResolver $nodeScopeResolver, Stmt $stmt, Ex } $valueResult = $nodeScopeResolver->processExprNode($stmt, $arrayItem->value, $scope, $storage, $nodeCallback, $context->enterDeep()); + $itemResults[spl_object_id($arrayItem->value)] = $valueResult; $hasYield = $hasYield || $valueResult->hasYield(); $throwPoints = array_merge($throwPoints, $valueResult->getThrowPoints()); $impurePoints = array_merge($impurePoints, $valueResult->getImpurePoints()); @@ -109,12 +86,43 @@ public function processExpr(NodeScopeResolver $nodeScopeResolver, Stmt $stmt, Ex isAlwaysTerminating: $isAlwaysTerminating, throwPoints: $throwPoints, impurePoints: $impurePoints, - ); - } + typeCallback: function (MutatingScope $s) use ($expr, $itemResults): Type { + // each item type was captured at its own evaluation point in the + // sequence - resolving all items on any single scope (the old world) + // cannot handle items with side effects like [$b = 1, $b + 1, $b++] + $type = $this->initializerExprTypeResolver->getArrayType($expr, static function (Expr $inner) use ($itemResults, $s): Type { + $id = spl_object_id($inner); + if (array_key_exists($id, $itemResults)) { + return $s->nativeTypesPromoted + ? $itemResults[$id]->getNativeType() + : $itemResults[$id]->getType(); + } - public function specifyTypes(TypeSpecifier $typeSpecifier, Scope $scope, Expr $expr, TypeSpecifierContext $context): SpecifiedTypes - { - return $typeSpecifier->specifyDefaultTypes($scope, $expr, $context); + // getArrayType only asks about item keys and values - guarded + // legacy bridge just in case + return $s->getType($inner); + }); + + if ( + count($expr->items) === 2 + && isset($expr->items[0], $expr->items[1]) + && $type->isCallable()->maybe() + ) { + $isCallableCall = new FuncCall( + new FullyQualified('is_callable'), + [new Arg($expr)], + ); + if ( + $s->hasExpressionType($isCallableCall)->yes() + && $s->getType($isCallableCall)->isTrue()->yes() + ) { + $type = TypeCombinator::intersect($type, new CallableType()); + } + } + + return $type; + }, + ); } } diff --git a/tests/PHPStan/Analyser/nsrt/assign-in-array.php b/tests/PHPStan/Analyser/nsrt/assign-in-array.php new file mode 100644 index 00000000000..955df512571 --- /dev/null +++ b/tests/PHPStan/Analyser/nsrt/assign-in-array.php @@ -0,0 +1,23 @@ + Date: Fri, 12 Jun 2026 18:48:20 +0200 Subject: [PATCH 010/113] Throw on unbalanced ExpressionResultStorageStack pop An unbalanced push/pop is the one way the ambient storage design can still be misused - fail immediately instead of silently answering later type questions from the wrong storage. Co-Authored-By: Claude Fable 5 --- src/Analyser/ExpressionResultStorageStack.php | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/src/Analyser/ExpressionResultStorageStack.php b/src/Analyser/ExpressionResultStorageStack.php index 3bf644f7965..9d0e37f4e71 100644 --- a/src/Analyser/ExpressionResultStorageStack.php +++ b/src/Analyser/ExpressionResultStorageStack.php @@ -2,6 +2,7 @@ namespace PHPStan\Analyser; +use PHPStan\ShouldNotHappenException; use function array_pop; use function count; @@ -37,6 +38,10 @@ public function push(ExpressionResultStorage $storage): void public function pop(): void { + if (count($this->stack) === 0) { + throw new ShouldNotHappenException('Unbalanced ExpressionResultStorageStack pop.'); + } + array_pop($this->stack); } From 538dd886022a1910d03fbbc695a52a17f1d41e70 Mon Sep 17 00:00:00 2001 From: Ondrej Mirtes Date: Fri, 12 Jun 2026 19:19:11 +0200 Subject: [PATCH 011/113] Fix PHP 7.4 compat --- src/Analyser/ExpressionResultStorage.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Analyser/ExpressionResultStorage.php b/src/Analyser/ExpressionResultStorage.php index b04bfb507e2..10c14749296 100644 --- a/src/Analyser/ExpressionResultStorage.php +++ b/src/Analyser/ExpressionResultStorage.php @@ -51,7 +51,7 @@ public function storeExpressionResult(Expr $expr, ExpressionResult $expressionRe public function findExpressionResult(Expr $expr): ?ExpressionResult { - return $this->exprResults[$expr] ?? $this->fallback?->findExpressionResult($expr); + return $this->exprResults[$expr] ?? ($this->fallback !== null ? $this->fallback->findExpressionResult($expr) : null); } } From 9f40fe1986fd87f5963752b3af1f687a70cdd93a Mon Sep 17 00:00:00 2001 From: Ondrej Mirtes Date: Fri, 12 Jun 2026 19:35:07 +0200 Subject: [PATCH 012/113] Migrate VariableHandler and InstanceofHandler - narrowing from ExpressionResults Both handlers stop implementing TypeResolvingExprHandler and wire typeCallback + specifyTypesCallback, so their truthy/falsey scopes flow through MutatingScope::applySpecifiedTypes - the first real exercise of the new-world narrowing machinery. The old-world TypeSpecifier dispatcher answers nodes of converted handlers from the stored ExpressionResult (MutatingScope::specifyTypesOfNewWorldHandlerNode), processes synthetic nodes on demand (the 'foo' === $a::class rewrite builds synthetic Instanceof_ nodes), and falls back to specifyDefaultTypes when the result carries no callback - which is exactly what such handlers used to implement. Exercising the machinery surfaced two gaps in applySpecifiedTypes: - Expressions not tracked in the scope lost their sureNot narrowing ($var->name instanceof Identifier ? ... : ... stopped narrowing the else branch). The current type to intersect with or subtract from is now priced from the stored ExpressionResult (getCurrentTypesOfSpecifiedExpr) instead of Scope::getType(). - Only Yes-certainty holders hold the current type of an expression. A Maybe-certainty holder holds the when-defined type (falsy after an or-merge in nsrt/bug-pr-339.php), which the certainty-aware Scope::getType() never returned - narrowing against it produced NeverType and mis-fired conditional expression holders. AssignHandler's placeholder result for an assignment target now carries VariableHandler::createTypeCallback - every stored result for a converted handler's node type must answer type questions itself. Co-Authored-By: Claude Fable 5 --- src/Analyser/ExprHandler/AssignHandler.php | 3 + .../ExprHandler/InstanceofHandler.php | 193 +++++++++--------- src/Analyser/ExprHandler/VariableHandler.php | 92 +++++---- src/Analyser/ExpressionResult.php | 14 ++ src/Analyser/MutatingScope.php | 83 +++++++- src/Analyser/TypeSpecifier.php | 16 +- 6 files changed, 262 insertions(+), 139 deletions(-) diff --git a/src/Analyser/ExprHandler/AssignHandler.php b/src/Analyser/ExprHandler/AssignHandler.php index e0db4837ddc..9ce7f6739bd 100644 --- a/src/Analyser/ExprHandler/AssignHandler.php +++ b/src/Analyser/ExprHandler/AssignHandler.php @@ -420,6 +420,9 @@ public function processAssignVar( isAlwaysTerminating: false, throwPoints: [], impurePoints: [], + // VariableHandler no longer implements TypeResolvingExprHandler - + // type questions about the target node are answered from this result + typeCallback: $var instanceof Variable ? VariableHandler::createTypeCallback($var) : null, )); $nodeScopeResolver->callNodeCallback($nodeCallback, $var, $enterExpressionAssign ? $scope->enterExpressionAssign($var) : $scope, $storage); $hasYield = false; diff --git a/src/Analyser/ExprHandler/InstanceofHandler.php b/src/Analyser/ExprHandler/InstanceofHandler.php index 885a418f846..ff8bda6c552 100644 --- a/src/Analyser/ExprHandler/InstanceofHandler.php +++ b/src/Analyser/ExprHandler/InstanceofHandler.php @@ -10,11 +10,10 @@ use PHPStan\Analyser\ExpressionResult; use PHPStan\Analyser\ExpressionResultFactory; use PHPStan\Analyser\ExpressionResultStorage; +use PHPStan\Analyser\ExprHandler; use PHPStan\Analyser\MutatingScope; use PHPStan\Analyser\NodeScopeResolver; -use PHPStan\Analyser\Scope; use PHPStan\Analyser\SpecifiedTypes; -use PHPStan\Analyser\TypeResolvingExprHandler; use PHPStan\Analyser\TypeSpecifier; use PHPStan\Analyser\TypeSpecifierContext; use PHPStan\DependencyInjection\AutowiredService; @@ -33,13 +32,16 @@ use function strtolower; /** - * @implements TypeResolvingExprHandler + * @implements ExprHandler */ #[AutowiredService] -final class InstanceofHandler implements TypeResolvingExprHandler +final class InstanceofHandler implements ExprHandler { - public function __construct(private ExpressionResultFactory $expressionResultFactory) + public function __construct( + private ExpressionResultFactory $expressionResultFactory, + private TypeSpecifier $typeSpecifier, + ) { } @@ -57,6 +59,7 @@ public function processExpr(NodeScopeResolver $nodeScopeResolver, Stmt $stmt, Ex $impurePoints = $exprResult->getImpurePoints(); $isAlwaysTerminating = $exprResult->isAlwaysTerminating(); $scope = $exprResult->getScope(); + $classResult = null; if (!$expr->class instanceof Name) { $classResult = $nodeScopeResolver->processExprNode($stmt, $expr->class, $scope, $storage, $nodeCallback, $context->enterDeep()); $scope = $classResult->getScope(); @@ -74,104 +77,106 @@ public function processExpr(NodeScopeResolver $nodeScopeResolver, Stmt $stmt, Ex isAlwaysTerminating: $isAlwaysTerminating, throwPoints: $throwPoints, impurePoints: $impurePoints, - ); - } - - public function resolveType(MutatingScope $scope, Expr $expr): Type - { - $expressionType = $scope->getType($expr->expr); - if ( - $scope->isInTrait() - && TypeUtils::findThisType($expressionType) !== null - ) { - return new BooleanType(); - } - if ($expressionType instanceof NeverType) { - return new ConstantBooleanType(false); - } - - $uncertainty = false; - - if ($expr->class instanceof Name) { - $unresolvedClassName = $expr->class->toString(); - if ( - strtolower($unresolvedClassName) === 'static' - && $scope->isInClass() - ) { - $classType = new StaticType($scope->getClassReflection()); - } else { - $className = $scope->resolveName($expr->class); - $classType = new ObjectType($className); - } - } else { - $result = $scope->getType($expr->class)->toObjectTypeForInstanceofCheck(); - $classType = $result->type; - $uncertainty = $result->uncertainty; - } + typeCallback: static function (MutatingScope $s) use ($expr, $exprResult, $classResult): Type { + $expressionType = $exprResult->getTypeForScope($s); + if ( + $s->isInTrait() + && TypeUtils::findThisType($expressionType) !== null + ) { + return new BooleanType(); + } + if ($expressionType instanceof NeverType) { + return new ConstantBooleanType(false); + } - if ($classType->isSuperTypeOf(new MixedType())->yes()) { - return new BooleanType(); - } + $uncertainty = false; + + if ($expr->class instanceof Name) { + $unresolvedClassName = $expr->class->toString(); + if ( + strtolower($unresolvedClassName) === 'static' + && $s->isInClass() + ) { + $classType = new StaticType($s->getClassReflection()); + } else { + $className = $s->resolveName($expr->class); + $classType = new ObjectType($className); + } + } else { + $classNameType = $classResult !== null + ? $classResult->getTypeForScope($s) + : $s->getType($expr->class); + $result = $classNameType->toObjectTypeForInstanceofCheck(); + $classType = $result->type; + $uncertainty = $result->uncertainty; + } - $isSuperType = $classType->isSuperTypeOf($expressionType); + if ($classType->isSuperTypeOf(new MixedType())->yes()) { + return new BooleanType(); + } - if ($isSuperType->no()) { - return new ConstantBooleanType(false); - } elseif ($isSuperType->yes() && !$uncertainty) { - return new ConstantBooleanType(true); - } + $isSuperType = $classType->isSuperTypeOf($expressionType); - return new BooleanType(); - } + if ($isSuperType->no()) { + return new ConstantBooleanType(false); + } elseif ($isSuperType->yes() && !$uncertainty) { + return new ConstantBooleanType(true); + } - public function specifyTypes(TypeSpecifier $typeSpecifier, Scope $scope, Expr $expr, TypeSpecifierContext $context): SpecifiedTypes - { - $exprNode = $expr->expr; - if ($expr->class instanceof Name) { - $className = (string) $expr->class; - $lowercasedClassName = strtolower($className); - if ($lowercasedClassName === 'self' && $scope->isInClass()) { - $type = new ObjectType($scope->getClassReflection()->getName()); - } elseif ($lowercasedClassName === 'static' && $scope->isInClass()) { - $type = new StaticType($scope->getClassReflection()); - } elseif ($lowercasedClassName === 'parent') { - if ( - $scope->isInClass() - && $scope->getClassReflection()->getParentClass() !== null - ) { - $type = new ObjectType($scope->getClassReflection()->getParentClass()->getName()); - } else { - $type = new NonexistentParentClassType(); + return new BooleanType(); + }, + specifyTypesCallback: function (MutatingScope $s, TypeSpecifierContext $context) use ($expr, $exprResult, $classResult): SpecifiedTypes { + $exprNode = $expr->expr; + if ($expr->class instanceof Name) { + $className = (string) $expr->class; + $lowercasedClassName = strtolower($className); + if ($lowercasedClassName === 'self' && $s->isInClass()) { + $type = new ObjectType($s->getClassReflection()->getName()); + } elseif ($lowercasedClassName === 'static' && $s->isInClass()) { + $type = new StaticType($s->getClassReflection()); + } elseif ($lowercasedClassName === 'parent') { + if ( + $s->isInClass() + && $s->getClassReflection()->getParentClass() !== null + ) { + $type = new ObjectType($s->getClassReflection()->getParentClass()->getName()); + } else { + $type = new NonexistentParentClassType(); + } + } else { + $type = new ObjectType($className); + } + return $this->typeSpecifier->create($exprNode, $type, $context, $s)->setRootExpr($expr); } - } else { - $type = new ObjectType($className); - } - return $typeSpecifier->create($exprNode, $type, $context, $scope)->setRootExpr($expr); - } - $result = $scope->getType($expr->class)->toObjectTypeForInstanceofCheck(); - $type = $result->type; - $uncertainty = $result->uncertainty; - - if (!$type->isSuperTypeOf(new MixedType())->yes()) { - if ($context->true()) { - $type = TypeCombinator::intersect( - $type, - new ObjectWithoutClassType(), - ); - return $typeSpecifier->create($exprNode, $type, $context, $scope)->setRootExpr($expr); - } elseif ($context->false() && !$uncertainty) { - $exprType = $scope->getType($expr->expr); - if (!$type->isSuperTypeOf($exprType)->yes()) { - return $typeSpecifier->create($exprNode, $type, $context, $scope)->setRootExpr($expr); + $classNameType = $classResult !== null + ? $classResult->getTypeForScope($s) + : $s->getType($expr->class); + $result = $classNameType->toObjectTypeForInstanceofCheck(); + $type = $result->type; + $uncertainty = $result->uncertainty; + + if (!$type->isSuperTypeOf(new MixedType())->yes()) { + if ($context->true()) { + $type = TypeCombinator::intersect( + $type, + new ObjectWithoutClassType(), + ); + return $this->typeSpecifier->create($exprNode, $type, $context, $s)->setRootExpr($expr); + } elseif ($context->false() && !$uncertainty) { + $exprType = $exprResult->getTypeForScope($s); + if (!$type->isSuperTypeOf($exprType)->yes()) { + return $this->typeSpecifier->create($exprNode, $type, $context, $s)->setRootExpr($expr); + } + } + } + if ($context->true()) { + return $this->typeSpecifier->create($exprNode, new ObjectWithoutClassType(), $context, $s)->setRootExpr($exprNode); } - } - } - if ($context->true()) { - return $typeSpecifier->create($exprNode, new ObjectWithoutClassType(), $context, $scope)->setRootExpr($exprNode); - } - return (new SpecifiedTypes([], []))->setRootExpr($expr); + return (new SpecifiedTypes([], []))->setRootExpr($expr); + }, + ); } } diff --git a/src/Analyser/ExprHandler/VariableHandler.php b/src/Analyser/ExprHandler/VariableHandler.php index 70ce8439856..1fc9923355f 100644 --- a/src/Analyser/ExprHandler/VariableHandler.php +++ b/src/Analyser/ExprHandler/VariableHandler.php @@ -2,6 +2,7 @@ namespace PHPStan\Analyser\ExprHandler; +use Closure; use PhpParser\Node\Expr; use PhpParser\Node\Expr\BinaryOp\Identical; use PhpParser\Node\Expr\Variable; @@ -11,12 +12,12 @@ use PHPStan\Analyser\ExpressionResult; use PHPStan\Analyser\ExpressionResultFactory; use PHPStan\Analyser\ExpressionResultStorage; +use PHPStan\Analyser\ExprHandler; use PHPStan\Analyser\ImpurePoint; use PHPStan\Analyser\MutatingScope; use PHPStan\Analyser\NodeScopeResolver; use PHPStan\Analyser\Scope; use PHPStan\Analyser\SpecifiedTypes; -use PHPStan\Analyser\TypeResolvingExprHandler; use PHPStan\Analyser\TypeSpecifier; use PHPStan\Analyser\TypeSpecifierContext; use PHPStan\DependencyInjection\AutowiredService; @@ -29,13 +30,16 @@ use function is_string; /** - * @implements TypeResolvingExprHandler + * @implements ExprHandler */ #[AutowiredService] -final class VariableHandler implements TypeResolvingExprHandler +final class VariableHandler implements ExprHandler { - public function __construct(private ExpressionResultFactory $expressionResultFactory) + public function __construct( + private ExpressionResultFactory $expressionResultFactory, + private TypeSpecifier $typeSpecifier, + ) { } @@ -44,36 +48,49 @@ public function supports(Expr $expr): bool return $expr instanceof Variable; } - public function resolveType(MutatingScope $scope, Expr $expr): Type + /** + * Evaluates the variable as a read on the asking scope. Also used by + * AssignHandler for the placeholder result it stores for an assignment + * target - every stored result for a Variable node must carry a + * typeCallback now that this handler no longer implements + * TypeResolvingExprHandler. + * + * @return Closure(MutatingScope): Type + */ + public static function createTypeCallback(Variable $expr, ?ExpressionResult $nameResult = null): Closure { - if (is_string($expr->name)) { - if ($scope->hasVariableType($expr->name)->no()) { - return new ErrorType(); + return static function (MutatingScope $s) use ($expr, $nameResult): Type { + if (is_string($expr->name)) { + if ($s->hasVariableType($expr->name)->no()) { + return new ErrorType(); + } + + return $s->getVariableType($expr->name); } - return $scope->getVariableType($expr->name); - } + $nameType = $nameResult !== null + ? $nameResult->getTypeForScope($s) + : $s->getType($expr->name); + if (count($nameType->getConstantStrings()) > 0) { + $types = []; + foreach ($nameType->getConstantStrings() as $constantString) { + $variableScope = $s + ->filterByTruthyValue( + new Identical($expr->name, new String_($constantString->getValue())), + ); + if ($variableScope->hasVariableType($constantString->getValue())->no()) { + $types[] = new ErrorType(); + continue; + } - $nameType = $scope->getType($expr->name); - if (count($nameType->getConstantStrings()) > 0) { - $types = []; - foreach ($nameType->getConstantStrings() as $constantString) { - $variableScope = $scope - ->filterByTruthyValue( - new Identical($expr->name, new String_($constantString->getValue())), - ); - if ($variableScope->hasVariableType($constantString->getValue())->no()) { - $types[] = new ErrorType(); - continue; + $types[] = $variableScope->getVariableType($constantString->getValue()); } - $types[] = $variableScope->getVariableType($constantString->getValue()); + return TypeCombinator::union(...$types); } - return TypeCombinator::union(...$types); - } - - return new MixedType(); + return new MixedType(); + }; } public function processExpr(NodeScopeResolver $nodeScopeResolver, Stmt $stmt, Expr $expr, MutatingScope $scope, ExpressionResultStorage $storage, callable $nodeCallback, ExpressionContext $context): ExpressionResult @@ -83,6 +100,7 @@ public function processExpr(NodeScopeResolver $nodeScopeResolver, Stmt $stmt, Ex $throwPoints = []; $impurePoints = []; $isAlwaysTerminating = false; + $nameResult = null; if (is_string($expr->name)) { if (in_array($expr->name, Scope::SUPERGLOBAL_VARIABLES, true)) { $impurePoints[] = new ImpurePoint($scope, $expr, 'superglobal', 'access to superglobal variable', true); @@ -95,22 +113,18 @@ public function processExpr(NodeScopeResolver $nodeScopeResolver, Stmt $stmt, Ex $isAlwaysTerminating = $nameResult->isAlwaysTerminating(); $scope = $nameResult->getScope(); } + return $this->expressionResultFactory->create( $scope, - $beforeScope, - $expr, - $hasYield, - $isAlwaysTerminating, - $throwPoints, - $impurePoints, - static fn (): MutatingScope => $scope->filterByTruthyValue($expr), - static fn (): MutatingScope => $scope->filterByFalseyValue($expr), + beforeScope: $beforeScope, + expr: $expr, + hasYield: $hasYield, + isAlwaysTerminating: $isAlwaysTerminating, + throwPoints: $throwPoints, + impurePoints: $impurePoints, + typeCallback: self::createTypeCallback($expr, $nameResult), + specifyTypesCallback: fn (MutatingScope $s, TypeSpecifierContext $context): SpecifiedTypes => $this->typeSpecifier->specifyDefaultTypes($s, $expr, $context), ); } - public function specifyTypes(TypeSpecifier $typeSpecifier, Scope $scope, Expr $expr, TypeSpecifierContext $context): SpecifiedTypes - { - return $typeSpecifier->specifyDefaultTypes($scope, $expr, $context); - } - } diff --git a/src/Analyser/ExpressionResult.php b/src/Analyser/ExpressionResult.php index 9128ea6d2e0..411e1edeefc 100644 --- a/src/Analyser/ExpressionResult.php +++ b/src/Analyser/ExpressionResult.php @@ -175,6 +175,20 @@ public function hasTypeCallback(): bool return $this->typeCallback !== null; } + /** + * Re-evaluates the narrowing on a different scope (e.g. the one an old-world + * caller holds). Returns null when the handler wired no specifyTypesCallback - + * the caller falls back to default truthy/falsey narrowing. + */ + public function getSpecifiedTypesForScope(MutatingScope $scope, TypeSpecifierContext $context): ?SpecifiedTypes + { + if ($this->specifyTypesCallback === null) { + return null; + } + + return ($this->specifyTypesCallback)($scope, $context); + } + /** * Re-evaluates the expression type on a different scope (e.g. a narrowed one). * Unlike getType(), the result is not cached. diff --git a/src/Analyser/MutatingScope.php b/src/Analyser/MutatingScope.php index eedd593d888..1ec84a2cdf3 100644 --- a/src/Analyser/MutatingScope.php +++ b/src/Analyser/MutatingScope.php @@ -1037,6 +1037,64 @@ private function resolveTypeOfNewWorldHandlerNode(Expr $node): Type return $onDemandResult->getTypeForScope($this); } + /** + * Prices the current (phpdoc, native) type pair of an expression that + * applySpecifiedTypes() needs to intersect with or subtract from but that + * is not tracked in the scope. Old-world filterBySpecifiedTypes() asked + * Scope::getType() here; pricing from the stored ExpressionResult answers + * through the typeCallback for converted handlers and keeps the legacy + * resolution as a bridge for the rest. Returns null for nodes the analysis + * in progress never processed (synthetic ones). + * + * @return array{Type, Type}|null + */ + private function getCurrentTypesOfSpecifiedExpr(Expr $expr): ?array + { + $storage = $this->expressionResultStorageStack->getCurrent(); + if ($storage === null) { + return null; + } + + $result = $storage->findExpressionResult($expr); + if ($result === null) { + return null; + } + + return [ + $result->getTypeForScope($this), + $result->getTypeForScope($this->promoteNativeTypes()), + ]; + } + + /** + * Narrowing counterpart of resolveTypeOfNewWorldHandlerNode() - the old-world + * TypeSpecifier dispatcher asks here for nodes whose handler no longer + * implements TypeResolvingExprHandler. Returns null when the ExpressionResult + * carries no specifyTypesCallback - the dispatcher falls back to default + * truthy/falsey narrowing, which is what such handlers used to implement. + * + * @internal + */ + public function specifyTypesOfNewWorldHandlerNode(Expr $node, TypeSpecifierContext $context): ?SpecifiedTypes + { + $storage = $this->expressionResultStorageStack->getCurrent(); + if ($storage !== null) { + $result = $storage->findExpressionResult($node); + if ($result !== null) { + return $result->getSpecifiedTypesForScope($this, $context); + } + } + + // a synthetic node, or no analysis in progress + $onDemandResult = $this->container->getByType(NodeScopeResolver::class)->processExprOnDemand( + $node, + $this, + $storage !== null ? $storage->duplicate() : new ExpressionResultStorage(), + ); + + return $onDemandResult->getSpecifiedTypesForScope($this, $context); + } + /** * Makes the storage answer type questions asked on this scope (and every * scope sharing its ExpressionResultStorageStack) for the duration of an @@ -3542,14 +3600,35 @@ public function applySpecifiedTypes(SpecifiedTypes $specifiedTypes): self continue; } + // only Yes-certainty holders hold the current type of the expression - + // a Maybe-certainty holder holds the when-defined type (e.g. after + // merging a branch where the expression was never assigned), which + // the certainty-aware Scope::getType() of the old world never returned $trackedType = null; $trackedNativeType = null; - if (array_key_exists($exprString, $scope->expressionTypes)) { + if ( + array_key_exists($exprString, $scope->expressionTypes) + && $scope->expressionTypes[$exprString]->getCertainty()->yes() + ) { $trackedType = $scope->expressionTypes[$exprString]->getType(); } - if (array_key_exists($exprString, $scope->nativeExpressionTypes)) { + if ( + array_key_exists($exprString, $scope->nativeExpressionTypes) + && $scope->nativeExpressionTypes[$exprString]->getCertainty()->yes() + ) { $trackedNativeType = $scope->nativeExpressionTypes[$exprString]->getType(); } + if ($trackedType === null) { + $currentTypes = $scope->getCurrentTypesOfSpecifiedExpr($expr); + if ($currentTypes !== null) { + if ($scope->isComplexUnionType($currentTypes[0])) { + continue; + } + + $trackedType = $currentTypes[0]; + $trackedNativeType ??= $currentTypes[1]; + } + } if ($typeSpecification['sure']) { if ($specifiedTypes->shouldOverwrite()) { diff --git a/src/Analyser/TypeSpecifier.php b/src/Analyser/TypeSpecifier.php index dd1982e6694..b91b62fb2e0 100644 --- a/src/Analyser/TypeSpecifier.php +++ b/src/Analyser/TypeSpecifier.php @@ -91,14 +91,22 @@ public function specifyTypesInCondition( /** @var ExprHandler $exprHandler */ foreach ($this->container->getServicesByTag(ExprHandler::EXTENSION_TAG) as $exprHandler) { - if (!$exprHandler instanceof TypeResolvingExprHandler) { - continue; - } if (!$exprHandler->supports($expr)) { continue; } - return $exprHandler->specifyTypes($this, $scope, $expr, $context); + if ($exprHandler instanceof TypeResolvingExprHandler) { + return $exprHandler->specifyTypes($this, $scope, $expr, $context); + } + + if ($scope instanceof MutatingScope) { + $specifiedTypes = $scope->specifyTypesOfNewWorldHandlerNode($expr, $context); + if ($specifiedTypes !== null) { + return $specifiedTypes; + } + } + + break; } return $this->specifyDefaultTypes($scope, $expr, $context); From 9179b4994d123b25ce412b3e5d493610630b0e5c Mon Sep 17 00:00:00 2001 From: Ondrej Mirtes Date: Fri, 12 Jun 2026 19:39:49 +0200 Subject: [PATCH 013/113] Add regression tests for evaluation-point array item types Closes https://github.com/phpstan/phpstan/issues/13944 Closes https://github.com/phpstan/phpstan/issues/12207 Closes https://github.com/phpstan/phpstan/issues/7155 Co-Authored-By: Claude Fable 5 --- tests/PHPStan/Analyser/nsrt/bug-12207.php | 31 +++++++++++++++ tests/PHPStan/Analyser/nsrt/bug-13944.php | 48 +++++++++++++++++++++++ tests/PHPStan/Analyser/nsrt/bug-7155.php | 16 ++++++++ 3 files changed, 95 insertions(+) create mode 100644 tests/PHPStan/Analyser/nsrt/bug-12207.php create mode 100644 tests/PHPStan/Analyser/nsrt/bug-13944.php create mode 100644 tests/PHPStan/Analyser/nsrt/bug-7155.php diff --git a/tests/PHPStan/Analyser/nsrt/bug-12207.php b/tests/PHPStan/Analyser/nsrt/bug-12207.php new file mode 100644 index 00000000000..63990d759c4 --- /dev/null +++ b/tests/PHPStan/Analyser/nsrt/bug-12207.php @@ -0,0 +1,31 @@ +}> + */ + public function bar(): Generator + { + yield 'foo' => [ + $a = 'string', + ['string' => $a], + ]; + } + + public function baz(): void + { + $value = [ + $a = 'string', + ['string' => $a], + ]; + assertType("array{'string', array{string: 'string'}}", $value); + } + +} diff --git a/tests/PHPStan/Analyser/nsrt/bug-13944.php b/tests/PHPStan/Analyser/nsrt/bug-13944.php new file mode 100644 index 00000000000..ed1aeaf31b0 --- /dev/null +++ b/tests/PHPStan/Analyser/nsrt/bug-13944.php @@ -0,0 +1,48 @@ +, + * "when@stage"?: array, + * } $config + */ +function config(array $config): void +{ +} + +config([ + 'when@dev' => $does_not_work = [ + 'controllers' => [ + 'resource' => 'routing.controllers', + ], + ], + 'when@stage' => $does_not_work, +]); + +assertType("array{'when@dev': array{controllers: array{resource: 'routing.controllers'}}, 'when@stage': array{controllers: array{resource: 'routing.controllers'}}}", [ + 'when@dev' => $does_not_work, + 'when@stage' => $does_not_work, +]); + +assertType("array{'when@dev': array{controllers: array{resource: 'routing.controllers'}}, 'when@stage': array{controllers: array{resource: 'routing.controllers'}}}", [ + 'when@dev' => $defined_inside = [ + 'controllers' => [ + 'resource' => 'routing.controllers', + ], + ], + 'when@stage' => $defined_inside, +]); + +$does_work = [ + 'controllers' => [ + 'resource' => 'routing.controllers', + ], +]; +config([ + 'when@dev' => $does_work, + 'when@stage' => $does_work, +]); diff --git a/tests/PHPStan/Analyser/nsrt/bug-7155.php b/tests/PHPStan/Analyser/nsrt/bug-7155.php new file mode 100644 index 00000000000..13fc56e4830 --- /dev/null +++ b/tests/PHPStan/Analyser/nsrt/bug-7155.php @@ -0,0 +1,16 @@ + Date: Fri, 12 Jun 2026 19:49:13 +0200 Subject: [PATCH 014/113] Add regression test for certainty of undefined variables in loops Closes https://github.com/phpstan/phpstan/issues/2032 Co-Authored-By: Claude Fable 5 --- .../Variables/DefinedVariableRuleTest.php | 18 ++++++++++++++++++ .../PHPStan/Rules/Variables/data/bug-2032.php | 17 +++++++++++++++++ 2 files changed, 35 insertions(+) create mode 100644 tests/PHPStan/Rules/Variables/data/bug-2032.php diff --git a/tests/PHPStan/Rules/Variables/DefinedVariableRuleTest.php b/tests/PHPStan/Rules/Variables/DefinedVariableRuleTest.php index 9f2e93fc72a..3f274c7e5d8 100644 --- a/tests/PHPStan/Rules/Variables/DefinedVariableRuleTest.php +++ b/tests/PHPStan/Rules/Variables/DefinedVariableRuleTest.php @@ -1701,4 +1701,22 @@ public function testBug10090(): void ]); } + 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', + 15, + ], + ]); + } + } diff --git a/tests/PHPStan/Rules/Variables/data/bug-2032.php b/tests/PHPStan/Rules/Variables/data/bug-2032.php new file mode 100644 index 00000000000..65e9d1c5f62 --- /dev/null +++ b/tests/PHPStan/Rules/Variables/data/bug-2032.php @@ -0,0 +1,17 @@ + Date: Fri, 12 Jun 2026 20:01:15 +0200 Subject: [PATCH 015/113] Store expressions even without FNSR --- src/Analyser/Fiber/FiberNodeScopeResolver.php | 2 +- src/Analyser/MutatingScope.php | 10 ++++++++++ src/Analyser/NodeScopeResolver.php | 3 +++ .../Rules/Variables/DefinedVariableRuleTest.php | 4 ++++ 4 files changed, 18 insertions(+), 1 deletion(-) diff --git a/src/Analyser/Fiber/FiberNodeScopeResolver.php b/src/Analyser/Fiber/FiberNodeScopeResolver.php index 3d74df8e1d6..e49935f0023 100644 --- a/src/Analyser/Fiber/FiberNodeScopeResolver.php +++ b/src/Analyser/Fiber/FiberNodeScopeResolver.php @@ -58,7 +58,7 @@ public function callNodeCallback( public function storeExpressionResult(ExpressionResultStorage $storage, Expr $expr, ExpressionResult $expressionResult): void { - $storage->storeExpressionResult($expr, $expressionResult); + parent::storeExpressionResult($storage, $expr, $expressionResult); $this->processPendingFibersForRequestedExpr($storage, $expr, $expressionResult); } diff --git a/src/Analyser/MutatingScope.php b/src/Analyser/MutatingScope.php index 1ec84a2cdf3..36368213d33 100644 --- a/src/Analyser/MutatingScope.php +++ b/src/Analyser/MutatingScope.php @@ -3499,6 +3499,11 @@ public function filterBySpecifiedTypes(SpecifiedTypes $specifiedTypes): self continue; } + if ($expr instanceof Variable && is_string($expr->name) && $scope->hasVariableType($expr->name)->no()) { + // testing a certainly-undefined variable cannot make it defined + continue; + } + if ($typeSpecification['sure']) { if ($specifiedTypes->shouldOverwrite()) { $scope = $scope->assignExpression($expr, $type, $type); @@ -3600,6 +3605,11 @@ public function applySpecifiedTypes(SpecifiedTypes $specifiedTypes): self continue; } + if ($expr instanceof Variable && is_string($expr->name) && $scope->hasVariableType($expr->name)->no()) { + // testing a certainly-undefined variable cannot make it defined + continue; + } + // only Yes-certainty holders hold the current type of the expression - // a Maybe-certainty holder holds the when-defined type (e.g. after // merging a branch where the expression was never assigned), which diff --git a/src/Analyser/NodeScopeResolver.php b/src/Analyser/NodeScopeResolver.php index a90327178bb..d6ca37cb42c 100644 --- a/src/Analyser/NodeScopeResolver.php +++ b/src/Analyser/NodeScopeResolver.php @@ -381,6 +381,9 @@ private function processNodesWithStorage( public function storeExpressionResult(ExpressionResultStorage $storage, Expr $expr, ExpressionResult $expressionResult): void { + // converted handlers (no TypeResolvingExprHandler) are answered from + // stored results in both worlds - storing must not depend on fibers + $storage->storeExpressionResult($expr, $expressionResult); } protected function processPendingFibers(ExpressionResultStorage $storage): void diff --git a/tests/PHPStan/Rules/Variables/DefinedVariableRuleTest.php b/tests/PHPStan/Rules/Variables/DefinedVariableRuleTest.php index 3f274c7e5d8..0b03c8d2907 100644 --- a/tests/PHPStan/Rules/Variables/DefinedVariableRuleTest.php +++ b/tests/PHPStan/Rules/Variables/DefinedVariableRuleTest.php @@ -1712,6 +1712,10 @@ public function testBug2032(): void 'Undefined variable: $undefined', 6, ], + [ + 'Undefined variable: $undefined', + 9, + ], [ 'Undefined variable: $undefined', 15, From 12ca8df5ccc344b7f3c68e42466712aaefb6a7ec Mon Sep 17 00:00:00 2001 From: Ondrej Mirtes Date: Fri, 12 Jun 2026 20:07:57 +0200 Subject: [PATCH 016/113] Only sureNot specifications skip certainly-undefined variables A sure specification (e.g. is_string($a)) can only hold for a defined variable, so it still makes the variable defined inside the branch - one error at the test site, no cascade. Removing a type from a certainly-undefined variable proves nothing about its definedness. Co-Authored-By: Claude Fable 5 --- src/Analyser/MutatingScope.php | 20 ++++++++++++++++---- 1 file changed, 16 insertions(+), 4 deletions(-) diff --git a/src/Analyser/MutatingScope.php b/src/Analyser/MutatingScope.php index 36368213d33..9d0649da409 100644 --- a/src/Analyser/MutatingScope.php +++ b/src/Analyser/MutatingScope.php @@ -3499,8 +3499,14 @@ public function filterBySpecifiedTypes(SpecifiedTypes $specifiedTypes): self continue; } - if ($expr instanceof Variable && is_string($expr->name) && $scope->hasVariableType($expr->name)->no()) { - // testing a certainly-undefined variable cannot make it defined + if ( + !$typeSpecification['sure'] + && $expr instanceof Variable && is_string($expr->name) + && $scope->hasVariableType($expr->name)->no() + ) { + // removing type from a certainly-undefined variable cannot make + // it defined; a sure specification (e.g. is_string($a)) still can - + // the condition can only hold for a defined variable continue; } @@ -3605,8 +3611,14 @@ public function applySpecifiedTypes(SpecifiedTypes $specifiedTypes): self continue; } - if ($expr instanceof Variable && is_string($expr->name) && $scope->hasVariableType($expr->name)->no()) { - // testing a certainly-undefined variable cannot make it defined + if ( + !$typeSpecification['sure'] + && $expr instanceof Variable && is_string($expr->name) + && $scope->hasVariableType($expr->name)->no() + ) { + // removing type from a certainly-undefined variable cannot make + // it defined; a sure specification (e.g. is_string($a)) still can - + // the condition can only hold for a defined variable continue; } From 9d8fefa2a90b578bf5b88893ea28f2512d01c81b Mon Sep 17 00:00:00 2001 From: Ondrej Mirtes Date: Fri, 12 Jun 2026 21:49:02 +0200 Subject: [PATCH 017/113] This is better --- src/Analyser/MutatingScope.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Analyser/MutatingScope.php b/src/Analyser/MutatingScope.php index 9d0649da409..188d8277791 100644 --- a/src/Analyser/MutatingScope.php +++ b/src/Analyser/MutatingScope.php @@ -1061,8 +1061,8 @@ private function getCurrentTypesOfSpecifiedExpr(Expr $expr): ?array } return [ - $result->getTypeForScope($this), - $result->getTypeForScope($this->promoteNativeTypes()), + $result->getType(), + $result->getNativeType(), ]; } From e7621fa1a3a7c0fa9149a43dc9fd5ad940609b64 Mon Sep 17 00:00:00 2001 From: Ondrej Mirtes Date: Fri, 12 Jun 2026 23:00:24 +0200 Subject: [PATCH 018/113] ExpressionResult::createTypesCallback - the inside-out TypeSpecifier::create() How a type constraint on a node translates into narrowing entries is the producing handler's knowledge, declared on its ExpressionResult - never re-derived by unwrapping the AST elsewhere. DefaultNarrowingHelper (recreated from the first rewrite attempt) carries the default boolean-context narrowing (one sureNot entry, no type ask, no nullsafe chain-walking) and createSubjectTypes(): ask the subject result's createTypesCallback, fall back to a single sure/sureNot entry for the subject node. No purity gates, no structural unwrapping. AssignHandler fans a type constraint out to the assigned variable and the assigned expression - nested assignments compose through the assigned expression's own result, which is what will delete unwrapAssign. CoalesceHandler delegates to its left side when the type rules the right side in or out, so ($e ?? null) instanceof Foo narrows $e. AssignHandler also wires specifyTypesCallback: the assigned variable narrows by the boolean outcome, plus the $arr[$key] inference after $key = array_key_first/array_key_last/array_search/array_find_key. The null-context inferences stay in the old-world specifyTypes() - result-based asks are always truthy or falsey. specifyTypesCallbacks no longer touch TypeSpecifier: VariableHandler uses DefaultNarrowingHelper, InstanceofHandler narrows its subject through createSubjectTypes(). TypeSpecifierTest's assign-in-instanceof expectations hold unchanged - the new channel reproduces create()'s emission exactly. Co-Authored-By: Claude Fable 5 --- src/Analyser/ExprHandler/AssignHandler.php | 148 +++++++++++++++++- src/Analyser/ExprHandler/CoalesceHandler.php | 18 +++ .../Helper/DefaultNarrowingHelper.php | 91 +++++++++++ .../ExprHandler/InstanceofHandler.php | 12 +- src/Analyser/ExprHandler/VariableHandler.php | 6 +- src/Analyser/ExpressionResult.php | 25 +++ src/Analyser/ExpressionResultFactory.php | 2 + 7 files changed, 292 insertions(+), 10 deletions(-) create mode 100644 src/Analyser/ExprHandler/Helper/DefaultNarrowingHelper.php diff --git a/src/Analyser/ExprHandler/AssignHandler.php b/src/Analyser/ExprHandler/AssignHandler.php index 9ce7f6739bd..362a9619126 100644 --- a/src/Analyser/ExprHandler/AssignHandler.php +++ b/src/Analyser/ExprHandler/AssignHandler.php @@ -28,6 +28,7 @@ use PHPStan\Analyser\ExpressionResultFactory; use PHPStan\Analyser\ExpressionResultStorage; use PHPStan\Analyser\ExpressionTypeHolder; +use PHPStan\Analyser\ExprHandler\Helper\DefaultNarrowingHelper; use PHPStan\Analyser\ImpurePoint; use PHPStan\Analyser\InternalThrowPoint; use PHPStan\Analyser\MutatingScope; @@ -97,6 +98,7 @@ public function __construct( private ExprPrinter $exprPrinter, private MatchHandler $matchHandler, private ExpressionResultFactory $expressionResultFactory, + private DefaultNarrowingHelper $defaultNarrowingHelper, ) { } @@ -292,6 +294,7 @@ public function specifyTypes(TypeSpecifier $typeSpecifier, Scope $scope, Expr $e public function processExpr(NodeScopeResolver $nodeScopeResolver, Stmt $stmt, Expr $expr, MutatingScope $scope, ExpressionResultStorage $storage, callable $nodeCallback, ExpressionContext $context): ExpressionResult { $beforeScope = $scope; + $assignedExprResult = null; $result = $this->processAssignVar( $nodeScopeResolver, $scope, @@ -301,7 +304,7 @@ public function processExpr(NodeScopeResolver $nodeScopeResolver, Stmt $stmt, Ex $expr->expr, $nodeCallback, $context, - function (MutatingScope $scope) use ($stmt, $expr, $nodeCallback, $context, $storage, $nodeScopeResolver): ExpressionResult { + function (MutatingScope $scope) use ($stmt, $expr, $nodeCallback, $context, $storage, $nodeScopeResolver, &$assignedExprResult): ExpressionResult { $beforeScope = $scope; $impurePoints = []; if ($expr instanceof AssignRef) { @@ -331,6 +334,7 @@ function (MutatingScope $scope) use ($stmt, $expr, $nodeCallback, $context, $sto } $result = $nodeScopeResolver->processExprNode($stmt, $expr->expr, $scope, $storage, $nodeCallback, $context->enterDeep()); + $assignedExprResult = $result; $hasYield = $result->hasYield(); $throwPoints = $result->getThrowPoints(); $impurePoints = array_merge($impurePoints, $result->getImpurePoints()); @@ -391,9 +395,151 @@ function (MutatingScope $scope) use ($stmt, $expr, $nodeCallback, $context, $sto isAlwaysTerminating: $result->isAlwaysTerminating(), throwPoints: $result->getThrowPoints(), impurePoints: $result->getImpurePoints(), + specifyTypesCallback: $expr instanceof Assign ? $this->createSpecifyTypesCallback($expr) : null, + createTypesCallback: $expr instanceof Assign ? $this->createCreateTypesCallback($expr, $assignedExprResult) : null, ); } + /** + * A type constraint on an assignment constrains the assigned variable + * and the assigned expression - what TypeSpecifier::create() recovered + * by unwrapping assign chains. Nested assignments compose through the + * assigned expression's own result. + * + * @return Closure(MutatingScope, Type, TypeSpecifierContext): SpecifiedTypes + */ + private function createCreateTypesCallback(Assign $expr, ?ExpressionResult $assignedExprResult): Closure + { + return function (MutatingScope $s, Type $type, TypeSpecifierContext $context) use ($expr, $assignedExprResult): SpecifiedTypes { + $types = $this->defaultNarrowingHelper->createSubjectTypes($s, $expr->var, null, $type, $context); + + return $types->unionWith( + $this->defaultNarrowingHelper->createSubjectTypes($s, $expr->expr, $assignedExprResult, $type, $context), + ); + }; + } + + /** + * New-world copy of the non-null contexts of specifyTypes(): the assigned + * variable narrows by the boolean outcome, plus the $arr[$key] inference + * after $key = array_key_first/array_key_last/array_search/array_find_key. + * The null-context inferences stay in specifyTypes() - result-based asks + * are always truthy or falsey. + * + * @return Closure(MutatingScope, TypeSpecifierContext): SpecifiedTypes + */ + private function createSpecifyTypesCallback(Assign $expr): Closure + { + return function (MutatingScope $s, TypeSpecifierContext $context) use ($expr): SpecifiedTypes { + if ($context->null()) { + return (new SpecifiedTypes([], []))->setRootExpr($expr); + } + + $specifiedTypes = $this->defaultNarrowingHelper->specifyDefaultTypes($expr->var, $context)->setRootExpr($expr); + + // infer $arr[$key] after $key = array_key_first/last($arr) + if ( + $expr->expr instanceof FuncCall + && $expr->expr->name instanceof Name + && !$expr->expr->isFirstClassCallable() + && in_array($expr->expr->name->toLowerString(), ['array_key_first', 'array_key_last'], true) + && count($expr->expr->getArgs()) >= 1 + ) { + $arrayArg = $expr->expr->getArgs()[0]->value; + $arrayType = $s->getType($arrayArg); + + if ($arrayType->isArray()->yes()) { + if ($context->true()) { + $specifiedTypes = $specifiedTypes->unionWith( + $this->defaultNarrowingHelper->createSubjectTypes($s, $arrayArg, null, new NonEmptyArrayType(), TypeSpecifierContext::createTrue()), + ); + $isNonEmpty = true; + } else { + $isNonEmpty = $arrayType->isIterableAtLeastOnce()->yes(); + } + + if ($isNonEmpty) { + $dimFetch = new ArrayDimFetch($arrayArg, $expr->var); + $specifiedTypes = $specifiedTypes->unionWith( + $this->defaultNarrowingHelper->createSubjectTypes($s, $dimFetch, null, $arrayType->getIterableValueType(), TypeSpecifierContext::createTrue()), + ); + } elseif ($expr->var instanceof Variable && is_string($expr->var->name)) { + $keyType = $s->getType($expr->expr); + $nonNullKeyType = TypeCombinator::removeNull($keyType); + if (!$nonNullKeyType instanceof NeverType) { + $specifiedTypes = $specifiedTypes->unionWith( + $this->createArrayDimFetchConditionalExpressionHolder($expr->var, $arrayArg, $nonNullKeyType, $arrayType->getIterableValueType()), + ); + } + } + } + } + + // infer $arr[$key] after $key = array_search($needle, $arr) or $key = array_find_key($arr, $callback) + if ( + $expr->expr instanceof FuncCall + && $expr->expr->name instanceof Name + && !$expr->expr->isFirstClassCallable() + && count($expr->expr->getArgs()) >= 2 + ) { + $funcName = $expr->expr->name->toLowerString(); + $arrayArg = null; + $sentinelType = null; + $isStrictArraySearch = false; + + if ($funcName === 'array_search') { + $arrayArg = $expr->expr->getArgs()[1]->value; + $sentinelType = new ConstantBooleanType(false); + $isStrictArraySearch = count($expr->expr->getArgs()) >= 3 && $s->getType($expr->expr->getArgs()[2]->value)->isTrue()->yes(); + } elseif ($funcName === 'array_find_key') { + $arrayArg = $expr->expr->getArgs()[0]->value; + $sentinelType = new NullType(); + } + + if ($arrayArg !== null) { + $arrayType = $s->getType($arrayArg); + + if ($arrayType->isArray()->yes()) { + if ($context->true()) { + $specifiedTypes = $specifiedTypes->unionWith( + $this->defaultNarrowingHelper->createSubjectTypes($s, $arrayArg, null, new NonEmptyArrayType(), TypeSpecifierContext::createTrue()), + ); + + $dimFetch = new ArrayDimFetch($arrayArg, $expr->var); + + if ($isStrictArraySearch) { + $needleType = $s->getType($expr->expr->getArgs()[0]->value); + $dimFetchType = TypeCombinator::intersect($needleType, $arrayType->getIterableValueType()); + } else { + $dimFetchType = $arrayType->getIterableValueType(); + } + + $specifiedTypes = $specifiedTypes->unionWith( + $this->defaultNarrowingHelper->createSubjectTypes($s, $dimFetch, null, $dimFetchType, TypeSpecifierContext::createTrue()), + ); + } elseif ($expr->var instanceof Variable && is_string($expr->var->name)) { + $keyType = $s->getType($expr->expr); + $narrowedKeyType = TypeCombinator::remove($keyType, $sentinelType); + if (!$narrowedKeyType instanceof NeverType) { + if ($isStrictArraySearch) { + $needleType = $s->getType($expr->expr->getArgs()[0]->value); + $dimFetchType = TypeCombinator::intersect($needleType, $arrayType->getIterableValueType()); + } else { + $dimFetchType = $arrayType->getIterableValueType(); + } + $specifiedTypes = $specifiedTypes->unionWith( + $this->createArrayDimFetchConditionalExpressionHolder($expr->var, $arrayArg, $narrowedKeyType, $dimFetchType), + ); + } + } + } + } + } + + return $specifiedTypes; + }; + } + /** * @param callable(Node $node, Scope $scope): void $nodeCallback * @param Closure(MutatingScope $scope): ExpressionResult $processExprCallback diff --git a/src/Analyser/ExprHandler/CoalesceHandler.php b/src/Analyser/ExprHandler/CoalesceHandler.php index fa8cc9fcbc8..70541ebae02 100644 --- a/src/Analyser/ExprHandler/CoalesceHandler.php +++ b/src/Analyser/ExprHandler/CoalesceHandler.php @@ -9,6 +9,7 @@ use PHPStan\Analyser\ExpressionResult; use PHPStan\Analyser\ExpressionResultFactory; use PHPStan\Analyser\ExpressionResultStorage; +use PHPStan\Analyser\ExprHandler\Helper\DefaultNarrowingHelper; use PHPStan\Analyser\ExprHandler\Helper\NonNullabilityHelper; use PHPStan\Analyser\MutatingScope; use PHPStan\Analyser\NodeScopeResolver; @@ -36,6 +37,7 @@ final class CoalesceHandler implements TypeResolvingExprHandler public function __construct( private NonNullabilityHelper $nonNullabilityHelper, private ExpressionResultFactory $expressionResultFactory, + private DefaultNarrowingHelper $defaultNarrowingHelper, ) { } @@ -140,6 +142,22 @@ public function processExpr(NodeScopeResolver $nodeScopeResolver, Stmt $stmt, Ex isAlwaysTerminating: $condResult->isAlwaysTerminating(), throwPoints: array_merge($condResult->getThrowPoints(), $rightResult->getThrowPoints()), impurePoints: array_merge($condResult->getImpurePoints(), $rightResult->getImpurePoints()), + // a type constraint on the coalesce constrains its left side when + // the type rules the right side in or out - what + // TypeSpecifier::create() recovered by unwrapping the coalesce + createTypesCallback: function (MutatingScope $s, Type $type, TypeSpecifierContext $context) use ($expr, $condResult, $rightResult): SpecifiedTypes { + if (!$context->null()) { + $rightType = $rightResult->getTypeForScope($s); + if ( + ($context->true() && $type->isSuperTypeOf($rightType)->no()) + || ($context->false() && $type->isSuperTypeOf($rightType)->yes()) + ) { + return $this->defaultNarrowingHelper->createSubjectTypes($s, $expr->left, $condResult, $type, $context); + } + } + + return $this->defaultNarrowingHelper->createSubjectTypes($s, $expr, null, $type, $context); + }, ); } diff --git a/src/Analyser/ExprHandler/Helper/DefaultNarrowingHelper.php b/src/Analyser/ExprHandler/Helper/DefaultNarrowingHelper.php new file mode 100644 index 00000000000..a82899110bb --- /dev/null +++ b/src/Analyser/ExprHandler/Helper/DefaultNarrowingHelper.php @@ -0,0 +1,91 @@ +` - they + * emit the plain-chain variant alongside their own key once, and every parent + * simply composes their results. No recursive chain-walking, no type ask. + */ +#[AutowiredService] +final class DefaultNarrowingHelper +{ + + public function __construct(private ExprPrinter $exprPrinter) + { + } + + public function specifyDefaultTypes(Expr $expr, TypeSpecifierContext $context): SpecifiedTypes + { + if ($context->null()) { + return (new SpecifiedTypes([], []))->setRootExpr($expr); + } + + if (!$context->truthy()) { + $removedType = StaticTypeFactory::truthy(); + } elseif (!$context->falsey()) { + $removedType = StaticTypeFactory::falsey(); + } else { + return (new SpecifiedTypes([], []))->setRootExpr($expr); + } + + return (new SpecifiedTypes(sureNotTypes: [ + $this->exprPrinter->printExpr($expr) => [$expr, $removedType], + ]))->setRootExpr($expr); + } + + /** + * A greatly simplified TypeSpecifier::create() for a subject the calling + * handler has already processed: one sure (truthy) or sureNot (falsey) + * entry for the subject node. A coalesce subject narrows its left side + * when the narrowed type rules the right side in or out. No purity gates, + * no nullsafe chain-walking, no assignment fan-out - an entry about an + * assignment narrows the assigned variables in the appliers, and the + * subject's own narrowing composes in through + * ExpressionResult::getSpecifiedTypesForScope() at the call site. + */ + /** + * A greatly simplified TypeSpecifier::create() for a subject the calling + * handler has already processed: the subject's own result says how a type + * constraint on it translates into entries (an assignment fans out to the + * assigned variable, a coalesce delegates to its left side); without a + * createTypesCallback a single sure (truthy) or sureNot (falsey) entry + * for the subject node is emitted. No purity gates, no nullsafe + * chain-walking, no structural unwrapping - the handlers that own those + * nodes compose their children's results inside-out. + */ + public function createSubjectTypes(MutatingScope $s, Expr $subject, ?ExpressionResult $subjectResult, Type $type, TypeSpecifierContext $context): SpecifiedTypes + { + if ($subjectResult !== null) { + $createdTypes = $subjectResult->getCreatedTypesForScope($s, $type, $context); + if ($createdTypes !== null) { + return $createdTypes; + } + } + + $exprString = $this->exprPrinter->printExpr($subject); + if ($context->true()) { + return new SpecifiedTypes([$exprString => [$subject, $type]], []); + } + if ($context->false()) { + return new SpecifiedTypes(sureNotTypes: [$exprString => [$subject, $type]]); + } + + return new SpecifiedTypes([], []); + } + +} diff --git a/src/Analyser/ExprHandler/InstanceofHandler.php b/src/Analyser/ExprHandler/InstanceofHandler.php index ff8bda6c552..aaaa04fbed1 100644 --- a/src/Analyser/ExprHandler/InstanceofHandler.php +++ b/src/Analyser/ExprHandler/InstanceofHandler.php @@ -11,10 +11,10 @@ use PHPStan\Analyser\ExpressionResultFactory; use PHPStan\Analyser\ExpressionResultStorage; use PHPStan\Analyser\ExprHandler; +use PHPStan\Analyser\ExprHandler\Helper\DefaultNarrowingHelper; use PHPStan\Analyser\MutatingScope; use PHPStan\Analyser\NodeScopeResolver; use PHPStan\Analyser\SpecifiedTypes; -use PHPStan\Analyser\TypeSpecifier; use PHPStan\Analyser\TypeSpecifierContext; use PHPStan\DependencyInjection\AutowiredService; use PHPStan\Type\BooleanType; @@ -40,7 +40,7 @@ final class InstanceofHandler implements ExprHandler public function __construct( private ExpressionResultFactory $expressionResultFactory, - private TypeSpecifier $typeSpecifier, + private DefaultNarrowingHelper $defaultNarrowingHelper, ) { } @@ -146,7 +146,7 @@ public function processExpr(NodeScopeResolver $nodeScopeResolver, Stmt $stmt, Ex } else { $type = new ObjectType($className); } - return $this->typeSpecifier->create($exprNode, $type, $context, $s)->setRootExpr($expr); + return $this->defaultNarrowingHelper->createSubjectTypes($s, $exprNode, $exprResult, $type, $context)->setRootExpr($expr); } $classNameType = $classResult !== null @@ -162,16 +162,16 @@ public function processExpr(NodeScopeResolver $nodeScopeResolver, Stmt $stmt, Ex $type, new ObjectWithoutClassType(), ); - return $this->typeSpecifier->create($exprNode, $type, $context, $s)->setRootExpr($expr); + return $this->defaultNarrowingHelper->createSubjectTypes($s, $exprNode, $exprResult, $type, $context)->setRootExpr($expr); } elseif ($context->false() && !$uncertainty) { $exprType = $exprResult->getTypeForScope($s); if (!$type->isSuperTypeOf($exprType)->yes()) { - return $this->typeSpecifier->create($exprNode, $type, $context, $s)->setRootExpr($expr); + return $this->defaultNarrowingHelper->createSubjectTypes($s, $exprNode, $exprResult, $type, $context)->setRootExpr($expr); } } } if ($context->true()) { - return $this->typeSpecifier->create($exprNode, new ObjectWithoutClassType(), $context, $s)->setRootExpr($exprNode); + return $this->defaultNarrowingHelper->createSubjectTypes($s, $exprNode, $exprResult, new ObjectWithoutClassType(), $context)->setRootExpr($exprNode); } return (new SpecifiedTypes([], []))->setRootExpr($expr); diff --git a/src/Analyser/ExprHandler/VariableHandler.php b/src/Analyser/ExprHandler/VariableHandler.php index 1fc9923355f..03692785a53 100644 --- a/src/Analyser/ExprHandler/VariableHandler.php +++ b/src/Analyser/ExprHandler/VariableHandler.php @@ -13,12 +13,12 @@ use PHPStan\Analyser\ExpressionResultFactory; use PHPStan\Analyser\ExpressionResultStorage; use PHPStan\Analyser\ExprHandler; +use PHPStan\Analyser\ExprHandler\Helper\DefaultNarrowingHelper; use PHPStan\Analyser\ImpurePoint; use PHPStan\Analyser\MutatingScope; use PHPStan\Analyser\NodeScopeResolver; use PHPStan\Analyser\Scope; use PHPStan\Analyser\SpecifiedTypes; -use PHPStan\Analyser\TypeSpecifier; use PHPStan\Analyser\TypeSpecifierContext; use PHPStan\DependencyInjection\AutowiredService; use PHPStan\Type\ErrorType; @@ -38,7 +38,7 @@ final class VariableHandler implements ExprHandler public function __construct( private ExpressionResultFactory $expressionResultFactory, - private TypeSpecifier $typeSpecifier, + private DefaultNarrowingHelper $defaultNarrowingHelper, ) { } @@ -123,7 +123,7 @@ public function processExpr(NodeScopeResolver $nodeScopeResolver, Stmt $stmt, Ex throwPoints: $throwPoints, impurePoints: $impurePoints, typeCallback: self::createTypeCallback($expr, $nameResult), - specifyTypesCallback: fn (MutatingScope $s, TypeSpecifierContext $context): SpecifiedTypes => $this->typeSpecifier->specifyDefaultTypes($s, $expr, $context), + specifyTypesCallback: fn (MutatingScope $s, TypeSpecifierContext $context): SpecifiedTypes => $this->defaultNarrowingHelper->specifyDefaultTypes($expr, $context), ); } diff --git a/src/Analyser/ExpressionResult.php b/src/Analyser/ExpressionResult.php index 411e1edeefc..d1e1949d8bc 100644 --- a/src/Analyser/ExpressionResult.php +++ b/src/Analyser/ExpressionResult.php @@ -18,6 +18,9 @@ final class ExpressionResult /** @var (callable(MutatingScope, TypeSpecifierContext): SpecifiedTypes)|null */ private $specifyTypesCallback; + /** @var (callable(MutatingScope, Type, TypeSpecifierContext): SpecifiedTypes)|null */ + private $createTypesCallback; + /** @var (callable(): MutatingScope)|null */ private $truthyScopeCallback; @@ -37,6 +40,7 @@ final class ExpressionResult * @param ImpurePoint[] $impurePoints * @param (callable(MutatingScope, Expr): Type)|null $typeCallback * @param (callable(MutatingScope, TypeSpecifierContext): SpecifiedTypes)|null $specifyTypesCallback + * @param (callable(MutatingScope, Type, TypeSpecifierContext): SpecifiedTypes)|null $createTypesCallback * @param (callable(): MutatingScope)|null $truthyScopeCallback * @param (callable(): MutatingScope)|null $falseyScopeCallback */ @@ -53,12 +57,14 @@ public function __construct( ?callable $falseyScopeCallback = null, ?callable $typeCallback = null, ?callable $specifyTypesCallback = null, + ?callable $createTypesCallback = null, ) { $this->truthyScopeCallback = $truthyScopeCallback; $this->falseyScopeCallback = $falseyScopeCallback; $this->typeCallback = $typeCallback; $this->specifyTypesCallback = $specifyTypesCallback; + $this->createTypesCallback = $createTypesCallback; } public function getScope(): MutatingScope @@ -189,6 +195,25 @@ public function getSpecifiedTypesForScope(MutatingScope $scope, TypeSpecifierCon return ($this->specifyTypesCallback)($scope, $context); } + /** + * How a type constraint on this expression translates into narrowing + * entries - the inside-out counterpart of TypeSpecifier::create(). The + * handler that produced this result knows the structure: an assignment + * fans out to the assigned variable and the assigned expression + * (recursing through the assigned expression's own result), a coalesce + * delegates to its left side when the type rules the right side in or + * out. Returns null when the handler wired no createTypesCallback - the + * caller emits a single entry for the expression itself. + */ + public function getCreatedTypesForScope(MutatingScope $scope, Type $type, TypeSpecifierContext $context): ?SpecifiedTypes + { + if ($this->createTypesCallback === null) { + return null; + } + + return ($this->createTypesCallback)($scope, $type, $context); + } + /** * Re-evaluates the expression type on a different scope (e.g. a narrowed one). * Unlike getType(), the result is not cached. diff --git a/src/Analyser/ExpressionResultFactory.php b/src/Analyser/ExpressionResultFactory.php index 43926896f5f..83172cd7eb6 100644 --- a/src/Analyser/ExpressionResultFactory.php +++ b/src/Analyser/ExpressionResultFactory.php @@ -15,6 +15,7 @@ interface ExpressionResultFactory * @param (callable(): MutatingScope)|null $falseyScopeCallback * @param (callable(MutatingScope, Expr): Type)|null $typeCallback * @param (callable(MutatingScope, TypeSpecifierContext): SpecifiedTypes)|null $specifyTypesCallback + * @param (callable(MutatingScope, Type, TypeSpecifierContext): SpecifiedTypes)|null $createTypesCallback */ public function create( MutatingScope $scope, @@ -28,6 +29,7 @@ public function create( ?callable $falseyScopeCallback = null, ?callable $typeCallback = null, ?callable $specifyTypesCallback = null, + ?callable $createTypesCallback = null, ): ExpressionResult; } From cbd9441bcc34a44d9ffc79194f6541706c060e58 Mon Sep 17 00:00:00 2001 From: Ondrej Mirtes Date: Sat, 13 Jun 2026 00:02:06 +0200 Subject: [PATCH 019/113] Coalesce, Ternary, BooleanAnd, BooleanOr stop implementing TypeResolvingExprHandler The composite handlers wire typeCallback and specifyTypesCallback composed from their operands' results. This deletes the founding pathology of the rewrite: BooleanAndHandler::resolveType's re-walk of the left operand on a throwaway storage, BOOLEAN_EXPRESSION_MAX_PROCESS_DEPTH and both flattened-chain code paths - deep chains compose through nested results. Child narrowing flows through DefaultNarrowingHelper::getChildSpecifiedTypes(): the child result's specifyTypesCallback first, bridged through the old-world dispatcher for unmigrated children (the dispatcher answers converted handlers from stored results, so the bridge terminates; it dies in 3.0). The ternary still rewrites itself as (cond && if) || (!cond && else) - the synthetic takes the on-demand path where its real subnodes answer from stored results. Lessons the conversion forced out of the engine: - A handler must never ask the scope about its own node mid-processing - no stored result exists yet, so the ask takes the on-demand path and recurses infinitely (CoalesceHandler's filterByFalseyValue($expr) for the right-side scope hung the suite). The equivalent narrowing is built directly from the left result instead. - Composite typeCallbacks evaluate later operands on their captured processing scopes. Re-filtering the asking scope loses the left side's side effects (by-ref writes, inline assignments); the child result's own point breaks synthetic compositions (min()'s $a < $b ? $a : $b reuses stored results of the real arg nodes, predating the synthetic's branch narrowing). The captured scope has both; native asks flavor it with doNotTreatPhpDocTypesAsCertain(). - ExpressionResult::getType()/getNativeType()/getTypeForScope() consult tracked expression holders before the typeCallback, mirroring the early return in MutatingScope::resolveType() - that is how the nullsafe handlers' ensured non-nullability of ($x ?? null) reaches type asks. Co-Authored-By: Claude Fable 5 --- .../ExprHandler/BooleanAndHandler.php | 285 ++++++------------ src/Analyser/ExprHandler/BooleanOrHandler.php | 251 +++++---------- src/Analyser/ExprHandler/CoalesceHandler.php | 135 ++++----- .../Helper/DefaultNarrowingHelper.php | 27 +- src/Analyser/ExprHandler/TernaryHandler.php | 132 ++++---- src/Analyser/ExpressionResult.php | 21 +- 6 files changed, 347 insertions(+), 504 deletions(-) diff --git a/src/Analyser/ExprHandler/BooleanAndHandler.php b/src/Analyser/ExprHandler/BooleanAndHandler.php index 7ceb06af1bd..3e6854b175c 100644 --- a/src/Analyser/ExprHandler/BooleanAndHandler.php +++ b/src/Analyser/ExprHandler/BooleanAndHandler.php @@ -4,48 +4,39 @@ use PhpParser\Node\Expr; use PhpParser\Node\Expr\BinaryOp\BooleanAnd; -use PhpParser\Node\Expr\BinaryOp\BooleanOr; use PhpParser\Node\Expr\BinaryOp\LogicalAnd; -use PhpParser\Node\Expr\BinaryOp\LogicalOr; use PhpParser\Node\Stmt; use PHPStan\Analyser\ExpressionContext; use PHPStan\Analyser\ExpressionResult; use PHPStan\Analyser\ExpressionResultFactory; use PHPStan\Analyser\ExpressionResultStorage; +use PHPStan\Analyser\ExprHandler; use PHPStan\Analyser\ExprHandler\Helper\ConditionalExpressionHolderHelper; +use PHPStan\Analyser\ExprHandler\Helper\DefaultNarrowingHelper; use PHPStan\Analyser\MutatingScope; use PHPStan\Analyser\NodeScopeResolver; -use PHPStan\Analyser\NoopNodeCallback; -use PHPStan\Analyser\Scope; use PHPStan\Analyser\SpecifiedTypes; -use PHPStan\Analyser\TypeResolvingExprHandler; -use PHPStan\Analyser\TypeSpecifier; use PHPStan\Analyser\TypeSpecifierContext; use PHPStan\DependencyInjection\AutowiredService; use PHPStan\Node\BooleanAndNode; -use PHPStan\ShouldNotHappenException; use PHPStan\Type\BooleanType; use PHPStan\Type\Constant\ConstantBooleanType; use PHPStan\Type\NeverType; use PHPStan\Type\Type; -use PHPStan\Type\TypeCombinator; use function array_merge; -use function array_reverse; use function is_string; /** - * @implements TypeResolvingExprHandler + * @implements ExprHandler */ #[AutowiredService] -final class BooleanAndHandler implements TypeResolvingExprHandler +final class BooleanAndHandler implements ExprHandler { - private const BOOLEAN_EXPRESSION_MAX_PROCESS_DEPTH = 4; - public function __construct( - private NodeScopeResolver $nodeScopeResolver, private ConditionalExpressionHolderHelper $conditionalExpressionHolderHelper, private ExpressionResultFactory $expressionResultFactory, + private DefaultNarrowingHelper $defaultNarrowingHelper, ) { } @@ -55,183 +46,6 @@ public function supports(Expr $expr): bool return $expr instanceof BooleanAnd || $expr instanceof LogicalAnd; } - public function resolveType(MutatingScope $scope, Expr $expr): Type - { - $leftBooleanType = $scope->getType($expr->left)->toBoolean(); - if ($leftBooleanType->isFalse()->yes()) { - return new ConstantBooleanType(false); - } - - if (self::getBooleanExpressionDepth($expr->left) <= self::BOOLEAN_EXPRESSION_MAX_PROCESS_DEPTH) { - $leftResult = $this->nodeScopeResolver->processExprNode(new Stmt\Expression($expr->left), $expr->left, $scope, new ExpressionResultStorage(), new NoopNodeCallback(), ExpressionContext::createDeep()); - $rightBooleanType = $leftResult->getTruthyScope()->getType($expr->right)->toBoolean(); - } else { - $rightBooleanType = $scope->filterByTruthyValue($expr->left)->getType($expr->right)->toBoolean(); - } - - if ($rightBooleanType->isFalse()->yes()) { - return new ConstantBooleanType(false); - } - - if ( - $leftBooleanType->isTrue()->yes() - && $rightBooleanType->isTrue()->yes() - ) { - return new ConstantBooleanType(true); - } - - return new BooleanType(); - } - - public function specifyTypes(TypeSpecifier $typeSpecifier, Scope $scope, Expr $expr, TypeSpecifierContext $context): SpecifiedTypes - { - if (!$scope instanceof MutatingScope) { - throw new ShouldNotHappenException(); - } - - // For deep BooleanAnd chains in truthy context, flatten and - // process all arms at once to avoid O(N²) recursive - // filterByTruthyValue calls. - if ( - $context->true() - && self::getBooleanExpressionDepth($expr) > self::BOOLEAN_EXPRESSION_MAX_PROCESS_DEPTH - ) { - return $this->specifyTypesForFlattenedBooleanAnd($typeSpecifier, $scope, $expr, $context); - } - - $leftTypes = $typeSpecifier->specifyTypesInCondition($scope, $expr->left, $context)->setRootExpr($expr); - $rightScope = $scope->filterByTruthyValue($expr->left); - $rightTypes = $typeSpecifier->specifyTypesInCondition($rightScope, $expr->right, $context)->setRootExpr($expr); - if ($context->true()) { - $types = $leftTypes->unionWith($rightTypes); - } else { - $leftNormalized = $leftTypes->normalize($scope); - $rightNormalized = $rightTypes->normalize($rightScope); - $types = $leftNormalized->intersectWith($rightNormalized); - $types = $this->conditionalExpressionHolderHelper->augmentDisjunctionTypes($scope, $rightScope, $leftNormalized, $rightNormalized, $expr->left, $expr->right, false, $types); - } - if ($context->false()) { - // Consequent (holder) narrowings projected by each holder: these must be - // the genuine falsey narrowing of the arm. When that is empty, the arm - // has no sound falsey narrowing and must not contribute a consequent. - $leftHolderTypes = $leftTypes; - $rightHolderTypes = $rightTypes; - // In a mixed truthy-and-false context, re-derive empty holders from the falsey narrowing. - if ($context->truthy()) { - if ($leftHolderTypes->getSureTypes() === [] && $leftHolderTypes->getSureNotTypes() === []) { - $leftHolderTypes = $typeSpecifier->specifyTypesInCondition($scope, $expr->left, TypeSpecifierContext::createFalsey())->setRootExpr($expr); - } - if ($rightHolderTypes->getSureTypes() === [] && $rightHolderTypes->getSureNotTypes() === []) { - $rightHolderTypes = $typeSpecifier->specifyTypesInCondition($rightScope, $expr->right, TypeSpecifierContext::createFalsey())->setRootExpr($expr); - } - } - // Condition (antecedent) narrowings: when an arm has no falsey narrowing - // (e.g. isset() on an array dim fetch), derive the condition from the truthy - // narrowing by swapping sure/sureNot types. This swap is only sound for the - // antecedent — processBooleanConditionalTypes inverts it back to the truthy - // narrowing. It must NOT feed the consequent: inverting a comparison's truthy - // narrowing (e.g. `$a === $b` narrowing `$a` to `$b`'s broad type) would - // over-narrow the consequent (see regression for `$x === $nonConstantString`). - $leftCondTypes = $leftHolderTypes; - $rightCondTypes = $rightHolderTypes; - if ($leftCondTypes->getSureTypes() === [] && $leftCondTypes->getSureNotTypes() === []) { - $truthyLeftTypes = $typeSpecifier->specifyTypesInCondition($scope, $expr->left, TypeSpecifierContext::createTruthy()); - if ($this->allExpressionsTrackable($truthyLeftTypes)) { - $leftCondTypes = new SpecifiedTypes($truthyLeftTypes->getSureNotTypes(), $truthyLeftTypes->getSureTypes()); - } - } - if ($rightCondTypes->getSureTypes() === [] && $rightCondTypes->getSureNotTypes() === []) { - $truthyRightTypes = $typeSpecifier->specifyTypesInCondition($rightScope, $expr->right, TypeSpecifierContext::createTruthy()); - if ($this->allExpressionsTrackable($truthyRightTypes)) { - $rightCondTypes = new SpecifiedTypes($truthyRightTypes->getSureNotTypes(), $truthyRightTypes->getSureTypes()); - } - } - $result = new SpecifiedTypes( - $types->getSureTypes(), - $types->getSureNotTypes(), - ); - if ($types->shouldOverwrite()) { - $result = $result->setAlwaysOverwriteTypes(); - } - return $result->setNewConditionalExpressionHolders($this->conditionalExpressionHolderHelper->mergeConditionalHolders([ - $this->conditionalExpressionHolderHelper->processBooleanConditionalTypes($scope, $leftCondTypes, $rightHolderTypes, false, true, $rightScope, $expr->right), - $this->conditionalExpressionHolderHelper->processBooleanConditionalTypes($scope, $rightCondTypes, $leftHolderTypes, false, true, $scope, $expr->left), - $this->conditionalExpressionHolderHelper->processBooleanConditionalTypes($scope, $leftCondTypes, $rightHolderTypes, true, true, $rightScope, $expr->right), - $this->conditionalExpressionHolderHelper->processBooleanConditionalTypes($scope, $rightCondTypes, $leftHolderTypes, true, true, $scope, $expr->left), - ]))->setRootExpr($expr); - } - - return $types; - } - - public static function getBooleanExpressionDepth(Expr $expr, int $depth = 0): int - { - while ( - $expr instanceof BooleanOr - || $expr instanceof LogicalOr - || $expr instanceof BooleanAnd - || $expr instanceof LogicalAnd - ) { - return self::getBooleanExpressionDepth($expr->left, $depth + 1); - } - - return $depth; - } - - /** - * Flatten a deep BooleanAnd chain into leaf expressions and process them - * without recursive filterByTruthyValue calls. - * - * @param BooleanAnd|LogicalAnd $expr - */ - private function specifyTypesForFlattenedBooleanAnd( - TypeSpecifier $typeSpecifier, - MutatingScope $scope, - Expr $expr, - TypeSpecifierContext $context, - ): SpecifiedTypes - { - $arms = []; - $current = $expr; - while ($current instanceof BooleanAnd || $current instanceof LogicalAnd) { - $arms[] = $current->right; - $current = $current->left; - } - $arms[] = $current; - $arms = array_reverse($arms); - - // Truthy: all arms are true → union all SpecifiedTypes. - // Collect per-expression types first, then build unions once - // to avoid O(N²) from incremental growth. - /** @var array}> $sureTypesPerExpr */ - $sureTypesPerExpr = []; - /** @var array}> $sureNotTypesPerExpr */ - $sureNotTypesPerExpr = []; - - foreach ($arms as $arm) { - $armTypes = $typeSpecifier->specifyTypesInCondition($scope, $arm, $context); - foreach ($armTypes->getSureTypes() as $exprString => [$exprNode, $type]) { - $sureTypesPerExpr[$exprString][0] = $exprNode; - $sureTypesPerExpr[$exprString][1][] = $type; - } - foreach ($armTypes->getSureNotTypes() as $exprString => [$exprNode, $type]) { - $sureNotTypesPerExpr[$exprString][0] = $exprNode; - $sureNotTypesPerExpr[$exprString][1][] = $type; - } - } - - $sureTypes = []; - foreach ($sureTypesPerExpr as $exprString => [$exprNode, $types]) { - $sureTypes[$exprString] = [$exprNode, TypeCombinator::union(...$types)]; - } - $sureNotTypes = []; - foreach ($sureNotTypesPerExpr as $exprString => [$exprNode, $types]) { - $sureNotTypes[$exprString] = [$exprNode, TypeCombinator::union(...$types)]; - } - - return (new SpecifiedTypes($sureTypes, $sureNotTypes))->setRootExpr($expr); - } - private function allExpressionsTrackable(SpecifiedTypes $types): bool { foreach ($types->getSureTypes() as [$expr]) { @@ -283,6 +97,95 @@ public function processExpr(NodeScopeResolver $nodeScopeResolver, Stmt $stmt, Ex impurePoints: array_merge($leftResult->getImpurePoints(), $rightResult->getImpurePoints()), truthyScopeCallback: static fn (): MutatingScope => $rightResult->getScope()->filterByTruthyValue($expr->right), falseyScopeCallback: static fn (): MutatingScope => $leftMergedWithRightScope->filterByFalseyValue($expr), + typeCallback: static function (MutatingScope $s) use ($leftResult, $rightResult, $leftTruthyScope): Type { + $leftBooleanType = $leftResult->getTypeForScope($s)->toBoolean(); + if ($leftBooleanType->isFalse()->yes()) { + return new ConstantBooleanType(false); + } + + // the right side was processed on the left-truthy scope including + // the left's side effects (assignments, by-ref writes) - that + // captured scope is the evaluation point, no re-walk and no + // depth cap needed + $rightBooleanType = $rightResult->getTypeForScope($s->nativeTypesPromoted ? $leftTruthyScope->doNotTreatPhpDocTypesAsCertain() : $leftTruthyScope)->toBoolean(); + if ($rightBooleanType->isFalse()->yes()) { + return new ConstantBooleanType(false); + } + + if ( + $leftBooleanType->isTrue()->yes() + && $rightBooleanType->isTrue()->yes() + ) { + return new ConstantBooleanType(true); + } + + return new BooleanType(); + }, + specifyTypesCallback: function (MutatingScope $s, TypeSpecifierContext $context) use ($expr, $leftResult, $rightResult): SpecifiedTypes { + $leftTypes = $this->defaultNarrowingHelper->getChildSpecifiedTypes($s, $expr->left, $leftResult, $context)->setRootExpr($expr); + $rightScope = $s->filterByTruthyValue($expr->left); + $rightTypes = $this->defaultNarrowingHelper->getChildSpecifiedTypes($rightScope, $expr->right, $rightResult, $context)->setRootExpr($expr); + if ($context->true()) { + $types = $leftTypes->unionWith($rightTypes); + } else { + $leftNormalized = $leftTypes->normalize($s); + $rightNormalized = $rightTypes->normalize($rightScope); + $types = $leftNormalized->intersectWith($rightNormalized); + $types = $this->conditionalExpressionHolderHelper->augmentDisjunctionTypes($s, $rightScope, $leftNormalized, $rightNormalized, $expr->left, $expr->right, false, $types); + } + if ($context->false()) { + // Consequent (holder) narrowings projected by each holder: these must be + // the genuine falsey narrowing of the arm. When that is empty, the arm + // has no sound falsey narrowing and must not contribute a consequent. + $leftHolderTypes = $leftTypes; + $rightHolderTypes = $rightTypes; + // In a mixed truthy-and-false context, re-derive empty holders from the falsey narrowing. + if ($context->truthy()) { + if ($leftHolderTypes->getSureTypes() === [] && $leftHolderTypes->getSureNotTypes() === []) { + $leftHolderTypes = $this->defaultNarrowingHelper->getChildSpecifiedTypes($s, $expr->left, $leftResult, TypeSpecifierContext::createFalsey())->setRootExpr($expr); + } + if ($rightHolderTypes->getSureTypes() === [] && $rightHolderTypes->getSureNotTypes() === []) { + $rightHolderTypes = $this->defaultNarrowingHelper->getChildSpecifiedTypes($rightScope, $expr->right, $rightResult, TypeSpecifierContext::createFalsey())->setRootExpr($expr); + } + } + // Condition (antecedent) narrowings: when an arm has no falsey narrowing + // (e.g. isset() on an array dim fetch), derive the condition from the truthy + // narrowing by swapping sure/sureNot types. This swap is only sound for the + // antecedent — processBooleanConditionalTypes inverts it back to the truthy + // narrowing. It must NOT feed the consequent: inverting a comparison's truthy + // narrowing (e.g. `$a === $b` narrowing `$a` to `$b`'s broad type) would + // over-narrow the consequent (see regression for `$x === $nonConstantString`). + $leftCondTypes = $leftHolderTypes; + $rightCondTypes = $rightHolderTypes; + if ($leftCondTypes->getSureTypes() === [] && $leftCondTypes->getSureNotTypes() === []) { + $truthyLeftTypes = $this->defaultNarrowingHelper->getChildSpecifiedTypes($s, $expr->left, $leftResult, TypeSpecifierContext::createTruthy()); + if ($this->allExpressionsTrackable($truthyLeftTypes)) { + $leftCondTypes = new SpecifiedTypes($truthyLeftTypes->getSureNotTypes(), $truthyLeftTypes->getSureTypes()); + } + } + if ($rightCondTypes->getSureTypes() === [] && $rightCondTypes->getSureNotTypes() === []) { + $truthyRightTypes = $this->defaultNarrowingHelper->getChildSpecifiedTypes($rightScope, $expr->right, $rightResult, TypeSpecifierContext::createTruthy()); + if ($this->allExpressionsTrackable($truthyRightTypes)) { + $rightCondTypes = new SpecifiedTypes($truthyRightTypes->getSureNotTypes(), $truthyRightTypes->getSureTypes()); + } + } + $result = new SpecifiedTypes( + $types->getSureTypes(), + $types->getSureNotTypes(), + ); + if ($types->shouldOverwrite()) { + $result = $result->setAlwaysOverwriteTypes(); + } + return $result->setNewConditionalExpressionHolders($this->conditionalExpressionHolderHelper->mergeConditionalHolders([ + $this->conditionalExpressionHolderHelper->processBooleanConditionalTypes($s, $leftCondTypes, $rightHolderTypes, false, true, $rightScope, $expr->right), + $this->conditionalExpressionHolderHelper->processBooleanConditionalTypes($s, $rightCondTypes, $leftHolderTypes, false, true, $s, $expr->left), + $this->conditionalExpressionHolderHelper->processBooleanConditionalTypes($s, $leftCondTypes, $rightHolderTypes, true, true, $rightScope, $expr->right), + $this->conditionalExpressionHolderHelper->processBooleanConditionalTypes($s, $rightCondTypes, $leftHolderTypes, true, true, $s, $expr->left), + ]))->setRootExpr($expr); + } + + return $types; + }, ); } diff --git a/src/Analyser/ExprHandler/BooleanOrHandler.php b/src/Analyser/ExprHandler/BooleanOrHandler.php index c0ffac43f45..6e9857ce6ba 100644 --- a/src/Analyser/ExprHandler/BooleanOrHandler.php +++ b/src/Analyser/ExprHandler/BooleanOrHandler.php @@ -10,18 +10,15 @@ use PHPStan\Analyser\ExpressionResult; use PHPStan\Analyser\ExpressionResultFactory; use PHPStan\Analyser\ExpressionResultStorage; +use PHPStan\Analyser\ExprHandler; use PHPStan\Analyser\ExprHandler\Helper\ConditionalExpressionHolderHelper; +use PHPStan\Analyser\ExprHandler\Helper\DefaultNarrowingHelper; use PHPStan\Analyser\MutatingScope; use PHPStan\Analyser\NodeScopeResolver; -use PHPStan\Analyser\NoopNodeCallback; -use PHPStan\Analyser\Scope; use PHPStan\Analyser\SpecifiedTypes; -use PHPStan\Analyser\TypeResolvingExprHandler; -use PHPStan\Analyser\TypeSpecifier; use PHPStan\Analyser\TypeSpecifierContext; use PHPStan\DependencyInjection\AutowiredService; use PHPStan\Node\BooleanOrNode; -use PHPStan\ShouldNotHappenException; use PHPStan\Type\BooleanType; use PHPStan\Type\Constant\ConstantBooleanType; use PHPStan\Type\NeverType; @@ -29,22 +26,18 @@ use PHPStan\Type\TypeCombinator; use function array_key_first; use function array_merge; -use function array_reverse; -use function count; /** - * @implements TypeResolvingExprHandler + * @implements ExprHandler */ #[AutowiredService] -final class BooleanOrHandler implements TypeResolvingExprHandler +final class BooleanOrHandler implements ExprHandler { - private const BOOLEAN_EXPRESSION_MAX_PROCESS_DEPTH = 4; - public function __construct( - private NodeScopeResolver $nodeScopeResolver, private ConditionalExpressionHolderHelper $conditionalExpressionHolderHelper, private ExpressionResultFactory $expressionResultFactory, + private DefaultNarrowingHelper $defaultNarrowingHelper, ) { } @@ -54,168 +47,6 @@ public function supports(Expr $expr): bool return $expr instanceof BooleanOr || $expr instanceof LogicalOr; } - public function resolveType(MutatingScope $scope, Expr $expr): Type - { - $leftBooleanType = $scope->getType($expr->left)->toBoolean(); - if ($leftBooleanType->isTrue()->yes()) { - return new ConstantBooleanType(true); - } - - if (BooleanAndHandler::getBooleanExpressionDepth($expr->left) <= self::BOOLEAN_EXPRESSION_MAX_PROCESS_DEPTH) { - $leftResult = $this->nodeScopeResolver->processExprNode(new Stmt\Expression($expr->left), $expr->left, $scope, new ExpressionResultStorage(), new NoopNodeCallback(), ExpressionContext::createDeep()); - $rightBooleanType = $leftResult->getFalseyScope()->getType($expr->right)->toBoolean(); - } else { - $rightBooleanType = $scope->filterByFalseyValue($expr->left)->getType($expr->right)->toBoolean(); - } - - if ($rightBooleanType->isTrue()->yes()) { - return new ConstantBooleanType(true); - } - - if ( - $leftBooleanType->isFalse()->yes() - && $rightBooleanType->isFalse()->yes() - ) { - return new ConstantBooleanType(false); - } - - return new BooleanType(); - } - - public function specifyTypes(TypeSpecifier $typeSpecifier, Scope $scope, Expr $expr, TypeSpecifierContext $context): SpecifiedTypes - { - if (!$scope instanceof MutatingScope) { - throw new ShouldNotHappenException(); - } - - // For deep BooleanOr chains, flatten and process all arms at once - // to avoid O(n^2) recursive filterByFalseyValue calls - if (BooleanAndHandler::getBooleanExpressionDepth($expr) > self::BOOLEAN_EXPRESSION_MAX_PROCESS_DEPTH) { - return $this->specifyTypesForFlattenedBooleanOr($typeSpecifier, $scope, $expr, $context); - } - - $leftTypes = $typeSpecifier->specifyTypesInCondition($scope, $expr->left, $context)->setRootExpr($expr); - $rightScope = $scope->filterByFalseyValue($expr->left); - $rightTypes = $typeSpecifier->specifyTypesInCondition($rightScope, $expr->right, $context)->setRootExpr($expr); - - if ($context->true()) { - if ( - $scope->getType($expr->left)->toBoolean()->isFalse()->yes() - ) { - $types = $rightTypes->normalize($rightScope); - } elseif ( - $scope->getType($expr->left)->toBoolean()->isTrue()->yes() - || $scope->getType($expr->right)->toBoolean()->isFalse()->yes() - ) { - $types = $leftTypes->normalize($scope); - } else { - $leftNormalized = $leftTypes->normalize($scope); - $rightNormalized = $rightTypes->normalize($rightScope); - $types = $leftNormalized->intersectWith($rightNormalized); - $types = $this->augmentBooleanOrTruthyWithConditionalHolders($typeSpecifier, $scope, $rightScope, $expr, $types); - $types = $this->conditionalExpressionHolderHelper->augmentDisjunctionTypes($scope, $rightScope, $leftNormalized, $rightNormalized, $expr->left, $expr->right, true, $types); - } - } else { - $types = $leftTypes->unionWith($rightTypes); - } - - if ($context->true()) { - $result = new SpecifiedTypes( - $types->getSureTypes(), - $types->getSureNotTypes(), - ); - if ($types->shouldOverwrite()) { - $result = $result->setAlwaysOverwriteTypes(); - } - return $result->setNewConditionalExpressionHolders($this->conditionalExpressionHolderHelper->mergeConditionalHolders([ - $this->conditionalExpressionHolderHelper->processBooleanConditionalTypes($scope, $leftTypes, $rightTypes, false, false, $rightScope, $expr->right), - $this->conditionalExpressionHolderHelper->processBooleanConditionalTypes($scope, $rightTypes, $leftTypes, false, false, $scope, $expr->left), - $this->conditionalExpressionHolderHelper->processBooleanConditionalTypes($scope, $leftTypes, $rightTypes, true, false, $rightScope, $expr->right), - $this->conditionalExpressionHolderHelper->processBooleanConditionalTypes($scope, $rightTypes, $leftTypes, true, false, $scope, $expr->left), - ]))->setRootExpr($expr); - } - - return $types; - } - - /** - * Flatten a deep BooleanOr chain into leaf expressions and process them - * without recursive filterByFalseyValue calls. This reduces O(n^2) to O(n) - * for chains with many arms (e.g., 80+ === comparisons in ||). - */ - private function specifyTypesForFlattenedBooleanOr( - TypeSpecifier $typeSpecifier, - MutatingScope $scope, - BooleanOr|LogicalOr $expr, - TypeSpecifierContext $context, - ): SpecifiedTypes - { - // Collect all leaf expressions from the chain - $arms = []; - $current = $expr; - while ($current instanceof BooleanOr || $current instanceof LogicalOr) { - $arms[] = $current->right; - $current = $current->left; - } - $arms[] = $current; // leftmost leaf - $arms = array_reverse($arms); - - if ($context->false() || $context->falsey()) { - // Falsey: all arms are false → union all SpecifiedTypes. - // Collect per-expression types first, then build unions once - // to avoid O(N²) from incremental TypeCombinator::union() growth. - /** @var array}> $sureTypesPerExpr */ - $sureTypesPerExpr = []; - /** @var array}> $sureNotTypesPerExpr */ - $sureNotTypesPerExpr = []; - - foreach ($arms as $arm) { - $armTypes = $typeSpecifier->specifyTypesInCondition($scope, $arm, $context); - foreach ($armTypes->getSureTypes() as $exprString => [$exprNode, $type]) { - $sureTypesPerExpr[$exprString][0] = $exprNode; - $sureTypesPerExpr[$exprString][1][] = $type; - } - foreach ($armTypes->getSureNotTypes() as $exprString => [$exprNode, $type]) { - $sureNotTypesPerExpr[$exprString][0] = $exprNode; - $sureNotTypesPerExpr[$exprString][1][] = $type; - } - } - - $sureTypes = []; - foreach ($sureTypesPerExpr as $exprString => [$exprNode, $types]) { - $sureTypes[$exprString] = [$exprNode, TypeCombinator::intersect(...$types)]; - } - $sureNotTypes = []; - foreach ($sureNotTypesPerExpr as $exprString => [$exprNode, $types]) { - $sureNotTypes[$exprString] = [$exprNode, TypeCombinator::union(...$types)]; - } - - return (new SpecifiedTypes($sureTypes, $sureNotTypes))->setRootExpr($expr); - } - - // Truthy: at least one arm is true → intersect all normalized SpecifiedTypes - $armSpecifiedTypes = []; - foreach ($arms as $arm) { - $armTypes = $typeSpecifier->specifyTypesInCondition($scope, $arm, $context); - $armSpecifiedTypes[] = $armTypes->normalize($scope); - } - - $types = $armSpecifiedTypes[0]; - for ($i = 1; $i < count($armSpecifiedTypes); $i++) { - $types = $types->intersectWith($armSpecifiedTypes[$i]); - } - - $result = new SpecifiedTypes( - $types->getSureTypes(), - $types->getSureNotTypes(), - ); - if ($types->shouldOverwrite()) { - $result = $result->setAlwaysOverwriteTypes(); - } - - return $result->setRootExpr($expr); - } - /** * For `if ($a || $b)` truthy, expressions narrowed by stored conditional * holders (e.g. `$a = $obj instanceof ClassA;` records "when `$a` is @@ -234,7 +65,7 @@ private function specifyTypesForFlattenedBooleanOr( * skipped: in the OR-truthy scope the arm that didn't narrow could still be * the truthy one, so the sound result is the original (unnarrowed) type. */ - private function augmentBooleanOrTruthyWithConditionalHolders(TypeSpecifier $typeSpecifier, MutatingScope $scope, MutatingScope $rightScope, BooleanOr|LogicalOr $expr, SpecifiedTypes $types): SpecifiedTypes + private function augmentBooleanOrTruthyWithConditionalHolders(MutatingScope $scope, MutatingScope $rightScope, BooleanOr|LogicalOr $expr, SpecifiedTypes $types): SpecifiedTypes { $leftTruthyScope = $scope->filterByTruthyValue($expr->left); $rightTruthyScope = $rightScope->filterByTruthyValue($expr->right); @@ -283,7 +114,7 @@ private function augmentBooleanOrTruthyWithConditionalHolders(TypeSpecifier $typ } $types = $types->unionWith( - $typeSpecifier->create($targetExpr, $unionType, TypeSpecifierContext::createTrue(), $scope), + $this->defaultNarrowingHelper->createSubjectTypes($scope, $targetExpr, null, $unionType, TypeSpecifierContext::createTrue()), ); } } @@ -315,6 +146,74 @@ public function processExpr(NodeScopeResolver $nodeScopeResolver, Stmt $stmt, Ex impurePoints: array_merge($leftResult->getImpurePoints(), $rightResult->getImpurePoints()), truthyScopeCallback: static fn (): MutatingScope => $leftMergedWithRightScope->filterByTruthyValue($expr), falseyScopeCallback: static fn (): MutatingScope => $rightResult->getScope()->filterByFalseyValue($expr->right), + typeCallback: static function (MutatingScope $s) use ($leftResult, $rightResult, $leftFalseyScope): Type { + $leftBooleanType = $leftResult->getTypeForScope($s)->toBoolean(); + if ($leftBooleanType->isTrue()->yes()) { + return new ConstantBooleanType(true); + } + + // the right side was processed on the left-falsey scope including + // the left's side effects (assignments, by-ref writes) - that + // captured scope is the evaluation point, no re-walk and no + // depth cap needed + $rightBooleanType = $rightResult->getTypeForScope($s->nativeTypesPromoted ? $leftFalseyScope->doNotTreatPhpDocTypesAsCertain() : $leftFalseyScope)->toBoolean(); + if ($rightBooleanType->isTrue()->yes()) { + return new ConstantBooleanType(true); + } + + if ( + $leftBooleanType->isFalse()->yes() + && $rightBooleanType->isFalse()->yes() + ) { + return new ConstantBooleanType(false); + } + + return new BooleanType(); + }, + specifyTypesCallback: function (MutatingScope $s, TypeSpecifierContext $context) use ($expr, $leftResult, $rightResult): SpecifiedTypes { + $leftTypes = $this->defaultNarrowingHelper->getChildSpecifiedTypes($s, $expr->left, $leftResult, $context)->setRootExpr($expr); + $rightScope = $s->filterByFalseyValue($expr->left); + $rightTypes = $this->defaultNarrowingHelper->getChildSpecifiedTypes($rightScope, $expr->right, $rightResult, $context)->setRootExpr($expr); + + if ($context->true()) { + if ( + $leftResult->getTypeForScope($s)->toBoolean()->isFalse()->yes() + ) { + $types = $rightTypes->normalize($rightScope); + } elseif ( + $leftResult->getTypeForScope($s)->toBoolean()->isTrue()->yes() + || $rightResult->getTypeForScope($s)->toBoolean()->isFalse()->yes() + ) { + $types = $leftTypes->normalize($s); + } else { + $leftNormalized = $leftTypes->normalize($s); + $rightNormalized = $rightTypes->normalize($rightScope); + $types = $leftNormalized->intersectWith($rightNormalized); + $types = $this->augmentBooleanOrTruthyWithConditionalHolders($s, $rightScope, $expr, $types); + $types = $this->conditionalExpressionHolderHelper->augmentDisjunctionTypes($s, $rightScope, $leftNormalized, $rightNormalized, $expr->left, $expr->right, true, $types); + } + } else { + $types = $leftTypes->unionWith($rightTypes); + } + + if ($context->true()) { + $result = new SpecifiedTypes( + $types->getSureTypes(), + $types->getSureNotTypes(), + ); + if ($types->shouldOverwrite()) { + $result = $result->setAlwaysOverwriteTypes(); + } + return $result->setNewConditionalExpressionHolders($this->conditionalExpressionHolderHelper->mergeConditionalHolders([ + $this->conditionalExpressionHolderHelper->processBooleanConditionalTypes($s, $leftTypes, $rightTypes, false, false, $rightScope, $expr->right), + $this->conditionalExpressionHolderHelper->processBooleanConditionalTypes($s, $rightTypes, $leftTypes, false, false, $s, $expr->left), + $this->conditionalExpressionHolderHelper->processBooleanConditionalTypes($s, $leftTypes, $rightTypes, true, false, $rightScope, $expr->right), + $this->conditionalExpressionHolderHelper->processBooleanConditionalTypes($s, $rightTypes, $leftTypes, true, false, $s, $expr->left), + ]))->setRootExpr($expr); + } + + return $types; + }, ); } diff --git a/src/Analyser/ExprHandler/CoalesceHandler.php b/src/Analyser/ExprHandler/CoalesceHandler.php index 70541ebae02..27c0fc4a889 100644 --- a/src/Analyser/ExprHandler/CoalesceHandler.php +++ b/src/Analyser/ExprHandler/CoalesceHandler.php @@ -9,17 +9,14 @@ use PHPStan\Analyser\ExpressionResult; use PHPStan\Analyser\ExpressionResultFactory; use PHPStan\Analyser\ExpressionResultStorage; +use PHPStan\Analyser\ExprHandler; use PHPStan\Analyser\ExprHandler\Helper\DefaultNarrowingHelper; use PHPStan\Analyser\ExprHandler\Helper\NonNullabilityHelper; use PHPStan\Analyser\MutatingScope; use PHPStan\Analyser\NodeScopeResolver; -use PHPStan\Analyser\Scope; use PHPStan\Analyser\SpecifiedTypes; -use PHPStan\Analyser\TypeResolvingExprHandler; -use PHPStan\Analyser\TypeSpecifier; use PHPStan\Analyser\TypeSpecifierContext; use PHPStan\DependencyInjection\AutowiredService; -use PHPStan\ShouldNotHappenException; use PHPStan\Type\Constant\ConstantBooleanType; use PHPStan\Type\NeverType; use PHPStan\Type\NullType; @@ -28,10 +25,10 @@ use function array_merge; /** - * @implements TypeResolvingExprHandler + * @implements ExprHandler */ #[AutowiredService] -final class CoalesceHandler implements TypeResolvingExprHandler +final class CoalesceHandler implements ExprHandler { public function __construct( @@ -47,73 +44,22 @@ public function supports(Expr $expr): bool return $expr instanceof Coalesce; } - public function resolveType(MutatingScope $scope, Expr $expr): Type + /** + * A falsey coalesce means its left side was null (when it was surely set) - + * shared by the specifyTypesCallback and by processExpr() for the scope + * the right side evaluates under. + * + * @param Coalesce $expr + */ + private function getFalseySpecifiedTypes(MutatingScope $s, Expr $expr, ExpressionResult $condResult, TypeSpecifierContext $context): SpecifiedTypes { - $issetLeftExpr = new Expr\Isset_([$expr->left]); + $isset = $s->issetCheck($expr->left, static fn () => true); - $result = $scope->issetCheck($expr->left, static function (Type $type): ?bool { - $isNull = $type->isNull(); - if ($isNull->maybe()) { - return null; - } - - return !$isNull->yes(); - }); - - if ($result !== null && $result !== false) { - return TypeCombinator::removeNull($scope->filterByTruthyValue($issetLeftExpr)->getType($expr->left)); - } - - $rightType = $scope->filterByFalseyValue($issetLeftExpr)->getType($expr->right); - - if ($result === null) { - return TypeCombinator::union( - TypeCombinator::removeNull($scope->filterByTruthyValue($issetLeftExpr)->getType($expr->left)), - $rightType, - ); - } - - return $rightType; - } - - public function specifyTypes(TypeSpecifier $typeSpecifier, Scope $scope, Expr $expr, TypeSpecifierContext $context): SpecifiedTypes - { - if ($context->null()) { - return $typeSpecifier->specifyDefaultTypes($scope, $expr, $context); - } - - if (!$context->true()) { - if (!$scope instanceof MutatingScope) { - throw new ShouldNotHappenException(); - } - - $isset = $scope->issetCheck($expr->left, static fn () => true); - - if ($isset !== true) { - return new SpecifiedTypes(); - } - - return $typeSpecifier->create( - $expr->left, - new NullType(), - $context->negate(), - $scope, - )->setRootExpr($expr); - } - - if ((new ConstantBooleanType(false))->isSuperTypeOf($scope->getType($expr->right)->toBoolean())->yes()) { - return $typeSpecifier->create( - $expr->left, - new NullType(), - TypeSpecifierContext::createFalse(), - $scope, - )->setRootExpr($expr); + if ($isset !== true) { + return new SpecifiedTypes(); } - // The Coalesce condition matched but produced no narrowing; the legacy - // if/elseif chain fell through to its empty-SpecifiedTypes tail here, - // not to the truthy/falsey default. - return (new SpecifiedTypes([], []))->setRootExpr($expr); + return $this->defaultNarrowingHelper->createSubjectTypes($s, $expr->left, $condResult, new NullType(), $context->negate())->setRootExpr($expr); } public function processExpr(NodeScopeResolver $nodeScopeResolver, Stmt $stmt, Expr $expr, MutatingScope $scope, ExpressionResultStorage $storage, callable $nodeCallback, ExpressionContext $context): ExpressionResult @@ -125,7 +71,9 @@ public function processExpr(NodeScopeResolver $nodeScopeResolver, Stmt $stmt, Ex $scope = $this->nonNullabilityHelper->revertNonNullability($condResult->getScope(), $nonNullabilityResult->getSpecifiedExpressions()); $scope = $nodeScopeResolver->lookForUnsetAllowedUndefinedExpressions($scope, $expr->left); - $rightScope = $scope->filterByFalseyValue($expr); + // the falsey narrowing of this very node - asking the scope about it + // mid-processing would take the on-demand path and recurse + $rightScope = $scope->applySpecifiedTypes($this->getFalseySpecifiedTypes($scope, $expr, $condResult, TypeSpecifierContext::createFalsey())); $rightResult = $nodeScopeResolver->processExprNode($stmt, $expr->right, $rightScope, $storage, $nodeCallback, $context->enterDeep()); $rightExprType = $scope->getType($expr->right); if ($rightExprType instanceof NeverType && $rightExprType->isExplicit()) { @@ -142,6 +90,53 @@ public function processExpr(NodeScopeResolver $nodeScopeResolver, Stmt $stmt, Ex isAlwaysTerminating: $condResult->isAlwaysTerminating(), throwPoints: array_merge($condResult->getThrowPoints(), $rightResult->getThrowPoints()), impurePoints: array_merge($condResult->getImpurePoints(), $rightResult->getImpurePoints()), + typeCallback: static function (MutatingScope $s) use ($expr, $condResult, $rightResult, $rightScope): Type { + $issetLeftExpr = new Expr\Isset_([$expr->left]); + + $result = $s->issetCheck($expr->left, static function (Type $type): ?bool { + $isNull = $type->isNull(); + if ($isNull->maybe()) { + return null; + } + + return !$isNull->yes(); + }); + + if ($result !== null && $result !== false) { + return TypeCombinator::removeNull($condResult->getTypeForScope($s->filterByTruthyValue($issetLeftExpr))); + } + + // the right side was processed on the left-is-null scope - that + // captured scope is the evaluation point + $rightType = $rightResult->getTypeForScope($s->nativeTypesPromoted ? $rightScope->doNotTreatPhpDocTypesAsCertain() : $rightScope); + + if ($result === null) { + return TypeCombinator::union( + TypeCombinator::removeNull($condResult->getTypeForScope($s->filterByTruthyValue($issetLeftExpr))), + $rightType, + ); + } + + return $rightType; + }, + specifyTypesCallback: function (MutatingScope $s, TypeSpecifierContext $context) use ($expr, $condResult, $rightResult): SpecifiedTypes { + if ($context->null()) { + return $this->defaultNarrowingHelper->specifyDefaultTypes($expr, $context); + } + + if (!$context->true()) { + return $this->getFalseySpecifiedTypes($s, $expr, $condResult, $context); + } + + if ((new ConstantBooleanType(false))->isSuperTypeOf($rightResult->getTypeForScope($s)->toBoolean())->yes()) { + return $this->defaultNarrowingHelper->createSubjectTypes($s, $expr->left, $condResult, new NullType(), TypeSpecifierContext::createFalse())->setRootExpr($expr); + } + + // The Coalesce condition matched but produced no narrowing; the legacy + // if/elseif chain fell through to its empty-SpecifiedTypes tail here, + // not to the truthy/falsey default. + return (new SpecifiedTypes([], []))->setRootExpr($expr); + }, // a type constraint on the coalesce constrains its left side when // the type rules the right side in or out - what // TypeSpecifier::create() recovered by unwrapping the coalesce diff --git a/src/Analyser/ExprHandler/Helper/DefaultNarrowingHelper.php b/src/Analyser/ExprHandler/Helper/DefaultNarrowingHelper.php index a82899110bb..84395b4b3d3 100644 --- a/src/Analyser/ExprHandler/Helper/DefaultNarrowingHelper.php +++ b/src/Analyser/ExprHandler/Helper/DefaultNarrowingHelper.php @@ -6,6 +6,7 @@ use PHPStan\Analyser\ExpressionResult; use PHPStan\Analyser\MutatingScope; use PHPStan\Analyser\SpecifiedTypes; +use PHPStan\Analyser\TypeSpecifier; use PHPStan\Analyser\TypeSpecifierContext; use PHPStan\DependencyInjection\AutowiredService; use PHPStan\Node\Printer\ExprPrinter; @@ -25,10 +26,34 @@ final class DefaultNarrowingHelper { - public function __construct(private ExprPrinter $exprPrinter) + public function __construct( + private ExprPrinter $exprPrinter, + private TypeSpecifier $typeSpecifier, + ) { } + /** + * The narrowing of an already-processed child expression in the given + * boolean context: answered by the child result's specifyTypesCallback. + * Until the child's handler migrates its narrowing - or when the child + * is a synthetic node with no result - this bridges through the + * old-world dispatcher, which answers converted handlers from stored + * results, so the bridge terminates. The bridge dies in 3.0 together + * with TypeSpecifier::specifyTypesInCondition(). + */ + public function getChildSpecifiedTypes(MutatingScope $s, Expr $childExpr, ?ExpressionResult $childResult, TypeSpecifierContext $context): SpecifiedTypes + { + if ($childResult !== null) { + $types = $childResult->getSpecifiedTypesForScope($s, $context); + if ($types !== null) { + return $types; + } + } + + return $this->typeSpecifier->specifyTypesInCondition($s, $childExpr, $context); + } + public function specifyDefaultTypes(Expr $expr, TypeSpecifierContext $context): SpecifiedTypes { if ($context->null()) { diff --git a/src/Analyser/ExprHandler/TernaryHandler.php b/src/Analyser/ExprHandler/TernaryHandler.php index bccabd76e0b..79ece5c6c51 100644 --- a/src/Analyser/ExprHandler/TernaryHandler.php +++ b/src/Analyser/ExprHandler/TernaryHandler.php @@ -11,13 +11,11 @@ use PHPStan\Analyser\ExpressionResult; use PHPStan\Analyser\ExpressionResultFactory; use PHPStan\Analyser\ExpressionResultStorage; +use PHPStan\Analyser\ExprHandler; +use PHPStan\Analyser\ExprHandler\Helper\DefaultNarrowingHelper; use PHPStan\Analyser\MutatingScope; use PHPStan\Analyser\NodeScopeResolver; -use PHPStan\Analyser\NoopNodeCallback; -use PHPStan\Analyser\Scope; use PHPStan\Analyser\SpecifiedTypes; -use PHPStan\Analyser\TypeResolvingExprHandler; -use PHPStan\Analyser\TypeSpecifier; use PHPStan\Analyser\TypeSpecifierContext; use PHPStan\DependencyInjection\AutowiredService; use PHPStan\Type\NeverType; @@ -26,15 +24,15 @@ use function array_merge; /** - * @implements TypeResolvingExprHandler + * @implements ExprHandler */ #[AutowiredService] -final class TernaryHandler implements TypeResolvingExprHandler +final class TernaryHandler implements ExprHandler { public function __construct( - private NodeScopeResolver $nodeScopeResolver, private ExpressionResultFactory $expressionResultFactory, + private DefaultNarrowingHelper $defaultNarrowingHelper, ) { } @@ -44,62 +42,6 @@ public function supports(Expr $expr): bool return $expr instanceof Ternary; } - public function resolveType(MutatingScope $scope, Expr $expr): Type - { - $condResult = $this->nodeScopeResolver->processExprNode(new Stmt\Expression($expr->cond), $expr->cond, $scope, new ExpressionResultStorage(), new NoopNodeCallback(), ExpressionContext::createDeep()); - if ($expr->if === null) { - $conditionType = $scope->getType($expr->cond); - $booleanConditionType = $conditionType->toBoolean(); - if ($booleanConditionType->isTrue()->yes()) { - return $condResult->getTruthyScope()->getType($expr->cond); - } - - if ($booleanConditionType->isFalse()->yes()) { - return $condResult->getFalseyScope()->getType($expr->else); - } - - return TypeCombinator::union( - TypeCombinator::removeFalsey($condResult->getTruthyScope()->getType($expr->cond)), - $condResult->getFalseyScope()->getType($expr->else), - ); - } - - $booleanConditionType = $scope->getType($expr->cond)->toBoolean(); - if ($booleanConditionType->isTrue()->yes()) { - return $condResult->getTruthyScope()->getType($expr->if); - } - - if ($booleanConditionType->isFalse()->yes()) { - return $condResult->getFalseyScope()->getType($expr->else); - } - - return TypeCombinator::union( - $condResult->getTruthyScope()->getType($expr->if), - $condResult->getFalseyScope()->getType($expr->else), - ); - } - - public function specifyTypes(TypeSpecifier $typeSpecifier, Scope $scope, Expr $expr, TypeSpecifierContext $context): SpecifiedTypes - { - if ($expr->cond instanceof Ternary || $context->null()) { - return $typeSpecifier->specifyDefaultTypes($scope, $expr, $context); - } - - if ($expr->if !== null) { - $conditionExpr = new BooleanOr( - new BooleanAnd($expr->cond, $expr->if), - new BooleanAnd(new Expr\BooleanNot($expr->cond), $expr->else), - ); - } else { - $conditionExpr = new BooleanOr( - $expr->cond, - new BooleanAnd(new Expr\BooleanNot($expr->cond), $expr->else), - ); - } - - return $typeSpecifier->specifyTypesInCondition($scope, $conditionExpr, $context)->setRootExpr($expr); - } - public function processExpr(NodeScopeResolver $nodeScopeResolver, Stmt $stmt, Expr $expr, MutatingScope $scope, ExpressionResultStorage $storage, callable $nodeCallback, ExpressionContext $context): ExpressionResult { $ternaryCondResult = $nodeScopeResolver->processExprNode($stmt, $expr->cond, $scope, $storage, $nodeCallback, $context->enterDeep()); @@ -108,7 +50,10 @@ public function processExpr(NodeScopeResolver $nodeScopeResolver, Stmt $stmt, Ex $ifTrueScope = $ternaryCondResult->getTruthyScope(); $ifFalseScope = $ternaryCondResult->getFalseyScope(); $ifTrueType = null; + $ifResult = null; + $ifProcessingScope = $ifTrueScope; + $elseProcessingScope = $ifFalseScope; if ($expr->if === null) { $elseResult = $nodeScopeResolver->processExprNode($stmt, $expr->else, $ifFalseScope, $storage, $nodeCallback, $context); $throwPoints = array_merge($throwPoints, $elseResult->getThrowPoints()); @@ -154,6 +99,67 @@ public function processExpr(NodeScopeResolver $nodeScopeResolver, Stmt $stmt, Ex isAlwaysTerminating: $ternaryCondResult->isAlwaysTerminating(), throwPoints: $throwPoints, impurePoints: $impurePoints, + // the branches were processed on the cond-truthy/cond-falsey scopes + // including the condition's side effects - those captured scopes + // are the evaluation points, no re-walk needed + typeCallback: static function (MutatingScope $s) use ($expr, $ternaryCondResult, $ifResult, $elseResult, $ifProcessingScope, $elseProcessingScope): Type { + if ($s->nativeTypesPromoted) { + $ifProcessingScope = $ifProcessingScope->doNotTreatPhpDocTypesAsCertain(); + $elseProcessingScope = $elseProcessingScope->doNotTreatPhpDocTypesAsCertain(); + } + $booleanConditionType = $ternaryCondResult->getTypeForScope($s)->toBoolean(); + $elseType = $elseResult->getTypeForScope($elseProcessingScope); + if ($expr->if === null || $ifResult === null) { + $condTruthyType = $ternaryCondResult->getTypeForScope($ifProcessingScope); + if ($booleanConditionType->isTrue()->yes()) { + return $condTruthyType; + } + + if ($booleanConditionType->isFalse()->yes()) { + return $elseType; + } + + return TypeCombinator::union( + TypeCombinator::removeFalsey($condTruthyType), + $elseType, + ); + } + + $ifType = $ifResult->getTypeForScope($ifProcessingScope); + if ($booleanConditionType->isTrue()->yes()) { + return $ifType; + } + + if ($booleanConditionType->isFalse()->yes()) { + return $elseType; + } + + return TypeCombinator::union( + $ifType, + $elseType, + ); + }, + specifyTypesCallback: function (MutatingScope $s, TypeSpecifierContext $context) use ($expr): SpecifiedTypes { + if ($expr->cond instanceof Ternary || $context->null()) { + return $this->defaultNarrowingHelper->specifyDefaultTypes($expr, $context); + } + + if ($expr->if !== null) { + $conditionExpr = new BooleanOr( + new BooleanAnd($expr->cond, $expr->if), + new BooleanAnd(new Expr\BooleanNot($expr->cond), $expr->else), + ); + } else { + $conditionExpr = new BooleanOr( + $expr->cond, + new BooleanAnd(new Expr\BooleanNot($expr->cond), $expr->else), + ); + } + + // the synthetic condition takes the on-demand bridge; its real + // subnodes answer from stored results + return $this->defaultNarrowingHelper->getChildSpecifiedTypes($s, $conditionExpr, null, $context)->setRootExpr($expr); + }, ); } diff --git a/src/Analyser/ExpressionResult.php b/src/Analyser/ExpressionResult.php index d1e1949d8bc..43de235c169 100644 --- a/src/Analyser/ExpressionResult.php +++ b/src/Analyser/ExpressionResult.php @@ -156,7 +156,7 @@ public function getType(): Type } } - if ($this->typeCallback !== null) { + if ($this->typeCallback !== null && !$this->hasTrackedExpressionType($this->beforeScope)) { return $this->cachedType = TypeUtils::resolveLateResolvableTypes(($this->typeCallback)($this->beforeScope, $this->expr)); } @@ -169,13 +169,28 @@ public function getNativeType(): Type return $this->cachedNativeType; } - if ($this->typeCallback !== null) { + if ($this->typeCallback !== null && !$this->hasTrackedExpressionType($this->beforeScope->doNotTreatPhpDocTypesAsCertain())) { return $this->cachedNativeType = TypeUtils::resolveLateResolvableTypes(($this->typeCallback)($this->beforeScope->doNotTreatPhpDocTypesAsCertain(), $this->expr)); } return $this->cachedNativeType = $this->beforeScope->getNativeType($this->expr); } + /** + * A narrowed or ensured type tracked for the whole expression (e.g. the + * nullsafe handlers ensure `($x ?? null)` is not null before processing + * the chain) wins over recomputing the type - mirrors the tracked-holder + * early return in MutatingScope::resolveType(). Asking the scope is safe: + * its own early return answers from the holder without dispatching back. + */ + private function hasTrackedExpressionType(MutatingScope $scope): bool + { + return !$this->expr instanceof Expr\Variable + && !$this->expr instanceof Expr\Closure + && !$this->expr instanceof Expr\ArrowFunction + && $scope->hasExpressionType($this->expr)->yes(); + } + public function hasTypeCallback(): bool { return $this->typeCallback !== null; @@ -220,7 +235,7 @@ public function getCreatedTypesForScope(MutatingScope $scope, Type $type, TypeSp */ public function getTypeForScope(MutatingScope $scope): Type { - if ($this->typeCallback !== null) { + if ($this->typeCallback !== null && !$this->hasTrackedExpressionType($scope)) { return TypeUtils::resolveLateResolvableTypes(($this->typeCallback)($scope, $this->expr)); } From b3197c9ff02bf4a2a051e2c890132fd058cf3249 Mon Sep 17 00:00:00 2001 From: Ondrej Mirtes Date: Sat, 13 Jun 2026 00:11:12 +0200 Subject: [PATCH 020/113] Add regression test for conditional holders narrowing coalesce of properties Closes https://github.com/phpstan/phpstan/issues/10786 Co-Authored-By: Claude Fable 5 --- tests/PHPStan/Analyser/nsrt/bug-10786.php | 23 +++++++++++++++++++++++ 1 file changed, 23 insertions(+) create mode 100644 tests/PHPStan/Analyser/nsrt/bug-10786.php diff --git a/tests/PHPStan/Analyser/nsrt/bug-10786.php b/tests/PHPStan/Analyser/nsrt/bug-10786.php new file mode 100644 index 00000000000..4dc9333aba6 --- /dev/null +++ b/tests/PHPStan/Analyser/nsrt/bug-10786.php @@ -0,0 +1,23 @@ +value) && is_null($b->value)) { + throw new \Exception(); + } + + assertType('int', $a->value ?? $b->value); + + return $a->value ?? $b->value; + } +} From 7ebb050c5efbb444e7188f2a02bfd2355aded00d Mon Sep 17 00:00:00 2001 From: Ondrej Mirtes Date: Sat, 13 Jun 2026 00:19:31 +0200 Subject: [PATCH 021/113] Never process expressions on a FiberScope The engine never processes on the rule-facing FiberScope - its type asks suspend, which crashes outside a fiber. One can reach processExprNode through a stored result's memoized truthy/falsey scope (first computed inside a rule fiber) consumed by a composite handler for a child's processing scope: phpstan-phpunit's assertEmpty() extension builds a synthetic BooleanOr, the converted BooleanOrHandler processes it on demand, and the right arm's scope comes from the left arm's stored result. Convert to the mutating flavor at the processExprNode boundary. Co-Authored-By: Claude Fable 5 --- src/Analyser/NodeScopeResolver.php | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/src/Analyser/NodeScopeResolver.php b/src/Analyser/NodeScopeResolver.php index d6ca37cb42c..862d7eb3aee 100644 --- a/src/Analyser/NodeScopeResolver.php +++ b/src/Analyser/NodeScopeResolver.php @@ -52,6 +52,7 @@ use PhpParser\NodeVisitorAbstract; use PHPStan\Analyser\ExprHandler\AssignHandler; use PHPStan\Analyser\ExprHandler\Helper\ImplicitToStringCallHelper; +use PHPStan\Analyser\Fiber\FiberScope; use PHPStan\BetterReflection\Reflection\Adapter\ReflectionClass; use PHPStan\BetterReflection\Reflection\ReflectionEnum; use PHPStan\BetterReflection\Reflector\Reflector; @@ -2818,6 +2819,15 @@ public function processExprNode( ExpressionContext $context, ): ExpressionResult { + if ($scope instanceof FiberScope) { + // the engine never processes on the rule-facing FiberScope - one can + // arrive here through a stored result's memoized truthy/falsey scope + // (first computed inside a rule fiber) consumed by a handler for a + // child's processing scope; its type asks would suspend outside + // a fiber + $scope = $scope->toMutatingScope(); + } + if ($this->returnStoredExpressionResults) { $storedResult = $storage->findExpressionResult($expr); if ($storedResult !== null) { From b48a9c5fb684f26c2e5c816fea5d422b9c5a122e Mon Sep 17 00:00:00 2001 From: Ondrej Mirtes Date: Sat, 13 Jun 2026 00:36:03 +0200 Subject: [PATCH 022/113] Convert rule-facing FiberScope at the new-world hook boundary Replaces the blanket processExprNode conversion with the root cause: the hooks are the boundary between the rule-facing world and the engine. Rules hold FiberScopes and feed them straight into the old-world dispatcher (ImpossibleCheckTypeHelper passes the rule's scope to specifyTypesInCondition, phpstan-phpunit's assert extension builds a synthetic BooleanOr there), so resolveTypeOfNewWorldHandlerNode() and specifyTypesOfNewWorldHandlerNode() can run with $this being a FiberScope. They now call toMutatingScope() - identity on a plain scope, a state-preserving copy on a FiberScope - before invoking result callbacks and on-demand processing. Without the conversion the engine processes synthetic nodes on the rule-facing scope, whose type asks suspend: wasteful inside a rule fiber, fatal outside one ("Cannot suspend outside of a fiber"). Found by an ExpressionResult creation tripwire after the CI-only crash never reproduced locally. Co-Authored-By: Claude Fable 5 --- src/Analyser/MutatingScope.php | 21 +++++++++++++++------ src/Analyser/NodeScopeResolver.php | 10 ---------- 2 files changed, 15 insertions(+), 16 deletions(-) diff --git a/src/Analyser/MutatingScope.php b/src/Analyser/MutatingScope.php index 188d8277791..5253aa1ee36 100644 --- a/src/Analyser/MutatingScope.php +++ b/src/Analyser/MutatingScope.php @@ -1012,6 +1012,11 @@ private function resolveType(string $exprString, Expr $node): Type */ private function resolveTypeOfNewWorldHandlerNode(Expr $node): Type { + // the hooks are the boundary between the rule-facing world and the + // engine - a rule's FiberScope must not flow into result callbacks or + // on-demand processing, where its suspending type asks crash outside + // a fiber + $scope = $this->toMutatingScope(); $storage = $this->expressionResultStorageStack->getCurrent(); if ($storage !== null) { $result = $storage->findExpressionResult($node); @@ -1023,18 +1028,18 @@ private function resolveTypeOfNewWorldHandlerNode(Expr $node): Type )); } - return $result->getTypeForScope($this); + return $result->getTypeForScope($scope); } } // a synthetic node, or no analysis in progress $onDemandResult = $this->container->getByType(NodeScopeResolver::class)->processExprOnDemand( $node, - $this, + $scope, $storage !== null ? $storage->duplicate() : new ExpressionResultStorage(), ); - return $onDemandResult->getTypeForScope($this); + return $onDemandResult->getTypeForScope($scope); } /** @@ -1077,22 +1082,26 @@ private function getCurrentTypesOfSpecifiedExpr(Expr $expr): ?array */ public function specifyTypesOfNewWorldHandlerNode(Expr $node, TypeSpecifierContext $context): ?SpecifiedTypes { + // see resolveTypeOfNewWorldHandlerNode() - rules ask the dispatcher + // with their FiberScope (e.g. ImpossibleCheckTypeHelper), the engine + // side of the boundary works with the mutating flavor + $scope = $this->toMutatingScope(); $storage = $this->expressionResultStorageStack->getCurrent(); if ($storage !== null) { $result = $storage->findExpressionResult($node); if ($result !== null) { - return $result->getSpecifiedTypesForScope($this, $context); + return $result->getSpecifiedTypesForScope($scope, $context); } } // a synthetic node, or no analysis in progress $onDemandResult = $this->container->getByType(NodeScopeResolver::class)->processExprOnDemand( $node, - $this, + $scope, $storage !== null ? $storage->duplicate() : new ExpressionResultStorage(), ); - return $onDemandResult->getSpecifiedTypesForScope($this, $context); + return $onDemandResult->getSpecifiedTypesForScope($scope, $context); } /** diff --git a/src/Analyser/NodeScopeResolver.php b/src/Analyser/NodeScopeResolver.php index 862d7eb3aee..d6ca37cb42c 100644 --- a/src/Analyser/NodeScopeResolver.php +++ b/src/Analyser/NodeScopeResolver.php @@ -52,7 +52,6 @@ use PhpParser\NodeVisitorAbstract; use PHPStan\Analyser\ExprHandler\AssignHandler; use PHPStan\Analyser\ExprHandler\Helper\ImplicitToStringCallHelper; -use PHPStan\Analyser\Fiber\FiberScope; use PHPStan\BetterReflection\Reflection\Adapter\ReflectionClass; use PHPStan\BetterReflection\Reflection\ReflectionEnum; use PHPStan\BetterReflection\Reflector\Reflector; @@ -2819,15 +2818,6 @@ public function processExprNode( ExpressionContext $context, ): ExpressionResult { - if ($scope instanceof FiberScope) { - // the engine never processes on the rule-facing FiberScope - one can - // arrive here through a stored result's memoized truthy/falsey scope - // (first computed inside a rule fiber) consumed by a handler for a - // child's processing scope; its type asks would suspend outside - // a fiber - $scope = $scope->toMutatingScope(); - } - if ($this->returnStoredExpressionResults) { $storedResult = $storage->findExpressionResult($expr); if ($storedResult !== null) { From 5fdb4513f86ee5e4f89ccf575da05c8adee3b46d Mon Sep 17 00:00:00 2001 From: Ondrej Mirtes Date: Sat, 13 Jun 2026 13:26:34 +0200 Subject: [PATCH 023/113] Guard that only synthetic nodes reach the pending-fiber on-demand path processPendingFibers handles fibers suspended on a type ask whose node was never stored during natural traversal. By design those should only be synthetic nodes (built during analysis, no source position) - a real AST node left pending means a rule asked about its type but the producing handler never processed and stored it, so the ask falls back to on-demand processing (correct but wasteful, and on the asker's scope). The guard throws on a non-synthetic node here. It stays dormant by default and fires only with PHPSTAN_GUARD_NW=1, so it is a diagnostic for finding the remaining gaps (e.g. immediately-invoked closures), not a runtime check. Co-Authored-By: Claude Fable 5 --- src/Analyser/Fiber/FiberNodeScopeResolver.php | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/src/Analyser/Fiber/FiberNodeScopeResolver.php b/src/Analyser/Fiber/FiberNodeScopeResolver.php index e49935f0023..a47f6f2cf42 100644 --- a/src/Analyser/Fiber/FiberNodeScopeResolver.php +++ b/src/Analyser/Fiber/FiberNodeScopeResolver.php @@ -15,7 +15,10 @@ use PHPStan\ShouldNotHappenException; use function array_pop; use function count; +use function get_class; use function get_debug_type; +use function getenv; +use function sprintf; #[AutowiredService(as: FiberNodeScopeResolver::class)] final class FiberNodeScopeResolver extends NodeScopeResolver @@ -114,6 +117,19 @@ protected function processPendingFibers(ExpressionResultStorage $storage): void throw new ShouldNotHappenException('Pending fibers at the end should be about synthetic nodes'); } + // Only synthetic nodes (built during analysis, no source position) + // should reach the on-demand path here. A real AST node left pending + // means a rule asked about its type but it was never processed and + // stored during natural traversal - a gap to fix at the producing + // handler. Guard kept dormant; enable with PHPSTAN_GUARD_NW=1. + if (getenv('PHPSTAN_GUARD_NW') === '1' && $request->expr->getStartLine() !== -1) { + throw new ShouldNotHappenException(sprintf( + 'Pending fiber about non-synthetic node %s on line %d - it should have been processed and its result stored during natural traversal.', + get_class($request->expr), + $request->expr->getStartLine(), + )); + } + unset($storage->pendingFibers[$key]); $fiber = $pending['fiber']; From eb81e6325c4e07b7f2bce486c0e5d53841b99040 Mon Sep 17 00:00:00 2001 From: Ondrej Mirtes Date: Sat, 13 Jun 2026 20:03:07 +0200 Subject: [PATCH 024/113] Fix immediately-invoked-closure fiber flush; make the new-world guard accurate Two related changes to the PHPSTAN_GUARD_NW diagnostic and the gap it found: - Immediately-invoked closures: a fiber suspended on the IIFE FuncCall (a rule asked its type) was flushed by a nested statement-list boundary inside the IIFE's own closure body, before the IIFE result was stored - so it took the on-demand path. processExprNode now records the expression it is processing (processingExprIds) and processPendingFibers skips a fiber whose node is still being processed; it is resumed when that processExprNode stores the result. Measured slightly faster on NodeScopeResolverTest (less on-demand reprocessing). - The guard now tells a real AST node from a node built during analysis (rule-constructed comparisons, call_user_func/ArgumentsNormalizer rewrites) by membership in the parsed file (guardRealExprIds, collected once per file only when the flag is set), not by source line - synthetic nodes copy their origin's line, so the previous line check mis-flagged them as gaps. Rule-built synthetics legitimately resolve on demand. Off by default (zero cost). With it on, the only nsrt real-node gap was the IIFE, now fixed; the remaining src gaps (Closure/String_ in arg and key positions, comparison operands) are step-1 work, documented in NEW_WORLD.md. Co-Authored-By: Claude Fable 5 --- src/Analyser/Fiber/FiberNodeScopeResolver.php | 20 +++++-- src/Analyser/NodeScopeResolver.php | 58 +++++++++++++++++++ 2 files changed, 74 insertions(+), 4 deletions(-) diff --git a/src/Analyser/Fiber/FiberNodeScopeResolver.php b/src/Analyser/Fiber/FiberNodeScopeResolver.php index a47f6f2cf42..186de5fdcb7 100644 --- a/src/Analyser/Fiber/FiberNodeScopeResolver.php +++ b/src/Analyser/Fiber/FiberNodeScopeResolver.php @@ -18,6 +18,7 @@ use function get_class; use function get_debug_type; use function getenv; +use function spl_object_id; use function sprintf; #[AutowiredService(as: FiberNodeScopeResolver::class)] @@ -111,20 +112,31 @@ protected function processPendingFibers(ExpressionResultStorage $storage): void foreach ($storage->pendingFibers as $key => $pending) { $request = $pending['request']; + + // A fiber suspended on an expression that is still being processed + // must not be flushed here: this boundary is a nested statement list + // inside that very expression (e.g. an immediately-invoked closure's + // body). The fiber is resumed when the enclosing processExprNode + // stores the result. + if (isset($this->processingExprIds[spl_object_id($request->expr)])) { + continue; + } + $expressionResult = $storage->findExpressionResult($request->expr); if ($expressionResult !== null) { throw new ShouldNotHappenException('Pending fibers at the end should be about synthetic nodes'); } - // Only synthetic nodes (built during analysis, no source position) - // should reach the on-demand path here. A real AST node left pending + // Only nodes built during analysis (rules constructing synthetic + // comparisons, ArgumentsNormalizer rewrites, ...) should reach the + // on-demand path here. A node from the file's parsed AST left pending // means a rule asked about its type but it was never processed and // stored during natural traversal - a gap to fix at the producing // handler. Guard kept dormant; enable with PHPSTAN_GUARD_NW=1. - if (getenv('PHPSTAN_GUARD_NW') === '1' && $request->expr->getStartLine() !== -1) { + if (getenv('PHPSTAN_GUARD_NW') === '1' && isset($this->guardRealExprIds[spl_object_id($request->expr)])) { throw new ShouldNotHappenException(sprintf( - 'Pending fiber about non-synthetic node %s on line %d - it should have been processed and its result stored during natural traversal.', + 'Pending fiber about real AST node %s on line %d - it should have been processed and its result stored during natural traversal.', get_class($request->expr), $request->expr->getStartLine(), )); diff --git a/src/Analyser/NodeScopeResolver.php b/src/Analyser/NodeScopeResolver.php index d6ca37cb42c..33d48c54766 100644 --- a/src/Analyser/NodeScopeResolver.php +++ b/src/Analyser/NodeScopeResolver.php @@ -175,10 +175,12 @@ use function array_slice; use function array_values; use function count; +use function getenv; use function in_array; use function is_array; use function is_int; use function is_string; +use function spl_object_id; use function sprintf; use function strtolower; use function trim; @@ -212,6 +214,26 @@ class NodeScopeResolver */ protected bool $returnStoredExpressionResults = false; + /** + * spl_object_id => recursion depth of the expressions currently being + * processed by processExprNode. A fiber pending on one of them must not be + * flushed at a nested statement-list boundary inside that expression - it + * is resumed when the expression's own processing stores its result. + * + * @var array + */ + protected array $processingExprIds = []; + + /** + * 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 pending-fiber + * guard can tell a real AST node (a genuine gap) from a node a rule built + * during analysis (legitimately resolved on demand). + * + * @var array + */ + protected array $guardRealExprIds = []; + /** * @param string[][] $earlyTerminatingMethodCalls className(string) => methods(string[]) * @param array $earlyTerminatingFunctionCalls @@ -282,6 +304,13 @@ public function processNodes( callable $nodeCallback, ): void { + if (getenv('PHPSTAN_GUARD_NW') === '1') { + $this->guardRealExprIds = []; + foreach ((new NodeFinder())->findInstanceOf($nodes, Expr::class) as $realExpr) { + $this->guardRealExprIds[spl_object_id($realExpr)] = true; + } + } + $expressionResultStorage = new ExpressionResultStorage(); $scope->pushExpressionResultStorage($expressionResultStorage); try { @@ -2825,6 +2854,35 @@ public function processExprNode( } } + // Track that this expression is being processed. A fiber suspended on it + // (a rule asked its type before processing reached it) must not be + // flushed at a nested statement-list boundary inside this very + // expression - e.g. an immediately-invoked closure's body. It is resumed + // when this processExprNode stores the result below. + $exprId = spl_object_id($expr); + $this->processingExprIds[$exprId] = ($this->processingExprIds[$exprId] ?? 0) + 1; + + try { + return $this->processExprNodeInternal($stmt, $expr, $scope, $storage, $nodeCallback, $context); + } finally { + if (--$this->processingExprIds[$exprId] === 0) { + unset($this->processingExprIds[$exprId]); + } + } + } + + /** + * @param callable(Node $node, Scope $scope): void $nodeCallback + */ + private function processExprNodeInternal( + Node\Stmt $stmt, + Expr $expr, + MutatingScope $scope, + ExpressionResultStorage $storage, + callable $nodeCallback, + ExpressionContext $context, + ): ExpressionResult + { if ($expr instanceof Expr\CallLike && $expr->isFirstClassCallable()) { if ($expr instanceof FuncCall) { $newExpr = new FunctionCallableNode($expr->name, $expr); From eaeba8fe9960812b240fa673e7dc02e91def4d34 Mon Sep 17 00:00:00 2001 From: Ondrej Mirtes Date: Sat, 13 Jun 2026 20:44:36 +0200 Subject: [PATCH 025/113] Flush pending fibers only at scope boundaries; process dropped call args Fixes the real-AST-node gaps PHPSTAN_GUARD_NW reports across the test suite (closures, compact()/InClassMethodNode, named-argument errors). processStmtNodesInternal used to flush pending fibers at the end of every statement list, including nested control flow (if/else branches, loop and switch/try bodies). A rule invoked at a scope's entry - e.g. UnusedConstructorParametersRule on InClassMethodNode - asks the types of expressions appearing later in the body; flushing at an earlier branch resolved those fibers on the asker's scope before natural traversal stored the results. The flush now happens only when the statement list is a scope body (FunctionLike, ClassLike, Namespace_); nested lists defer to it, and fibers are resumed when their expression stores its result. This is the documented design ("flush only at unit boundaries"). processClosureNode tracks the closure in processingExprIds while its body is processed, so a scope-boundary flush triggered inside the body (a nested closure or anonymous class) does not flush the fiber pending on the enclosing closure itself. processDroppedArgs handles arguments dropped by ArgumentsNormalizer - duplicate, unknown-named or extra arguments in an invalid call. The parameters check still asks their types to report the error, so they are processed (NoopNodeCallback) and stored even though processArgs iterates only the normalized argument list. Co-Authored-By: Claude Fable 5 --- src/Analyser/ExprHandler/FuncCallHandler.php | 1 + .../ExprHandler/MethodCallHandler.php | 1 + src/Analyser/ExprHandler/NewHandler.php | 1 + .../ExprHandler/StaticCallHandler.php | 1 + src/Analyser/NodeScopeResolver.php | 83 ++++++++++++++++++- 5 files changed, 86 insertions(+), 1 deletion(-) diff --git a/src/Analyser/ExprHandler/FuncCallHandler.php b/src/Analyser/ExprHandler/FuncCallHandler.php index dbc496274ed..07b5198172b 100644 --- a/src/Analyser/ExprHandler/FuncCallHandler.php +++ b/src/Analyser/ExprHandler/FuncCallHandler.php @@ -278,6 +278,7 @@ public function processExpr(NodeScopeResolver $nodeScopeResolver, Stmt $stmt, Ex $scopeBeforeArgs = $scope; $argsResult = $nodeScopeResolver->processArgs($stmt, $functionReflection, null, $parametersAcceptor, $normalizedExpr, $scope, $storage, $nodeCallbackForArgs, $context); $scope = $argsResult->getScope(); + $nodeScopeResolver->processDroppedArgs($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/MethodCallHandler.php b/src/Analyser/ExprHandler/MethodCallHandler.php index 2fdc2afb877..cb45d5441c5 100644 --- a/src/Analyser/ExprHandler/MethodCallHandler.php +++ b/src/Analyser/ExprHandler/MethodCallHandler.php @@ -151,6 +151,7 @@ public function processExpr(NodeScopeResolver $nodeScopeResolver, Stmt $stmt, Ex $context, ); $scope = $argsResult->getScope(); + $nodeScopeResolver->processDroppedArgs($stmt, $expr, $normalizedExpr, $scope, $storage, $context); if ($methodReflection !== null) { $methodThrowPoint = $this->methodThrowPointHelper->getThrowPoint($methodReflection, $parametersAcceptor, $normalizedExpr, $scope, $context); diff --git a/src/Analyser/ExprHandler/NewHandler.php b/src/Analyser/ExprHandler/NewHandler.php index d96d297d4f6..f96552e523a 100644 --- a/src/Analyser/ExprHandler/NewHandler.php +++ b/src/Analyser/ExprHandler/NewHandler.php @@ -203,6 +203,7 @@ public function processExpr(NodeScopeResolver $nodeScopeResolver, Stmt $stmt, Ex $argsResult = $nodeScopeResolver->processArgs($stmt, $constructorReflection, null, $parametersAcceptor, $normalizedExpr, $scope, $storage, $nodeCallback, $context); $scope = $argsResult->getScope(); + $nodeScopeResolver->processDroppedArgs($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 e3a96facf05..833bc52a4ef 100644 --- a/src/Analyser/ExprHandler/StaticCallHandler.php +++ b/src/Analyser/ExprHandler/StaticCallHandler.php @@ -217,6 +217,7 @@ public function processExpr(NodeScopeResolver $nodeScopeResolver, Stmt $stmt, Ex } $argsResult = $nodeScopeResolver->processArgs($stmt, $methodReflection, null, $parametersAcceptor, $normalizedExpr, $scope, $storage, $nodeCallback, $context, $closureBindScope); $scope = $argsResult->getScope(); + $nodeScopeResolver->processDroppedArgs($stmt, $expr, $normalizedExpr, $scope, $storage, $context); $scopeFunction = $scope->getFunction(); if ($methodReflection !== null) { diff --git a/src/Analyser/NodeScopeResolver.php b/src/Analyser/NodeScopeResolver.php index 33d48c54766..50abb2eee64 100644 --- a/src/Analyser/NodeScopeResolver.php +++ b/src/Analyser/NodeScopeResolver.php @@ -585,7 +585,22 @@ private function processStmtNodesInternal( $nodeCallback, $context, ); - $this->processPendingFibers($storage); + // Flush pending fibers only at a scope boundary - a function/method body, + // a class/trait body, a namespace. Nested control-flow statement lists + // (if/else branches, loop and switch/try bodies) must NOT flush: a rule + // invoked at the scope's entry node (e.g. UnusedConstructorParametersRule + // on InClassMethodNode) asks the types of expressions appearing later in + // the body, and a flush at an earlier branch would resolve those fibers + // on the asker's scope before natural traversal stores the results. Such + // fibers are resumed when their expression stores its result, or at this + // scope boundary once the whole body is processed. + if ( + $parentNode instanceof Node\FunctionLike + || $parentNode instanceof Node\Stmt\ClassLike + || $parentNode instanceof Node\Stmt\Namespace_ + ) { + $this->processPendingFibers($storage); + } return $statementResult; } @@ -3066,6 +3081,37 @@ public function processClosureNode( ?Type $passedToType, ?Type $nativePassedToType = null, ): ProcessClosureResult + { + // Closures reached as call arguments are processed here directly rather + // than through processExprNode (which tracks the node), so track the + // closure too: the dependency/node callbacks fired for it ask its type + // and suspend a fiber that must not be flushed at a nested boundary + // inside the closure body before the caller stores the closure result. + $exprId = spl_object_id($expr); + $this->processingExprIds[$exprId] = ($this->processingExprIds[$exprId] ?? 0) + 1; + + try { + return $this->processClosureNodeInternal($stmt, $expr, $scope, $storage, $nodeCallback, $context, $passedToType, $nativePassedToType); + } finally { + if (--$this->processingExprIds[$exprId] === 0) { + unset($this->processingExprIds[$exprId]); + } + } + } + + /** + * @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); @@ -4008,6 +4054,41 @@ public function processArgs( return $this->expressionResultFactory->create($scope, $scope, $callLike, $hasYield, $isAlwaysTerminating, $throwPoints, $impurePoints); } + /** + * 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()); + } + } + /** * @param MethodReflection|FunctionReflection|null $calleeReflection */ From 92cc156779efc9a7e8a3013469b1be1e39f1b3b2 Mon Sep 17 00:00:00 2001 From: Ondrej Mirtes Date: Sat, 13 Jun 2026 20:51:23 +0200 Subject: [PATCH 026/113] Add PHPSTAN_GUARD_NW guard: no getType on a real node before processExprNode The engine, handlers and extensions must not ask MutatingScope::getType() about a node from the file's parsed AST before processExprNode has processed it. Such an ask resolves the type by re-processing the node out of order - the double-processing that makes a node analysed several times. Tracking is collected per file only when the diagnostic is enabled (cached as NodeScopeResolver::$guardNewWorld): $guardRealExprIds is every parsed-AST Expr, $guardProcessedExprIds is every Expr processExprNode has stored. MutatingScope::getType throws when a real, not-yet-processed node is asked. Rules ask through FiberScope (which suspends and is answered from the result), so this targets only the old-world MutatingScope asks. Off by default - one static bool read per getType when disabled, no measurable cost. The existing pending-fiber guard and the real-node set are reused, now static so MutatingScope can read them. Co-Authored-By: Claude Fable 5 --- src/Analyser/Fiber/FiberNodeScopeResolver.php | 3 +- src/Analyser/MutatingScope.php | 13 ++++++++ src/Analyser/NodeScopeResolver.php | 32 +++++++++++++++---- 3 files changed, 39 insertions(+), 9 deletions(-) diff --git a/src/Analyser/Fiber/FiberNodeScopeResolver.php b/src/Analyser/Fiber/FiberNodeScopeResolver.php index 186de5fdcb7..6e7d2cd8df8 100644 --- a/src/Analyser/Fiber/FiberNodeScopeResolver.php +++ b/src/Analyser/Fiber/FiberNodeScopeResolver.php @@ -17,7 +17,6 @@ use function count; use function get_class; use function get_debug_type; -use function getenv; use function spl_object_id; use function sprintf; @@ -134,7 +133,7 @@ protected function processPendingFibers(ExpressionResultStorage $storage): void // means a rule asked about its type but it was never processed and // stored during natural traversal - a gap to fix at the producing // handler. Guard kept dormant; enable with PHPSTAN_GUARD_NW=1. - if (getenv('PHPSTAN_GUARD_NW') === '1' && isset($this->guardRealExprIds[spl_object_id($request->expr)])) { + if (self::$guardNewWorld && isset(self::$guardRealExprIds[spl_object_id($request->expr)])) { throw new ShouldNotHappenException(sprintf( 'Pending fiber about real AST node %s on line %d - it should have been processed and its result stored during natural traversal.', get_class($request->expr), diff --git a/src/Analyser/MutatingScope.php b/src/Analyser/MutatingScope.php index 5253aa1ee36..ce5e995e4a0 100644 --- a/src/Analyser/MutatingScope.php +++ b/src/Analyser/MutatingScope.php @@ -126,6 +126,7 @@ use function is_string; use function ltrim; use function md5; +use function spl_object_id; use function sprintf; use function str_contains; use function str_starts_with; @@ -897,6 +898,18 @@ public function getAnonymousFunctionReturnType(): ?Type /** @api */ public function getType(Expr $node): Type { + if ( + NodeScopeResolver::$guardNewWorld + && isset(NodeScopeResolver::$guardRealExprIds[spl_object_id($node)]) + && !isset(NodeScopeResolver::$guardProcessedExprIds[spl_object_id($node)]) + ) { + throw new ShouldNotHappenException(sprintf( + 'getType() asked about non-synthetic %s on line %d before it was processed by processExprNode() - it should consume the node\'s ExpressionResult instead.', + get_class($node), + $node->getStartLine(), + )); + } + $key = $this->getNodeKey($node); if (!array_key_exists($key, $this->resolvedTypes)) { diff --git a/src/Analyser/NodeScopeResolver.php b/src/Analyser/NodeScopeResolver.php index 50abb2eee64..ec2472606d6 100644 --- a/src/Analyser/NodeScopeResolver.php +++ b/src/Analyser/NodeScopeResolver.php @@ -224,15 +224,27 @@ class NodeScopeResolver */ protected array $processingExprIds = []; + /** Whether the PHPSTAN_GUARD_NW diagnostic is enabled (cached from the env). */ + public static bool $guardNewWorld = 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 pending-fiber - * guard can tell a real AST node (a genuine gap) from a node a rule built - * during analysis (legitimately resolved on demand). + * only when the PHPSTAN_GUARD_NW diagnostic is enabled, so the guards can + * tell a real AST node from a node a rule built during analysis (which + * legitimately resolves on demand). Static so MutatingScope can read it. + * + * @var array + */ + public static array $guardRealExprIds = []; + + /** + * spl_object_id => true of every Expr already processed by processExprNode + * in the current file. Used by the MutatingScope::getType guard to detect a + * real AST node whose type is asked before it was processed. * * @var array */ - protected array $guardRealExprIds = []; + public static array $guardProcessedExprIds = []; /** * @param string[][] $earlyTerminatingMethodCalls className(string) => methods(string[]) @@ -282,6 +294,8 @@ public function __construct( } } $this->earlyTerminatingMethodNames = $earlyTerminatingMethodNames; + + self::$guardNewWorld = getenv('PHPSTAN_GUARD_NW') === '1'; } /** @@ -304,10 +318,11 @@ public function processNodes( callable $nodeCallback, ): void { - if (getenv('PHPSTAN_GUARD_NW') === '1') { - $this->guardRealExprIds = []; + if (self::$guardNewWorld) { + self::$guardRealExprIds = []; + self::$guardProcessedExprIds = []; foreach ((new NodeFinder())->findInstanceOf($nodes, Expr::class) as $realExpr) { - $this->guardRealExprIds[spl_object_id($realExpr)] = true; + self::$guardRealExprIds[spl_object_id($realExpr)] = true; } } @@ -410,6 +425,9 @@ private function processNodesWithStorage( public function storeExpressionResult(ExpressionResultStorage $storage, Expr $expr, ExpressionResult $expressionResult): void { + if (self::$guardNewWorld) { + self::$guardProcessedExprIds[spl_object_id($expr)] = true; + } // converted handlers (no TypeResolvingExprHandler) are answered from // stored results in both worlds - storing must not depend on fibers $storage->storeExpressionResult($expr, $expressionResult); From 875919868ff22b445266e4bbac6db453c4bffd9d Mon Sep 17 00:00:00 2001 From: Ondrej Mirtes Date: Tue, 16 Jun 2026 14:06:15 +0200 Subject: [PATCH 027/113] BitwiseNotHandler is no longer TypeResolvingExprHandler --- .../ExprHandler/BitwiseNotHandler.php | 30 +++++++++---------- 1 file changed, 14 insertions(+), 16 deletions(-) diff --git a/src/Analyser/ExprHandler/BitwiseNotHandler.php b/src/Analyser/ExprHandler/BitwiseNotHandler.php index 5efa2fdef6d..95564a87bf3 100644 --- a/src/Analyser/ExprHandler/BitwiseNotHandler.php +++ b/src/Analyser/ExprHandler/BitwiseNotHandler.php @@ -9,27 +9,27 @@ use PHPStan\Analyser\ExpressionResult; use PHPStan\Analyser\ExpressionResultFactory; use PHPStan\Analyser\ExpressionResultStorage; +use PHPStan\Analyser\ExprHandler; +use PHPStan\Analyser\ExprHandler\Helper\DefaultNarrowingHelper; use PHPStan\Analyser\MutatingScope; use PHPStan\Analyser\NodeScopeResolver; -use PHPStan\Analyser\Scope; -use PHPStan\Analyser\SpecifiedTypes; -use PHPStan\Analyser\TypeResolvingExprHandler; -use PHPStan\Analyser\TypeSpecifier; use PHPStan\Analyser\TypeSpecifierContext; use PHPStan\DependencyInjection\AutowiredService; use PHPStan\Reflection\InitializerExprTypeResolver; +use PHPStan\ShouldNotHappenException; use PHPStan\Type\Type; /** - * @implements TypeResolvingExprHandler + * @implements ExprHandler */ #[AutowiredService] -final class BitwiseNotHandler implements TypeResolvingExprHandler +final class BitwiseNotHandler implements ExprHandler { public function __construct( private InitializerExprTypeResolver $initializerExprTypeResolver, private ExpressionResultFactory $expressionResultFactory, + private DefaultNarrowingHelper $defaultNarrowingHelper, ) { } @@ -51,17 +51,15 @@ public function processExpr(NodeScopeResolver $nodeScopeResolver, Stmt $stmt, Ex isAlwaysTerminating: $exprResult->isAlwaysTerminating(), throwPoints: $exprResult->getThrowPoints(), impurePoints: $exprResult->getImpurePoints(), - ); - } + typeCallback: fn (MutatingScope $scope) => $this->initializerExprTypeResolver->getBitwiseNotType($expr->expr, static function (Expr $e) use ($scope, $expr, $exprResult): Type { + if ($e === $expr->expr) { + return $exprResult->getTypeForScope($scope); + } - public function resolveType(MutatingScope $scope, Expr $expr): Type - { - return $this->initializerExprTypeResolver->getBitwiseNotType($expr->expr, static fn (Expr $expr): Type => $scope->getType($expr)); - } - - public function specifyTypes(TypeSpecifier $typeSpecifier, Scope $scope, Expr $expr, TypeSpecifierContext $context): SpecifiedTypes - { - return $typeSpecifier->specifyDefaultTypes($scope, $expr, $context); + throw new ShouldNotHappenException(); + }), + specifyTypesCallback: fn (MutatingScope $s, TypeSpecifierContext $context) => $this->defaultNarrowingHelper->specifyDefaultTypes($expr, $context), + ); } } From 5b01972abdf9b47ba552bc4ad6c6e99046abfa74 Mon Sep 17 00:00:00 2001 From: Ondrej Mirtes Date: Tue, 16 Jun 2026 14:18:48 +0200 Subject: [PATCH 028/113] One more should-not --- src/Analyser/ExprHandler/ArrayHandler.php | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/src/Analyser/ExprHandler/ArrayHandler.php b/src/Analyser/ExprHandler/ArrayHandler.php index 501fd5074de..35bcd555286 100644 --- a/src/Analyser/ExprHandler/ArrayHandler.php +++ b/src/Analyser/ExprHandler/ArrayHandler.php @@ -19,6 +19,7 @@ use PHPStan\Node\LiteralArrayItem; use PHPStan\Node\LiteralArrayNode; use PHPStan\Reflection\InitializerExprTypeResolver; +use PHPStan\ShouldNotHappenException; use PHPStan\Type\CallableType; use PHPStan\Type\Type; use PHPStan\Type\TypeCombinator; @@ -98,9 +99,7 @@ public function processExpr(NodeScopeResolver $nodeScopeResolver, Stmt $stmt, Ex : $itemResults[$id]->getType(); } - // getArrayType only asks about item keys and values - guarded - // legacy bridge just in case - return $s->getType($inner); + throw new ShouldNotHappenException(); }); if ( From 3bfef7c7492ee779f61e129a81e620625e7ffec3 Mon Sep 17 00:00:00 2001 From: Ondrej Mirtes Date: Tue, 16 Jun 2026 14:19:41 +0200 Subject: [PATCH 029/113] UnaryMinusHandler is no longer TypeResolvingExprHandler Co-Authored-By: Claude Opus 4.8 (1M context) --- .../ExprHandler/UnaryMinusHandler.php | 31 +++++++++---------- 1 file changed, 15 insertions(+), 16 deletions(-) diff --git a/src/Analyser/ExprHandler/UnaryMinusHandler.php b/src/Analyser/ExprHandler/UnaryMinusHandler.php index ad84baeee02..b2826e29cb4 100644 --- a/src/Analyser/ExprHandler/UnaryMinusHandler.php +++ b/src/Analyser/ExprHandler/UnaryMinusHandler.php @@ -9,27 +9,26 @@ use PHPStan\Analyser\ExpressionResult; use PHPStan\Analyser\ExpressionResultFactory; use PHPStan\Analyser\ExpressionResultStorage; +use PHPStan\Analyser\ExprHandler; +use PHPStan\Analyser\ExprHandler\Helper\DefaultNarrowingHelper; use PHPStan\Analyser\MutatingScope; use PHPStan\Analyser\NodeScopeResolver; -use PHPStan\Analyser\Scope; -use PHPStan\Analyser\SpecifiedTypes; -use PHPStan\Analyser\TypeResolvingExprHandler; -use PHPStan\Analyser\TypeSpecifier; use PHPStan\Analyser\TypeSpecifierContext; use PHPStan\DependencyInjection\AutowiredService; use PHPStan\Reflection\InitializerExprTypeResolver; use PHPStan\Type\Type; /** - * @implements TypeResolvingExprHandler + * @implements ExprHandler */ #[AutowiredService] -final class UnaryMinusHandler implements TypeResolvingExprHandler +final class UnaryMinusHandler implements ExprHandler { public function __construct( private InitializerExprTypeResolver $initializerExprTypeResolver, private ExpressionResultFactory $expressionResultFactory, + private DefaultNarrowingHelper $defaultNarrowingHelper, ) { } @@ -51,17 +50,17 @@ public function processExpr(NodeScopeResolver $nodeScopeResolver, Stmt $stmt, Ex isAlwaysTerminating: $exprResult->isAlwaysTerminating(), throwPoints: $exprResult->getThrowPoints(), impurePoints: $exprResult->getImpurePoints(), - ); - } + typeCallback: fn (MutatingScope $scope) => $this->initializerExprTypeResolver->getUnaryMinusType($expr->expr, static function (Expr $e) use ($scope, $expr, $exprResult): Type { + if ($e === $expr->expr) { + return $exprResult->getTypeForScope($scope); + } - public function resolveType(MutatingScope $scope, Expr $expr): Type - { - return $this->initializerExprTypeResolver->getUnaryMinusType($expr->expr, static fn (Expr $expr): Type => $scope->getType($expr)); - } - - public function specifyTypes(TypeSpecifier $typeSpecifier, Scope $scope, Expr $expr, TypeSpecifierContext $context): SpecifiedTypes - { - return $typeSpecifier->specifyDefaultTypes($scope, $expr, $context); + // a synthetic node ($expr->expr * -1, derived for an IntegerRangeType + // operand) - not a child result, resolved on demand + return $scope->getType($e); + }), + specifyTypesCallback: fn (MutatingScope $s, TypeSpecifierContext $context) => $this->defaultNarrowingHelper->specifyDefaultTypes($expr, $context), + ); } } From a9fc38f25bf030297f4acb36f35e29faedaa06d3 Mon Sep 17 00:00:00 2001 From: Ondrej Mirtes Date: Tue, 16 Jun 2026 14:19:46 +0200 Subject: [PATCH 030/113] UnaryPlusHandler is no longer TypeResolvingExprHandler Co-Authored-By: Claude Opus 4.8 (1M context) --- src/Analyser/ExprHandler/UnaryPlusHandler.php | 30 +++++++++---------- 1 file changed, 14 insertions(+), 16 deletions(-) diff --git a/src/Analyser/ExprHandler/UnaryPlusHandler.php b/src/Analyser/ExprHandler/UnaryPlusHandler.php index c3f38936c05..994f1f2500b 100644 --- a/src/Analyser/ExprHandler/UnaryPlusHandler.php +++ b/src/Analyser/ExprHandler/UnaryPlusHandler.php @@ -9,27 +9,27 @@ use PHPStan\Analyser\ExpressionResult; use PHPStan\Analyser\ExpressionResultFactory; use PHPStan\Analyser\ExpressionResultStorage; +use PHPStan\Analyser\ExprHandler; +use PHPStan\Analyser\ExprHandler\Helper\DefaultNarrowingHelper; use PHPStan\Analyser\MutatingScope; use PHPStan\Analyser\NodeScopeResolver; -use PHPStan\Analyser\Scope; -use PHPStan\Analyser\SpecifiedTypes; -use PHPStan\Analyser\TypeResolvingExprHandler; -use PHPStan\Analyser\TypeSpecifier; use PHPStan\Analyser\TypeSpecifierContext; use PHPStan\DependencyInjection\AutowiredService; use PHPStan\Reflection\InitializerExprTypeResolver; +use PHPStan\ShouldNotHappenException; use PHPStan\Type\Type; /** - * @implements TypeResolvingExprHandler + * @implements ExprHandler */ #[AutowiredService] -final class UnaryPlusHandler implements TypeResolvingExprHandler +final class UnaryPlusHandler implements ExprHandler { public function __construct( private InitializerExprTypeResolver $initializerExprTypeResolver, private ExpressionResultFactory $expressionResultFactory, + private DefaultNarrowingHelper $defaultNarrowingHelper, ) { } @@ -51,17 +51,15 @@ public function processExpr(NodeScopeResolver $nodeScopeResolver, Stmt $stmt, Ex isAlwaysTerminating: $exprResult->isAlwaysTerminating(), throwPoints: $exprResult->getThrowPoints(), impurePoints: $exprResult->getImpurePoints(), - ); - } + typeCallback: fn (MutatingScope $scope) => $this->initializerExprTypeResolver->getUnaryPlusType($expr->expr, static function (Expr $e) use ($scope, $expr, $exprResult): Type { + if ($e === $expr->expr) { + return $exprResult->getTypeForScope($scope); + } - public function resolveType(MutatingScope $scope, Expr $expr): Type - { - return $this->initializerExprTypeResolver->getUnaryPlusType($expr->expr, static fn (Expr $expr): Type => $scope->getType($expr)); - } - - public function specifyTypes(TypeSpecifier $typeSpecifier, Scope $scope, Expr $expr, TypeSpecifierContext $context): SpecifiedTypes - { - return $typeSpecifier->specifyDefaultTypes($scope, $expr, $context); + throw new ShouldNotHappenException(); + }), + specifyTypesCallback: fn (MutatingScope $s, TypeSpecifierContext $context) => $this->defaultNarrowingHelper->specifyDefaultTypes($expr, $context), + ); } } From e2843b6a21dfe000455af3c70c657d2d7f38495b Mon Sep 17 00:00:00 2001 From: Ondrej Mirtes Date: Tue, 16 Jun 2026 14:24:31 +0200 Subject: [PATCH 031/113] ConstFetchHandler is no longer TypeResolvingExprHandler Co-Authored-By: Claude Opus 4.8 (1M context) --- .../ExprHandler/ConstFetchHandler.php | 87 +++++++++---------- 1 file changed, 40 insertions(+), 47 deletions(-) diff --git a/src/Analyser/ExprHandler/ConstFetchHandler.php b/src/Analyser/ExprHandler/ConstFetchHandler.php index 6f1518f163c..02965694035 100644 --- a/src/Analyser/ExprHandler/ConstFetchHandler.php +++ b/src/Analyser/ExprHandler/ConstFetchHandler.php @@ -11,12 +11,10 @@ use PHPStan\Analyser\ExpressionResult; use PHPStan\Analyser\ExpressionResultFactory; use PHPStan\Analyser\ExpressionResultStorage; +use PHPStan\Analyser\ExprHandler; +use PHPStan\Analyser\ExprHandler\Helper\DefaultNarrowingHelper; use PHPStan\Analyser\MutatingScope; use PHPStan\Analyser\NodeScopeResolver; -use PHPStan\Analyser\Scope; -use PHPStan\Analyser\SpecifiedTypes; -use PHPStan\Analyser\TypeResolvingExprHandler; -use PHPStan\Analyser\TypeSpecifier; use PHPStan\Analyser\TypeSpecifierContext; use PHPStan\DependencyInjection\AutowiredService; use PHPStan\Type\Constant\ConstantBooleanType; @@ -26,15 +24,16 @@ use function strtolower; /** - * @implements TypeResolvingExprHandler + * @implements ExprHandler */ #[AutowiredService] -final class ConstFetchHandler implements TypeResolvingExprHandler +final class ConstFetchHandler implements ExprHandler { public function __construct( private ConstantResolver $constantResolver, private ExpressionResultFactory $expressionResultFactory, + private DefaultNarrowingHelper $defaultNarrowingHelper, ) { } @@ -56,51 +55,45 @@ public function processExpr(NodeScopeResolver $nodeScopeResolver, Stmt $stmt, Ex isAlwaysTerminating: false, throwPoints: [], impurePoints: [], - ); - } - - public function resolveType(MutatingScope $scope, Expr $expr): Type - { - $constName = (string) $expr->name; - $loweredConstName = strtolower($constName); - if ($loweredConstName === 'true') { - return new ConstantBooleanType(true); - } elseif ($loweredConstName === 'false') { - return new ConstantBooleanType(false); - } elseif ($loweredConstName === 'null') { - return new NullType(); - } + typeCallback: function (MutatingScope $scope) use ($expr): Type { + $constName = (string) $expr->name; + $loweredConstName = strtolower($constName); + if ($loweredConstName === 'true') { + return new ConstantBooleanType(true); + } elseif ($loweredConstName === 'false') { + return new ConstantBooleanType(false); + } elseif ($loweredConstName === 'null') { + return new NullType(); + } - $namespacedName = null; - if (!$expr->name->isFullyQualified() && $scope->getNamespace() !== null) { - $namespacedName = new FullyQualified([$scope->getNamespace(), $expr->name->toString()]); - } - $globalName = new FullyQualified($expr->name->toString()); + $namespacedName = null; + if (!$expr->name->isFullyQualified() && $scope->getNamespace() !== null) { + $namespacedName = new FullyQualified([$scope->getNamespace(), $expr->name->toString()]); + } + $globalName = new FullyQualified($expr->name->toString()); - foreach ([$namespacedName, $globalName] as $name) { - if ($name === null) { - continue; - } - $constFetch = new ConstFetch($name); - if ($scope->hasExpressionType($constFetch)->yes()) { - return $this->constantResolver->resolveConstantType( - $name->toString(), - $scope->expressionTypes[$scope->getNodeKey($constFetch)]->getType(), - ); - } - } + foreach ([$namespacedName, $globalName] as $name) { + if ($name === null) { + continue; + } + $constFetch = new ConstFetch($name); + if ($scope->hasExpressionType($constFetch)->yes()) { + return $this->constantResolver->resolveConstantType( + $name->toString(), + $scope->expressionTypes[$scope->getNodeKey($constFetch)]->getType(), + ); + } + } - $constantType = $this->constantResolver->resolveConstant($expr->name, $scope); - if ($constantType !== null) { - return $constantType; - } + $constantType = $this->constantResolver->resolveConstant($expr->name, $scope); + if ($constantType !== null) { + return $constantType; + } - return new ErrorType(); - } - - public function specifyTypes(TypeSpecifier $typeSpecifier, Scope $scope, Expr $expr, TypeSpecifierContext $context): SpecifiedTypes - { - return $typeSpecifier->specifyDefaultTypes($scope, $expr, $context); + return new ErrorType(); + }, + specifyTypesCallback: fn (MutatingScope $s, TypeSpecifierContext $context) => $this->defaultNarrowingHelper->specifyDefaultTypes($expr, $context), + ); } } From 7838476031f36fa8cbae84ec6775489d6aea9a6d Mon Sep 17 00:00:00 2001 From: Ondrej Mirtes Date: Tue, 16 Jun 2026 14:27:32 +0200 Subject: [PATCH 032/113] PrintHandler is no longer TypeResolvingExprHandler Co-Authored-By: Claude Opus 4.8 (1M context) --- src/Analyser/ExprHandler/PrintHandler.php | 23 +++++++---------------- 1 file changed, 7 insertions(+), 16 deletions(-) diff --git a/src/Analyser/ExprHandler/PrintHandler.php b/src/Analyser/ExprHandler/PrintHandler.php index 2b21db38ec4..37172802e83 100644 --- a/src/Analyser/ExprHandler/PrintHandler.php +++ b/src/Analyser/ExprHandler/PrintHandler.php @@ -9,14 +9,12 @@ use PHPStan\Analyser\ExpressionResult; use PHPStan\Analyser\ExpressionResultFactory; use PHPStan\Analyser\ExpressionResultStorage; +use PHPStan\Analyser\ExprHandler; +use PHPStan\Analyser\ExprHandler\Helper\DefaultNarrowingHelper; use PHPStan\Analyser\ExprHandler\Helper\ImplicitToStringCallHelper; use PHPStan\Analyser\ImpurePoint; use PHPStan\Analyser\MutatingScope; use PHPStan\Analyser\NodeScopeResolver; -use PHPStan\Analyser\Scope; -use PHPStan\Analyser\SpecifiedTypes; -use PHPStan\Analyser\TypeResolvingExprHandler; -use PHPStan\Analyser\TypeSpecifier; use PHPStan\Analyser\TypeSpecifierContext; use PHPStan\DependencyInjection\AutowiredService; use PHPStan\Type\Constant\ConstantIntegerType; @@ -24,15 +22,16 @@ use function array_merge; /** - * @implements TypeResolvingExprHandler + * @implements ExprHandler */ #[AutowiredService] -final class PrintHandler implements TypeResolvingExprHandler +final class PrintHandler implements ExprHandler { public function __construct( private ImplicitToStringCallHelper $implicitToStringCallHelper, private ExpressionResultFactory $expressionResultFactory, + private DefaultNarrowingHelper $defaultNarrowingHelper, ) { } @@ -42,11 +41,6 @@ public function supports(Expr $expr): bool return $expr instanceof Print_; } - public function resolveType(MutatingScope $scope, Expr $expr): Type - { - return new ConstantIntegerType(1); - } - public function processExpr(NodeScopeResolver $nodeScopeResolver, Stmt $stmt, Expr $expr, MutatingScope $scope, ExpressionResultStorage $storage, callable $nodeCallback, ExpressionContext $context): ExpressionResult { $beforeScope = $scope; @@ -68,12 +62,9 @@ public function processExpr(NodeScopeResolver $nodeScopeResolver, Stmt $stmt, Ex isAlwaysTerminating: $exprResult->isAlwaysTerminating(), throwPoints: $throwPoints, impurePoints: array_merge($impurePoints, [new ImpurePoint($scope, $expr, 'print', 'print', true)]), + typeCallback: static fn (MutatingScope $scope): Type => new ConstantIntegerType(1), + specifyTypesCallback: fn (MutatingScope $s, TypeSpecifierContext $context) => $this->defaultNarrowingHelper->specifyDefaultTypes($expr, $context), ); } - public function specifyTypes(TypeSpecifier $typeSpecifier, Scope $scope, Expr $expr, TypeSpecifierContext $context): SpecifiedTypes - { - return $typeSpecifier->specifyDefaultTypes($scope, $expr, $context); - } - } From 23e7cdd6baf59eb6d596dc9018d393c6a73a1619 Mon Sep 17 00:00:00 2001 From: Ondrej Mirtes Date: Tue, 16 Jun 2026 14:27:32 +0200 Subject: [PATCH 033/113] ThrowHandler is no longer TypeResolvingExprHandler Co-Authored-By: Claude Opus 4.8 (1M context) --- src/Analyser/ExprHandler/ThrowHandler.php | 27 +++++++++-------------- 1 file changed, 10 insertions(+), 17 deletions(-) diff --git a/src/Analyser/ExprHandler/ThrowHandler.php b/src/Analyser/ExprHandler/ThrowHandler.php index 8231e9b6726..22bd0e1dc6e 100644 --- a/src/Analyser/ExprHandler/ThrowHandler.php +++ b/src/Analyser/ExprHandler/ThrowHandler.php @@ -9,13 +9,11 @@ use PHPStan\Analyser\ExpressionResult; use PHPStan\Analyser\ExpressionResultFactory; use PHPStan\Analyser\ExpressionResultStorage; +use PHPStan\Analyser\ExprHandler; +use PHPStan\Analyser\ExprHandler\Helper\DefaultNarrowingHelper; use PHPStan\Analyser\InternalThrowPoint; use PHPStan\Analyser\MutatingScope; use PHPStan\Analyser\NodeScopeResolver; -use PHPStan\Analyser\Scope; -use PHPStan\Analyser\SpecifiedTypes; -use PHPStan\Analyser\TypeResolvingExprHandler; -use PHPStan\Analyser\TypeSpecifier; use PHPStan\Analyser\TypeSpecifierContext; use PHPStan\DependencyInjection\AutowiredService; use PHPStan\Type\NonAcceptingNeverType; @@ -23,13 +21,16 @@ use function array_merge; /** - * @implements TypeResolvingExprHandler + * @implements ExprHandler */ #[AutowiredService] -final class ThrowHandler implements TypeResolvingExprHandler +final class ThrowHandler implements ExprHandler { - public function __construct(private ExpressionResultFactory $expressionResultFactory) + public function __construct( + private ExpressionResultFactory $expressionResultFactory, + private DefaultNarrowingHelper $defaultNarrowingHelper, + ) { } @@ -50,17 +51,9 @@ public function processExpr(NodeScopeResolver $nodeScopeResolver, Stmt $stmt, Ex isAlwaysTerminating: true, throwPoints: array_merge($exprResult->getThrowPoints(), [InternalThrowPoint::createExplicit($scope, $scope->getType($expr->expr), $expr, false)]), impurePoints: $exprResult->getImpurePoints(), + typeCallback: static fn (MutatingScope $scope): Type => new NonAcceptingNeverType(), + specifyTypesCallback: fn (MutatingScope $s, TypeSpecifierContext $context) => $this->defaultNarrowingHelper->specifyDefaultTypes($expr, $context), ); } - public function resolveType(MutatingScope $scope, Expr $expr): Type - { - return new NonAcceptingNeverType(); - } - - public function specifyTypes(TypeSpecifier $typeSpecifier, Scope $scope, Expr $expr, TypeSpecifierContext $context): SpecifiedTypes - { - return $typeSpecifier->specifyDefaultTypes($scope, $expr, $context); - } - } From 65b57ce65101568fe4c3417e132a5d551d13b1b1 Mon Sep 17 00:00:00 2001 From: Ondrej Mirtes Date: Tue, 16 Jun 2026 14:27:32 +0200 Subject: [PATCH 034/113] ExitHandler is no longer TypeResolvingExprHandler Co-Authored-By: Claude Opus 4.8 (1M context) --- src/Analyser/ExprHandler/ExitHandler.php | 27 +++++++++--------------- 1 file changed, 10 insertions(+), 17 deletions(-) diff --git a/src/Analyser/ExprHandler/ExitHandler.php b/src/Analyser/ExprHandler/ExitHandler.php index c459f38dc11..eff4504ddf3 100644 --- a/src/Analyser/ExprHandler/ExitHandler.php +++ b/src/Analyser/ExprHandler/ExitHandler.php @@ -9,13 +9,11 @@ use PHPStan\Analyser\ExpressionResult; use PHPStan\Analyser\ExpressionResultFactory; use PHPStan\Analyser\ExpressionResultStorage; +use PHPStan\Analyser\ExprHandler; +use PHPStan\Analyser\ExprHandler\Helper\DefaultNarrowingHelper; use PHPStan\Analyser\ImpurePoint; use PHPStan\Analyser\MutatingScope; use PHPStan\Analyser\NodeScopeResolver; -use PHPStan\Analyser\Scope; -use PHPStan\Analyser\SpecifiedTypes; -use PHPStan\Analyser\TypeResolvingExprHandler; -use PHPStan\Analyser\TypeSpecifier; use PHPStan\Analyser\TypeSpecifierContext; use PHPStan\DependencyInjection\AutowiredService; use PHPStan\Type\NonAcceptingNeverType; @@ -23,13 +21,16 @@ use function array_merge; /** - * @implements TypeResolvingExprHandler + * @implements ExprHandler */ #[AutowiredService] -final class ExitHandler implements TypeResolvingExprHandler +final class ExitHandler implements ExprHandler { - public function __construct(private ExpressionResultFactory $expressionResultFactory) + public function __construct( + private ExpressionResultFactory $expressionResultFactory, + private DefaultNarrowingHelper $defaultNarrowingHelper, + ) { } @@ -65,17 +66,9 @@ public function processExpr(NodeScopeResolver $nodeScopeResolver, Stmt $stmt, Ex isAlwaysTerminating: true, throwPoints: $throwPoints, impurePoints: $impurePoints, + typeCallback: static fn (MutatingScope $scope): Type => new NonAcceptingNeverType(), + specifyTypesCallback: fn (MutatingScope $s, TypeSpecifierContext $context) => $this->defaultNarrowingHelper->specifyDefaultTypes($expr, $context), ); } - public function resolveType(MutatingScope $scope, Expr $expr): Type - { - return new NonAcceptingNeverType(); - } - - public function specifyTypes(TypeSpecifier $typeSpecifier, Scope $scope, Expr $expr, TypeSpecifierContext $context): SpecifiedTypes - { - return $typeSpecifier->specifyDefaultTypes($scope, $expr, $context); - } - } From 21ebb5e9ef2a3c55a4507cee7c144f6907353125 Mon Sep 17 00:00:00 2001 From: Ondrej Mirtes Date: Tue, 16 Jun 2026 14:27:32 +0200 Subject: [PATCH 035/113] EvalHandler is no longer TypeResolvingExprHandler Co-Authored-By: Claude Opus 4.8 (1M context) --- src/Analyser/ExprHandler/EvalHandler.php | 27 +++++++++--------------- 1 file changed, 10 insertions(+), 17 deletions(-) diff --git a/src/Analyser/ExprHandler/EvalHandler.php b/src/Analyser/ExprHandler/EvalHandler.php index d4425781435..8d31ed0aee9 100644 --- a/src/Analyser/ExprHandler/EvalHandler.php +++ b/src/Analyser/ExprHandler/EvalHandler.php @@ -9,14 +9,12 @@ use PHPStan\Analyser\ExpressionResult; use PHPStan\Analyser\ExpressionResultFactory; use PHPStan\Analyser\ExpressionResultStorage; +use PHPStan\Analyser\ExprHandler; +use PHPStan\Analyser\ExprHandler\Helper\DefaultNarrowingHelper; use PHPStan\Analyser\ImpurePoint; use PHPStan\Analyser\InternalThrowPoint; use PHPStan\Analyser\MutatingScope; use PHPStan\Analyser\NodeScopeResolver; -use PHPStan\Analyser\Scope; -use PHPStan\Analyser\SpecifiedTypes; -use PHPStan\Analyser\TypeResolvingExprHandler; -use PHPStan\Analyser\TypeSpecifier; use PHPStan\Analyser\TypeSpecifierContext; use PHPStan\DependencyInjection\AutowiredService; use PHPStan\Type\MixedType; @@ -24,13 +22,16 @@ use function array_merge; /** - * @implements TypeResolvingExprHandler + * @implements ExprHandler */ #[AutowiredService] -final class EvalHandler implements TypeResolvingExprHandler +final class EvalHandler implements ExprHandler { - public function __construct(private ExpressionResultFactory $expressionResultFactory) + public function __construct( + private ExpressionResultFactory $expressionResultFactory, + private DefaultNarrowingHelper $defaultNarrowingHelper, + ) { } @@ -39,11 +40,6 @@ public function supports(Expr $expr): bool return $expr instanceof Eval_; } - public function resolveType(MutatingScope $scope, Expr $expr): Type - { - return new MixedType(); - } - public function processExpr(NodeScopeResolver $nodeScopeResolver, Stmt $stmt, Expr $expr, MutatingScope $scope, ExpressionResultStorage $storage, callable $nodeCallback, ExpressionContext $context): ExpressionResult { $beforeScope = $scope; @@ -58,12 +54,9 @@ public function processExpr(NodeScopeResolver $nodeScopeResolver, Stmt $stmt, Ex isAlwaysTerminating: $exprResult->isAlwaysTerminating(), throwPoints: array_merge($exprResult->getThrowPoints(), [InternalThrowPoint::createImplicit($scope, $expr)]), impurePoints: array_merge($exprResult->getImpurePoints(), [new ImpurePoint($scope, $expr, 'eval', 'eval', true)]), + typeCallback: static fn (MutatingScope $scope): Type => new MixedType(), + specifyTypesCallback: fn (MutatingScope $s, TypeSpecifierContext $context) => $this->defaultNarrowingHelper->specifyDefaultTypes($expr, $context), ); } - public function specifyTypes(TypeSpecifier $typeSpecifier, Scope $scope, Expr $expr, TypeSpecifierContext $context): SpecifiedTypes - { - return $typeSpecifier->specifyDefaultTypes($scope, $expr, $context); - } - } From 4f90ae7fd904283edc3d32dac7adec83534de93b Mon Sep 17 00:00:00 2001 From: Ondrej Mirtes Date: Tue, 16 Jun 2026 14:27:32 +0200 Subject: [PATCH 036/113] IncludeHandler is no longer TypeResolvingExprHandler Co-Authored-By: Claude Opus 4.8 (1M context) --- src/Analyser/ExprHandler/IncludeHandler.php | 27 ++++++++------------- 1 file changed, 10 insertions(+), 17 deletions(-) diff --git a/src/Analyser/ExprHandler/IncludeHandler.php b/src/Analyser/ExprHandler/IncludeHandler.php index 528d2f0bd1d..0b0f4173821 100644 --- a/src/Analyser/ExprHandler/IncludeHandler.php +++ b/src/Analyser/ExprHandler/IncludeHandler.php @@ -9,14 +9,12 @@ use PHPStan\Analyser\ExpressionResult; use PHPStan\Analyser\ExpressionResultFactory; use PHPStan\Analyser\ExpressionResultStorage; +use PHPStan\Analyser\ExprHandler; +use PHPStan\Analyser\ExprHandler\Helper\DefaultNarrowingHelper; use PHPStan\Analyser\ImpurePoint; use PHPStan\Analyser\InternalThrowPoint; use PHPStan\Analyser\MutatingScope; use PHPStan\Analyser\NodeScopeResolver; -use PHPStan\Analyser\Scope; -use PHPStan\Analyser\SpecifiedTypes; -use PHPStan\Analyser\TypeResolvingExprHandler; -use PHPStan\Analyser\TypeSpecifier; use PHPStan\Analyser\TypeSpecifierContext; use PHPStan\DependencyInjection\AutowiredService; use PHPStan\Type\MixedType; @@ -25,13 +23,16 @@ use function in_array; /** - * @implements TypeResolvingExprHandler + * @implements ExprHandler */ #[AutowiredService] -final class IncludeHandler implements TypeResolvingExprHandler +final class IncludeHandler implements ExprHandler { - public function __construct(private ExpressionResultFactory $expressionResultFactory) + public function __construct( + private ExpressionResultFactory $expressionResultFactory, + private DefaultNarrowingHelper $defaultNarrowingHelper, + ) { } @@ -40,11 +41,6 @@ public function supports(Expr $expr): bool return $expr instanceof Include_; } - public function resolveType(MutatingScope $scope, Expr $expr): Type - { - return new MixedType(); - } - public function processExpr(NodeScopeResolver $nodeScopeResolver, Stmt $stmt, Expr $expr, MutatingScope $scope, ExpressionResultStorage $storage, callable $nodeCallback, ExpressionContext $context): ExpressionResult { $beforeScope = $scope; @@ -60,12 +56,9 @@ public function processExpr(NodeScopeResolver $nodeScopeResolver, Stmt $stmt, Ex isAlwaysTerminating: $exprResult->isAlwaysTerminating(), throwPoints: array_merge($exprResult->getThrowPoints(), [InternalThrowPoint::createImplicit($scope, $expr)]), impurePoints: array_merge($exprResult->getImpurePoints(), [new ImpurePoint($scope, $expr, $identifier, $identifier, true)]), + typeCallback: static fn (MutatingScope $scope): Type => new MixedType(), + specifyTypesCallback: fn (MutatingScope $s, TypeSpecifierContext $context) => $this->defaultNarrowingHelper->specifyDefaultTypes($expr, $context), ); } - public function specifyTypes(TypeSpecifier $typeSpecifier, Scope $scope, Expr $expr, TypeSpecifierContext $context): SpecifiedTypes - { - return $typeSpecifier->specifyDefaultTypes($scope, $expr, $context); - } - } From 90e32ad33730630ca706392ea17cd65d215ebd9e Mon Sep 17 00:00:00 2001 From: Ondrej Mirtes Date: Tue, 16 Jun 2026 14:29:54 +0200 Subject: [PATCH 037/113] YieldFromHandler is no longer TypeResolvingExprHandler Co-Authored-By: Claude Opus 4.8 (1M context) --- src/Analyser/ExprHandler/YieldFromHandler.php | 41 ++++++++----------- 1 file changed, 18 insertions(+), 23 deletions(-) diff --git a/src/Analyser/ExprHandler/YieldFromHandler.php b/src/Analyser/ExprHandler/YieldFromHandler.php index ca086700866..1de51432932 100644 --- a/src/Analyser/ExprHandler/YieldFromHandler.php +++ b/src/Analyser/ExprHandler/YieldFromHandler.php @@ -10,14 +10,12 @@ use PHPStan\Analyser\ExpressionResult; use PHPStan\Analyser\ExpressionResultFactory; use PHPStan\Analyser\ExpressionResultStorage; +use PHPStan\Analyser\ExprHandler; +use PHPStan\Analyser\ExprHandler\Helper\DefaultNarrowingHelper; use PHPStan\Analyser\ImpurePoint; use PHPStan\Analyser\InternalThrowPoint; use PHPStan\Analyser\MutatingScope; use PHPStan\Analyser\NodeScopeResolver; -use PHPStan\Analyser\Scope; -use PHPStan\Analyser\SpecifiedTypes; -use PHPStan\Analyser\TypeResolvingExprHandler; -use PHPStan\Analyser\TypeSpecifier; use PHPStan\Analyser\TypeSpecifierContext; use PHPStan\DependencyInjection\AutowiredService; use PHPStan\Type\ErrorType; @@ -26,13 +24,16 @@ use function array_merge; /** - * @implements TypeResolvingExprHandler + * @implements ExprHandler */ #[AutowiredService] -final class YieldFromHandler implements TypeResolvingExprHandler +final class YieldFromHandler implements ExprHandler { - public function __construct(private ExpressionResultFactory $expressionResultFactory) + public function __construct( + private ExpressionResultFactory $expressionResultFactory, + private DefaultNarrowingHelper $defaultNarrowingHelper, + ) { } @@ -41,17 +42,6 @@ public function supports(Expr $expr): bool return $expr instanceof YieldFrom; } - public function resolveType(MutatingScope $scope, Expr $expr): Type - { - $yieldFromType = $scope->getType($expr->expr); - $generatorReturnType = $yieldFromType->getTemplateType(Generator::class, 'TReturn'); - if ($generatorReturnType instanceof ErrorType) { - return new MixedType(); - } - - return $generatorReturnType; - } - public function processExpr(NodeScopeResolver $nodeScopeResolver, Stmt $stmt, Expr $expr, MutatingScope $scope, ExpressionResultStorage $storage, callable $nodeCallback, ExpressionContext $context): ExpressionResult { $beforeScope = $scope; @@ -66,12 +56,17 @@ public function processExpr(NodeScopeResolver $nodeScopeResolver, Stmt $stmt, Ex isAlwaysTerminating: $exprResult->isAlwaysTerminating(), throwPoints: array_merge($exprResult->getThrowPoints(), [InternalThrowPoint::createImplicit($scope, $expr)]), impurePoints: array_merge($exprResult->getImpurePoints(), [new ImpurePoint($scope, $expr, 'yieldFrom', 'yield from', true)]), - ); - } + typeCallback: static function (MutatingScope $scope) use ($exprResult): Type { + $yieldFromType = $exprResult->getTypeForScope($scope); + $generatorReturnType = $yieldFromType->getTemplateType(Generator::class, 'TReturn'); + if ($generatorReturnType instanceof ErrorType) { + return new MixedType(); + } - public function specifyTypes(TypeSpecifier $typeSpecifier, Scope $scope, Expr $expr, TypeSpecifierContext $context): SpecifiedTypes - { - return $typeSpecifier->specifyDefaultTypes($scope, $expr, $context); + return $generatorReturnType; + }, + specifyTypesCallback: fn (MutatingScope $s, TypeSpecifierContext $context) => $this->defaultNarrowingHelper->specifyDefaultTypes($expr, $context), + ); } } From 1a9a69a7c3e35b103084e72c5fc466a55611fbed Mon Sep 17 00:00:00 2001 From: Ondrej Mirtes Date: Tue, 16 Jun 2026 14:29:54 +0200 Subject: [PATCH 038/113] ClassConstFetchHandler is no longer TypeResolvingExprHandler Co-Authored-By: Claude Opus 4.8 (1M context) --- .../ExprHandler/ClassConstFetchHandler.php | 48 +++++++++---------- 1 file changed, 23 insertions(+), 25 deletions(-) diff --git a/src/Analyser/ExprHandler/ClassConstFetchHandler.php b/src/Analyser/ExprHandler/ClassConstFetchHandler.php index c00160c7c50..b0f2a1d48fe 100644 --- a/src/Analyser/ExprHandler/ClassConstFetchHandler.php +++ b/src/Analyser/ExprHandler/ClassConstFetchHandler.php @@ -10,12 +10,10 @@ use PHPStan\Analyser\ExpressionResult; use PHPStan\Analyser\ExpressionResultFactory; use PHPStan\Analyser\ExpressionResultStorage; +use PHPStan\Analyser\ExprHandler; +use PHPStan\Analyser\ExprHandler\Helper\DefaultNarrowingHelper; use PHPStan\Analyser\MutatingScope; use PHPStan\Analyser\NodeScopeResolver; -use PHPStan\Analyser\Scope; -use PHPStan\Analyser\SpecifiedTypes; -use PHPStan\Analyser\TypeResolvingExprHandler; -use PHPStan\Analyser\TypeSpecifier; use PHPStan\Analyser\TypeSpecifierContext; use PHPStan\DependencyInjection\AutowiredService; use PHPStan\Reflection\InitializerExprTypeResolver; @@ -24,15 +22,16 @@ use function array_merge; /** - * @implements TypeResolvingExprHandler + * @implements ExprHandler */ #[AutowiredService] -final class ClassConstFetchHandler implements TypeResolvingExprHandler +final class ClassConstFetchHandler implements ExprHandler { public function __construct( private InitializerExprTypeResolver $initializerExprTypeResolver, private ExpressionResultFactory $expressionResultFactory, + private DefaultNarrowingHelper $defaultNarrowingHelper, ) { } @@ -42,20 +41,6 @@ public function supports(Expr $expr): bool return $expr instanceof ClassConstFetch; } - public function resolveType(MutatingScope $scope, Expr $expr): Type - { - if (!$expr->name instanceof Identifier) { - return new MixedType(); - } - - return $this->initializerExprTypeResolver->getClassConstFetchTypeByReflection( - $expr->class, - $expr->name->name, - $scope->isInClass() ? $scope->getClassReflection() : null, - static fn (Expr $e): Type => $scope->getType($e), - ); - } - public function processExpr(NodeScopeResolver $nodeScopeResolver, Stmt $stmt, Expr $expr, MutatingScope $scope, ExpressionResultStorage $storage, callable $nodeCallback, ExpressionContext $context): ExpressionResult { $beforeScope = $scope; @@ -64,6 +49,7 @@ public function processExpr(NodeScopeResolver $nodeScopeResolver, Stmt $stmt, Ex $impurePoints = []; $isAlwaysTerminating = false; + $classResult = null; if ($expr->class instanceof Expr) { $classResult = $nodeScopeResolver->processExprNode($stmt, $expr->class, $scope, $storage, $nodeCallback, $context->enterDeep()); $scope = $classResult->getScope(); @@ -94,12 +80,24 @@ public function processExpr(NodeScopeResolver $nodeScopeResolver, Stmt $stmt, Ex isAlwaysTerminating: $isAlwaysTerminating, throwPoints: $throwPoints, impurePoints: $impurePoints, - ); - } + typeCallback: function (MutatingScope $scope) use ($expr, $classResult): Type { + if (!$expr->name instanceof Identifier) { + return new MixedType(); + } - public function specifyTypes(TypeSpecifier $typeSpecifier, Scope $scope, Expr $expr, TypeSpecifierContext $context): SpecifiedTypes - { - return $typeSpecifier->specifyDefaultTypes($scope, $expr, $context); + return $this->initializerExprTypeResolver->getClassConstFetchTypeByReflection( + $expr->class, + $expr->name->name, + $scope->isInClass() ? $scope->getClassReflection() : null, + // getClassConstFetchTypeByReflection only invokes this for $expr->class + // when it is an Expr, which is exactly when $classResult exists + static fn (Expr $e): Type => $classResult !== null && $e === $expr->class + ? $classResult->getTypeForScope($scope) + : $scope->getType($e), + ); + }, + specifyTypesCallback: fn (MutatingScope $s, TypeSpecifierContext $context) => $this->defaultNarrowingHelper->specifyDefaultTypes($expr, $context), + ); } } From c2e08067e0757a7b90a87436cc42bf805a991161 Mon Sep 17 00:00:00 2001 From: Ondrej Mirtes Date: Tue, 16 Jun 2026 14:31:32 +0200 Subject: [PATCH 039/113] InterpolatedStringHandler is no longer TypeResolvingExprHandler Co-Authored-By: Claude Opus 4.8 (1M context) --- .../ExprHandler/InterpolatedStringHandler.php | 57 +++++++++---------- 1 file changed, 27 insertions(+), 30 deletions(-) diff --git a/src/Analyser/ExprHandler/InterpolatedStringHandler.php b/src/Analyser/ExprHandler/InterpolatedStringHandler.php index 75547f573bb..cf7975d713f 100644 --- a/src/Analyser/ExprHandler/InterpolatedStringHandler.php +++ b/src/Analyser/ExprHandler/InterpolatedStringHandler.php @@ -10,31 +10,31 @@ use PHPStan\Analyser\ExpressionResult; use PHPStan\Analyser\ExpressionResultFactory; use PHPStan\Analyser\ExpressionResultStorage; +use PHPStan\Analyser\ExprHandler; +use PHPStan\Analyser\ExprHandler\Helper\DefaultNarrowingHelper; use PHPStan\Analyser\ExprHandler\Helper\ImplicitToStringCallHelper; use PHPStan\Analyser\MutatingScope; use PHPStan\Analyser\NodeScopeResolver; -use PHPStan\Analyser\Scope; -use PHPStan\Analyser\SpecifiedTypes; -use PHPStan\Analyser\TypeResolvingExprHandler; -use PHPStan\Analyser\TypeSpecifier; use PHPStan\Analyser\TypeSpecifierContext; use PHPStan\DependencyInjection\AutowiredService; use PHPStan\Reflection\InitializerExprTypeResolver; use PHPStan\Type\Constant\ConstantStringType; use PHPStan\Type\Type; use function array_merge; +use function spl_object_id; /** - * @implements TypeResolvingExprHandler + * @implements ExprHandler */ #[AutowiredService] -final class InterpolatedStringHandler implements TypeResolvingExprHandler +final class InterpolatedStringHandler implements ExprHandler { public function __construct( private InitializerExprTypeResolver $initializerExprTypeResolver, private ImplicitToStringCallHelper $implicitToStringCallHelper, private ExpressionResultFactory $expressionResultFactory, + private DefaultNarrowingHelper $defaultNarrowingHelper, ) { } @@ -51,11 +51,14 @@ public function processExpr(NodeScopeResolver $nodeScopeResolver, Stmt $stmt, Ex $throwPoints = []; $impurePoints = []; $isAlwaysTerminating = false; + /** @var array $partResults */ + $partResults = []; foreach ($expr->parts as $part) { if (!$part instanceof Expr) { continue; } $partResult = $nodeScopeResolver->processExprNode($stmt, $part, $scope, $storage, $nodeCallback, $context->enterDeep()); + $partResults[spl_object_id($part)] = $partResult; $hasYield = $hasYield || $partResult->hasYield(); $throwPoints = array_merge($throwPoints, $partResult->getThrowPoints()); $impurePoints = array_merge($impurePoints, $partResult->getImpurePoints()); @@ -76,32 +79,26 @@ public function processExpr(NodeScopeResolver $nodeScopeResolver, Stmt $stmt, Ex isAlwaysTerminating: $isAlwaysTerminating, throwPoints: $throwPoints, impurePoints: $impurePoints, - ); - } + typeCallback: function (MutatingScope $scope) use ($expr, $partResults): Type { + $resultType = null; + foreach ($expr->parts as $part) { + if ($part instanceof InterpolatedStringPart) { + $partType = new ConstantStringType($part->value); + } else { + $partType = $partResults[spl_object_id($part)]->getTypeForScope($scope)->toString(); + } + if ($resultType === null) { + $resultType = $partType; + continue; + } - public function resolveType(MutatingScope $scope, Expr $expr): Type - { - $resultType = null; - foreach ($expr->parts as $part) { - if ($part instanceof InterpolatedStringPart) { - $partType = new ConstantStringType($part->value); - } else { - $partType = $scope->getType($part)->toString(); - } - if ($resultType === null) { - $resultType = $partType; - continue; - } + $resultType = $this->initializerExprTypeResolver->resolveConcatType($resultType, $partType); + } - $resultType = $this->initializerExprTypeResolver->resolveConcatType($resultType, $partType); - } - - return $resultType ?? new ConstantStringType(''); - } - - public function specifyTypes(TypeSpecifier $typeSpecifier, Scope $scope, Expr $expr, TypeSpecifierContext $context): SpecifiedTypes - { - return $typeSpecifier->specifyDefaultTypes($scope, $expr, $context); + return $resultType ?? new ConstantStringType(''); + }, + specifyTypesCallback: fn (MutatingScope $s, TypeSpecifierContext $context) => $this->defaultNarrowingHelper->specifyDefaultTypes($expr, $context), + ); } } From f28c52f20522d654f47dc09ba1731b3208fac4c3 Mon Sep 17 00:00:00 2001 From: Ondrej Mirtes Date: Tue, 16 Jun 2026 15:07:07 +0200 Subject: [PATCH 040/113] CloneHandler is no longer TypeResolvingExprHandler Co-Authored-By: Claude Opus 4.8 (1M context) --- src/Analyser/ExprHandler/CloneHandler.php | 31 ++++++++++------------- 1 file changed, 13 insertions(+), 18 deletions(-) diff --git a/src/Analyser/ExprHandler/CloneHandler.php b/src/Analyser/ExprHandler/CloneHandler.php index caddb7d0889..0492de78402 100644 --- a/src/Analyser/ExprHandler/CloneHandler.php +++ b/src/Analyser/ExprHandler/CloneHandler.php @@ -9,13 +9,11 @@ use PHPStan\Analyser\ExpressionResult; use PHPStan\Analyser\ExpressionResultFactory; use PHPStan\Analyser\ExpressionResultStorage; +use PHPStan\Analyser\ExprHandler; +use PHPStan\Analyser\ExprHandler\Helper\DefaultNarrowingHelper; use PHPStan\Analyser\MutatingScope; use PHPStan\Analyser\NodeScopeResolver; -use PHPStan\Analyser\Scope; -use PHPStan\Analyser\SpecifiedTypes; use PHPStan\Analyser\Traverser\CloneTypeTraverser; -use PHPStan\Analyser\TypeResolvingExprHandler; -use PHPStan\Analyser\TypeSpecifier; use PHPStan\Analyser\TypeSpecifierContext; use PHPStan\DependencyInjection\AutowiredService; use PHPStan\Type\ObjectWithoutClassType; @@ -24,13 +22,16 @@ use PHPStan\Type\TypeTraverser; /** - * @implements TypeResolvingExprHandler + * @implements ExprHandler */ #[AutowiredService] -final class CloneHandler implements TypeResolvingExprHandler +final class CloneHandler implements ExprHandler { - public function __construct(private ExpressionResultFactory $expressionResultFactory) + public function __construct( + private ExpressionResultFactory $expressionResultFactory, + private DefaultNarrowingHelper $defaultNarrowingHelper, + ) { } @@ -51,18 +52,12 @@ public function processExpr(NodeScopeResolver $nodeScopeResolver, Stmt $stmt, Ex isAlwaysTerminating: $exprResult->isAlwaysTerminating(), throwPoints: $exprResult->getThrowPoints(), impurePoints: $exprResult->getImpurePoints(), + typeCallback: static function (MutatingScope $scope) use ($exprResult): Type { + $cloneType = TypeCombinator::intersect($exprResult->getTypeForScope($scope), new ObjectWithoutClassType()); + return TypeTraverser::map($cloneType, new CloneTypeTraverser()); + }, + specifyTypesCallback: fn (MutatingScope $s, TypeSpecifierContext $context) => $this->defaultNarrowingHelper->specifyDefaultTypes($expr, $context), ); } - public function resolveType(MutatingScope $scope, Expr $expr): Type - { - $cloneType = TypeCombinator::intersect($scope->getType($expr->expr), new ObjectWithoutClassType()); - return TypeTraverser::map($cloneType, new CloneTypeTraverser()); - } - - public function specifyTypes(TypeSpecifier $typeSpecifier, Scope $scope, Expr $expr, TypeSpecifierContext $context): SpecifiedTypes - { - return $typeSpecifier->specifyDefaultTypes($scope, $expr, $context); - } - } From af23a79624d16b5193f030b3a65886089bd488a5 Mon Sep 17 00:00:00 2001 From: Ondrej Mirtes Date: Tue, 16 Jun 2026 15:07:07 +0200 Subject: [PATCH 041/113] YieldHandler is no longer TypeResolvingExprHandler Co-Authored-By: Claude Opus 4.8 (1M context) --- src/Analyser/ExprHandler/YieldHandler.php | 51 ++++++++++------------- 1 file changed, 23 insertions(+), 28 deletions(-) diff --git a/src/Analyser/ExprHandler/YieldHandler.php b/src/Analyser/ExprHandler/YieldHandler.php index dd8a5478fc0..6ec1d13fc57 100644 --- a/src/Analyser/ExprHandler/YieldHandler.php +++ b/src/Analyser/ExprHandler/YieldHandler.php @@ -10,14 +10,12 @@ use PHPStan\Analyser\ExpressionResult; use PHPStan\Analyser\ExpressionResultFactory; use PHPStan\Analyser\ExpressionResultStorage; +use PHPStan\Analyser\ExprHandler; +use PHPStan\Analyser\ExprHandler\Helper\DefaultNarrowingHelper; use PHPStan\Analyser\ImpurePoint; use PHPStan\Analyser\InternalThrowPoint; use PHPStan\Analyser\MutatingScope; use PHPStan\Analyser\NodeScopeResolver; -use PHPStan\Analyser\Scope; -use PHPStan\Analyser\SpecifiedTypes; -use PHPStan\Analyser\TypeResolvingExprHandler; -use PHPStan\Analyser\TypeSpecifier; use PHPStan\Analyser\TypeSpecifierContext; use PHPStan\DependencyInjection\AutowiredService; use PHPStan\Type\ErrorType; @@ -26,13 +24,16 @@ use function array_merge; /** - * @implements TypeResolvingExprHandler + * @implements ExprHandler */ #[AutowiredService] -final class YieldHandler implements TypeResolvingExprHandler +final class YieldHandler implements ExprHandler { - public function __construct(private ExpressionResultFactory $expressionResultFactory) + public function __construct( + private ExpressionResultFactory $expressionResultFactory, + private DefaultNarrowingHelper $defaultNarrowingHelper, + ) { } @@ -41,22 +42,6 @@ public function supports(Expr $expr): bool return $expr instanceof Yield_; } - public function resolveType(MutatingScope $scope, Expr $expr): Type - { - $functionReflection = $scope->getFunction(); - if ($functionReflection === null) { - return new MixedType(); - } - - $returnType = $functionReflection->getReturnType(); - $generatorSendType = $returnType->getTemplateType(Generator::class, 'TSend'); - if ($generatorSendType instanceof ErrorType) { - return new MixedType(); - } - - return $generatorSendType; - } - public function processExpr(NodeScopeResolver $nodeScopeResolver, Stmt $stmt, Expr $expr, MutatingScope $scope, ExpressionResultStorage $storage, callable $nodeCallback, ExpressionContext $context): ExpressionResult { $beforeScope = $scope; @@ -96,12 +81,22 @@ public function processExpr(NodeScopeResolver $nodeScopeResolver, Stmt $stmt, Ex isAlwaysTerminating: $isAlwaysTerminating, throwPoints: $throwPoints, impurePoints: $impurePoints, - ); - } + typeCallback: static function (MutatingScope $scope): Type { + $functionReflection = $scope->getFunction(); + if ($functionReflection === null) { + return new MixedType(); + } - public function specifyTypes(TypeSpecifier $typeSpecifier, Scope $scope, Expr $expr, TypeSpecifierContext $context): SpecifiedTypes - { - return $typeSpecifier->specifyDefaultTypes($scope, $expr, $context); + $returnType = $functionReflection->getReturnType(); + $generatorSendType = $returnType->getTemplateType(Generator::class, 'TSend'); + if ($generatorSendType instanceof ErrorType) { + return new MixedType(); + } + + return $generatorSendType; + }, + specifyTypesCallback: fn (MutatingScope $s, TypeSpecifierContext $context) => $this->defaultNarrowingHelper->specifyDefaultTypes($expr, $context), + ); } } From 0c78464c45dce4b0130a3cb5f9dc51ea2319a4a3 Mon Sep 17 00:00:00 2001 From: Ondrej Mirtes Date: Tue, 16 Jun 2026 15:07:07 +0200 Subject: [PATCH 042/113] AlwaysRememberedExprHandler is no longer TypeResolvingExprHandler Co-Authored-By: Claude Opus 4.8 (1M context) --- .../Virtual/AlwaysRememberedExprHandler.php | 27 +++++++------------ 1 file changed, 10 insertions(+), 17 deletions(-) diff --git a/src/Analyser/ExprHandler/Virtual/AlwaysRememberedExprHandler.php b/src/Analyser/ExprHandler/Virtual/AlwaysRememberedExprHandler.php index d4700f6e048..350584cd091 100644 --- a/src/Analyser/ExprHandler/Virtual/AlwaysRememberedExprHandler.php +++ b/src/Analyser/ExprHandler/Virtual/AlwaysRememberedExprHandler.php @@ -8,25 +8,26 @@ use PHPStan\Analyser\ExpressionResult; use PHPStan\Analyser\ExpressionResultFactory; use PHPStan\Analyser\ExpressionResultStorage; +use PHPStan\Analyser\ExprHandler; +use PHPStan\Analyser\ExprHandler\Helper\DefaultNarrowingHelper; use PHPStan\Analyser\MutatingScope; use PHPStan\Analyser\NodeScopeResolver; -use PHPStan\Analyser\Scope; -use PHPStan\Analyser\SpecifiedTypes; -use PHPStan\Analyser\TypeResolvingExprHandler; -use PHPStan\Analyser\TypeSpecifier; use PHPStan\Analyser\TypeSpecifierContext; use PHPStan\DependencyInjection\AutowiredService; use PHPStan\Node\Expr\AlwaysRememberedExpr; use PHPStan\Type\Type; /** - * @implements TypeResolvingExprHandler + * @implements ExprHandler */ #[AutowiredService] -final class AlwaysRememberedExprHandler implements TypeResolvingExprHandler +final class AlwaysRememberedExprHandler implements ExprHandler { - public function __construct(private ExpressionResultFactory $expressionResultFactory) + public function __construct( + private ExpressionResultFactory $expressionResultFactory, + private DefaultNarrowingHelper $defaultNarrowingHelper, + ) { } @@ -58,17 +59,9 @@ public function processExpr( isAlwaysTerminating: $innerResult->isAlwaysTerminating(), throwPoints: $innerResult->getThrowPoints(), impurePoints: $innerResult->getImpurePoints(), + typeCallback: static fn (MutatingScope $scope): Type => $scope->nativeTypesPromoted ? $expr->getNativeExprType() : $expr->getExprType(), + specifyTypesCallback: fn (MutatingScope $s, TypeSpecifierContext $context) => $this->defaultNarrowingHelper->specifyDefaultTypes($expr, $context), ); } - public function resolveType(MutatingScope $scope, Expr $expr): Type - { - return $scope->nativeTypesPromoted ? $expr->getNativeExprType() : $expr->getExprType(); - } - - public function specifyTypes(TypeSpecifier $typeSpecifier, Scope $scope, Expr $expr, TypeSpecifierContext $context): SpecifiedTypes - { - return $typeSpecifier->specifyDefaultTypes($scope, $expr, $context); - } - } From 225cc2936b2e30e0542e029e6f5df3d1e6b4f009 Mon Sep 17 00:00:00 2001 From: Ondrej Mirtes Date: Tue, 16 Jun 2026 15:15:39 +0200 Subject: [PATCH 043/113] NativeTypeExprHandler is no longer TypeResolvingExprHandler Co-Authored-By: Claude Opus 4.8 (1M context) --- .../Virtual/NativeTypeExprHandler.php | 30 +++++++------------ 1 file changed, 10 insertions(+), 20 deletions(-) diff --git a/src/Analyser/ExprHandler/Virtual/NativeTypeExprHandler.php b/src/Analyser/ExprHandler/Virtual/NativeTypeExprHandler.php index 08715ad31cc..6ef2fa033f6 100644 --- a/src/Analyser/ExprHandler/Virtual/NativeTypeExprHandler.php +++ b/src/Analyser/ExprHandler/Virtual/NativeTypeExprHandler.php @@ -8,25 +8,26 @@ use PHPStan\Analyser\ExpressionResult; use PHPStan\Analyser\ExpressionResultFactory; use PHPStan\Analyser\ExpressionResultStorage; +use PHPStan\Analyser\ExprHandler; +use PHPStan\Analyser\ExprHandler\Helper\DefaultNarrowingHelper; use PHPStan\Analyser\MutatingScope; use PHPStan\Analyser\NodeScopeResolver; -use PHPStan\Analyser\Scope; -use PHPStan\Analyser\SpecifiedTypes; -use PHPStan\Analyser\TypeResolvingExprHandler; -use PHPStan\Analyser\TypeSpecifier; use PHPStan\Analyser\TypeSpecifierContext; use PHPStan\DependencyInjection\AutowiredService; use PHPStan\Node\Expr\NativeTypeExpr; use PHPStan\Type\Type; /** - * @implements TypeResolvingExprHandler + * @implements ExprHandler */ #[AutowiredService] -final class NativeTypeExprHandler implements TypeResolvingExprHandler +final class NativeTypeExprHandler implements ExprHandler { - public function __construct(private ExpressionResultFactory $expressionResultFactory) + public function __construct( + private ExpressionResultFactory $expressionResultFactory, + private DefaultNarrowingHelper $defaultNarrowingHelper, + ) { } @@ -48,20 +49,9 @@ public function processExpr(NodeScopeResolver $nodeScopeResolver, Stmt $stmt, Ex isAlwaysTerminating: false, throwPoints: [], impurePoints: [], + typeCallback: static fn (MutatingScope $scope): Type => $scope->nativeTypesPromoted ? $expr->getNativeType() : $expr->getPhpDocType(), + specifyTypesCallback: fn (MutatingScope $s, TypeSpecifierContext $context) => $this->defaultNarrowingHelper->specifyDefaultTypes($expr, $context), ); } - public function resolveType(MutatingScope $scope, Expr $expr): Type - { - if ($scope->nativeTypesPromoted) { - return $expr->getNativeType(); - } - return $expr->getPhpDocType(); - } - - public function specifyTypes(TypeSpecifier $typeSpecifier, Scope $scope, Expr $expr, TypeSpecifierContext $context): SpecifiedTypes - { - return $typeSpecifier->specifyDefaultTypes($scope, $expr, $context); - } - } From 0e4cb92d10d653c36690cc392663987d18e9686d Mon Sep 17 00:00:00 2001 From: Ondrej Mirtes Date: Tue, 16 Jun 2026 15:18:50 +0200 Subject: [PATCH 044/113] FunctionCallableNodeHandler is no longer TypeResolvingExprHandler Co-Authored-By: Claude Opus 4.8 (1M context) --- .../Virtual/FunctionCallableNodeHandler.php | 31 +++++++------------ 1 file changed, 12 insertions(+), 19 deletions(-) diff --git a/src/Analyser/ExprHandler/Virtual/FunctionCallableNodeHandler.php b/src/Analyser/ExprHandler/Virtual/FunctionCallableNodeHandler.php index b27515f40fd..ae01f6be2b0 100644 --- a/src/Analyser/ExprHandler/Virtual/FunctionCallableNodeHandler.php +++ b/src/Analyser/ExprHandler/Virtual/FunctionCallableNodeHandler.php @@ -8,12 +8,10 @@ use PHPStan\Analyser\ExpressionResult; use PHPStan\Analyser\ExpressionResultFactory; use PHPStan\Analyser\ExpressionResultStorage; +use PHPStan\Analyser\ExprHandler; +use PHPStan\Analyser\ExprHandler\Helper\DefaultNarrowingHelper; use PHPStan\Analyser\MutatingScope; use PHPStan\Analyser\NodeScopeResolver; -use PHPStan\Analyser\Scope; -use PHPStan\Analyser\SpecifiedTypes; -use PHPStan\Analyser\TypeResolvingExprHandler; -use PHPStan\Analyser\TypeSpecifier; use PHPStan\Analyser\TypeSpecifierContext; use PHPStan\DependencyInjection\AutowiredService; use PHPStan\Node\FunctionCallableNode; @@ -21,13 +19,16 @@ use PHPStan\Type\Type; /** - * @implements TypeResolvingExprHandler + * @implements ExprHandler */ #[AutowiredService] -final class FunctionCallableNodeHandler implements TypeResolvingExprHandler +final class FunctionCallableNodeHandler implements ExprHandler { - public function __construct(private ExpressionResultFactory $expressionResultFactory) + public function __construct( + private ExpressionResultFactory $expressionResultFactory, + private DefaultNarrowingHelper $defaultNarrowingHelper, + ) { } @@ -60,19 +61,11 @@ public function processExpr(NodeScopeResolver $nodeScopeResolver, Stmt $stmt, Ex isAlwaysTerminating: $isAlwaysTerminating, throwPoints: $throwPoints, impurePoints: $impurePoints, + // in practice the type of the first-class callable is resolved + // by FirstClassCallableFuncCallHandler + typeCallback: static fn (MutatingScope $scope): Type => new MixedType(), + specifyTypesCallback: fn (MutatingScope $s, TypeSpecifierContext $context) => $this->defaultNarrowingHelper->specifyDefaultTypes($expr, $context), ); } - public function resolveType(MutatingScope $scope, Expr $expr): Type - { - // in practice the type of the first-class callable is resolved - // by FirstClassCallableFuncCallHandler - return new MixedType(); - } - - public function specifyTypes(TypeSpecifier $typeSpecifier, Scope $scope, Expr $expr, TypeSpecifierContext $context): SpecifiedTypes - { - return $typeSpecifier->specifyDefaultTypes($scope, $expr, $context); - } - } From 97f60ed54a8925a2e05611fab4ceb72bb146005d Mon Sep 17 00:00:00 2001 From: Ondrej Mirtes Date: Tue, 16 Jun 2026 15:18:50 +0200 Subject: [PATCH 045/113] MethodCallableNodeHandler is no longer TypeResolvingExprHandler Co-Authored-By: Claude Opus 4.8 (1M context) --- .../Virtual/MethodCallableNodeHandler.php | 31 +++++++------------ 1 file changed, 12 insertions(+), 19 deletions(-) diff --git a/src/Analyser/ExprHandler/Virtual/MethodCallableNodeHandler.php b/src/Analyser/ExprHandler/Virtual/MethodCallableNodeHandler.php index 25ca81bbcd0..15aa475bbc9 100644 --- a/src/Analyser/ExprHandler/Virtual/MethodCallableNodeHandler.php +++ b/src/Analyser/ExprHandler/Virtual/MethodCallableNodeHandler.php @@ -8,12 +8,10 @@ use PHPStan\Analyser\ExpressionResult; use PHPStan\Analyser\ExpressionResultFactory; use PHPStan\Analyser\ExpressionResultStorage; +use PHPStan\Analyser\ExprHandler; +use PHPStan\Analyser\ExprHandler\Helper\DefaultNarrowingHelper; use PHPStan\Analyser\MutatingScope; use PHPStan\Analyser\NodeScopeResolver; -use PHPStan\Analyser\Scope; -use PHPStan\Analyser\SpecifiedTypes; -use PHPStan\Analyser\TypeResolvingExprHandler; -use PHPStan\Analyser\TypeSpecifier; use PHPStan\Analyser\TypeSpecifierContext; use PHPStan\DependencyInjection\AutowiredService; use PHPStan\Node\MethodCallableNode; @@ -22,13 +20,16 @@ use function array_merge; /** - * @implements TypeResolvingExprHandler + * @implements ExprHandler */ #[AutowiredService] -final class MethodCallableNodeHandler implements TypeResolvingExprHandler +final class MethodCallableNodeHandler implements ExprHandler { - public function __construct(private ExpressionResultFactory $expressionResultFactory) + public function __construct( + private ExpressionResultFactory $expressionResultFactory, + private DefaultNarrowingHelper $defaultNarrowingHelper, + ) { } @@ -63,19 +64,11 @@ public function processExpr(NodeScopeResolver $nodeScopeResolver, Stmt $stmt, Ex isAlwaysTerminating: $isAlwaysTerminating, throwPoints: $throwPoints, impurePoints: $impurePoints, + // in practice the type of the first-class callable is resolved + // by FirstClassCallableMethodCallHandler + typeCallback: static fn (MutatingScope $scope): Type => new MixedType(), + specifyTypesCallback: fn (MutatingScope $s, TypeSpecifierContext $context) => $this->defaultNarrowingHelper->specifyDefaultTypes($expr, $context), ); } - public function resolveType(MutatingScope $scope, Expr $expr): Type - { - // in practice the type of the first-class callable is resolved - // by FirstClassCallableMethodCallHandler - return new MixedType(); - } - - public function specifyTypes(TypeSpecifier $typeSpecifier, Scope $scope, Expr $expr, TypeSpecifierContext $context): SpecifiedTypes - { - return $typeSpecifier->specifyDefaultTypes($scope, $expr, $context); - } - } From acf2185e1f693484608df3c7c0115b8875f817d1 Mon Sep 17 00:00:00 2001 From: Ondrej Mirtes Date: Tue, 16 Jun 2026 15:18:50 +0200 Subject: [PATCH 046/113] StaticMethodCallableNodeHandler is no longer TypeResolvingExprHandler Co-Authored-By: Claude Opus 4.8 (1M context) --- .../StaticMethodCallableNodeHandler.php | 31 +++++++------------ 1 file changed, 12 insertions(+), 19 deletions(-) diff --git a/src/Analyser/ExprHandler/Virtual/StaticMethodCallableNodeHandler.php b/src/Analyser/ExprHandler/Virtual/StaticMethodCallableNodeHandler.php index 8f9570ee5ec..59a5a808985 100644 --- a/src/Analyser/ExprHandler/Virtual/StaticMethodCallableNodeHandler.php +++ b/src/Analyser/ExprHandler/Virtual/StaticMethodCallableNodeHandler.php @@ -8,12 +8,10 @@ use PHPStan\Analyser\ExpressionResult; use PHPStan\Analyser\ExpressionResultFactory; use PHPStan\Analyser\ExpressionResultStorage; +use PHPStan\Analyser\ExprHandler; +use PHPStan\Analyser\ExprHandler\Helper\DefaultNarrowingHelper; use PHPStan\Analyser\MutatingScope; use PHPStan\Analyser\NodeScopeResolver; -use PHPStan\Analyser\Scope; -use PHPStan\Analyser\SpecifiedTypes; -use PHPStan\Analyser\TypeResolvingExprHandler; -use PHPStan\Analyser\TypeSpecifier; use PHPStan\Analyser\TypeSpecifierContext; use PHPStan\DependencyInjection\AutowiredService; use PHPStan\Node\StaticMethodCallableNode; @@ -22,13 +20,16 @@ use function array_merge; /** - * @implements TypeResolvingExprHandler + * @implements ExprHandler */ #[AutowiredService] -final class StaticMethodCallableNodeHandler implements TypeResolvingExprHandler +final class StaticMethodCallableNodeHandler implements ExprHandler { - public function __construct(private ExpressionResultFactory $expressionResultFactory) + public function __construct( + private ExpressionResultFactory $expressionResultFactory, + private DefaultNarrowingHelper $defaultNarrowingHelper, + ) { } @@ -69,19 +70,11 @@ public function processExpr(NodeScopeResolver $nodeScopeResolver, Stmt $stmt, Ex isAlwaysTerminating: $isAlwaysTerminating, throwPoints: $throwPoints, impurePoints: $impurePoints, + // in practice the type of the first-class callable is resolved + // by FirstClassCallableStaticCallHandler + typeCallback: static fn (MutatingScope $scope): Type => new MixedType(), + specifyTypesCallback: fn (MutatingScope $s, TypeSpecifierContext $context) => $this->defaultNarrowingHelper->specifyDefaultTypes($expr, $context), ); } - public function resolveType(MutatingScope $scope, Expr $expr): Type - { - // in practice the type of the first-class callable is resolved - // by FirstClassCallableStaticCallHandler - return new MixedType(); - } - - public function specifyTypes(TypeSpecifier $typeSpecifier, Scope $scope, Expr $expr, TypeSpecifierContext $context): SpecifiedTypes - { - return $typeSpecifier->specifyDefaultTypes($scope, $expr, $context); - } - } From 9bb2c015540993b9e866e02fec1bfa387c229a9c Mon Sep 17 00:00:00 2001 From: Ondrej Mirtes Date: Tue, 16 Jun 2026 15:18:50 +0200 Subject: [PATCH 047/113] InstantiationCallableNodeHandler is no longer TypeResolvingExprHandler Co-Authored-By: Claude Opus 4.8 (1M context) --- .../InstantiationCallableNodeHandler.php | 31 +++++++------------ 1 file changed, 12 insertions(+), 19 deletions(-) diff --git a/src/Analyser/ExprHandler/Virtual/InstantiationCallableNodeHandler.php b/src/Analyser/ExprHandler/Virtual/InstantiationCallableNodeHandler.php index 304aedb86ad..9fd33f592e6 100644 --- a/src/Analyser/ExprHandler/Virtual/InstantiationCallableNodeHandler.php +++ b/src/Analyser/ExprHandler/Virtual/InstantiationCallableNodeHandler.php @@ -8,12 +8,10 @@ use PHPStan\Analyser\ExpressionResult; use PHPStan\Analyser\ExpressionResultFactory; use PHPStan\Analyser\ExpressionResultStorage; +use PHPStan\Analyser\ExprHandler; +use PHPStan\Analyser\ExprHandler\Helper\DefaultNarrowingHelper; use PHPStan\Analyser\MutatingScope; use PHPStan\Analyser\NodeScopeResolver; -use PHPStan\Analyser\Scope; -use PHPStan\Analyser\SpecifiedTypes; -use PHPStan\Analyser\TypeResolvingExprHandler; -use PHPStan\Analyser\TypeSpecifier; use PHPStan\Analyser\TypeSpecifierContext; use PHPStan\DependencyInjection\AutowiredService; use PHPStan\Node\InstantiationCallableNode; @@ -21,13 +19,16 @@ use PHPStan\Type\Type; /** - * @implements TypeResolvingExprHandler + * @implements ExprHandler */ #[AutowiredService] -final class InstantiationCallableNodeHandler implements TypeResolvingExprHandler +final class InstantiationCallableNodeHandler implements ExprHandler { - public function __construct(private ExpressionResultFactory $expressionResultFactory) + public function __construct( + private ExpressionResultFactory $expressionResultFactory, + private DefaultNarrowingHelper $defaultNarrowingHelper, + ) { } @@ -60,19 +61,11 @@ public function processExpr(NodeScopeResolver $nodeScopeResolver, Stmt $stmt, Ex isAlwaysTerminating: $isAlwaysTerminating, throwPoints: $throwPoints, impurePoints: $impurePoints, + // in practice the type of the first-class callable is resolved + // by FirstClassCallableNewHandler + typeCallback: static fn (MutatingScope $scope): Type => new MixedType(), + specifyTypesCallback: fn (MutatingScope $s, TypeSpecifierContext $context) => $this->defaultNarrowingHelper->specifyDefaultTypes($expr, $context), ); } - public function resolveType(MutatingScope $scope, Expr $expr): Type - { - // in practice the type of the first-class callable is resolved - // by FirstClassCallableNewHandler - return new MixedType(); - } - - public function specifyTypes(TypeSpecifier $typeSpecifier, Scope $scope, Expr $expr, TypeSpecifierContext $context): SpecifiedTypes - { - return $typeSpecifier->specifyDefaultTypes($scope, $expr, $context); - } - } From 140482bade0618ad0d12ef1240e422f4c9457f0d Mon Sep 17 00:00:00 2001 From: Ondrej Mirtes Date: Tue, 16 Jun 2026 18:19:55 +0200 Subject: [PATCH 048/113] TypeExprHandler is no longer TypeResolvingExprHandler --- .../ExprHandler/Virtual/TypeExprHandler.php | 27 +++++++------------ 1 file changed, 10 insertions(+), 17 deletions(-) diff --git a/src/Analyser/ExprHandler/Virtual/TypeExprHandler.php b/src/Analyser/ExprHandler/Virtual/TypeExprHandler.php index 32208c6a569..1efa5f6db19 100644 --- a/src/Analyser/ExprHandler/Virtual/TypeExprHandler.php +++ b/src/Analyser/ExprHandler/Virtual/TypeExprHandler.php @@ -8,25 +8,26 @@ use PHPStan\Analyser\ExpressionResult; use PHPStan\Analyser\ExpressionResultFactory; use PHPStan\Analyser\ExpressionResultStorage; +use PHPStan\Analyser\ExprHandler; +use PHPStan\Analyser\ExprHandler\Helper\DefaultNarrowingHelper; use PHPStan\Analyser\MutatingScope; use PHPStan\Analyser\NodeScopeResolver; -use PHPStan\Analyser\Scope; -use PHPStan\Analyser\SpecifiedTypes; -use PHPStan\Analyser\TypeResolvingExprHandler; -use PHPStan\Analyser\TypeSpecifier; use PHPStan\Analyser\TypeSpecifierContext; use PHPStan\DependencyInjection\AutowiredService; use PHPStan\Node\Expr\TypeExpr; use PHPStan\Type\Type; /** - * @implements TypeResolvingExprHandler + * @implements ExprHandler */ #[AutowiredService] -final class TypeExprHandler implements TypeResolvingExprHandler +final class TypeExprHandler implements ExprHandler { - public function __construct(private ExpressionResultFactory $expressionResultFactory) + public function __construct( + private ExpressionResultFactory $expressionResultFactory, + private DefaultNarrowingHelper $defaultNarrowingHelper, + ) { } @@ -48,17 +49,9 @@ public function processExpr(NodeScopeResolver $nodeScopeResolver, Stmt $stmt, Ex isAlwaysTerminating: false, throwPoints: [], impurePoints: [], + typeCallback: static fn (MutatingScope $scope): Type => $expr->getExprType(), + specifyTypesCallback: fn (MutatingScope $s, TypeSpecifierContext $context) => $this->defaultNarrowingHelper->specifyDefaultTypes($expr, $context), ); } - public function resolveType(MutatingScope $scope, Expr $expr): Type - { - return $expr->getExprType(); - } - - public function specifyTypes(TypeSpecifier $typeSpecifier, Scope $scope, Expr $expr, TypeSpecifierContext $context): SpecifiedTypes - { - return $typeSpecifier->specifyDefaultTypes($scope, $expr, $context); - } - } From 431f8471a663ac793053b1fa289acccdc251314b Mon Sep 17 00:00:00 2001 From: Ondrej Mirtes Date: Wed, 17 Jun 2026 12:03:00 +0200 Subject: [PATCH 049/113] Fix failure of forwarding ExpressionResult --- src/Analyser/ExprHandler/AssignHandler.php | 23 ++++++++++------------ 1 file changed, 10 insertions(+), 13 deletions(-) diff --git a/src/Analyser/ExprHandler/AssignHandler.php b/src/Analyser/ExprHandler/AssignHandler.php index 362a9619126..391a816f1e5 100644 --- a/src/Analyser/ExprHandler/AssignHandler.php +++ b/src/Analyser/ExprHandler/AssignHandler.php @@ -345,7 +345,16 @@ function (MutatingScope $scope) use ($stmt, $expr, $nodeCallback, $context, $sto $scope = $scope->exitExpressionAssign($expr->expr); } - return $this->expressionResultFactory->create($scope, $beforeScope, $expr->expr, $hasYield, $isAlwaysTerminating, $throwPoints, $impurePoints); + return $this->expressionResultFactory->create( + $scope, + beforeScope: $beforeScope, + expr: $expr->expr, + hasYield: $hasYield, + isAlwaysTerminating: $isAlwaysTerminating, + throwPoints: $throwPoints, + impurePoints: $impurePoints, + typeCallback: static fn ($scope) => $result->getTypeForScope($scope), + ); }, true, ); @@ -558,18 +567,6 @@ public function processAssignVar( ): ExpressionResult { $beforeScope = $scope; - $nodeScopeResolver->storeExpressionResult($storage, $var, $this->expressionResultFactory->create( - $scope, - beforeScope: $scope, - expr: $var, - hasYield: false, - isAlwaysTerminating: false, - throwPoints: [], - impurePoints: [], - // VariableHandler no longer implements TypeResolvingExprHandler - - // type questions about the target node are answered from this result - typeCallback: $var instanceof Variable ? VariableHandler::createTypeCallback($var) : null, - )); $nodeScopeResolver->callNodeCallback($nodeCallback, $var, $enterExpressionAssign ? $scope->enterExpressionAssign($var) : $scope, $storage); $hasYield = false; $throwPoints = []; From 09065cadf304ea85dca546c1ffac273eb825227c Mon Sep 17 00:00:00 2001 From: Ondrej Mirtes Date: Wed, 17 Jun 2026 14:07:43 +0200 Subject: [PATCH 050/113] ErrorSuppressHandler is no longer TypeResolvingExprHandler --- .../ExprHandler/ErrorSuppressHandler.php | 31 +++++++------------ 1 file changed, 12 insertions(+), 19 deletions(-) diff --git a/src/Analyser/ExprHandler/ErrorSuppressHandler.php b/src/Analyser/ExprHandler/ErrorSuppressHandler.php index 25bfa07bd36..3c847d9fe0c 100644 --- a/src/Analyser/ExprHandler/ErrorSuppressHandler.php +++ b/src/Analyser/ExprHandler/ErrorSuppressHandler.php @@ -9,24 +9,26 @@ use PHPStan\Analyser\ExpressionResult; use PHPStan\Analyser\ExpressionResultFactory; use PHPStan\Analyser\ExpressionResultStorage; +use PHPStan\Analyser\ExprHandler; +use PHPStan\Analyser\ExprHandler\Helper\DefaultNarrowingHelper; use PHPStan\Analyser\MutatingScope; use PHPStan\Analyser\NodeScopeResolver; -use PHPStan\Analyser\Scope; use PHPStan\Analyser\SpecifiedTypes; -use PHPStan\Analyser\TypeResolvingExprHandler; -use PHPStan\Analyser\TypeSpecifier; use PHPStan\Analyser\TypeSpecifierContext; use PHPStan\DependencyInjection\AutowiredService; use PHPStan\Type\Type; /** - * @implements TypeResolvingExprHandler + * @implements ExprHandler */ #[AutowiredService] -final class ErrorSuppressHandler implements TypeResolvingExprHandler +final class ErrorSuppressHandler implements ExprHandler { - public function __construct(private ExpressionResultFactory $expressionResultFactory) + public function __construct( + private ExpressionResultFactory $expressionResultFactory, + private DefaultNarrowingHelper $defaultNarrowingHelper, + ) { } @@ -37,29 +39,20 @@ public function supports(Expr $expr): bool public function processExpr(NodeScopeResolver $nodeScopeResolver, Stmt $stmt, Expr $expr, MutatingScope $scope, ExpressionResultStorage $storage, callable $nodeCallback, ExpressionContext $context): ExpressionResult { + $beforeScope = $scope; $exprResult = $nodeScopeResolver->processExprNode($stmt, $expr->expr, $scope, $storage, $nodeCallback, $context); return $this->expressionResultFactory->create( $exprResult->getScope(), - beforeScope: $scope, + beforeScope: $beforeScope, expr: $expr, hasYield: $exprResult->hasYield(), isAlwaysTerminating: $exprResult->isAlwaysTerminating(), throwPoints: $exprResult->getThrowPoints(), impurePoints: $exprResult->getImpurePoints(), - truthyScopeCallback: static fn (): MutatingScope => $exprResult->getTruthyScope(), - falseyScopeCallback: static fn (): MutatingScope => $exprResult->getFalseyScope(), + typeCallback: static fn (MutatingScope $s): Type => $exprResult->getTypeForScope($s), + specifyTypesCallback: fn (MutatingScope $s, TypeSpecifierContext $context): SpecifiedTypes => $this->defaultNarrowingHelper->getChildSpecifiedTypes($s, $expr->expr, $exprResult, $context)->setRootExpr($expr), ); } - public function resolveType(MutatingScope $scope, Expr $expr): Type - { - return $scope->getType($expr->expr); - } - - public function specifyTypes(TypeSpecifier $typeSpecifier, Scope $scope, Expr $expr, TypeSpecifierContext $context): SpecifiedTypes - { - return $typeSpecifier->specifyTypesInCondition($scope, $expr->expr, $context)->setRootExpr($expr); - } - } From ee55581d74034c0025f7619d113644322eb5db33 Mon Sep 17 00:00:00 2001 From: Ondrej Mirtes Date: Wed, 17 Jun 2026 14:07:43 +0200 Subject: [PATCH 051/113] CastHandler is no longer TypeResolvingExprHandler --- src/Analyser/ExprHandler/CastHandler.php | 84 +++++++++++++----------- 1 file changed, 45 insertions(+), 39 deletions(-) diff --git a/src/Analyser/ExprHandler/CastHandler.php b/src/Analyser/ExprHandler/CastHandler.php index af928d21ed1..ab4f122a54b 100644 --- a/src/Analyser/ExprHandler/CastHandler.php +++ b/src/Analyser/ExprHandler/CastHandler.php @@ -15,28 +15,29 @@ use PHPStan\Analyser\ExpressionResult; use PHPStan\Analyser\ExpressionResultFactory; use PHPStan\Analyser\ExpressionResultStorage; +use PHPStan\Analyser\ExprHandler; +use PHPStan\Analyser\ExprHandler\Helper\DefaultNarrowingHelper; use PHPStan\Analyser\MutatingScope; use PHPStan\Analyser\NodeScopeResolver; -use PHPStan\Analyser\Scope; use PHPStan\Analyser\SpecifiedTypes; -use PHPStan\Analyser\TypeResolvingExprHandler; -use PHPStan\Analyser\TypeSpecifier; use PHPStan\Analyser\TypeSpecifierContext; use PHPStan\DependencyInjection\AutowiredService; use PHPStan\Reflection\InitializerExprTypeResolver; +use PHPStan\ShouldNotHappenException; use PHPStan\Type\NullType; use PHPStan\Type\Type; /** - * @implements TypeResolvingExprHandler + * @implements ExprHandler */ #[AutowiredService] -final class CastHandler implements TypeResolvingExprHandler +final class CastHandler implements ExprHandler { public function __construct( private InitializerExprTypeResolver $initializerExprTypeResolver, private ExpressionResultFactory $expressionResultFactory, + private DefaultNarrowingHelper $defaultNarrowingHelper, ) { } @@ -60,45 +61,50 @@ public function processExpr(NodeScopeResolver $nodeScopeResolver, Stmt $stmt, Ex isAlwaysTerminating: $exprResult->isAlwaysTerminating(), throwPoints: $exprResult->getThrowPoints(), impurePoints: $exprResult->getImpurePoints(), - ); - } + typeCallback: function (MutatingScope $s) use ($expr, $exprResult): Type { + if ($expr instanceof Cast\Unset_) { + return new NullType(); + } - public function resolveType(MutatingScope $scope, Expr $expr): Type - { - if ($expr instanceof Cast\Unset_) { - return new NullType(); - } + return $this->initializerExprTypeResolver->getCastType($expr, static function (Expr $e) use ($s, $expr, $exprResult): Type { + if ($e === $expr->expr) { + return $exprResult->getTypeForScope($s); + } - return $this->initializerExprTypeResolver->getCastType($expr, static fn (Expr $expr): Type => $scope->getType($expr)); - } + throw new ShouldNotHappenException(); + }); + }, + specifyTypesCallback: function (MutatingScope $s, TypeSpecifierContext $context) use ($expr): SpecifiedTypes { + if ($expr instanceof Cast\Bool_) { + return $this->defaultNarrowingHelper->getChildSpecifiedTypes( + $s, + new Equal($expr->expr, new ConstFetch(new FullyQualified('true'))), + null, + $context, + )->setRootExpr($expr); + } - public function specifyTypes(TypeSpecifier $typeSpecifier, Scope $scope, Expr $expr, TypeSpecifierContext $context): SpecifiedTypes - { - if ($expr instanceof Cast\Bool_) { - return $typeSpecifier->specifyTypesInCondition( - $scope, - new Equal($expr->expr, new ConstFetch(new FullyQualified('true'))), - $context, - )->setRootExpr($expr); - } - - if ($expr instanceof Cast\Int_) { - return $typeSpecifier->specifyTypesInCondition( - $scope, - new NotEqual($expr->expr, new Int_(0)), - $context, - )->setRootExpr($expr); - } + if ($expr instanceof Cast\Int_) { + return $this->defaultNarrowingHelper->getChildSpecifiedTypes( + $s, + new NotEqual($expr->expr, new Int_(0)), + null, + $context, + )->setRootExpr($expr); + } - if ($expr instanceof Cast\Double) { - return $typeSpecifier->specifyTypesInCondition( - $scope, - new NotEqual($expr->expr, new Float_(0.0)), - $context, - )->setRootExpr($expr); - } + if ($expr instanceof Cast\Double) { + return $this->defaultNarrowingHelper->getChildSpecifiedTypes( + $s, + new NotEqual($expr->expr, new Float_(0.0)), + null, + $context, + )->setRootExpr($expr); + } - return $typeSpecifier->specifyDefaultTypes($scope, $expr, $context); + return $this->defaultNarrowingHelper->specifyDefaultTypes($expr, $context); + }, + ); } } From d40d377cb75f2d24002e4bcf52d40a27ecf1efaf Mon Sep 17 00:00:00 2001 From: Ondrej Mirtes Date: Wed, 17 Jun 2026 14:07:43 +0200 Subject: [PATCH 052/113] CastStringHandler is no longer TypeResolvingExprHandler --- .../ExprHandler/CastStringHandler.php | 38 +++++++++---------- 1 file changed, 19 insertions(+), 19 deletions(-) diff --git a/src/Analyser/ExprHandler/CastStringHandler.php b/src/Analyser/ExprHandler/CastStringHandler.php index daa2c148be6..6f3d36f59c9 100644 --- a/src/Analyser/ExprHandler/CastStringHandler.php +++ b/src/Analyser/ExprHandler/CastStringHandler.php @@ -11,30 +11,31 @@ use PHPStan\Analyser\ExpressionResult; use PHPStan\Analyser\ExpressionResultFactory; use PHPStan\Analyser\ExpressionResultStorage; +use PHPStan\Analyser\ExprHandler; +use PHPStan\Analyser\ExprHandler\Helper\DefaultNarrowingHelper; use PHPStan\Analyser\ExprHandler\Helper\ImplicitToStringCallHelper; use PHPStan\Analyser\MutatingScope; use PHPStan\Analyser\NodeScopeResolver; -use PHPStan\Analyser\Scope; use PHPStan\Analyser\SpecifiedTypes; -use PHPStan\Analyser\TypeResolvingExprHandler; -use PHPStan\Analyser\TypeSpecifier; use PHPStan\Analyser\TypeSpecifierContext; use PHPStan\DependencyInjection\AutowiredService; use PHPStan\Reflection\InitializerExprTypeResolver; +use PHPStan\ShouldNotHappenException; use PHPStan\Type\Type; use function array_merge; /** - * @implements TypeResolvingExprHandler + * @implements ExprHandler */ #[AutowiredService] -final class CastStringHandler implements TypeResolvingExprHandler +final class CastStringHandler implements ExprHandler { public function __construct( private InitializerExprTypeResolver $initializerExprTypeResolver, private ImplicitToStringCallHelper $implicitToStringCallHelper, private ExpressionResultFactory $expressionResultFactory, + private DefaultNarrowingHelper $defaultNarrowingHelper, ) { } @@ -65,21 +66,20 @@ public function processExpr(NodeScopeResolver $nodeScopeResolver, Stmt $stmt, Ex isAlwaysTerminating: $exprResult->isAlwaysTerminating(), throwPoints: $throwPoints, impurePoints: $impurePoints, - ); - } - - public function resolveType(MutatingScope $scope, Expr $expr): Type - { - return $this->initializerExprTypeResolver->getCastType($expr, static fn (Expr $expr): Type => $scope->getType($expr)); - } + typeCallback: fn (MutatingScope $s): Type => $this->initializerExprTypeResolver->getCastType($expr, static function (Expr $e) use ($s, $expr, $exprResult): Type { + if ($e === $expr->expr) { + return $exprResult->getTypeForScope($s); + } - public function specifyTypes(TypeSpecifier $typeSpecifier, Scope $scope, Expr $expr, TypeSpecifierContext $context): SpecifiedTypes - { - return $typeSpecifier->specifyTypesInCondition( - $scope, - new NotEqual($expr->expr, new String_('')), - $context, - )->setRootExpr($expr); + throw new ShouldNotHappenException(); + }), + specifyTypesCallback: fn (MutatingScope $s, TypeSpecifierContext $context): SpecifiedTypes => $this->defaultNarrowingHelper->getChildSpecifiedTypes( + $s, + new NotEqual($expr->expr, new String_('')), + null, + $context, + )->setRootExpr($expr), + ); } } From f77c8de9d14b341be8285f8230be8ae14de5432f Mon Sep 17 00:00:00 2001 From: Ondrej Mirtes Date: Wed, 17 Jun 2026 14:21:57 +0200 Subject: [PATCH 053/113] PostIncHandler is no longer TypeResolvingExprHandler --- src/Analyser/ExprHandler/PostIncHandler.php | 27 +++++++++------------ 1 file changed, 11 insertions(+), 16 deletions(-) diff --git a/src/Analyser/ExprHandler/PostIncHandler.php b/src/Analyser/ExprHandler/PostIncHandler.php index a04b0cfea75..a16eb22282f 100644 --- a/src/Analyser/ExprHandler/PostIncHandler.php +++ b/src/Analyser/ExprHandler/PostIncHandler.php @@ -10,24 +10,26 @@ use PHPStan\Analyser\ExpressionResult; use PHPStan\Analyser\ExpressionResultFactory; use PHPStan\Analyser\ExpressionResultStorage; +use PHPStan\Analyser\ExprHandler; +use PHPStan\Analyser\ExprHandler\Helper\DefaultNarrowingHelper; use PHPStan\Analyser\MutatingScope; use PHPStan\Analyser\NodeScopeResolver; -use PHPStan\Analyser\Scope; use PHPStan\Analyser\SpecifiedTypes; -use PHPStan\Analyser\TypeResolvingExprHandler; -use PHPStan\Analyser\TypeSpecifier; use PHPStan\Analyser\TypeSpecifierContext; use PHPStan\DependencyInjection\AutowiredService; use PHPStan\Type\Type; /** - * @implements TypeResolvingExprHandler + * @implements ExprHandler */ #[AutowiredService] -final class PostIncHandler implements TypeResolvingExprHandler +final class PostIncHandler implements ExprHandler { - public function __construct(private ExpressionResultFactory $expressionResultFactory) + public function __construct( + private ExpressionResultFactory $expressionResultFactory, + private DefaultNarrowingHelper $defaultNarrowingHelper, + ) { } @@ -55,17 +57,10 @@ public function processExpr(NodeScopeResolver $nodeScopeResolver, Stmt $stmt, Ex isAlwaysTerminating: $varResult->isAlwaysTerminating(), throwPoints: $varResult->getThrowPoints(), impurePoints: $varResult->getImpurePoints(), + // post-increment evaluates to the variable's pre-mutation value + typeCallback: static fn (MutatingScope $s): Type => $varResult->getTypeForScope($s), + specifyTypesCallback: fn (MutatingScope $s, TypeSpecifierContext $context): SpecifiedTypes => $this->defaultNarrowingHelper->specifyDefaultTypes($expr, $context), ); } - public function resolveType(MutatingScope $scope, Expr $expr): Type - { - return $scope->getType($expr->var); - } - - public function specifyTypes(TypeSpecifier $typeSpecifier, Scope $scope, Expr $expr, TypeSpecifierContext $context): SpecifiedTypes - { - return $typeSpecifier->specifyDefaultTypes($scope, $expr, $context); - } - } From 1fad8526b958b4d84e1b3f612a21d742b96bcab1 Mon Sep 17 00:00:00 2001 From: Ondrej Mirtes Date: Wed, 17 Jun 2026 14:21:57 +0200 Subject: [PATCH 054/113] PostDecHandler is no longer TypeResolvingExprHandler --- src/Analyser/ExprHandler/PostDecHandler.php | 27 +++++++++------------ 1 file changed, 11 insertions(+), 16 deletions(-) diff --git a/src/Analyser/ExprHandler/PostDecHandler.php b/src/Analyser/ExprHandler/PostDecHandler.php index 0cff682498c..d3fafe25b50 100644 --- a/src/Analyser/ExprHandler/PostDecHandler.php +++ b/src/Analyser/ExprHandler/PostDecHandler.php @@ -10,24 +10,26 @@ use PHPStan\Analyser\ExpressionResult; use PHPStan\Analyser\ExpressionResultFactory; use PHPStan\Analyser\ExpressionResultStorage; +use PHPStan\Analyser\ExprHandler; +use PHPStan\Analyser\ExprHandler\Helper\DefaultNarrowingHelper; use PHPStan\Analyser\MutatingScope; use PHPStan\Analyser\NodeScopeResolver; -use PHPStan\Analyser\Scope; use PHPStan\Analyser\SpecifiedTypes; -use PHPStan\Analyser\TypeResolvingExprHandler; -use PHPStan\Analyser\TypeSpecifier; use PHPStan\Analyser\TypeSpecifierContext; use PHPStan\DependencyInjection\AutowiredService; use PHPStan\Type\Type; /** - * @implements TypeResolvingExprHandler + * @implements ExprHandler */ #[AutowiredService] -final class PostDecHandler implements TypeResolvingExprHandler +final class PostDecHandler implements ExprHandler { - public function __construct(private ExpressionResultFactory $expressionResultFactory) + public function __construct( + private ExpressionResultFactory $expressionResultFactory, + private DefaultNarrowingHelper $defaultNarrowingHelper, + ) { } @@ -55,17 +57,10 @@ public function processExpr(NodeScopeResolver $nodeScopeResolver, Stmt $stmt, Ex isAlwaysTerminating: $varResult->isAlwaysTerminating(), throwPoints: $varResult->getThrowPoints(), impurePoints: $varResult->getImpurePoints(), + // post-decrement evaluates to the variable's pre-mutation value + typeCallback: static fn (MutatingScope $s): Type => $varResult->getTypeForScope($s), + specifyTypesCallback: fn (MutatingScope $s, TypeSpecifierContext $context): SpecifiedTypes => $this->defaultNarrowingHelper->specifyDefaultTypes($expr, $context), ); } - public function resolveType(MutatingScope $scope, Expr $expr): Type - { - return $scope->getType($expr->var); - } - - public function specifyTypes(TypeSpecifier $typeSpecifier, Scope $scope, Expr $expr, TypeSpecifierContext $context): SpecifiedTypes - { - return $typeSpecifier->specifyDefaultTypes($scope, $expr, $context); - } - } From f92af63200bed69adfc8719b361e0916b042d286 Mon Sep 17 00:00:00 2001 From: Ondrej Mirtes Date: Wed, 17 Jun 2026 14:38:00 +0200 Subject: [PATCH 055/113] PipeHandler is no longer TypeResolvingExprHandler --- src/Analyser/ExprHandler/PipeHandler.php | 43 ++++++------------------ 1 file changed, 11 insertions(+), 32 deletions(-) diff --git a/src/Analyser/ExprHandler/PipeHandler.php b/src/Analyser/ExprHandler/PipeHandler.php index 45a5e23322d..6f27e615d52 100644 --- a/src/Analyser/ExprHandler/PipeHandler.php +++ b/src/Analyser/ExprHandler/PipeHandler.php @@ -13,12 +13,11 @@ use PHPStan\Analyser\ExpressionResult; use PHPStan\Analyser\ExpressionResultFactory; use PHPStan\Analyser\ExpressionResultStorage; +use PHPStan\Analyser\ExprHandler; +use PHPStan\Analyser\ExprHandler\Helper\DefaultNarrowingHelper; use PHPStan\Analyser\MutatingScope; use PHPStan\Analyser\NodeScopeResolver; -use PHPStan\Analyser\Scope; use PHPStan\Analyser\SpecifiedTypes; -use PHPStan\Analyser\TypeResolvingExprHandler; -use PHPStan\Analyser\TypeSpecifier; use PHPStan\Analyser\TypeSpecifierContext; use PHPStan\DependencyInjection\AutowiredService; use PHPStan\Node\Printer\ExprPrinter; @@ -27,13 +26,16 @@ use function array_merge; /** - * @implements TypeResolvingExprHandler + * @implements ExprHandler */ #[AutowiredService] -final class PipeHandler implements TypeResolvingExprHandler +final class PipeHandler implements ExprHandler { - public function __construct(private ExpressionResultFactory $expressionResultFactory) + public function __construct( + private ExpressionResultFactory $expressionResultFactory, + private DefaultNarrowingHelper $defaultNarrowingHelper, + ) { } @@ -42,27 +44,6 @@ public function supports(Expr $expr): bool return $expr instanceof Pipe; } - public function resolveType(MutatingScope $scope, Expr $expr): Type - { - if ($expr->right instanceof FuncCall && $expr->right->isFirstClassCallable()) { - return $scope->getType(new FuncCall($expr->right->name, [ - new Arg($expr->left), - ])); - } elseif ($expr->right instanceof MethodCall && $expr->right->isFirstClassCallable()) { - return $scope->getType(new MethodCall($expr->right->var, $expr->right->name, [ - new Arg($expr->left), - ])); - } elseif ($expr->right instanceof StaticCall && $expr->right->isFirstClassCallable()) { - return $scope->getType(new StaticCall($expr->right->class, $expr->right->name, [ - new Arg($expr->left), - ])); - } - - return $scope->getType(new FuncCall($expr->right, [ - new Arg($expr->left), - ])); - } - public function processExpr(NodeScopeResolver $nodeScopeResolver, Stmt $stmt, Expr $expr, MutatingScope $scope, ExpressionResultStorage $storage, callable $nodeCallback, ExpressionContext $context): ExpressionResult { $rightAttributes = array_merge($expr->right->getAttributes(), ['virtualPipeOperatorCall' => true]); @@ -116,12 +97,10 @@ public function processExpr(NodeScopeResolver $nodeScopeResolver, Stmt $stmt, Ex isAlwaysTerminating: $callResult->isAlwaysTerminating(), throwPoints: $callResult->getThrowPoints(), impurePoints: $callResult->getImpurePoints(), + // the pipe evaluates to its rewritten call - read that child's result + typeCallback: static fn (MutatingScope $s): Type => $callResult->getTypeForScope($s), + specifyTypesCallback: fn (MutatingScope $s, TypeSpecifierContext $context): SpecifiedTypes => $this->defaultNarrowingHelper->specifyDefaultTypes($expr, $context), ); } - public function specifyTypes(TypeSpecifier $typeSpecifier, Scope $scope, Expr $expr, TypeSpecifierContext $context): SpecifiedTypes - { - return $typeSpecifier->specifyDefaultTypes($scope, $expr, $context); - } - } From bf43d04c2f935130ba73af8568c73f18c247b98f Mon Sep 17 00:00:00 2001 From: Ondrej Mirtes Date: Wed, 17 Jun 2026 15:29:41 +0200 Subject: [PATCH 056/113] PreIncHandler is no longer TypeResolvingExprHandler --- src/Analyser/ExprHandler/PreIncHandler.php | 129 +++++++++++++-------- 1 file changed, 78 insertions(+), 51 deletions(-) diff --git a/src/Analyser/ExprHandler/PreIncHandler.php b/src/Analyser/ExprHandler/PreIncHandler.php index 73fd74c7dc5..5391bffb825 100644 --- a/src/Analyser/ExprHandler/PreIncHandler.php +++ b/src/Analyser/ExprHandler/PreIncHandler.php @@ -3,7 +3,6 @@ namespace PHPStan\Analyser\ExprHandler; use PhpParser\Node\Expr; -use PhpParser\Node\Expr\BinaryOp\Plus; use PhpParser\Node\Expr\PreInc; use PhpParser\Node\Scalar\Int_; use PhpParser\Node\Stmt; @@ -11,16 +10,18 @@ use PHPStan\Analyser\ExpressionResult; use PHPStan\Analyser\ExpressionResultFactory; use PHPStan\Analyser\ExpressionResultStorage; +use PHPStan\Analyser\ExprHandler; +use PHPStan\Analyser\ExprHandler\Helper\DefaultNarrowingHelper; use PHPStan\Analyser\MutatingScope; use PHPStan\Analyser\NodeScopeResolver; -use PHPStan\Analyser\Scope; use PHPStan\Analyser\SpecifiedTypes; -use PHPStan\Analyser\TypeResolvingExprHandler; -use PHPStan\Analyser\TypeSpecifier; use PHPStan\Analyser\TypeSpecifierContext; use PHPStan\DependencyInjection\AutowiredService; +use PHPStan\Reflection\InitializerExprTypeResolver; +use PHPStan\ShouldNotHappenException; use PHPStan\Type\Accessory\AccessoryLiteralStringType; use PHPStan\Type\BenevolentUnionType; +use PHPStan\Type\Constant\ConstantIntegerType; use PHPStan\Type\FloatType; use PHPStan\Type\IntegerType; use PHPStan\Type\IntersectionType; @@ -36,13 +37,17 @@ use function str_increment; /** - * @implements TypeResolvingExprHandler + * @implements ExprHandler */ #[AutowiredService] -final class PreIncHandler implements TypeResolvingExprHandler +final class PreIncHandler implements ExprHandler { - public function __construct(private ExpressionResultFactory $expressionResultFactory) + public function __construct( + private ExpressionResultFactory $expressionResultFactory, + private InitializerExprTypeResolver $initializerExprTypeResolver, + private DefaultNarrowingHelper $defaultNarrowingHelper, + ) { } @@ -51,58 +56,83 @@ public function supports(Expr $expr): bool return $expr instanceof PreInc; } - public function resolveType(MutatingScope $scope, Expr $expr): Type + public function processExpr(NodeScopeResolver $nodeScopeResolver, Stmt $stmt, Expr $expr, MutatingScope $scope, ExpressionResultStorage $storage, callable $nodeCallback, ExpressionContext $context): ExpressionResult { - $varType = $scope->getType($expr->var); - $varScalars = $varType->getConstantScalarValues(); - - if (count($varScalars) > 0) { - $newTypes = []; - - foreach ($varScalars as $varValue) { - if ($varValue === '') { - $varValue = '1'; - } elseif (is_string($varValue) && !is_numeric($varValue)) { - try { - $varValue = str_increment($varValue); - } catch (ValueError) { - return new NeverType(); + $varResult = $nodeScopeResolver->processExprNode($stmt, $expr->var, $scope, $storage, $nodeCallback, $context->enterDeep()); + + $typeCallback = function (MutatingScope $s) use ($expr, $varResult): Type { + $varType = $varResult->getTypeForScope($s); + $varScalars = $varType->getConstantScalarValues(); + + if (count($varScalars) > 0) { + $newTypes = []; + + foreach ($varScalars as $varValue) { + if ($varValue === '') { + $varValue = '1'; + } elseif (is_string($varValue) && !is_numeric($varValue)) { + try { + $varValue = str_increment($varValue); + } catch (ValueError) { + return new NeverType(); + } + } elseif (!is_bool($varValue)) { + ++$varValue; } - } elseif (!is_bool($varValue)) { - ++$varValue; + + $newTypes[] = $s->getTypeFromValue($varValue); + } + return TypeCombinator::union(...$newTypes); + } elseif ($varType->isString()->yes()) { + if ($varType->isLiteralString()->yes()) { + return new IntersectionType([ + new StringType(), + new AccessoryLiteralStringType(), + ]); } - $newTypes[] = $scope->getTypeFromValue($varValue); - } - return TypeCombinator::union(...$newTypes); - } elseif ($varType->isString()->yes()) { - if ($varType->isLiteralString()->yes()) { - return new IntersectionType([ - new StringType(), - new AccessoryLiteralStringType(), - ]); - } + if ($varType->isNumericString()->yes()) { + return new BenevolentUnionType([ + new IntegerType(), + new FloatType(), + ]); + } - if ($varType->isNumericString()->yes()) { return new BenevolentUnionType([ + new StringType(), new IntegerType(), new FloatType(), ]); } - return new BenevolentUnionType([ - new StringType(), - new IntegerType(), - new FloatType(), - ]); - } + $one = new Int_(1); + return $this->initializerExprTypeResolver->getPlusType($expr->var, $one, static function (Expr $e) use ($s, $expr, $varResult, $one): Type { + if ($e === $expr->var) { + return $varResult->getTypeForScope($s); + } + if ($e === $one) { + return new ConstantIntegerType(1); + } - return $scope->getType(new Plus($expr->var, new Int_(1))); - } + throw new ShouldNotHappenException(); + }); + }; + $specifyTypesCallback = fn (MutatingScope $s, TypeSpecifierContext $context): SpecifiedTypes => $this->defaultNarrowingHelper->specifyDefaultTypes($expr, $context); - public function processExpr(NodeScopeResolver $nodeScopeResolver, Stmt $stmt, Expr $expr, MutatingScope $scope, ExpressionResultStorage $storage, callable $nodeCallback, ExpressionContext $context): ExpressionResult - { - $varResult = $nodeScopeResolver->processExprNode($stmt, $expr->var, $scope, $storage, $nodeCallback, $context->enterDeep()); + // processVirtualAssign asks getType($expr) for the value to assign; store + // this result first so that resolves from the typeCallback below rather + // than re-processing the node on demand (which would recurse). + $nodeScopeResolver->storeExpressionResult($storage, $expr, $this->expressionResultFactory->create( + $varResult->getScope(), + beforeScope: $scope, + expr: $expr, + hasYield: $varResult->hasYield(), + isAlwaysTerminating: $varResult->isAlwaysTerminating(), + throwPoints: $varResult->getThrowPoints(), + impurePoints: $varResult->getImpurePoints(), + typeCallback: $typeCallback, + specifyTypesCallback: $specifyTypesCallback, + )); return $this->expressionResultFactory->create( $nodeScopeResolver->processVirtualAssign( @@ -119,12 +149,9 @@ public function processExpr(NodeScopeResolver $nodeScopeResolver, Stmt $stmt, Ex isAlwaysTerminating: $varResult->isAlwaysTerminating(), throwPoints: $varResult->getThrowPoints(), impurePoints: $varResult->getImpurePoints(), + typeCallback: $typeCallback, + specifyTypesCallback: $specifyTypesCallback, ); } - public function specifyTypes(TypeSpecifier $typeSpecifier, Scope $scope, Expr $expr, TypeSpecifierContext $context): SpecifiedTypes - { - return $typeSpecifier->specifyDefaultTypes($scope, $expr, $context); - } - } From a9950bf163f1cd73ff08b0cd4212ab744603c0c9 Mon Sep 17 00:00:00 2001 From: Ondrej Mirtes Date: Wed, 17 Jun 2026 15:29:41 +0200 Subject: [PATCH 057/113] PreDecHandler is no longer TypeResolvingExprHandler --- src/Analyser/ExprHandler/PreDecHandler.php | 129 +++++++++++++-------- 1 file changed, 78 insertions(+), 51 deletions(-) diff --git a/src/Analyser/ExprHandler/PreDecHandler.php b/src/Analyser/ExprHandler/PreDecHandler.php index 221376fd209..b21eb79a145 100644 --- a/src/Analyser/ExprHandler/PreDecHandler.php +++ b/src/Analyser/ExprHandler/PreDecHandler.php @@ -3,7 +3,6 @@ namespace PHPStan\Analyser\ExprHandler; use PhpParser\Node\Expr; -use PhpParser\Node\Expr\BinaryOp\Minus; use PhpParser\Node\Expr\PreDec; use PhpParser\Node\Scalar\Int_; use PhpParser\Node\Stmt; @@ -11,16 +10,18 @@ use PHPStan\Analyser\ExpressionResult; use PHPStan\Analyser\ExpressionResultFactory; use PHPStan\Analyser\ExpressionResultStorage; +use PHPStan\Analyser\ExprHandler; +use PHPStan\Analyser\ExprHandler\Helper\DefaultNarrowingHelper; use PHPStan\Analyser\MutatingScope; use PHPStan\Analyser\NodeScopeResolver; -use PHPStan\Analyser\Scope; use PHPStan\Analyser\SpecifiedTypes; -use PHPStan\Analyser\TypeResolvingExprHandler; -use PHPStan\Analyser\TypeSpecifier; use PHPStan\Analyser\TypeSpecifierContext; use PHPStan\DependencyInjection\AutowiredService; +use PHPStan\Reflection\InitializerExprTypeResolver; +use PHPStan\ShouldNotHappenException; use PHPStan\Type\Accessory\AccessoryLiteralStringType; use PHPStan\Type\BenevolentUnionType; +use PHPStan\Type\Constant\ConstantIntegerType; use PHPStan\Type\FloatType; use PHPStan\Type\IntegerType; use PHPStan\Type\IntersectionType; @@ -35,13 +36,17 @@ use function str_decrement; /** - * @implements TypeResolvingExprHandler + * @implements ExprHandler */ #[AutowiredService] -final class PreDecHandler implements TypeResolvingExprHandler +final class PreDecHandler implements ExprHandler { - public function __construct(private ExpressionResultFactory $expressionResultFactory) + public function __construct( + private ExpressionResultFactory $expressionResultFactory, + private InitializerExprTypeResolver $initializerExprTypeResolver, + private DefaultNarrowingHelper $defaultNarrowingHelper, + ) { } @@ -50,58 +55,83 @@ public function supports(Expr $expr): bool return $expr instanceof PreDec; } - public function resolveType(MutatingScope $scope, Expr $expr): Type + public function processExpr(NodeScopeResolver $nodeScopeResolver, Stmt $stmt, Expr $expr, MutatingScope $scope, ExpressionResultStorage $storage, callable $nodeCallback, ExpressionContext $context): ExpressionResult { - $varType = $scope->getType($expr->var); - $varScalars = $varType->getConstantScalarValues(); - - if (count($varScalars) > 0) { - $newTypes = []; - - foreach ($varScalars as $varValue) { - if ($varValue === '') { - $varValue = -1; - } elseif (is_string($varValue) && !is_numeric($varValue)) { - try { - $varValue = str_decrement($varValue); - } catch (ValueError) { - return new NeverType(); + $varResult = $nodeScopeResolver->processExprNode($stmt, $expr->var, $scope, $storage, $nodeCallback, $context->enterDeep()); + + $typeCallback = function (MutatingScope $s) use ($expr, $varResult): Type { + $varType = $varResult->getTypeForScope($s); + $varScalars = $varType->getConstantScalarValues(); + + if (count($varScalars) > 0) { + $newTypes = []; + + foreach ($varScalars as $varValue) { + if ($varValue === '') { + $varValue = -1; + } elseif (is_string($varValue) && !is_numeric($varValue)) { + try { + $varValue = str_decrement($varValue); + } catch (ValueError) { + return new NeverType(); + } + } elseif (is_numeric($varValue)) { + --$varValue; } - } elseif (is_numeric($varValue)) { - --$varValue; + + $newTypes[] = $s->getTypeFromValue($varValue); + } + return TypeCombinator::union(...$newTypes); + } elseif ($varType->isString()->yes()) { + if ($varType->isLiteralString()->yes()) { + return new IntersectionType([ + new StringType(), + new AccessoryLiteralStringType(), + ]); } - $newTypes[] = $scope->getTypeFromValue($varValue); - } - return TypeCombinator::union(...$newTypes); - } elseif ($varType->isString()->yes()) { - if ($varType->isLiteralString()->yes()) { - return new IntersectionType([ - new StringType(), - new AccessoryLiteralStringType(), - ]); - } + if ($varType->isNumericString()->yes()) { + return new BenevolentUnionType([ + new IntegerType(), + new FloatType(), + ]); + } - if ($varType->isNumericString()->yes()) { return new BenevolentUnionType([ + new StringType(), new IntegerType(), new FloatType(), ]); } - return new BenevolentUnionType([ - new StringType(), - new IntegerType(), - new FloatType(), - ]); - } + $one = new Int_(1); + return $this->initializerExprTypeResolver->getMinusType($expr->var, $one, static function (Expr $e) use ($s, $expr, $varResult, $one): Type { + if ($e === $expr->var) { + return $varResult->getTypeForScope($s); + } + if ($e === $one) { + return new ConstantIntegerType(1); + } - return $scope->getType(new Minus($expr->var, new Int_(1))); - } + throw new ShouldNotHappenException(); + }); + }; + $specifyTypesCallback = fn (MutatingScope $s, TypeSpecifierContext $context): SpecifiedTypes => $this->defaultNarrowingHelper->specifyDefaultTypes($expr, $context); - public function processExpr(NodeScopeResolver $nodeScopeResolver, Stmt $stmt, Expr $expr, MutatingScope $scope, ExpressionResultStorage $storage, callable $nodeCallback, ExpressionContext $context): ExpressionResult - { - $varResult = $nodeScopeResolver->processExprNode($stmt, $expr->var, $scope, $storage, $nodeCallback, $context->enterDeep()); + // processVirtualAssign asks getType($expr) for the value to assign; store + // this result first so that resolves from the typeCallback below rather + // than re-processing the node on demand (which would recurse). + $nodeScopeResolver->storeExpressionResult($storage, $expr, $this->expressionResultFactory->create( + $varResult->getScope(), + beforeScope: $scope, + expr: $expr, + hasYield: $varResult->hasYield(), + isAlwaysTerminating: $varResult->isAlwaysTerminating(), + throwPoints: $varResult->getThrowPoints(), + impurePoints: $varResult->getImpurePoints(), + typeCallback: $typeCallback, + specifyTypesCallback: $specifyTypesCallback, + )); return $this->expressionResultFactory->create( $nodeScopeResolver->processVirtualAssign( @@ -118,12 +148,9 @@ public function processExpr(NodeScopeResolver $nodeScopeResolver, Stmt $stmt, Ex isAlwaysTerminating: $varResult->isAlwaysTerminating(), throwPoints: $varResult->getThrowPoints(), impurePoints: $varResult->getImpurePoints(), + typeCallback: $typeCallback, + specifyTypesCallback: $specifyTypesCallback, ); } - public function specifyTypes(TypeSpecifier $typeSpecifier, Scope $scope, Expr $expr, TypeSpecifierContext $context): SpecifiedTypes - { - return $typeSpecifier->specifyDefaultTypes($scope, $expr, $context); - } - } From 873cd34dfb28db6169498857b7a737abd7541dca Mon Sep 17 00:00:00 2001 From: Ondrej Mirtes Date: Wed, 17 Jun 2026 15:38:26 +0200 Subject: [PATCH 058/113] AssignOpHandler is no longer TypeResolvingExprHandler --- src/Analyser/ExprHandler/AssignOpHandler.php | 151 ++++++++++--------- 1 file changed, 82 insertions(+), 69 deletions(-) diff --git a/src/Analyser/ExprHandler/AssignOpHandler.php b/src/Analyser/ExprHandler/AssignOpHandler.php index 26d17c102be..cfefb0d0fa7 100644 --- a/src/Analyser/ExprHandler/AssignOpHandler.php +++ b/src/Analyser/ExprHandler/AssignOpHandler.php @@ -13,14 +13,13 @@ use PHPStan\Analyser\ExpressionResult; use PHPStan\Analyser\ExpressionResultFactory; use PHPStan\Analyser\ExpressionResultStorage; +use PHPStan\Analyser\ExprHandler; +use PHPStan\Analyser\ExprHandler\Helper\DefaultNarrowingHelper; use PHPStan\Analyser\ExprHandler\Helper\ImplicitToStringCallHelper; use PHPStan\Analyser\InternalThrowPoint; use PHPStan\Analyser\MutatingScope; use PHPStan\Analyser\NodeScopeResolver; -use PHPStan\Analyser\Scope; use PHPStan\Analyser\SpecifiedTypes; -use PHPStan\Analyser\TypeResolvingExprHandler; -use PHPStan\Analyser\TypeSpecifier; use PHPStan\Analyser\TypeSpecifierContext; use PHPStan\DependencyInjection\AutowiredService; use PHPStan\Reflection\InitializerExprTypeResolver; @@ -33,10 +32,10 @@ use function sprintf; /** - * @implements TypeResolvingExprHandler + * @implements ExprHandler */ #[AutowiredService] -final class AssignOpHandler implements TypeResolvingExprHandler +final class AssignOpHandler implements ExprHandler { public function __construct( @@ -44,6 +43,7 @@ public function __construct( private InitializerExprTypeResolver $initializerExprTypeResolver, private ImplicitToStringCallHelper $implicitToStringCallHelper, private ExpressionResultFactory $expressionResultFactory, + private DefaultNarrowingHelper $defaultNarrowingHelper, ) { } @@ -56,6 +56,81 @@ public function supports(Expr $expr): bool public function processExpr(NodeScopeResolver $nodeScopeResolver, Stmt $stmt, Expr $expr, MutatingScope $scope, ExpressionResultStorage $storage, callable $nodeCallback, ExpressionContext $context): ExpressionResult { $beforeScope = $scope; + + $typeCallback = function (MutatingScope $s) use ($expr): Type { + $getType = static fn (Expr $e): Type => $s->getType($e); + + if ($expr instanceof Expr\AssignOp\Coalesce) { + return $s->getType(new BinaryOp\Coalesce($expr->var, $expr->expr, $expr->getAttributes())); + } + + if ($expr instanceof Expr\AssignOp\Concat) { + return $this->initializerExprTypeResolver->getConcatType($expr->var, $expr->expr, $getType); + } + + if ($expr instanceof Expr\AssignOp\BitwiseAnd) { + return $this->initializerExprTypeResolver->getBitwiseAndType($expr->var, $expr->expr, $getType); + } + + if ($expr instanceof Expr\AssignOp\BitwiseOr) { + return $this->initializerExprTypeResolver->getBitwiseOrType($expr->var, $expr->expr, $getType); + } + + if ($expr instanceof Expr\AssignOp\BitwiseXor) { + return $this->initializerExprTypeResolver->getBitwiseXorType($expr->var, $expr->expr, $getType); + } + + if ($expr instanceof Expr\AssignOp\Div) { + return $this->initializerExprTypeResolver->getDivType($expr->var, $expr->expr, $getType); + } + + if ($expr instanceof Expr\AssignOp\Mod) { + return $this->initializerExprTypeResolver->getModType($expr->var, $expr->expr, $getType); + } + + if ($expr instanceof Expr\AssignOp\Plus) { + return $this->initializerExprTypeResolver->getPlusType($expr->var, $expr->expr, $getType); + } + + if ($expr instanceof Expr\AssignOp\Minus) { + return $this->initializerExprTypeResolver->getMinusType($expr->var, $expr->expr, $getType); + } + + if ($expr instanceof Expr\AssignOp\Mul) { + return $this->initializerExprTypeResolver->getMulType($expr->var, $expr->expr, $getType); + } + + if ($expr instanceof Expr\AssignOp\Pow) { + return $this->initializerExprTypeResolver->getPowType($expr->var, $expr->expr, $getType); + } + + if ($expr instanceof Expr\AssignOp\ShiftLeft) { + return $this->initializerExprTypeResolver->getShiftLeftType($expr->var, $expr->expr, $getType); + } + + if ($expr instanceof Expr\AssignOp\ShiftRight) { + return $this->initializerExprTypeResolver->getShiftRightType($expr->var, $expr->expr, $getType); + } + + throw new ShouldNotHappenException(sprintf('Unhandled %s', get_class($expr))); + }; + $specifyTypesCallback = fn (MutatingScope $s, TypeSpecifierContext $context): SpecifiedTypes => $this->defaultNarrowingHelper->specifyDefaultTypes($expr, $context); + + // processAssignVar asks getType($expr) for the value to assign; store this + // result first so it resolves from the typeCallback above rather than + // re-processing the node on demand (which would recurse). + $nodeScopeResolver->storeExpressionResult($storage, $expr, $this->expressionResultFactory->create( + $scope, + beforeScope: $beforeScope, + expr: $expr, + hasYield: false, + isAlwaysTerminating: false, + throwPoints: [], + impurePoints: [], + typeCallback: $typeCallback, + specifyTypesCallback: $specifyTypesCallback, + )); + $assignResult = $this->assignHandler->processAssignVar( $nodeScopeResolver, $scope, @@ -114,71 +189,9 @@ function (MutatingScope $scope) use ($stmt, $expr, $nodeCallback, $context, $sto isAlwaysTerminating: $assignResult->isAlwaysTerminating(), throwPoints: $throwPoints, impurePoints: $impurePoints, + typeCallback: $typeCallback, + specifyTypesCallback: $specifyTypesCallback, ); } - public function resolveType(MutatingScope $scope, Expr $expr): Type - { - $getType = static fn (Expr $expr): Type => $scope->getType($expr); - - if ($expr instanceof Expr\AssignOp\Coalesce) { - return $scope->getType(new BinaryOp\Coalesce($expr->var, $expr->expr, $expr->getAttributes())); - } - - if ($expr instanceof Expr\AssignOp\Concat) { - return $this->initializerExprTypeResolver->getConcatType($expr->var, $expr->expr, $getType); - } - - if ($expr instanceof Expr\AssignOp\BitwiseAnd) { - return $this->initializerExprTypeResolver->getBitwiseAndType($expr->var, $expr->expr, $getType); - } - - if ($expr instanceof Expr\AssignOp\BitwiseOr) { - return $this->initializerExprTypeResolver->getBitwiseOrType($expr->var, $expr->expr, $getType); - } - - if ($expr instanceof Expr\AssignOp\BitwiseXor) { - return $this->initializerExprTypeResolver->getBitwiseXorType($expr->var, $expr->expr, $getType); - } - - if ($expr instanceof Expr\AssignOp\Div) { - return $this->initializerExprTypeResolver->getDivType($expr->var, $expr->expr, $getType); - } - - if ($expr instanceof Expr\AssignOp\Mod) { - return $this->initializerExprTypeResolver->getModType($expr->var, $expr->expr, $getType); - } - - if ($expr instanceof Expr\AssignOp\Plus) { - return $this->initializerExprTypeResolver->getPlusType($expr->var, $expr->expr, $getType); - } - - if ($expr instanceof Expr\AssignOp\Minus) { - return $this->initializerExprTypeResolver->getMinusType($expr->var, $expr->expr, $getType); - } - - if ($expr instanceof Expr\AssignOp\Mul) { - return $this->initializerExprTypeResolver->getMulType($expr->var, $expr->expr, $getType); - } - - if ($expr instanceof Expr\AssignOp\Pow) { - return $this->initializerExprTypeResolver->getPowType($expr->var, $expr->expr, $getType); - } - - if ($expr instanceof Expr\AssignOp\ShiftLeft) { - return $this->initializerExprTypeResolver->getShiftLeftType($expr->var, $expr->expr, $getType); - } - - if ($expr instanceof Expr\AssignOp\ShiftRight) { - return $this->initializerExprTypeResolver->getShiftRightType($expr->var, $expr->expr, $getType); - } - - throw new ShouldNotHappenException(sprintf('Unhandled %s', get_class($expr))); - } - - public function specifyTypes(TypeSpecifier $typeSpecifier, Scope $scope, Expr $expr, TypeSpecifierContext $context): SpecifiedTypes - { - return $typeSpecifier->specifyDefaultTypes($scope, $expr, $context); - } - } From bfaea31dddb53f340a35bb16b6e5ca37685db358 Mon Sep 17 00:00:00 2001 From: Ondrej Mirtes Date: Wed, 17 Jun 2026 15:52:19 +0200 Subject: [PATCH 059/113] AssignHandler is no longer TypeResolvingExprHandler --- src/Analyser/ExprHandler/AssignHandler.php | 260 ++++++--------------- 1 file changed, 68 insertions(+), 192 deletions(-) diff --git a/src/Analyser/ExprHandler/AssignHandler.php b/src/Analyser/ExprHandler/AssignHandler.php index 391a816f1e5..8acf202fd5d 100644 --- a/src/Analyser/ExprHandler/AssignHandler.php +++ b/src/Analyser/ExprHandler/AssignHandler.php @@ -28,6 +28,7 @@ use PHPStan\Analyser\ExpressionResultFactory; use PHPStan\Analyser\ExpressionResultStorage; use PHPStan\Analyser\ExpressionTypeHolder; +use PHPStan\Analyser\ExprHandler; use PHPStan\Analyser\ExprHandler\Helper\DefaultNarrowingHelper; use PHPStan\Analyser\ImpurePoint; use PHPStan\Analyser\InternalThrowPoint; @@ -36,7 +37,6 @@ use PHPStan\Analyser\NoopNodeCallback; use PHPStan\Analyser\Scope; use PHPStan\Analyser\SpecifiedTypes; -use PHPStan\Analyser\TypeResolvingExprHandler; use PHPStan\Analyser\TypeSpecifier; use PHPStan\Analyser\TypeSpecifierContext; use PHPStan\DependencyInjection\AutowiredService; @@ -86,10 +86,10 @@ use function is_string; /** - * @implements TypeResolvingExprHandler + * @implements ExprHandler */ #[AutowiredService] -final class AssignHandler implements TypeResolvingExprHandler +final class AssignHandler implements ExprHandler { public function __construct( @@ -108,189 +108,6 @@ public function supports(Expr $expr): bool return $expr instanceof Assign || $expr instanceof AssignRef; } - public function resolveType(MutatingScope $scope, Expr $expr): Type - { - return $scope->getType($expr->expr); - } - - public function specifyTypes(TypeSpecifier $typeSpecifier, Scope $scope, Expr $expr, TypeSpecifierContext $context): SpecifiedTypes - { - if (!$expr instanceof Assign) { - return $typeSpecifier->specifyDefaultTypes($scope, $expr, $context); - } - - if (!$scope instanceof MutatingScope) { - throw new ShouldNotHappenException(); - } - - if ($context->null()) { - $specifiedTypes = $typeSpecifier->specifyTypesInCondition($scope->exitFirstLevelStatements(), $expr->expr, $context)->setRootExpr($expr); - $specifiedTypes = $specifiedTypes->removeExpr($this->exprPrinter->printExpr($expr->var)); - } else { - $specifiedTypes = $typeSpecifier->specifyTypesInCondition($scope->exitFirstLevelStatements(), $expr->var, $context)->setRootExpr($expr); - } - - // infer $arr[$key] after $key = array_key_first/last($arr) - if ( - $expr->expr instanceof FuncCall - && $expr->expr->name instanceof Name - && !$expr->expr->isFirstClassCallable() - && in_array($expr->expr->name->toLowerString(), ['array_key_first', 'array_key_last'], true) - && count($expr->expr->getArgs()) >= 1 - ) { - $arrayArg = $expr->expr->getArgs()[0]->value; - $arrayType = $scope->getType($arrayArg); - - if ($arrayType->isArray()->yes()) { - if ($context->true()) { - $specifiedTypes = $specifiedTypes->unionWith( - $typeSpecifier->create($arrayArg, new NonEmptyArrayType(), TypeSpecifierContext::createTrue(), $scope), - ); - $isNonEmpty = true; - } else { - $isNonEmpty = $arrayType->isIterableAtLeastOnce()->yes(); - } - - if ($isNonEmpty) { - $dimFetch = new ArrayDimFetch($arrayArg, $expr->var); - $specifiedTypes = $specifiedTypes->unionWith( - $typeSpecifier->create($dimFetch, $arrayType->getIterableValueType(), TypeSpecifierContext::createTrue(), $scope), - ); - } elseif ($expr->var instanceof Expr\Variable && is_string($expr->var->name)) { - $keyType = $scope->getType($expr->expr); - $nonNullKeyType = TypeCombinator::removeNull($keyType); - if (!$nonNullKeyType instanceof NeverType) { - $specifiedTypes = $specifiedTypes->unionWith( - $this->createArrayDimFetchConditionalExpressionHolder($expr->var, $arrayArg, $nonNullKeyType, $arrayType->getIterableValueType()), - ); - } - } - } - } - - // infer $arr[$key] after $key = array_search($needle, $arr) or $key = array_find_key($arr, $callback) - if ( - $expr->expr instanceof FuncCall - && $expr->expr->name instanceof Name - && !$expr->expr->isFirstClassCallable() - && count($expr->expr->getArgs()) >= 2 - ) { - $funcName = $expr->expr->name->toLowerString(); - $arrayArg = null; - $sentinelType = null; - $isStrictArraySearch = false; - - if ($funcName === 'array_search') { - $arrayArg = $expr->expr->getArgs()[1]->value; - $sentinelType = new ConstantBooleanType(false); - $isStrictArraySearch = count($expr->expr->getArgs()) >= 3 && $scope->getType($expr->expr->getArgs()[2]->value)->isTrue()->yes(); - } elseif ($funcName === 'array_find_key') { - $arrayArg = $expr->expr->getArgs()[0]->value; - $sentinelType = new NullType(); - } - - if ($arrayArg !== null) { - $arrayType = $scope->getType($arrayArg); - - if ($arrayType->isArray()->yes()) { - if ($context->true()) { - $specifiedTypes = $specifiedTypes->unionWith( - $typeSpecifier->create($arrayArg, new NonEmptyArrayType(), TypeSpecifierContext::createTrue(), $scope), - ); - - $dimFetch = new ArrayDimFetch($arrayArg, $expr->var); - - if ($isStrictArraySearch) { - $needleType = $scope->getType($expr->expr->getArgs()[0]->value); - $dimFetchType = TypeCombinator::intersect($needleType, $arrayType->getIterableValueType()); - } else { - $dimFetchType = $arrayType->getIterableValueType(); - } - - $specifiedTypes = $specifiedTypes->unionWith( - $typeSpecifier->create($dimFetch, $dimFetchType, TypeSpecifierContext::createTrue(), $scope), - ); - } elseif ($expr->var instanceof Expr\Variable && is_string($expr->var->name)) { - $keyType = $scope->getType($expr->expr); - $narrowedKeyType = TypeCombinator::remove($keyType, $sentinelType); - if (!$narrowedKeyType instanceof NeverType) { - if ($isStrictArraySearch) { - $needleType = $scope->getType($expr->expr->getArgs()[0]->value); - $dimFetchType = TypeCombinator::intersect($needleType, $arrayType->getIterableValueType()); - } else { - $dimFetchType = $arrayType->getIterableValueType(); - } - $specifiedTypes = $specifiedTypes->unionWith( - $this->createArrayDimFetchConditionalExpressionHolder($expr->var, $arrayArg, $narrowedKeyType, $dimFetchType), - ); - } - } - } - } - } - - if ($context->null()) { - // infer $arr[$key] after $key = array_rand($arr) - if ( - $expr->expr instanceof FuncCall - && $expr->expr->name instanceof Name - && !$expr->expr->isFirstClassCallable() - && in_array($expr->expr->name->toLowerString(), ['array_rand'], true) - && count($expr->expr->getArgs()) >= 1 - ) { - $numArg = null; - $args = $expr->expr->getArgs(); - $arrayArg = $args[0]->value; - if (count($args) > 1) { - $numArg = $args[1]->value; - } - $one = new ConstantIntegerType(1); - $arrayType = $scope->getType($arrayArg); - - if ( - $arrayType->isArray()->yes() - && $arrayType->isIterableAtLeastOnce()->yes() - && ($numArg === null || $one->isSuperTypeOf($scope->getType($numArg))->yes()) - ) { - $dimFetch = new ArrayDimFetch($arrayArg, $expr->var); - - return $specifiedTypes->unionWith( - $typeSpecifier->create($dimFetch, $arrayType->getIterableValueType(), TypeSpecifierContext::createTrue(), $scope), - ); - } - } - - // infer $list[$count] after $count = count($list) - 1 - if ( - $expr->expr instanceof Expr\BinaryOp\Minus - && $expr->expr->left instanceof FuncCall - && $expr->expr->left->name instanceof Name - && !$expr->expr->left->isFirstClassCallable() - && $expr->expr->right instanceof Node\Scalar\Int_ - && $expr->expr->right->value === 1 - && in_array($expr->expr->left->name->toLowerString(), ['count', 'sizeof'], true) - && count($expr->expr->left->getArgs()) >= 1 - ) { - $arrayArg = $expr->expr->left->getArgs()[0]->value; - $arrayType = $scope->getType($arrayArg); - if ( - $arrayType->isList()->yes() - && $arrayType->isIterableAtLeastOnce()->yes() - ) { - $dimFetch = new ArrayDimFetch($arrayArg, $expr->var); - - return $specifiedTypes->unionWith( - $typeSpecifier->create($dimFetch, $arrayType->getIterableValueType(), TypeSpecifierContext::createTrue(), $scope), - ); - } - } - - return $specifiedTypes; - } - - return $specifiedTypes; - } - public function processExpr(NodeScopeResolver $nodeScopeResolver, Stmt $stmt, Expr $expr, MutatingScope $scope, ExpressionResultStorage $storage, callable $nodeCallback, ExpressionContext $context): ExpressionResult { $beforeScope = $scope; @@ -404,7 +221,8 @@ function (MutatingScope $scope) use ($stmt, $expr, $nodeCallback, $context, $sto isAlwaysTerminating: $result->isAlwaysTerminating(), throwPoints: $result->getThrowPoints(), impurePoints: $result->getImpurePoints(), - specifyTypesCallback: $expr instanceof Assign ? $this->createSpecifyTypesCallback($expr) : null, + typeCallback: static fn (MutatingScope $s): Type => $assignedExprResult !== null ? $assignedExprResult->getTypeForScope($s) : $s->getType($expr->expr), + specifyTypesCallback: $expr instanceof Assign ? $this->createSpecifyTypesCallback($expr, $assignedExprResult) : null, createTypesCallback: $expr instanceof Assign ? $this->createCreateTypesCallback($expr, $assignedExprResult) : null, ); } @@ -437,15 +255,16 @@ private function createCreateTypesCallback(Assign $expr, ?ExpressionResult $assi * * @return Closure(MutatingScope, TypeSpecifierContext): SpecifiedTypes */ - private function createSpecifyTypesCallback(Assign $expr): Closure + private function createSpecifyTypesCallback(Assign $expr, ?ExpressionResult $assignedExprResult): Closure { - return function (MutatingScope $s, TypeSpecifierContext $context) use ($expr): SpecifiedTypes { + return function (MutatingScope $s, TypeSpecifierContext $context) use ($expr, $assignedExprResult): SpecifiedTypes { if ($context->null()) { - return (new SpecifiedTypes([], []))->setRootExpr($expr); + $specifiedTypes = $this->defaultNarrowingHelper->getChildSpecifiedTypes($s->exitFirstLevelStatements(), $expr->expr, $assignedExprResult, $context)->setRootExpr($expr); + $specifiedTypes = $specifiedTypes->removeExpr($this->exprPrinter->printExpr($expr->var)); + } else { + $specifiedTypes = $this->defaultNarrowingHelper->specifyDefaultTypes($expr->var, $context)->setRootExpr($expr); } - $specifiedTypes = $this->defaultNarrowingHelper->specifyDefaultTypes($expr->var, $context)->setRootExpr($expr); - // infer $arr[$key] after $key = array_key_first/last($arr) if ( $expr->expr instanceof FuncCall @@ -545,6 +364,63 @@ private function createSpecifyTypesCallback(Assign $expr): Closure } } + if ($context->null()) { + // infer $arr[$key] after $key = array_rand($arr) + if ( + $expr->expr instanceof FuncCall + && $expr->expr->name instanceof Name + && !$expr->expr->isFirstClassCallable() + && in_array($expr->expr->name->toLowerString(), ['array_rand'], true) + && count($expr->expr->getArgs()) >= 1 + ) { + $numArg = null; + $args = $expr->expr->getArgs(); + $arrayArg = $args[0]->value; + if (count($args) > 1) { + $numArg = $args[1]->value; + } + $one = new ConstantIntegerType(1); + $arrayType = $s->getType($arrayArg); + + if ( + $arrayType->isArray()->yes() + && $arrayType->isIterableAtLeastOnce()->yes() + && ($numArg === null || $one->isSuperTypeOf($s->getType($numArg))->yes()) + ) { + $dimFetch = new ArrayDimFetch($arrayArg, $expr->var); + + return $specifiedTypes->unionWith( + $this->defaultNarrowingHelper->createSubjectTypes($s, $dimFetch, null, $arrayType->getIterableValueType(), TypeSpecifierContext::createTrue()), + ); + } + } + + // infer $list[$count] after $count = count($list) - 1 + if ( + $expr->expr instanceof Expr\BinaryOp\Minus + && $expr->expr->left instanceof FuncCall + && $expr->expr->left->name instanceof Name + && !$expr->expr->left->isFirstClassCallable() + && $expr->expr->right instanceof Node\Scalar\Int_ + && $expr->expr->right->value === 1 + && in_array($expr->expr->left->name->toLowerString(), ['count', 'sizeof'], true) + && count($expr->expr->left->getArgs()) >= 1 + ) { + $arrayArg = $expr->expr->left->getArgs()[0]->value; + $arrayType = $s->getType($arrayArg); + if ( + $arrayType->isList()->yes() + && $arrayType->isIterableAtLeastOnce()->yes() + ) { + $dimFetch = new ArrayDimFetch($arrayArg, $expr->var); + + return $specifiedTypes->unionWith( + $this->defaultNarrowingHelper->createSubjectTypes($s, $dimFetch, null, $arrayType->getIterableValueType(), TypeSpecifierContext::createTrue()), + ); + } + } + } + return $specifiedTypes; }; } From 8bbf88043863ea2410137b846201e588ede90748 Mon Sep 17 00:00:00 2001 From: Ondrej Mirtes Date: Wed, 17 Jun 2026 16:53:08 +0200 Subject: [PATCH 060/113] Track containsNullsafe on ExpressionResult and propagate it through fetch/call chains --- src/Analyser/ExprHandler/ArrayDimFetchHandler.php | 2 ++ src/Analyser/ExprHandler/MethodCallHandler.php | 1 + .../ExprHandler/NullsafeMethodCallHandler.php | 1 + .../ExprHandler/NullsafePropertyFetchHandler.php | 1 + src/Analyser/ExprHandler/PropertyFetchHandler.php | 1 + src/Analyser/ExprHandler/StaticCallHandler.php | 3 +++ .../ExprHandler/StaticPropertyFetchHandler.php | 3 +++ src/Analyser/ExpressionResult.php | 12 ++++++++++++ src/Analyser/ExpressionResultFactory.php | 1 + 9 files changed, 25 insertions(+) diff --git a/src/Analyser/ExprHandler/ArrayDimFetchHandler.php b/src/Analyser/ExprHandler/ArrayDimFetchHandler.php index 14dec18a003..891aa7cc1c6 100644 --- a/src/Analyser/ExprHandler/ArrayDimFetchHandler.php +++ b/src/Analyser/ExprHandler/ArrayDimFetchHandler.php @@ -94,6 +94,7 @@ public function processExpr(NodeScopeResolver $nodeScopeResolver, Stmt $stmt, Ex isAlwaysTerminating: $varResult->isAlwaysTerminating(), throwPoints: $varResult->getThrowPoints(), impurePoints: $varResult->getImpurePoints(), + containsNullsafe: $varResult->containsNullsafe(), ); } @@ -123,6 +124,7 @@ public function processExpr(NodeScopeResolver $nodeScopeResolver, Stmt $stmt, Ex isAlwaysTerminating: $dimResult->isAlwaysTerminating() || $varResult->isAlwaysTerminating(), throwPoints: $throwPoints, impurePoints: $impurePoints, + containsNullsafe: $varResult->containsNullsafe(), ); } diff --git a/src/Analyser/ExprHandler/MethodCallHandler.php b/src/Analyser/ExprHandler/MethodCallHandler.php index cb45d5441c5..d538a176625 100644 --- a/src/Analyser/ExprHandler/MethodCallHandler.php +++ b/src/Analyser/ExprHandler/MethodCallHandler.php @@ -203,6 +203,7 @@ public function processExpr(NodeScopeResolver $nodeScopeResolver, Stmt $stmt, Ex isAlwaysTerminating: $isAlwaysTerminating, throwPoints: $throwPoints, impurePoints: $impurePoints, + containsNullsafe: $varResult->containsNullsafe(), ); $calledOnType = $originalScope->getType($expr->var); diff --git a/src/Analyser/ExprHandler/NullsafeMethodCallHandler.php b/src/Analyser/ExprHandler/NullsafeMethodCallHandler.php index b5db844ed26..9f8ec0fce9f 100644 --- a/src/Analyser/ExprHandler/NullsafeMethodCallHandler.php +++ b/src/Analyser/ExprHandler/NullsafeMethodCallHandler.php @@ -126,6 +126,7 @@ public function processExpr(NodeScopeResolver $nodeScopeResolver, Stmt $stmt, Ex isAlwaysTerminating: false, throwPoints: $exprResult->getThrowPoints(), impurePoints: $exprResult->getImpurePoints(), + containsNullsafe: true, ); } diff --git a/src/Analyser/ExprHandler/NullsafePropertyFetchHandler.php b/src/Analyser/ExprHandler/NullsafePropertyFetchHandler.php index 017a57b723d..26bd5d41224 100644 --- a/src/Analyser/ExprHandler/NullsafePropertyFetchHandler.php +++ b/src/Analyser/ExprHandler/NullsafePropertyFetchHandler.php @@ -105,6 +105,7 @@ public function processExpr(NodeScopeResolver $nodeScopeResolver, Stmt $stmt, Ex isAlwaysTerminating: false, throwPoints: $exprResult->getThrowPoints(), impurePoints: $exprResult->getImpurePoints(), + containsNullsafe: true, ); } diff --git a/src/Analyser/ExprHandler/PropertyFetchHandler.php b/src/Analyser/ExprHandler/PropertyFetchHandler.php index f8a4ab70259..1eae089c621 100644 --- a/src/Analyser/ExprHandler/PropertyFetchHandler.php +++ b/src/Analyser/ExprHandler/PropertyFetchHandler.php @@ -92,6 +92,7 @@ public function processExpr(NodeScopeResolver $nodeScopeResolver, Stmt $stmt, Ex isAlwaysTerminating: $isAlwaysTerminating, throwPoints: $throwPoints, impurePoints: $impurePoints, + containsNullsafe: $varResult->containsNullsafe(), ); } diff --git a/src/Analyser/ExprHandler/StaticCallHandler.php b/src/Analyser/ExprHandler/StaticCallHandler.php index 833bc52a4ef..b99e49f9e93 100644 --- a/src/Analyser/ExprHandler/StaticCallHandler.php +++ b/src/Analyser/ExprHandler/StaticCallHandler.php @@ -86,6 +86,7 @@ public function processExpr(NodeScopeResolver $nodeScopeResolver, Stmt $stmt, Ex $throwPoints = []; $impurePoints = []; $isAlwaysTerminating = false; + $containsNullsafe = false; if ($expr->class instanceof Expr) { $classResult = $nodeScopeResolver->processExprNode($stmt, $expr->class, $scope, $storage, $nodeCallback, $context->enterDeep()); $hasYield = $classResult->hasYield(); @@ -94,6 +95,7 @@ public function processExpr(NodeScopeResolver $nodeScopeResolver, Stmt $stmt, Ex $isAlwaysTerminating = $classResult->isAlwaysTerminating(); $scope = $classResult->getScope(); + $containsNullsafe = $classResult->containsNullsafe(); } $parametersAcceptor = null; @@ -291,6 +293,7 @@ public function processExpr(NodeScopeResolver $nodeScopeResolver, Stmt $stmt, Ex isAlwaysTerminating: $isAlwaysTerminating, throwPoints: $throwPoints, impurePoints: $impurePoints, + containsNullsafe: $containsNullsafe, ); } diff --git a/src/Analyser/ExprHandler/StaticPropertyFetchHandler.php b/src/Analyser/ExprHandler/StaticPropertyFetchHandler.php index 4a1388c50e4..a6bdb2bf2fc 100644 --- a/src/Analyser/ExprHandler/StaticPropertyFetchHandler.php +++ b/src/Analyser/ExprHandler/StaticPropertyFetchHandler.php @@ -66,6 +66,7 @@ public function processExpr(NodeScopeResolver $nodeScopeResolver, Stmt $stmt, Ex ), ]; $isAlwaysTerminating = false; + $containsNullsafe = false; if ($expr->class instanceof Expr) { $classResult = $nodeScopeResolver->processExprNode($stmt, $expr->class, $scope, $storage, $nodeCallback, $context->enterDeep()); $hasYield = $classResult->hasYield(); @@ -73,6 +74,7 @@ public function processExpr(NodeScopeResolver $nodeScopeResolver, Stmt $stmt, Ex $impurePoints = $classResult->getImpurePoints(); $isAlwaysTerminating = $classResult->isAlwaysTerminating(); $scope = $classResult->getScope(); + $containsNullsafe = $classResult->containsNullsafe(); } if (!$expr->name instanceof VarLikeIdentifier) { $nameResult = $nodeScopeResolver->processExprNode($stmt, $expr->name, $scope, $storage, $nodeCallback, $context->enterDeep()); @@ -91,6 +93,7 @@ public function processExpr(NodeScopeResolver $nodeScopeResolver, Stmt $stmt, Ex isAlwaysTerminating: $isAlwaysTerminating, throwPoints: $throwPoints, impurePoints: $impurePoints, + containsNullsafe: $containsNullsafe, ); } diff --git a/src/Analyser/ExpressionResult.php b/src/Analyser/ExpressionResult.php index 43de235c169..736af44d8dd 100644 --- a/src/Analyser/ExpressionResult.php +++ b/src/Analyser/ExpressionResult.php @@ -53,6 +53,7 @@ public function __construct( private bool $isAlwaysTerminating, private array $throwPoints, private array $impurePoints, + private bool $containsNullsafe = false, ?callable $truthyScopeCallback = null, ?callable $falseyScopeCallback = null, ?callable $typeCallback = null, @@ -82,6 +83,17 @@ public function hasYield(): bool return $this->hasYield; } + /** + * Whether this expression's chain contains a nullsafe operator (?->). A + * fetch/call on a receiver whose chain short-circuits propagates null, + * which a plain nullable receiver (e.g. a nullable variable) does not - + * this flag is what tells them apart. + */ + public function containsNullsafe(): bool + { + return $this->containsNullsafe; + } + /** * @return InternalThrowPoint[] */ diff --git a/src/Analyser/ExpressionResultFactory.php b/src/Analyser/ExpressionResultFactory.php index 83172cd7eb6..355bd4bf615 100644 --- a/src/Analyser/ExpressionResultFactory.php +++ b/src/Analyser/ExpressionResultFactory.php @@ -25,6 +25,7 @@ public function create( bool $isAlwaysTerminating, array $throwPoints, array $impurePoints, + bool $containsNullsafe = false, ?callable $truthyScopeCallback = null, ?callable $falseyScopeCallback = null, ?callable $typeCallback = null, From c017942cf067430e0f28850dcdd0df624a0be086 Mon Sep 17 00:00:00 2001 From: Ondrej Mirtes Date: Wed, 17 Jun 2026 17:02:56 +0200 Subject: [PATCH 061/113] PropertyFetchHandler is no longer TypeResolvingExprHandler --- .../ExprHandler/PropertyFetchHandler.php | 105 +++++++++--------- 1 file changed, 52 insertions(+), 53 deletions(-) diff --git a/src/Analyser/ExprHandler/PropertyFetchHandler.php b/src/Analyser/ExprHandler/PropertyFetchHandler.php index 1eae089c621..5fa0298e1bf 100644 --- a/src/Analyser/ExprHandler/PropertyFetchHandler.php +++ b/src/Analyser/ExprHandler/PropertyFetchHandler.php @@ -11,14 +11,12 @@ use PHPStan\Analyser\ExpressionResult; use PHPStan\Analyser\ExpressionResultFactory; use PHPStan\Analyser\ExpressionResultStorage; -use PHPStan\Analyser\ExprHandler\Helper\NullsafeShortCircuitingHelper; +use PHPStan\Analyser\ExprHandler; +use PHPStan\Analyser\ExprHandler\Helper\DefaultNarrowingHelper; use PHPStan\Analyser\InternalThrowPoint; use PHPStan\Analyser\MutatingScope; use PHPStan\Analyser\NodeScopeResolver; -use PHPStan\Analyser\Scope; use PHPStan\Analyser\SpecifiedTypes; -use PHPStan\Analyser\TypeResolvingExprHandler; -use PHPStan\Analyser\TypeSpecifier; use PHPStan\Analyser\TypeSpecifierContext; use PHPStan\DependencyInjection\AutowiredService; use PHPStan\Php\PhpVersion; @@ -32,16 +30,17 @@ use function count; /** - * @implements TypeResolvingExprHandler + * @implements ExprHandler */ #[AutowiredService] -final class PropertyFetchHandler implements TypeResolvingExprHandler +final class PropertyFetchHandler implements ExprHandler { public function __construct( private PhpVersion $phpVersion, private PropertyReflectionFinder $propertyReflectionFinder, private ExpressionResultFactory $expressionResultFactory, + private DefaultNarrowingHelper $defaultNarrowingHelper, ) { } @@ -61,6 +60,7 @@ public function processExpr(NodeScopeResolver $nodeScopeResolver, Stmt $stmt, Ex $impurePoints = $varResult->getImpurePoints(); $isAlwaysTerminating = $varResult->isAlwaysTerminating(); $scope = $varResult->getScope(); + $nameResult = null; if ($expr->name instanceof Identifier) { $propertyName = $expr->name->toString(); $propertyHolderType = $scopeBeforeVar->getType($expr->var); @@ -93,52 +93,56 @@ public function processExpr(NodeScopeResolver $nodeScopeResolver, Stmt $stmt, Ex throwPoints: $throwPoints, impurePoints: $impurePoints, containsNullsafe: $varResult->containsNullsafe(), - ); - } - - public function resolveType(MutatingScope $scope, Expr $expr): Type - { - if ($expr->name instanceof Identifier) { - if ($scope->nativeTypesPromoted) { - $propertyReflection = $this->propertyReflectionFinder->findPropertyReflectionFromNode($expr, $scope); - if ($propertyReflection === null) { - return new ErrorType(); + typeCallback: function (MutatingScope $s) use ($expr, $varResult, $nameResult): Type { + // a fetch on a nullsafe chain whose receiver is currently nullable + // short-circuits to null - the receiver result carries whether the + // chain contains a ?-> (a plain nullable receiver does not propagate) + $shortCircuit = static fn (Type $type): Type => $varResult->containsNullsafe() && TypeCombinator::containsNull($varResult->getTypeForScope($s)) + ? TypeCombinator::addNull($type) + : $type; + + if ($expr->name instanceof Identifier) { + if ($s->nativeTypesPromoted) { + $propertyReflection = $this->propertyReflectionFinder->findPropertyReflectionFromNode($expr, $s); + if ($propertyReflection === null) { + return new ErrorType(); + } + + if (!$propertyReflection->hasNativeType()) { + return new MixedType(); + } + + return $shortCircuit($propertyReflection->getNativeType()); + } + + $returnType = $this->propertyFetchType( + $s, + $varResult->getTypeForScope($s), + $expr->name->name, + $expr, + ); + if ($returnType === null) { + $returnType = new ErrorType(); + } + + return $shortCircuit($returnType); } - if (!$propertyReflection->hasNativeType()) { - return new MixedType(); + $nameType = $nameResult !== null ? $nameResult->getTypeForScope($s) : $s->getType($expr->name); + if (count($nameType->getConstantStrings()) > 0) { + return TypeCombinator::union( + ...array_map(static fn ($constantString) => $constantString->getValue() === '' ? new ErrorType() : $s + ->filterByTruthyValue(new Expr\BinaryOp\Identical($expr->name, new String_($constantString->getValue()))) + ->getType( + new PropertyFetch($expr->var, new Identifier($constantString->getValue())), + ), $nameType->getConstantStrings()), + ); } - $nativeType = $propertyReflection->getNativeType(); - - return NullsafeShortCircuitingHelper::getType($scope, $expr->var, $nativeType); - } - - $returnType = $this->propertyFetchType( - $scope, - $scope->getType($expr->var), - $expr->name->name, - $expr, - ); - if ($returnType === null) { - $returnType = new ErrorType(); - } - - return NullsafeShortCircuitingHelper::getType($scope, $expr->var, $returnType); - } - - $nameType = $scope->getType($expr->name); - if (count($nameType->getConstantStrings()) > 0) { - return TypeCombinator::union( - ...array_map(static fn ($constantString) => $constantString->getValue() === '' ? new ErrorType() : $scope - ->filterByTruthyValue(new Expr\BinaryOp\Identical($expr->name, new String_($constantString->getValue()))) - ->getType( - new PropertyFetch($expr->var, new Identifier($constantString->getValue())), - ), $nameType->getConstantStrings()), - ); - } - - return new MixedType(); + return new MixedType(); + }, + specifyTypesCallback: fn (MutatingScope $s, TypeSpecifierContext $context): SpecifiedTypes => $this->defaultNarrowingHelper->specifyDefaultTypes($expr, $context), + ); } private function propertyFetchType(MutatingScope $scope, Type $fetchedOnType, string $propertyName, PropertyFetch $propertyFetch): ?Type @@ -155,9 +159,4 @@ private function propertyFetchType(MutatingScope $scope, Type $fetchedOnType, st return $propertyReflection->getReadableType(); } - public function specifyTypes(TypeSpecifier $typeSpecifier, Scope $scope, Expr $expr, TypeSpecifierContext $context): SpecifiedTypes - { - return $typeSpecifier->specifyDefaultTypes($scope, $expr, $context); - } - } From 9a8958839a90786cf5d29d45aad622ccafbb639e Mon Sep 17 00:00:00 2001 From: Ondrej Mirtes Date: Wed, 17 Jun 2026 17:23:40 +0200 Subject: [PATCH 062/113] StaticPropertyFetchHandler is no longer TypeResolvingExprHandler --- .../StaticPropertyFetchHandler.php | 122 ++++++++---------- 1 file changed, 55 insertions(+), 67 deletions(-) diff --git a/src/Analyser/ExprHandler/StaticPropertyFetchHandler.php b/src/Analyser/ExprHandler/StaticPropertyFetchHandler.php index a6bdb2bf2fc..0f1e84574f6 100644 --- a/src/Analyser/ExprHandler/StaticPropertyFetchHandler.php +++ b/src/Analyser/ExprHandler/StaticPropertyFetchHandler.php @@ -13,14 +13,12 @@ use PHPStan\Analyser\ExpressionResult; use PHPStan\Analyser\ExpressionResultFactory; use PHPStan\Analyser\ExpressionResultStorage; -use PHPStan\Analyser\ExprHandler\Helper\NullsafeShortCircuitingHelper; +use PHPStan\Analyser\ExprHandler; +use PHPStan\Analyser\ExprHandler\Helper\DefaultNarrowingHelper; use PHPStan\Analyser\ImpurePoint; use PHPStan\Analyser\MutatingScope; use PHPStan\Analyser\NodeScopeResolver; -use PHPStan\Analyser\Scope; use PHPStan\Analyser\SpecifiedTypes; -use PHPStan\Analyser\TypeResolvingExprHandler; -use PHPStan\Analyser\TypeSpecifier; use PHPStan\Analyser\TypeSpecifierContext; use PHPStan\DependencyInjection\AutowiredService; use PHPStan\Rules\Properties\PropertyReflectionFinder; @@ -33,15 +31,16 @@ use function count; /** - * @implements TypeResolvingExprHandler + * @implements ExprHandler */ #[AutowiredService] -final class StaticPropertyFetchHandler implements TypeResolvingExprHandler +final class StaticPropertyFetchHandler implements ExprHandler { public function __construct( private PropertyReflectionFinder $propertyReflectionFinder, private ExpressionResultFactory $expressionResultFactory, + private DefaultNarrowingHelper $defaultNarrowingHelper, ) { } @@ -66,7 +65,7 @@ public function processExpr(NodeScopeResolver $nodeScopeResolver, Stmt $stmt, Ex ), ]; $isAlwaysTerminating = false; - $containsNullsafe = false; + $classResult = null; if ($expr->class instanceof Expr) { $classResult = $nodeScopeResolver->processExprNode($stmt, $expr->class, $scope, $storage, $nodeCallback, $context->enterDeep()); $hasYield = $classResult->hasYield(); @@ -74,8 +73,8 @@ public function processExpr(NodeScopeResolver $nodeScopeResolver, Stmt $stmt, Ex $impurePoints = $classResult->getImpurePoints(); $isAlwaysTerminating = $classResult->isAlwaysTerminating(); $scope = $classResult->getScope(); - $containsNullsafe = $classResult->containsNullsafe(); } + $nameResult = null; if (!$expr->name instanceof VarLikeIdentifier) { $nameResult = $nodeScopeResolver->processExprNode($stmt, $expr->name, $scope, $storage, $nodeCallback, $context->enterDeep()); $hasYield = $hasYield || $nameResult->hasYield(); @@ -93,64 +92,58 @@ public function processExpr(NodeScopeResolver $nodeScopeResolver, Stmt $stmt, Ex isAlwaysTerminating: $isAlwaysTerminating, throwPoints: $throwPoints, impurePoints: $impurePoints, - containsNullsafe: $containsNullsafe, - ); - } - - public function resolveType(MutatingScope $scope, Expr $expr): Type - { - if ($expr->name instanceof VarLikeIdentifier) { - if ($scope->nativeTypesPromoted) { - $propertyReflection = $this->propertyReflectionFinder->findPropertyReflectionFromNode($expr, $scope); - if ($propertyReflection === null) { - return new ErrorType(); - } - if (!$propertyReflection->hasNativeType()) { - return new MixedType(); + containsNullsafe: $classResult !== null && $classResult->containsNullsafe(), + typeCallback: function (MutatingScope $s) use ($expr, $classResult, $nameResult): Type { + $shortCircuit = static fn (Type $type): Type => $classResult !== null && $classResult->containsNullsafe() && TypeCombinator::containsNull($classResult->getTypeForScope($s)) + ? TypeCombinator::addNull($type) + : $type; + + if ($expr->name instanceof VarLikeIdentifier) { + if ($s->nativeTypesPromoted) { + $propertyReflection = $this->propertyReflectionFinder->findPropertyReflectionFromNode($expr, $s); + if ($propertyReflection === null) { + return new ErrorType(); + } + if (!$propertyReflection->hasNativeType()) { + return new MixedType(); + } + + return $shortCircuit($propertyReflection->getNativeType()); + } + + if ($expr->class instanceof Name) { + $staticPropertyFetchedOnType = $s->resolveTypeByName($expr->class); + } else { + $classType = $classResult !== null ? $classResult->getTypeForScope($s) : $s->getType($expr->class); + $staticPropertyFetchedOnType = TypeCombinator::removeNull($classType)->getObjectTypeOrClassStringObjectType(); + } + + $fetchType = $this->propertyFetchType( + $s, + $staticPropertyFetchedOnType, + $expr->name->toString(), + $expr, + ); + if ($fetchType === null) { + $fetchType = new ErrorType(); + } + + return $shortCircuit($fetchType); } - $nativeType = $propertyReflection->getNativeType(); - - if ($expr->class instanceof Expr) { - return NullsafeShortCircuitingHelper::getType($scope, $expr->class, $nativeType); + $nameType = $nameResult !== null ? $nameResult->getTypeForScope($s) : $s->getType($expr->name); + if (count($nameType->getConstantStrings()) > 0) { + return TypeCombinator::union( + ...array_map(static fn ($constantString) => $constantString->getValue() === '' ? new ErrorType() : $s + ->filterByTruthyValue(new Identical($expr->name, new String_($constantString->getValue()))) + ->getType(new Expr\StaticPropertyFetch($expr->class, new VarLikeIdentifier($constantString->getValue()))), $nameType->getConstantStrings()), + ); } - return $nativeType; - } - - if ($expr->class instanceof Name) { - $staticPropertyFetchedOnType = $scope->resolveTypeByName($expr->class); - } else { - $staticPropertyFetchedOnType = TypeCombinator::removeNull($scope->getType($expr->class))->getObjectTypeOrClassStringObjectType(); - } - - $fetchType = $this->propertyFetchType( - $scope, - $staticPropertyFetchedOnType, - $expr->name->toString(), - $expr, - ); - if ($fetchType === null) { - $fetchType = new ErrorType(); - } - - if ($expr->class instanceof Expr) { - return NullsafeShortCircuitingHelper::getType($scope, $expr->class, $fetchType); - } - - return $fetchType; - } - - $nameType = $scope->getType($expr->name); - if (count($nameType->getConstantStrings()) > 0) { - return TypeCombinator::union( - ...array_map(static fn ($constantString) => $constantString->getValue() === '' ? new ErrorType() : $scope - ->filterByTruthyValue(new Identical($expr->name, new String_($constantString->getValue()))) - ->getType(new Expr\StaticPropertyFetch($expr->class, new VarLikeIdentifier($constantString->getValue()))), $nameType->getConstantStrings()), - ); - } - - return new MixedType(); + return new MixedType(); + }, + specifyTypesCallback: fn (MutatingScope $s, TypeSpecifierContext $context): SpecifiedTypes => $this->defaultNarrowingHelper->specifyDefaultTypes($expr, $context), + ); } private function propertyFetchType(MutatingScope $scope, Type $fetchedOnType, string $propertyName, StaticPropertyFetch $propertyFetch): ?Type @@ -167,9 +160,4 @@ private function propertyFetchType(MutatingScope $scope, Type $fetchedOnType, st return $propertyReflection->getReadableType(); } - public function specifyTypes(TypeSpecifier $typeSpecifier, Scope $scope, Expr $expr, TypeSpecifierContext $context): SpecifiedTypes - { - return $typeSpecifier->specifyDefaultTypes($scope, $expr, $context); - } - } From 3e4e36771674900b2602be41133c985b2bae20c8 Mon Sep 17 00:00:00 2001 From: Ondrej Mirtes Date: Wed, 17 Jun 2026 17:32:05 +0200 Subject: [PATCH 063/113] Store ArrayDimFetch assign-target results with a typeCallback --- src/Analyser/ExprHandler/AssignHandler.php | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/Analyser/ExprHandler/AssignHandler.php b/src/Analyser/ExprHandler/AssignHandler.php index 8acf202fd5d..181b7536332 100644 --- a/src/Analyser/ExprHandler/AssignHandler.php +++ b/src/Analyser/ExprHandler/AssignHandler.php @@ -629,6 +629,7 @@ public function processAssignVar( isAlwaysTerminating: false, throwPoints: [], impurePoints: [], + typeCallback: static fn (): Type => new NeverType(), )); } else { @@ -646,6 +647,7 @@ public function processAssignVar( isAlwaysTerminating: false, throwPoints: [], impurePoints: [], + typeCallback: static fn (MutatingScope $s): Type => $s->getType($dimFetch->var)->getOffsetValueType($s->getType($dimExpr)), )); $result = $nodeScopeResolver->processExprNode($stmt, $dimExpr, $scope, $storage, $nodeCallback, $context->enterDeep()); $hasYield = $hasYield || $result->hasYield(); From d4fdf57eab8a7ad55edb9a79d9a3036fcc41e2bf Mon Sep 17 00:00:00 2001 From: Ondrej Mirtes Date: Wed, 17 Jun 2026 17:32:05 +0200 Subject: [PATCH 064/113] ArrayDimFetchHandler is no longer TypeResolvingExprHandler --- .../ExprHandler/ArrayDimFetchHandler.php | 82 ++++++++----------- 1 file changed, 36 insertions(+), 46 deletions(-) diff --git a/src/Analyser/ExprHandler/ArrayDimFetchHandler.php b/src/Analyser/ExprHandler/ArrayDimFetchHandler.php index 891aa7cc1c6..d4f95b7abb9 100644 --- a/src/Analyser/ExprHandler/ArrayDimFetchHandler.php +++ b/src/Analyser/ExprHandler/ArrayDimFetchHandler.php @@ -13,30 +13,32 @@ use PHPStan\Analyser\ExpressionResult; use PHPStan\Analyser\ExpressionResultFactory; use PHPStan\Analyser\ExpressionResultStorage; -use PHPStan\Analyser\ExprHandler\Helper\NullsafeShortCircuitingHelper; +use PHPStan\Analyser\ExprHandler; +use PHPStan\Analyser\ExprHandler\Helper\DefaultNarrowingHelper; use PHPStan\Analyser\MutatingScope; use PHPStan\Analyser\NodeScopeResolver; use PHPStan\Analyser\NoopNodeCallback; -use PHPStan\Analyser\Scope; use PHPStan\Analyser\SpecifiedTypes; -use PHPStan\Analyser\TypeResolvingExprHandler; -use PHPStan\Analyser\TypeSpecifier; use PHPStan\Analyser\TypeSpecifierContext; use PHPStan\DependencyInjection\AutowiredService; use PHPStan\Node\Expr\TypeExpr; use PHPStan\Type\NeverType; use PHPStan\Type\ObjectType; use PHPStan\Type\Type; +use PHPStan\Type\TypeCombinator; use function array_merge; /** - * @implements TypeResolvingExprHandler + * @implements ExprHandler */ #[AutowiredService] -final class ArrayDimFetchHandler implements TypeResolvingExprHandler +final class ArrayDimFetchHandler implements ExprHandler { - public function __construct(private ExpressionResultFactory $expressionResultFactory) + public function __construct( + private ExpressionResultFactory $expressionResultFactory, + private DefaultNarrowingHelper $defaultNarrowingHelper, + ) { } @@ -45,40 +47,6 @@ public function supports(Expr $expr): bool return $expr instanceof ArrayDimFetch; } - public function resolveType(MutatingScope $scope, Expr $expr): Type - { - if ($expr->dim === null) { - return new NeverType(); - } - - $offsetAccessibleType = $scope->getType($expr->var); - if ( - !$offsetAccessibleType->isArray()->yes() - && (new ObjectType(ArrayAccess::class))->isSuperTypeOf($offsetAccessibleType)->yes() - ) { - return NullsafeShortCircuitingHelper::getType( - $scope, - $expr->var, - $scope->getType( - new MethodCall( - $expr->var, - new Identifier('offsetGet'), - [ - new Arg($expr->dim), - ], - ), - ), - ); - } - - $offsetType = $scope->getType($expr->dim); - return NullsafeShortCircuitingHelper::getType( - $scope, - $expr->var, - $offsetAccessibleType->getOffsetValueType($offsetType), - ); - } - public function processExpr(NodeScopeResolver $nodeScopeResolver, Stmt $stmt, Expr $expr, MutatingScope $scope, ExpressionResultStorage $storage, callable $nodeCallback, ExpressionContext $context): ExpressionResult { $beforeScope = $scope; @@ -95,6 +63,9 @@ public function processExpr(NodeScopeResolver $nodeScopeResolver, Stmt $stmt, Ex throwPoints: $varResult->getThrowPoints(), impurePoints: $varResult->getImpurePoints(), containsNullsafe: $varResult->containsNullsafe(), + // `$arr[]` only appears as an assignment target; reading it is a NeverType + typeCallback: static fn (): Type => new NeverType(), + specifyTypesCallback: fn (MutatingScope $s, TypeSpecifierContext $context): SpecifiedTypes => $this->defaultNarrowingHelper->specifyDefaultTypes($expr, $context), ); } @@ -125,12 +96,31 @@ public function processExpr(NodeScopeResolver $nodeScopeResolver, Stmt $stmt, Ex throwPoints: $throwPoints, impurePoints: $impurePoints, containsNullsafe: $varResult->containsNullsafe(), - ); - } + typeCallback: function (MutatingScope $s) use ($expr, $varResult, $dimResult): Type { + $offsetAccessibleType = $varResult->getTypeForScope($s); + $shortCircuit = static fn (Type $type): Type => $varResult->containsNullsafe() && TypeCombinator::containsNull($offsetAccessibleType) + ? TypeCombinator::addNull($type) + : $type; - public function specifyTypes(TypeSpecifier $typeSpecifier, Scope $scope, Expr $expr, TypeSpecifierContext $context): SpecifiedTypes - { - return $typeSpecifier->specifyDefaultTypes($scope, $expr, $context); + if ( + !$offsetAccessibleType->isArray()->yes() + && (new ObjectType(ArrayAccess::class))->isSuperTypeOf($offsetAccessibleType)->yes() + ) { + return $shortCircuit($s->getType( + new MethodCall( + $expr->var, + new Identifier('offsetGet'), + [ + new Arg($expr->dim), + ], + ), + )); + } + + return $shortCircuit($offsetAccessibleType->getOffsetValueType($dimResult->getTypeForScope($s))); + }, + specifyTypesCallback: fn (MutatingScope $s, TypeSpecifierContext $context): SpecifiedTypes => $this->defaultNarrowingHelper->specifyDefaultTypes($expr, $context), + ); } } From 3ea7cdd6203e557b52dbb7874588f417702aa9e5 Mon Sep 17 00:00:00 2001 From: Ondrej Mirtes Date: Wed, 17 Jun 2026 18:23:04 +0200 Subject: [PATCH 065/113] Introduce IssetabilityDescriptor and fold it in MutatingScope::issetCheck --- .../ExprHandler/ArrayDimFetchHandler.php | 2 + .../ExprHandler/PropertyFetchHandler.php | 3 + .../StaticPropertyFetchHandler.php | 3 + src/Analyser/ExprHandler/VariableHandler.php | 2 + src/Analyser/ExpressionResult.php | 11 + src/Analyser/ExpressionResultFactory.php | 1 + src/Analyser/IssetabilityDescriptor.php | 228 ++++++++++++++++++ src/Analyser/MutatingScope.php | 11 + 8 files changed, 261 insertions(+) create mode 100644 src/Analyser/IssetabilityDescriptor.php diff --git a/src/Analyser/ExprHandler/ArrayDimFetchHandler.php b/src/Analyser/ExprHandler/ArrayDimFetchHandler.php index d4f95b7abb9..efcb21cce2d 100644 --- a/src/Analyser/ExprHandler/ArrayDimFetchHandler.php +++ b/src/Analyser/ExprHandler/ArrayDimFetchHandler.php @@ -15,6 +15,7 @@ use PHPStan\Analyser\ExpressionResultStorage; use PHPStan\Analyser\ExprHandler; use PHPStan\Analyser\ExprHandler\Helper\DefaultNarrowingHelper; +use PHPStan\Analyser\IssetabilityDescriptor; use PHPStan\Analyser\MutatingScope; use PHPStan\Analyser\NodeScopeResolver; use PHPStan\Analyser\NoopNodeCallback; @@ -96,6 +97,7 @@ public function processExpr(NodeScopeResolver $nodeScopeResolver, Stmt $stmt, Ex throwPoints: $throwPoints, impurePoints: $impurePoints, containsNullsafe: $varResult->containsNullsafe(), + issetabilityDescriptor: IssetabilityDescriptor::offset($varResult, $dimResult), typeCallback: function (MutatingScope $s) use ($expr, $varResult, $dimResult): Type { $offsetAccessibleType = $varResult->getTypeForScope($s); $shortCircuit = static fn (Type $type): Type => $varResult->containsNullsafe() && TypeCombinator::containsNull($offsetAccessibleType) diff --git a/src/Analyser/ExprHandler/PropertyFetchHandler.php b/src/Analyser/ExprHandler/PropertyFetchHandler.php index 5fa0298e1bf..69a39439697 100644 --- a/src/Analyser/ExprHandler/PropertyFetchHandler.php +++ b/src/Analyser/ExprHandler/PropertyFetchHandler.php @@ -14,12 +14,14 @@ use PHPStan\Analyser\ExprHandler; use PHPStan\Analyser\ExprHandler\Helper\DefaultNarrowingHelper; use PHPStan\Analyser\InternalThrowPoint; +use PHPStan\Analyser\IssetabilityDescriptor; use PHPStan\Analyser\MutatingScope; use PHPStan\Analyser\NodeScopeResolver; use PHPStan\Analyser\SpecifiedTypes; use PHPStan\Analyser\TypeSpecifierContext; use PHPStan\DependencyInjection\AutowiredService; use PHPStan\Php\PhpVersion; +use PHPStan\Rules\Properties\FoundPropertyReflection; use PHPStan\Rules\Properties\PropertyReflectionFinder; use PHPStan\Type\ErrorType; use PHPStan\Type\MixedType; @@ -93,6 +95,7 @@ public function processExpr(NodeScopeResolver $nodeScopeResolver, Stmt $stmt, Ex throwPoints: $throwPoints, impurePoints: $impurePoints, containsNullsafe: $varResult->containsNullsafe(), + issetabilityDescriptor: IssetabilityDescriptor::property($varResult, fn (MutatingScope $s): ?FoundPropertyReflection => $this->propertyReflectionFinder->findPropertyReflectionFromNode($expr, $s), $expr), typeCallback: function (MutatingScope $s) use ($expr, $varResult, $nameResult): Type { // a fetch on a nullsafe chain whose receiver is currently nullable // short-circuits to null - the receiver result carries whether the diff --git a/src/Analyser/ExprHandler/StaticPropertyFetchHandler.php b/src/Analyser/ExprHandler/StaticPropertyFetchHandler.php index 0f1e84574f6..607ad2df610 100644 --- a/src/Analyser/ExprHandler/StaticPropertyFetchHandler.php +++ b/src/Analyser/ExprHandler/StaticPropertyFetchHandler.php @@ -16,11 +16,13 @@ use PHPStan\Analyser\ExprHandler; use PHPStan\Analyser\ExprHandler\Helper\DefaultNarrowingHelper; use PHPStan\Analyser\ImpurePoint; +use PHPStan\Analyser\IssetabilityDescriptor; use PHPStan\Analyser\MutatingScope; use PHPStan\Analyser\NodeScopeResolver; use PHPStan\Analyser\SpecifiedTypes; use PHPStan\Analyser\TypeSpecifierContext; use PHPStan\DependencyInjection\AutowiredService; +use PHPStan\Rules\Properties\FoundPropertyReflection; use PHPStan\Rules\Properties\PropertyReflectionFinder; use PHPStan\Type\ErrorType; use PHPStan\Type\MixedType; @@ -93,6 +95,7 @@ public function processExpr(NodeScopeResolver $nodeScopeResolver, Stmt $stmt, Ex throwPoints: $throwPoints, impurePoints: $impurePoints, containsNullsafe: $classResult !== null && $classResult->containsNullsafe(), + issetabilityDescriptor: IssetabilityDescriptor::property($classResult, fn (MutatingScope $s): ?FoundPropertyReflection => $this->propertyReflectionFinder->findPropertyReflectionFromNode($expr, $s), $expr), typeCallback: function (MutatingScope $s) use ($expr, $classResult, $nameResult): Type { $shortCircuit = static fn (Type $type): Type => $classResult !== null && $classResult->containsNullsafe() && TypeCombinator::containsNull($classResult->getTypeForScope($s)) ? TypeCombinator::addNull($type) diff --git a/src/Analyser/ExprHandler/VariableHandler.php b/src/Analyser/ExprHandler/VariableHandler.php index 03692785a53..07ab8493bf3 100644 --- a/src/Analyser/ExprHandler/VariableHandler.php +++ b/src/Analyser/ExprHandler/VariableHandler.php @@ -15,6 +15,7 @@ use PHPStan\Analyser\ExprHandler; use PHPStan\Analyser\ExprHandler\Helper\DefaultNarrowingHelper; use PHPStan\Analyser\ImpurePoint; +use PHPStan\Analyser\IssetabilityDescriptor; use PHPStan\Analyser\MutatingScope; use PHPStan\Analyser\NodeScopeResolver; use PHPStan\Analyser\Scope; @@ -122,6 +123,7 @@ public function processExpr(NodeScopeResolver $nodeScopeResolver, Stmt $stmt, Ex isAlwaysTerminating: $isAlwaysTerminating, throwPoints: $throwPoints, impurePoints: $impurePoints, + issetabilityDescriptor: is_string($expr->name) ? IssetabilityDescriptor::variable($expr->name) : null, typeCallback: self::createTypeCallback($expr, $nameResult), specifyTypesCallback: fn (MutatingScope $s, TypeSpecifierContext $context): SpecifiedTypes => $this->defaultNarrowingHelper->specifyDefaultTypes($expr, $context), ); diff --git a/src/Analyser/ExpressionResult.php b/src/Analyser/ExpressionResult.php index 736af44d8dd..d1e80322384 100644 --- a/src/Analyser/ExpressionResult.php +++ b/src/Analyser/ExpressionResult.php @@ -54,6 +54,7 @@ public function __construct( private array $throwPoints, private array $impurePoints, private bool $containsNullsafe = false, + private ?IssetabilityDescriptor $issetabilityDescriptor = null, ?callable $truthyScopeCallback = null, ?callable $falseyScopeCallback = null, ?callable $typeCallback = null, @@ -94,6 +95,16 @@ public function containsNullsafe(): bool return $this->containsNullsafe; } + /** + * The isset/empty/?? chain descriptor for this expression, or null when the + * expression is not a variable / array dim fetch / property fetch chain link + * (in which case isset() falls back to the leaf type check). + */ + public function getIssetabilityDescriptor(): ?IssetabilityDescriptor + { + return $this->issetabilityDescriptor; + } + /** * @return InternalThrowPoint[] */ diff --git a/src/Analyser/ExpressionResultFactory.php b/src/Analyser/ExpressionResultFactory.php index 355bd4bf615..b996c62ce30 100644 --- a/src/Analyser/ExpressionResultFactory.php +++ b/src/Analyser/ExpressionResultFactory.php @@ -26,6 +26,7 @@ public function create( array $throwPoints, array $impurePoints, bool $containsNullsafe = false, + ?IssetabilityDescriptor $issetabilityDescriptor = null, ?callable $truthyScopeCallback = null, ?callable $falseyScopeCallback = null, ?callable $typeCallback = null, diff --git a/src/Analyser/IssetabilityDescriptor.php b/src/Analyser/IssetabilityDescriptor.php new file mode 100644 index 00000000000..c4d29259025 --- /dev/null +++ b/src/Analyser/IssetabilityDescriptor.php @@ -0,0 +1,228 @@ +kind === self::KIND_VARIABLE) { + $variableName = $this->variableName; + if ($variableName === null) { + throw new ShouldNotHappenException(); + } + + $hasVariable = $scope->hasVariableType($variableName); + if ($hasVariable->maybe()) { + return null; + } + + if ($result === null) { + if ($hasVariable->yes()) { + if ($variableName === '_SESSION') { + return null; + } + + return $typeCallback($scope->getVariableType($variableName)); + } + + return false; + } + + return $result; + } + + if ($this->kind === self::KIND_OFFSET) { + $varResult = $this->varResult; + $dimResult = $this->dimResult; + if ($varResult === null || $dimResult === null) { + throw new ShouldNotHappenException(); + } + + $type = $varResult->getTypeForScope($scope); + if (!$type->isOffsetAccessible()->yes()) { + return $result ?? $this->checkUndefinedInner($varResult, $scope); + } + + $dimType = $dimResult->getTypeForScope($scope); + $hasOffsetValue = $type->hasOffsetValueType($dimType); + if ($hasOffsetValue->no()) { + return false; + } + + // If offset cannot be null, store this error message and see if one of the earlier offsets is. + // E.g. $array['a']['b']['c'] ?? null; is a valid coalesce if a OR b or C might be null. + if ($hasOffsetValue->yes()) { + $result = $typeCallback($type->getOffsetValueType($dimType)); + + if ($result !== null) { + return $this->checkInner($varResult, $scope, $typeCallback, $result); + } + } + + // Has offset, it is nullable + return null; + } + + $reflectionResolver = $this->reflectionResolver; + $propertyFetch = $this->propertyFetch; + if ($reflectionResolver === null || $propertyFetch === null) { + throw new ShouldNotHappenException(); + } + $innerResult = $this->innerResult; + + $propertyReflection = $reflectionResolver($scope); + if ($propertyReflection === null) { + return $innerResult !== null ? $this->checkUndefinedInner($innerResult, $scope) : null; + } + + if (!$propertyReflection->isNative()) { + return $innerResult !== null ? $this->checkUndefinedInner($innerResult, $scope) : null; + } + + if ($propertyReflection->hasNativeType() && !$propertyReflection->isVirtual()->yes()) { + if (!$scope->hasExpressionType($propertyFetch)->yes()) { + $nativeReflection = $propertyReflection->getNativeReflection(); + if ($nativeReflection === null || !$nativeReflection->isPromoted() || (!$nativeReflection->isReadOnly() && !$nativeReflection->isHooked())) { + return $innerResult !== null ? $this->checkUndefinedInner($innerResult, $scope) : null; + } + } + } + + if ($result !== null) { + return $innerResult !== null ? $this->checkInner($innerResult, $scope, $typeCallback, $result) : $result; + } + + $result = $typeCallback($propertyReflection->getWritableType()); + if ($result !== null && $innerResult !== null) { + return $this->checkInner($innerResult, $scope, $typeCallback, $result); + } + + return $result; + } + + public function checkUndefined(MutatingScope $scope): ?bool + { + if ($this->kind === self::KIND_VARIABLE) { + $variableName = $this->variableName; + if ($variableName === null) { + throw new ShouldNotHappenException(); + } + + $hasVariable = $scope->hasVariableType($variableName); + if (!$hasVariable->no()) { + return null; + } + + return false; + } + + if ($this->kind === self::KIND_OFFSET) { + $varResult = $this->varResult; + $dimResult = $this->dimResult; + if ($varResult === null || $dimResult === null) { + throw new ShouldNotHappenException(); + } + + $type = $varResult->getTypeForScope($scope); + $dimType = $dimResult->getTypeForScope($scope); + $hasOffsetValue = $type->hasOffsetValueType($dimType); + if (!$type->isOffsetAccessible()->yes()) { + return $this->checkUndefinedInner($varResult, $scope); + } + + if (!$hasOffsetValue->no()) { + return $this->checkUndefinedInner($varResult, $scope); + } + + return false; + } + + $innerResult = $this->innerResult; + + return $innerResult !== null ? $this->checkUndefinedInner($innerResult, $scope) : null; + } + + /** + * @param callable(Type): ?bool $typeCallback + */ + private function checkInner(ExpressionResult $inner, MutatingScope $scope, callable $typeCallback, ?bool $result): ?bool + { + $innerDescriptor = $inner->getIssetabilityDescriptor(); + if ($innerDescriptor !== null) { + return $innerDescriptor->check($scope, $typeCallback, $result); + } + + return $result ?? $typeCallback($inner->getTypeForScope($scope)); + } + + private function checkUndefinedInner(ExpressionResult $inner, MutatingScope $scope): ?bool + { + $innerDescriptor = $inner->getIssetabilityDescriptor(); + if ($innerDescriptor !== null) { + return $innerDescriptor->checkUndefined($scope); + } + + return null; + } + +} diff --git a/src/Analyser/MutatingScope.php b/src/Analyser/MutatingScope.php index ce5e995e4a0..cbb01e187f6 100644 --- a/src/Analyser/MutatingScope.php +++ b/src/Analyser/MutatingScope.php @@ -1138,6 +1138,17 @@ public function popExpressionResultStorage(): void public function issetCheck(Expr $expr, callable $typeCallback, ?bool $result = null): ?bool { // mirrored in PHPStan\Rules\IssetCheck + $storage = $this->expressionResultStorageStack->getCurrent(); + if ($storage !== null) { + $exprResult = $storage->findExpressionResult($expr); + if ($exprResult !== null) { + $descriptor = $exprResult->getIssetabilityDescriptor(); + if ($descriptor !== null) { + return $descriptor->check($this, $typeCallback, $result); + } + } + } + if ($expr instanceof Node\Expr\Variable && is_string($expr->name)) { $hasVariable = $this->hasVariableType($expr->name); if ($hasVariable->maybe()) { From a8cf30650295a0836bb282e2a57ececb24c3139e Mon Sep 17 00:00:00 2001 From: Ondrej Mirtes Date: Wed, 17 Jun 2026 18:44:48 +0200 Subject: [PATCH 066/113] Move issetCheck onto ExpressionResult with isset()/empty() convenience --- src/Analyser/ExpressionResult.php | 57 +++++++++++++++++++++++++ src/Analyser/IssetabilityDescriptor.php | 14 +----- src/Analyser/MutatingScope.php | 7 +-- 3 files changed, 61 insertions(+), 17 deletions(-) diff --git a/src/Analyser/ExpressionResult.php b/src/Analyser/ExpressionResult.php index d1e80322384..a24a0aa2ce2 100644 --- a/src/Analyser/ExpressionResult.php +++ b/src/Analyser/ExpressionResult.php @@ -105,6 +105,63 @@ public function getIssetabilityDescriptor(): ?IssetabilityDescriptor return $this->issetabilityDescriptor; } + /** + * Whether isset($expr) holds: folds the isset/empty/?? chain descriptor, or + * applies the leaf type check when the expression is not a chain link. + * + * @param callable(Type): ?bool $typeCallback + */ + public function issetCheck(MutatingScope $scope, callable $typeCallback, ?bool $result = null): ?bool + { + if ($this->issetabilityDescriptor !== null) { + return $this->issetabilityDescriptor->check($scope, $typeCallback, $result); + } + + return $result ?? $typeCallback($this->getTypeForScope($scope)); + } + + public function issetCheckUndefined(MutatingScope $scope): ?bool + { + return $this->issetabilityDescriptor?->checkUndefined($scope); + } + + /** Whether isset($expr) is definitely true/false (null = maybe). */ + public function isset(MutatingScope $scope): ?bool + { + return $this->issetCheck($scope, static function (Type $type): ?bool { + $isNull = $type->isNull(); + if ($isNull->maybe()) { + return null; + } + + return !$isNull->yes(); + }); + } + + /** + * Whether $expr is definitely set-and-non-falsey (i.e. the negation of + * empty($expr)); null = maybe. EmptyHandler negates the result. + */ + public function empty(MutatingScope $scope): ?bool + { + return $this->issetCheck($scope, static function (Type $type): ?bool { + $isNull = $type->isNull(); + $isFalsey = $type->toBoolean()->isFalse(); + if ($isNull->maybe()) { + return null; + } + if ($isFalsey->maybe()) { + return null; + } + + if ($isNull->yes()) { + return $isFalsey->no(); + } + + return !$isFalsey->yes(); + }); + } + /** * @return InternalThrowPoint[] */ diff --git a/src/Analyser/IssetabilityDescriptor.php b/src/Analyser/IssetabilityDescriptor.php index c4d29259025..4164b6fd2a5 100644 --- a/src/Analyser/IssetabilityDescriptor.php +++ b/src/Analyser/IssetabilityDescriptor.php @@ -207,22 +207,12 @@ public function checkUndefined(MutatingScope $scope): ?bool */ private function checkInner(ExpressionResult $inner, MutatingScope $scope, callable $typeCallback, ?bool $result): ?bool { - $innerDescriptor = $inner->getIssetabilityDescriptor(); - if ($innerDescriptor !== null) { - return $innerDescriptor->check($scope, $typeCallback, $result); - } - - return $result ?? $typeCallback($inner->getTypeForScope($scope)); + return $inner->issetCheck($scope, $typeCallback, $result); } private function checkUndefinedInner(ExpressionResult $inner, MutatingScope $scope): ?bool { - $innerDescriptor = $inner->getIssetabilityDescriptor(); - if ($innerDescriptor !== null) { - return $innerDescriptor->checkUndefined($scope); - } - - return null; + return $inner->issetCheckUndefined($scope); } } diff --git a/src/Analyser/MutatingScope.php b/src/Analyser/MutatingScope.php index cbb01e187f6..f117e3b2543 100644 --- a/src/Analyser/MutatingScope.php +++ b/src/Analyser/MutatingScope.php @@ -1141,11 +1141,8 @@ public function issetCheck(Expr $expr, callable $typeCallback, ?bool $result = n $storage = $this->expressionResultStorageStack->getCurrent(); if ($storage !== null) { $exprResult = $storage->findExpressionResult($expr); - if ($exprResult !== null) { - $descriptor = $exprResult->getIssetabilityDescriptor(); - if ($descriptor !== null) { - return $descriptor->check($this, $typeCallback, $result); - } + if ($exprResult !== null && $exprResult->getIssetabilityDescriptor() !== null) { + return $exprResult->issetCheck($this, $typeCallback, $result); } } From 38995a22929afd78e1b83a6e679ba8f14cb7773e Mon Sep 17 00:00:00 2001 From: Ondrej Mirtes Date: Wed, 17 Jun 2026 18:48:08 +0200 Subject: [PATCH 067/113] CoalesceHandler reads issetCheck from the left result instead of the scope --- src/Analyser/ExprHandler/CoalesceHandler.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Analyser/ExprHandler/CoalesceHandler.php b/src/Analyser/ExprHandler/CoalesceHandler.php index 27c0fc4a889..0b496e8488a 100644 --- a/src/Analyser/ExprHandler/CoalesceHandler.php +++ b/src/Analyser/ExprHandler/CoalesceHandler.php @@ -53,7 +53,7 @@ public function supports(Expr $expr): bool */ private function getFalseySpecifiedTypes(MutatingScope $s, Expr $expr, ExpressionResult $condResult, TypeSpecifierContext $context): SpecifiedTypes { - $isset = $s->issetCheck($expr->left, static fn () => true); + $isset = $condResult->issetCheck($s, static fn () => true); if ($isset !== true) { return new SpecifiedTypes(); @@ -93,7 +93,7 @@ public function processExpr(NodeScopeResolver $nodeScopeResolver, Stmt $stmt, Ex typeCallback: static function (MutatingScope $s) use ($expr, $condResult, $rightResult, $rightScope): Type { $issetLeftExpr = new Expr\Isset_([$expr->left]); - $result = $s->issetCheck($expr->left, static function (Type $type): ?bool { + $result = $condResult->issetCheck($s, static function (Type $type): ?bool { $isNull = $type->isNull(); if ($isNull->maybe()) { return null; From 4c46aa49db6324ac5b359b94b3e4540bff1dbab1 Mon Sep 17 00:00:00 2001 From: Ondrej Mirtes Date: Wed, 17 Jun 2026 18:51:47 +0200 Subject: [PATCH 068/113] EmptyHandler is no longer TypeResolvingExprHandler --- src/Analyser/ExprHandler/EmptyHandler.php | 70 ++++++++--------------- 1 file changed, 23 insertions(+), 47 deletions(-) diff --git a/src/Analyser/ExprHandler/EmptyHandler.php b/src/Analyser/ExprHandler/EmptyHandler.php index 1939315de1b..55f32fde61f 100644 --- a/src/Analyser/ExprHandler/EmptyHandler.php +++ b/src/Analyser/ExprHandler/EmptyHandler.php @@ -10,30 +10,29 @@ use PHPStan\Analyser\ExpressionResult; use PHPStan\Analyser\ExpressionResultFactory; use PHPStan\Analyser\ExpressionResultStorage; +use PHPStan\Analyser\ExprHandler; use PHPStan\Analyser\ExprHandler\Helper\NonNullabilityHelper; use PHPStan\Analyser\MutatingScope; use PHPStan\Analyser\NodeScopeResolver; -use PHPStan\Analyser\Scope; use PHPStan\Analyser\SpecifiedTypes; -use PHPStan\Analyser\TypeResolvingExprHandler; use PHPStan\Analyser\TypeSpecifier; use PHPStan\Analyser\TypeSpecifierContext; use PHPStan\DependencyInjection\AutowiredService; -use PHPStan\ShouldNotHappenException; use PHPStan\Type\BooleanType; use PHPStan\Type\Constant\ConstantBooleanType; use PHPStan\Type\Type; /** - * @implements TypeResolvingExprHandler + * @implements ExprHandler */ #[AutowiredService] -final class EmptyHandler implements TypeResolvingExprHandler +final class EmptyHandler implements ExprHandler { public function __construct( private NonNullabilityHelper $nonNullabilityHelper, private ExpressionResultFactory $expressionResultFactory, + private TypeSpecifier $typeSpecifier, ) { } @@ -43,48 +42,6 @@ public function supports(Expr $expr): bool return $expr instanceof Empty_; } - public function resolveType(MutatingScope $scope, Expr $expr): Type - { - $result = $scope->issetCheck($expr->expr, static function (Type $type): ?bool { - $isNull = $type->isNull(); - $isFalsey = $type->toBoolean()->isFalse(); - if ($isNull->maybe()) { - return null; - } - if ($isFalsey->maybe()) { - return null; - } - - if ($isNull->yes()) { - return $isFalsey->no(); - } - - return !$isFalsey->yes(); - }); - if ($result === null) { - return new BooleanType(); - } - - return new ConstantBooleanType(!$result); - } - - public function specifyTypes(TypeSpecifier $typeSpecifier, Scope $scope, Expr $expr, TypeSpecifierContext $context): SpecifiedTypes - { - if (!$scope instanceof MutatingScope) { - throw new ShouldNotHappenException(); - } - - $isset = $scope->issetCheck($expr->expr, static fn () => true); - if ($isset === false) { - return new SpecifiedTypes(); - } - - return $typeSpecifier->specifyTypesInCondition($scope, new BooleanOr( - new Expr\BooleanNot(new Expr\Isset_([$expr->expr])), - new Expr\BooleanNot($expr->expr), - ), $context)->setRootExpr($expr); - } - public function processExpr(NodeScopeResolver $nodeScopeResolver, Stmt $stmt, Expr $expr, MutatingScope $scope, ExpressionResultStorage $storage, callable $nodeCallback, ExpressionContext $context): ExpressionResult { $beforeScope = $scope; @@ -103,6 +60,25 @@ public function processExpr(NodeScopeResolver $nodeScopeResolver, Stmt $stmt, Ex isAlwaysTerminating: $exprResult->isAlwaysTerminating(), throwPoints: $exprResult->getThrowPoints(), impurePoints: $exprResult->getImpurePoints(), + typeCallback: static function (MutatingScope $s) use ($exprResult): Type { + $result = $exprResult->empty($s); + if ($result === null) { + return new BooleanType(); + } + + return new ConstantBooleanType(!$result); + }, + specifyTypesCallback: function (MutatingScope $s, TypeSpecifierContext $context) use ($expr, $exprResult): SpecifiedTypes { + $isset = $exprResult->issetCheck($s, static fn () => true); + if ($isset === false) { + return new SpecifiedTypes(); + } + + return $this->typeSpecifier->specifyTypesInCondition($s, new BooleanOr( + new Expr\BooleanNot(new Expr\Isset_([$expr->expr])), + new Expr\BooleanNot($expr->expr), + ), $context)->setRootExpr($expr); + }, ); } From 7b09a8045fb52af58cac72d508042eaf48a55059 Mon Sep 17 00:00:00 2001 From: Ondrej Mirtes Date: Wed, 17 Jun 2026 19:08:57 +0200 Subject: [PATCH 069/113] Match issetCheckUndefined ordering in IssetabilityDescriptor::checkUndefined --- src/Analyser/IssetabilityDescriptor.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Analyser/IssetabilityDescriptor.php b/src/Analyser/IssetabilityDescriptor.php index 4164b6fd2a5..a060ef1ae81 100644 --- a/src/Analyser/IssetabilityDescriptor.php +++ b/src/Analyser/IssetabilityDescriptor.php @@ -184,12 +184,12 @@ public function checkUndefined(MutatingScope $scope): ?bool } $type = $varResult->getTypeForScope($scope); - $dimType = $dimResult->getTypeForScope($scope); - $hasOffsetValue = $type->hasOffsetValueType($dimType); if (!$type->isOffsetAccessible()->yes()) { return $this->checkUndefinedInner($varResult, $scope); } + $dimType = $dimResult->getTypeForScope($scope); + $hasOffsetValue = $type->hasOffsetValueType($dimType); if (!$hasOffsetValue->no()) { return $this->checkUndefinedInner($varResult, $scope); } From ba726dd4f11d924a36d61750178cbdd5885d0515 Mon Sep 17 00:00:00 2001 From: Ondrej Mirtes Date: Wed, 17 Jun 2026 19:41:49 +0200 Subject: [PATCH 070/113] Re-evaluate getCurrentTypesOfSpecifiedExpr on the asking scope --- src/Analyser/ExpressionResult.php | 11 +++++++++++ src/Analyser/MutatingScope.php | 8 ++++++-- 2 files changed, 17 insertions(+), 2 deletions(-) diff --git a/src/Analyser/ExpressionResult.php b/src/Analyser/ExpressionResult.php index a24a0aa2ce2..dc2a9160129 100644 --- a/src/Analyser/ExpressionResult.php +++ b/src/Analyser/ExpressionResult.php @@ -322,4 +322,15 @@ public function getTypeForScope(MutatingScope $scope): Type return $scope->getType($this->expr); } + /** Native counterpart of getTypeForScope(). */ + public function getNativeTypeForScope(MutatingScope $scope): Type + { + $nativeScope = $scope->doNotTreatPhpDocTypesAsCertain(); + if ($this->typeCallback !== null && !$this->hasTrackedExpressionType($nativeScope)) { + return TypeUtils::resolveLateResolvableTypes(($this->typeCallback)($nativeScope, $this->expr)); + } + + return $scope->getNativeType($this->expr); + } + } diff --git a/src/Analyser/MutatingScope.php b/src/Analyser/MutatingScope.php index f117e3b2543..417a0056a60 100644 --- a/src/Analyser/MutatingScope.php +++ b/src/Analyser/MutatingScope.php @@ -1078,9 +1078,13 @@ private function getCurrentTypesOfSpecifiedExpr(Expr $expr): ?array return null; } + // re-evaluate on the asking scope, not the stored beforeScope: a handler + // (e.g. isset/empty via NonNullabilityHelper) may have processed the + // inner expression on a scope that strips null, so the cached type would + // be stale for the narrowing the caller is applying return [ - $result->getType(), - $result->getNativeType(), + $result->getTypeForScope($this), + $result->getNativeTypeForScope($this), ]; } From 2bad7b01d1afb929157f7261f4b41ed0e9a0d2dc Mon Sep 17 00:00:00 2001 From: Ondrej Mirtes Date: Wed, 17 Jun 2026 21:36:30 +0200 Subject: [PATCH 071/113] Rules\IssetCheck folds IssetabilityDescriptor instead of re-walking the AST --- src/Analyser/ExpressionResult.php | 5 + src/Analyser/IssetabilityDescriptor.php | 56 ++++++ src/Analyser/MutatingScope.php | 29 +++ src/Rules/IssetCheck.php | 184 ++++++++++-------- .../PHPStan/Rules/Variables/EmptyRuleTest.php | 2 - .../PHPStan/Rules/Variables/IssetRuleTest.php | 2 - .../Rules/Variables/NullCoalesceRuleTest.php | 2 - 7 files changed, 191 insertions(+), 89 deletions(-) diff --git a/src/Analyser/ExpressionResult.php b/src/Analyser/ExpressionResult.php index dc2a9160129..5453896d5c2 100644 --- a/src/Analyser/ExpressionResult.php +++ b/src/Analyser/ExpressionResult.php @@ -74,6 +74,11 @@ public function getScope(): MutatingScope return $this->scope; } + public function getExpr(): Expr + { + return $this->expr; + } + public function getBeforeScope(): MutatingScope { return $this->beforeScope; diff --git a/src/Analyser/IssetabilityDescriptor.php b/src/Analyser/IssetabilityDescriptor.php index a060ef1ae81..869d4654d8c 100644 --- a/src/Analyser/IssetabilityDescriptor.php +++ b/src/Analyser/IssetabilityDescriptor.php @@ -4,6 +4,8 @@ use Closure; use PhpParser\Node\Expr; +use PhpParser\Node\Expr\PropertyFetch; +use PhpParser\Node\Expr\StaticPropertyFetch; use PHPStan\Rules\Properties\FoundPropertyReflection; use PHPStan\ShouldNotHappenException; use PHPStan\Type\Type; @@ -29,6 +31,7 @@ final class IssetabilityDescriptor /** * @param Closure(MutatingScope): ?FoundPropertyReflection|null $reflectionResolver + * @param PropertyFetch|StaticPropertyFetch|null $propertyFetch */ private function __construct( private string $kind, @@ -54,12 +57,65 @@ public static function offset(ExpressionResult $varResult, ExpressionResult $dim /** * @param Closure(MutatingScope): ?FoundPropertyReflection $reflectionResolver + * @param PropertyFetch|StaticPropertyFetch $propertyFetch */ public static function property(?ExpressionResult $innerResult, Closure $reflectionResolver, Expr $propertyFetch): self { return new self(self::KIND_PROPERTY, innerResult: $innerResult, reflectionResolver: $reflectionResolver, propertyFetch: $propertyFetch); } + public function isVariable(): bool + { + return $this->kind === self::KIND_VARIABLE; + } + + public function isOffset(): bool + { + return $this->kind === self::KIND_OFFSET; + } + + public function isProperty(): bool + { + return $this->kind === self::KIND_PROPERTY; + } + + public function getVariableName(): ?string + { + return $this->variableName; + } + + public function getVarResult(): ?ExpressionResult + { + return $this->varResult; + } + + public function getDimResult(): ?ExpressionResult + { + return $this->dimResult; + } + + public function getInnerResult(): ?ExpressionResult + { + return $this->innerResult; + } + + public function resolvePropertyReflection(MutatingScope $scope): ?FoundPropertyReflection + { + if ($this->reflectionResolver === null) { + throw new ShouldNotHappenException(); + } + + return ($this->reflectionResolver)($scope); + } + + /** + * @return PropertyFetch|StaticPropertyFetch|null + */ + public function getPropertyFetch(): ?Expr + { + return $this->propertyFetch; + } + /** * @param callable(Type): ?bool $typeCallback */ diff --git a/src/Analyser/MutatingScope.php b/src/Analyser/MutatingScope.php index 417a0056a60..348ef238646 100644 --- a/src/Analyser/MutatingScope.php +++ b/src/Analyser/MutatingScope.php @@ -1136,6 +1136,35 @@ public function popExpressionResultStorage(): void $this->expressionResultStorageStack->pop(); } + /** + * The isset/empty/?? chain descriptor PHPStan\Rules\IssetCheck folds. Reads + * it from the current expression-result storage; when the rule asks before + * the engine has stored the expression's result (the rule callback fires + * before the chain-link handlers run), the expression is processed on demand + * just like resolveTypeOfNewWorldHandlerNode(). + * + * @internal + */ + public function getIssetabilityDescriptor(Expr $expr): ?IssetabilityDescriptor + { + $scope = $this->toMutatingScope(); + $storage = $this->expressionResultStorageStack->getCurrent(); + if ($storage !== null) { + $result = $storage->findExpressionResult($expr); + if ($result !== null) { + return $result->getIssetabilityDescriptor(); + } + } + + $onDemandResult = $this->container->getByType(NodeScopeResolver::class)->processExprOnDemand( + $expr, + $scope, + $storage !== null ? $storage->duplicate() : new ExpressionResultStorage(), + ); + + return $onDemandResult->getIssetabilityDescriptor(); + } + /** * @param callable(Type): ?bool $typeCallback */ diff --git a/src/Rules/IssetCheck.php b/src/Rules/IssetCheck.php index 35195e7ea52..e0a2ccf409c 100644 --- a/src/Rules/IssetCheck.php +++ b/src/Rules/IssetCheck.php @@ -4,16 +4,18 @@ use PhpParser\Node; use PhpParser\Node\Expr; +use PHPStan\Analyser\ExpressionResult; +use PHPStan\Analyser\IssetabilityDescriptor; +use PHPStan\Analyser\MutatingScope; use PHPStan\Analyser\Scope; use PHPStan\DependencyInjection\AutowiredParameter; use PHPStan\DependencyInjection\AutowiredService; use PHPStan\Node\Expr\PropertyInitializationExpr; use PHPStan\Rules\Properties\PropertyDescriptor; -use PHPStan\Rules\Properties\PropertyReflectionFinder; +use PHPStan\ShouldNotHappenException; use PHPStan\Type\NeverType; use PHPStan\Type\Type; use PHPStan\Type\VerbosityLevel; -use function is_string; use function sprintf; use function str_starts_with; @@ -26,7 +28,6 @@ final class IssetCheck public function __construct( private PropertyDescriptor $propertyDescriptor, - private PropertyReflectionFinder $propertyReflectionFinder, #[AutowiredParameter] private bool $checkAdvancedIsset, #[AutowiredParameter] @@ -41,16 +42,32 @@ public function __construct( */ public function check(Expr $expr, Scope $scope, string $operatorDescription, string $identifier, callable $typeMessageCallback, ?IdentifierRuleError $error = null): ?IdentifierRuleError { - // mirrored in PHPStan\Analyser\MutatingScope::issetCheck() - if ($expr instanceof Node\Expr\Variable && is_string($expr->name)) { - $hasVariable = $scope->hasVariableType($expr->name); + $mutatingScope = $scope->toMutatingScope(); + + return $this->doCheck($mutatingScope->getIssetabilityDescriptor($expr), $expr, $scope, $mutatingScope, $operatorDescription, $identifier, $typeMessageCallback, $error); + } + + /** + * @param ErrorIdentifier $identifier + * @param callable(Type): ?string $typeMessageCallback + */ + private function doCheck(?IssetabilityDescriptor $descriptor, Expr $expr, Scope $scope, MutatingScope $mutatingScope, string $operatorDescription, string $identifier, callable $typeMessageCallback, ?IdentifierRuleError $error): ?IdentifierRuleError + { + // folds PHPStan\Analyser\IssetabilityDescriptor; mirrors PHPStan\Analyser\MutatingScope::issetCheck() + if ($descriptor !== null && $descriptor->isVariable()) { + $variableName = $descriptor->getVariableName(); + if ($variableName === null) { + throw new ShouldNotHappenException(); + } + + $hasVariable = $scope->hasVariableType($variableName); if ($hasVariable->maybe()) { return null; } if ($error === null) { if ($hasVariable->yes()) { - if ($expr->name === '_SESSION') { + if ($variableName === '_SESSION') { return null; } @@ -58,7 +75,7 @@ public function check(Expr $expr, Scope $scope, string $operatorDescription, str if (!$type instanceof NeverType) { return $this->generateError( $type, - sprintf('Variable $%s %s always exists and', $expr->name, $operatorDescription), + sprintf('Variable $%s %s always exists and', $variableName, $operatorDescription), $typeMessageCallback, $identifier, 'variable', @@ -66,23 +83,29 @@ public function check(Expr $expr, Scope $scope, string $operatorDescription, str } } - return RuleErrorBuilder::message(sprintf('Variable $%s %s is never defined.', $expr->name, $operatorDescription)) + return RuleErrorBuilder::message(sprintf('Variable $%s %s is never defined.', $variableName, $operatorDescription)) ->identifier(sprintf('%s.variable', $identifier)) ->build(); } return $error; - } elseif ($expr instanceof Node\Expr\ArrayDimFetch && $expr->dim !== null) { + } elseif ($descriptor !== null && $descriptor->isOffset()) { + $varResult = $descriptor->getVarResult(); + $dimResult = $descriptor->getDimResult(); + if ($varResult === null || $dimResult === null) { + throw new ShouldNotHappenException(); + } + $type = $this->treatPhpDocTypesAsCertain - ? $scope->getScopeType($expr->var) - : $scope->getScopeNativeType($expr->var); + ? $varResult->getTypeForScope($mutatingScope) + : $varResult->getNativeTypeForScope($mutatingScope); if (!$type->isOffsetAccessible()->yes()) { - return $error ?? $this->checkUndefined($expr->var, $scope, $operatorDescription, $identifier); + return $error ?? $this->checkUndefinedInner($varResult, $scope, $mutatingScope, $operatorDescription, $identifier); } $dimType = $this->treatPhpDocTypesAsCertain - ? $scope->getScopeType($expr->dim) - : $scope->getScopeNativeType($expr->dim); + ? $dimResult->getTypeForScope($mutatingScope) + : $dimResult->getNativeTypeForScope($mutatingScope); $hasOffsetValue = $type->hasOffsetValueType($dimType); if ($hasOffsetValue->no()) { if (!$this->checkAdvancedIsset) { @@ -114,54 +137,48 @@ public function check(Expr $expr, Scope $scope, string $operatorDescription, str ), $typeMessageCallback, $identifier, 'offset'); if ($error !== null) { - return $this->check($expr->var, $scope, $operatorDescription, $identifier, $typeMessageCallback, $error); + return $this->doCheck($varResult->getIssetabilityDescriptor(), $varResult->getExpr(), $scope, $mutatingScope, $operatorDescription, $identifier, $typeMessageCallback, $error); } } // Has offset, it is nullable return null; - } elseif ($expr instanceof Node\Expr\PropertyFetch || $expr instanceof Node\Expr\StaticPropertyFetch) { + } elseif ($descriptor !== null && $descriptor->isProperty()) { - $propertyReflection = $this->propertyReflectionFinder->findPropertyReflectionFromNode($expr, $scope->toMutatingScope()); - - if ($propertyReflection === null) { - if ($expr instanceof Node\Expr\PropertyFetch) { - return $this->checkUndefined($expr->var, $scope, $operatorDescription, $identifier); - } + $propertyFetch = $descriptor->getPropertyFetch(); + if ($propertyFetch === null) { + throw new ShouldNotHappenException(); + } + $innerResult = $descriptor->getInnerResult(); - if ($expr->class instanceof Expr) { - return $this->checkUndefined($expr->class, $scope, $operatorDescription, $identifier); - } + $propertyReflection = $descriptor->resolvePropertyReflection($mutatingScope); - return null; + if ($propertyReflection === null) { + return $innerResult !== null + ? $this->checkUndefinedInner($innerResult, $scope, $mutatingScope, $operatorDescription, $identifier) + : null; } if (!$propertyReflection->isNative()) { - if ($expr instanceof Node\Expr\PropertyFetch) { - return $this->checkUndefined($expr->var, $scope, $operatorDescription, $identifier); - } - - if ($expr->class instanceof Expr) { - return $this->checkUndefined($expr->class, $scope, $operatorDescription, $identifier); - } - - return null; + return $innerResult !== null + ? $this->checkUndefinedInner($innerResult, $scope, $mutatingScope, $operatorDescription, $identifier) + : null; } if ($propertyReflection->hasNativeType() && !$propertyReflection->isVirtual()->yes()) { if ( - $expr instanceof Node\Expr\PropertyFetch - && $expr->name instanceof Node\Identifier - && $expr->var instanceof Expr\Variable - && $expr->var->name === 'this' + $propertyFetch instanceof Node\Expr\PropertyFetch + && $propertyFetch->name instanceof Node\Identifier + && $propertyFetch->var instanceof Expr\Variable + && $propertyFetch->var->name === 'this' && $scope->hasExpressionType(new PropertyInitializationExpr($propertyReflection->getName()))->yes() ) { return $this->generateError( $propertyReflection->getNativeType(), sprintf( '%s %s', - $this->propertyDescriptor->describeProperty($propertyReflection, $scope, $expr), + $this->propertyDescriptor->describeProperty($propertyReflection, $scope, $propertyFetch), $operatorDescription, ), static function (Type $type) use ($typeMessageCallback): ?string { @@ -181,7 +198,7 @@ static function (Type $type) use ($typeMessageCallback): ?string { ); } - if (!$scope->hasExpressionType($expr)->yes()) { + if (!$scope->hasExpressionType($propertyFetch)->yes()) { $nativeReflection = $propertyReflection->getNativeReflection(); if ( $nativeReflection !== null @@ -193,29 +210,17 @@ static function (Type $type) use ($typeMessageCallback): ?string { } } - $propertyDescription = $this->propertyDescriptor->describeProperty($propertyReflection, $scope, $expr); + $propertyDescription = $this->propertyDescriptor->describeProperty($propertyReflection, $scope, $propertyFetch); $propertyType = $propertyReflection->getWritableType(); if ($error !== null) { - if ($expr instanceof Node\Expr\PropertyFetch) { - return $this->check($expr->var, $scope, $operatorDescription, $identifier, $typeMessageCallback, $error); - } - - if ($expr->class instanceof Expr) { - return $this->check($expr->class, $scope, $operatorDescription, $identifier, $typeMessageCallback, $error); - } - - return $error; + return $innerResult !== null + ? $this->doCheck($innerResult->getIssetabilityDescriptor(), $innerResult->getExpr(), $scope, $mutatingScope, $operatorDescription, $identifier, $typeMessageCallback, $error) + : $error; } if (!$this->checkAdvancedIsset) { - if ($expr instanceof Node\Expr\PropertyFetch) { - return $this->checkUndefined($expr->var, $scope, $operatorDescription, $identifier); - } - - if ($expr->class instanceof Expr) { - return $this->checkUndefined($expr->class, $scope, $operatorDescription, $identifier); - } - - return null; + return $innerResult !== null + ? $this->checkUndefinedInner($innerResult, $scope, $mutatingScope, $operatorDescription, $identifier) + : null; } $error = $this->generateError( @@ -226,14 +231,8 @@ static function (Type $type) use ($typeMessageCallback): ?string { 'property', ); - if ($error !== null) { - if ($expr instanceof Node\Expr\PropertyFetch) { - return $this->check($expr->var, $scope, $operatorDescription, $identifier, $typeMessageCallback, $error); - } - - if ($expr->class instanceof Expr) { - return $this->check($expr->class, $scope, $operatorDescription, $identifier, $typeMessageCallback, $error); - } + if ($error !== null && $innerResult !== null) { + return $this->doCheck($innerResult->getIssetabilityDescriptor(), $innerResult->getExpr(), $scope, $mutatingScope, $operatorDescription, $identifier, $typeMessageCallback, $error); } return $error; @@ -276,29 +275,48 @@ static function (Type $type) use ($typeMessageCallback): ?string { /** * @param ErrorIdentifier $identifier */ - private function checkUndefined(Expr $expr, Scope $scope, string $operatorDescription, string $identifier): ?IdentifierRuleError + private function checkUndefinedInner(ExpressionResult $inner, Scope $scope, MutatingScope $mutatingScope, string $operatorDescription, string $identifier): ?IdentifierRuleError + { + return $this->checkUndefined($inner->getIssetabilityDescriptor(), $inner->getExpr(), $scope, $mutatingScope, $operatorDescription, $identifier); + } + + /** + * @param ErrorIdentifier $identifier + */ + private function checkUndefined(?IssetabilityDescriptor $descriptor, Expr $expr, Scope $scope, MutatingScope $mutatingScope, string $operatorDescription, string $identifier): ?IdentifierRuleError { - if ($expr instanceof Node\Expr\Variable && is_string($expr->name)) { - $hasVariable = $scope->hasVariableType($expr->name); + if ($descriptor !== null && $descriptor->isVariable()) { + $variableName = $descriptor->getVariableName(); + if ($variableName === null) { + throw new ShouldNotHappenException(); + } + + $hasVariable = $scope->hasVariableType($variableName); if (!$hasVariable->no()) { return null; } - return RuleErrorBuilder::message(sprintf('Variable $%s %s is never defined.', $expr->name, $operatorDescription)) + return RuleErrorBuilder::message(sprintf('Variable $%s %s is never defined.', $variableName, $operatorDescription)) ->identifier(sprintf('%s.variable', $identifier)) ->build(); } - if ($expr instanceof Node\Expr\ArrayDimFetch && $expr->dim !== null) { - $type = $this->treatPhpDocTypesAsCertain ? $scope->getScopeType($expr->var) : $scope->getScopeNativeType($expr->var); - $dimType = $this->treatPhpDocTypesAsCertain ? $scope->getScopeType($expr->dim) : $scope->getScopeNativeType($expr->dim); + if ($descriptor !== null && $descriptor->isOffset()) { + $varResult = $descriptor->getVarResult(); + $dimResult = $descriptor->getDimResult(); + if ($varResult === null || $dimResult === null) { + throw new ShouldNotHappenException(); + } + + $type = $this->treatPhpDocTypesAsCertain ? $varResult->getTypeForScope($mutatingScope) : $varResult->getNativeTypeForScope($mutatingScope); + $dimType = $this->treatPhpDocTypesAsCertain ? $dimResult->getTypeForScope($mutatingScope) : $dimResult->getNativeTypeForScope($mutatingScope); $hasOffsetValue = $type->hasOffsetValueType($dimType); if (!$type->isOffsetAccessible()->yes()) { - return $this->checkUndefined($expr->var, $scope, $operatorDescription, $identifier); + return $this->checkUndefinedInner($varResult, $scope, $mutatingScope, $operatorDescription, $identifier); } if (!$hasOffsetValue->no()) { - return $this->checkUndefined($expr->var, $scope, $operatorDescription, $identifier); + return $this->checkUndefinedInner($varResult, $scope, $mutatingScope, $operatorDescription, $identifier); } return RuleErrorBuilder::message( @@ -311,12 +329,12 @@ private function checkUndefined(Expr $expr, Scope $scope, string $operatorDescri )->identifier(sprintf('%s.offset', $identifier))->build(); } - if ($expr instanceof Expr\PropertyFetch) { - return $this->checkUndefined($expr->var, $scope, $operatorDescription, $identifier); - } + if ($descriptor !== null && $descriptor->isProperty()) { + $innerResult = $descriptor->getInnerResult(); - if ($expr instanceof Expr\StaticPropertyFetch && $expr->class instanceof Expr) { - return $this->checkUndefined($expr->class, $scope, $operatorDescription, $identifier); + return $innerResult !== null + ? $this->checkUndefinedInner($innerResult, $scope, $mutatingScope, $operatorDescription, $identifier) + : null; } return null; diff --git a/tests/PHPStan/Rules/Variables/EmptyRuleTest.php b/tests/PHPStan/Rules/Variables/EmptyRuleTest.php index 582fdeb1076..079b032e8ee 100644 --- a/tests/PHPStan/Rules/Variables/EmptyRuleTest.php +++ b/tests/PHPStan/Rules/Variables/EmptyRuleTest.php @@ -4,7 +4,6 @@ use PHPStan\Rules\IssetCheck; use PHPStan\Rules\Properties\PropertyDescriptor; -use PHPStan\Rules\Properties\PropertyReflectionFinder; use PHPStan\Rules\Rule; use PHPStan\Testing\RuleTestCase; use PHPUnit\Framework\Attributes\DataProvider; @@ -22,7 +21,6 @@ protected function getRule(): Rule { return new EmptyRule(new IssetCheck( new PropertyDescriptor(), - new PropertyReflectionFinder(), true, $this->treatPhpDocTypesAsCertain, )); diff --git a/tests/PHPStan/Rules/Variables/IssetRuleTest.php b/tests/PHPStan/Rules/Variables/IssetRuleTest.php index 613e2688a5f..4847e879561 100644 --- a/tests/PHPStan/Rules/Variables/IssetRuleTest.php +++ b/tests/PHPStan/Rules/Variables/IssetRuleTest.php @@ -4,7 +4,6 @@ use PHPStan\Rules\IssetCheck; use PHPStan\Rules\Properties\PropertyDescriptor; -use PHPStan\Rules\Properties\PropertyReflectionFinder; use PHPStan\Rules\Rule; use PHPStan\Testing\RuleTestCase; use PHPUnit\Framework\Attributes\RequiresPhp; @@ -21,7 +20,6 @@ protected function getRule(): Rule { return new IssetRule(new IssetCheck( new PropertyDescriptor(), - new PropertyReflectionFinder(), true, $this->treatPhpDocTypesAsCertain, )); diff --git a/tests/PHPStan/Rules/Variables/NullCoalesceRuleTest.php b/tests/PHPStan/Rules/Variables/NullCoalesceRuleTest.php index e22565393fe..c07c746d3c8 100644 --- a/tests/PHPStan/Rules/Variables/NullCoalesceRuleTest.php +++ b/tests/PHPStan/Rules/Variables/NullCoalesceRuleTest.php @@ -4,7 +4,6 @@ use PHPStan\Rules\IssetCheck; use PHPStan\Rules\Properties\PropertyDescriptor; -use PHPStan\Rules\Properties\PropertyReflectionFinder; use PHPStan\Rules\Rule; use PHPStan\Testing\RuleTestCase; use PHPUnit\Framework\Attributes\RequiresPhp; @@ -20,7 +19,6 @@ protected function getRule(): Rule { return new NullCoalesceRule(new IssetCheck( new PropertyDescriptor(), - new PropertyReflectionFinder(), true, $this->shouldTreatPhpDocTypesAsCertain(), )); From d3391804ff578edd3dfead11d1076b388389a43f Mon Sep 17 00:00:00 2001 From: Ondrej Mirtes Date: Wed, 17 Jun 2026 22:11:45 +0200 Subject: [PATCH 072/113] MatchHandler is no longer TypeResolvingExprHandler --- src/Analyser/ExprHandler/MatchHandler.php | 69 ++++++++++++++++------- 1 file changed, 49 insertions(+), 20 deletions(-) diff --git a/src/Analyser/ExprHandler/MatchHandler.php b/src/Analyser/ExprHandler/MatchHandler.php index 88e80239efa..c44ca579f91 100644 --- a/src/Analyser/ExprHandler/MatchHandler.php +++ b/src/Analyser/ExprHandler/MatchHandler.php @@ -18,13 +18,12 @@ use PHPStan\Analyser\ExpressionResult; use PHPStan\Analyser\ExpressionResultFactory; use PHPStan\Analyser\ExpressionResultStorage; +use PHPStan\Analyser\ExprHandler; +use PHPStan\Analyser\ExprHandler\Helper\DefaultNarrowingHelper; use PHPStan\Analyser\InternalThrowPoint; use PHPStan\Analyser\MutatingScope; use PHPStan\Analyser\NodeScopeResolver; -use PHPStan\Analyser\Scope; use PHPStan\Analyser\SpecifiedTypes; -use PHPStan\Analyser\TypeResolvingExprHandler; -use PHPStan\Analyser\TypeSpecifier; use PHPStan\Analyser\TypeSpecifierContext; use PHPStan\DependencyInjection\AutowiredParameter; use PHPStan\DependencyInjection\AutowiredService; @@ -49,16 +48,17 @@ use const SORT_NUMERIC; /** - * @implements TypeResolvingExprHandler + * @implements ExprHandler */ #[AutowiredService] -final class MatchHandler implements TypeResolvingExprHandler +final class MatchHandler implements ExprHandler { public function __construct( #[AutowiredParameter] private bool $treatPhpDocTypesAsCertain, private ExpressionResultFactory $expressionResultFactory, + private DefaultNarrowingHelper $defaultNarrowingHelper, ) { } @@ -68,16 +68,6 @@ public function supports(Expr $expr): bool return $expr instanceof Match_; } - public function resolveType(MutatingScope $scope, Expr $expr): Type - { - $types = []; - foreach ($this->getArmScopesAndTypes($scope, $expr) as [$armScope, $armType]) { - $types[] = $armType; - } - - return TypeCombinator::union(...$types); - } - /** * For each reachable match arm, returns the arm's body type together with the * scope in which the match subject is narrowed to that arm's condition. This @@ -226,6 +216,16 @@ public function processExpr(NodeScopeResolver $nodeScopeResolver, Stmt $stmt, Ex $arms = $expr->arms; $armCondsToSkip = []; $armBodyScopes = []; + // Capture, for each reachable arm, the body's already-computed + // ExpressionResult together with the scope it was processed on and the + // body node itself. The typeCallback unions these inside-out instead of + // re-walking the arms (which getArmScopesAndTypes/the old resolveType + // did). The set of contributing arms mirrors getArmScopesAndTypes + // exactly. The body node is kept so the keepVoid projection (the only + // caller is getKeepVoidType, via a synthetic clone of the match) can be + // computed for it. + /** @var list $armTypeResults */ + $armTypeResults = []; if ($condType->isEnum()->yes()) { // enum match analysis would work even without this if branch // but would be much slower @@ -367,6 +367,7 @@ public function processExpr(NodeScopeResolver $nodeScopeResolver, Stmt $stmt, Ex $hasYield = $hasYield || $armResult->hasYield(); $throwPoints = array_merge($throwPoints, $armResult->getThrowPoints()); $impurePoints = array_merge($impurePoints, $armResult->getImpurePoints()); + $armTypeResults[] = [$armResult, $matchArmBodyScope, $arm->body]; unset($arms[$i]); } @@ -393,6 +394,7 @@ public function processExpr(NodeScopeResolver $nodeScopeResolver, Stmt $stmt, Ex foreach ($arms as $i => $arm) { if ($arm->conds === null) { $hasDefaultCond = true; + $defaultArmBodyScope = $matchScope; $matchArmBody = new MatchExpressionArmBody($matchScope, $arm->body); $armNodes[$i] = new MatchExpressionArm($matchArmBody, [], $arm->getStartLine()); $armResult = $nodeScopeResolver->processExprNode($stmt, $arm->body, $matchScope, $storage, $nodeCallback, ExpressionContext::createTopLevel()); @@ -403,6 +405,7 @@ public function processExpr(NodeScopeResolver $nodeScopeResolver, Stmt $stmt, Ex if (!$armResult->isAlwaysTerminating()) { $armBodyScopes[] = $matchScope; } + $armTypeResults[] = [$armResult, $defaultArmBodyScope, $arm->body]; continue; } @@ -459,6 +462,13 @@ public function processExpr(NodeScopeResolver $nodeScopeResolver, Stmt $stmt, Ex $hasYield = $hasYield || $armResult->hasYield(); $throwPoints = array_merge($throwPoints, $armResult->getThrowPoints()); $impurePoints = array_merge($impurePoints, $armResult->getImpurePoints()); + // Mirror getArmScopesAndTypes: an arm whose filtering expression is + // always false is unreachable and does not contribute to the result + // type. + $filteringExprType = $matchScope->getType($filteringExpr); + if (!$filteringExprType->isFalse()->yes()) { + $armTypeResults[] = [$armResult, $bodyScope, $arm->body]; + } $matchScope = $armCondScope->filterByFalseyValue($filteringExpr); } @@ -512,6 +522,30 @@ public function processExpr(NodeScopeResolver $nodeScopeResolver, Stmt $stmt, Ex isAlwaysTerminating: $isAlwaysTerminating, throwPoints: $throwPoints, impurePoints: $impurePoints, + // Each arm body was already processed on the scope where the subject + // is narrowed to that arm's condition - those captured scopes are the + // evaluation points, so the result type is just the union of the arm + // body types, no re-walk of the arms needed. + typeCallback: static function (MutatingScope $s) use ($expr, $armTypeResults): Type { + $keepVoid = $expr->getAttribute(MutatingScope::KEEP_VOID_ATTRIBUTE_NAME) === true; + $types = []; + foreach ($armTypeResults as [$armResult, $bodyScope, $armBody]) { + if ($s->nativeTypesPromoted) { + $bodyScope = $bodyScope->doNotTreatPhpDocTypesAsCertain(); + } + if ($keepVoid) { + // The only caller is getKeepVoidType (via a synthetic + // clone of the match) - it keeps void in the arm bodies + // instead of transforming it to null. + $types[] = $bodyScope->getKeepVoidType($armBody); + } else { + $types[] = $armResult->getTypeForScope($bodyScope); + } + } + + return TypeCombinator::union(...$types); + }, + specifyTypesCallback: fn (MutatingScope $s, TypeSpecifierContext $context): SpecifiedTypes => $this->defaultNarrowingHelper->specifyDefaultTypes($expr, $context), ); } @@ -590,9 +624,4 @@ private function scopeHasNeverVariable(MutatingScope $scope, array $varNames): b return false; } - public function specifyTypes(TypeSpecifier $typeSpecifier, Scope $scope, Expr $expr, TypeSpecifierContext $context): SpecifiedTypes - { - return $typeSpecifier->specifyDefaultTypes($scope, $expr, $context); - } - } From 4c784db75b7e42e4359352622ae1f05ba13bb42d Mon Sep 17 00:00:00 2001 From: Ondrej Mirtes Date: Wed, 17 Jun 2026 22:13:53 +0200 Subject: [PATCH 073/113] NullsafePropertyFetchHandler and NullsafeMethodCallHandler are no longer TypeResolvingExprHandler --- .../ExprHandler/NullsafeMethodCallHandler.php | 91 +++++++++---------- .../NullsafePropertyFetchHandler.php | 83 +++++++++-------- 2 files changed, 86 insertions(+), 88 deletions(-) diff --git a/src/Analyser/ExprHandler/NullsafeMethodCallHandler.php b/src/Analyser/ExprHandler/NullsafeMethodCallHandler.php index 9f8ec0fce9f..fd3d7c61c67 100644 --- a/src/Analyser/ExprHandler/NullsafeMethodCallHandler.php +++ b/src/Analyser/ExprHandler/NullsafeMethodCallHandler.php @@ -14,12 +14,12 @@ use PHPStan\Analyser\ExpressionResult; use PHPStan\Analyser\ExpressionResultFactory; use PHPStan\Analyser\ExpressionResultStorage; +use PHPStan\Analyser\ExprHandler; +use PHPStan\Analyser\ExprHandler\Helper\DefaultNarrowingHelper; use PHPStan\Analyser\ExprHandler\Helper\NonNullabilityHelper; use PHPStan\Analyser\MutatingScope; use PHPStan\Analyser\NodeScopeResolver; -use PHPStan\Analyser\Scope; use PHPStan\Analyser\SpecifiedTypes; -use PHPStan\Analyser\TypeResolvingExprHandler; use PHPStan\Analyser\TypeSpecifier; use PHPStan\Analyser\TypeSpecifierContext; use PHPStan\DependencyInjection\AutowiredService; @@ -30,15 +30,17 @@ use function array_merge; /** - * @implements TypeResolvingExprHandler + * @implements ExprHandler */ #[AutowiredService] -final class NullsafeMethodCallHandler implements TypeResolvingExprHandler +final class NullsafeMethodCallHandler implements ExprHandler { public function __construct( private NonNullabilityHelper $nonNullabilityHelper, private ExpressionResultFactory $expressionResultFactory, + private TypeSpecifier $typeSpecifier, + private DefaultNarrowingHelper $defaultNarrowingHelper, ) { } @@ -48,42 +50,6 @@ public function supports(Expr $expr): bool return $expr instanceof NullsafeMethodCall; } - public function resolveType(MutatingScope $scope, Expr $expr): Type - { - $varType = $scope->getType($expr->var); - if ($varType->isNull()->yes()) { - return new NullType(); - } - if (!TypeCombinator::containsNull($varType)) { - return $scope->getType(new MethodCall($expr->var, $expr->name, $expr->args)); - } - - return TypeCombinator::union( - $scope->filterByTruthyValue(new NotIdentical($expr->var, new ConstFetch(new Name('null')))) - ->getType(new MethodCall($expr->var, $expr->name, $expr->args)), - new NullType(), - ); - } - - public function specifyTypes(TypeSpecifier $typeSpecifier, Scope $scope, Expr $expr, TypeSpecifierContext $context): SpecifiedTypes - { - if ($context->null()) { - return $typeSpecifier->specifyDefaultTypes($scope, $expr, $context); - } - - $types = $typeSpecifier->specifyTypesInCondition( - $scope, - new BooleanAnd( - new NotIdentical($expr->var, new ConstFetch(new Name('null'))), - new MethodCall($expr->var, $expr->name, $expr->args), - ), - $context, - )->setRootExpr($expr); - - $nullSafeTypes = $typeSpecifier->handleDefaultTruthyOrFalseyContext($context, $expr, $scope); - return $context->true() ? $types->unionWith($nullSafeTypes) : $types->normalize($scope)->intersectWith($nullSafeTypes->normalize($scope)); - } - public function processExpr(NodeScopeResolver $nodeScopeResolver, Stmt $stmt, Expr $expr, MutatingScope $scope, ExpressionResultStorage $storage, callable $nodeCallback, ExpressionContext $context): ExpressionResult { $beforeScope = $scope; @@ -93,14 +59,15 @@ public function processExpr(NodeScopeResolver $nodeScopeResolver, Stmt $stmt, Ex $nonNullabilityResult = $this->nonNullabilityHelper->ensureShallowNonNullability($scope, $scope, $expr->var); $attributes = array_merge($expr->getAttributes(), ['virtualNullsafeMethodCall' => true]); unset($attributes[ExprPrinter::ATTRIBUTE_CACHE_KEY]); + $methodCall = new MethodCall( + $expr->var, + $expr->name, + $expr->args, + $attributes, + ); $exprResult = $nodeScopeResolver->processExprNode( $stmt, - new MethodCall( - $expr->var, - $expr->name, - $expr->args, - $attributes, - ), + $methodCall, $nonNullabilityResult->getScope(), $storage, $nodeCallback, @@ -127,6 +94,38 @@ public function processExpr(NodeScopeResolver $nodeScopeResolver, Stmt $stmt, Ex throwPoints: $exprResult->getThrowPoints(), impurePoints: $exprResult->getImpurePoints(), containsNullsafe: true, + typeCallback: static function (MutatingScope $s) use ($expr, $exprResult): Type { + $varType = $s->getType($expr->var); + if ($varType->isNull()->yes()) { + return new NullType(); + } + if (!TypeCombinator::containsNull($varType)) { + return $exprResult->getTypeForScope($s); + } + + return TypeCombinator::union( + $s->filterByTruthyValue(new NotIdentical($expr->var, new ConstFetch(new Name('null')))) + ->getType(new MethodCall($expr->var, $expr->name, $expr->args)), + new NullType(), + ); + }, + specifyTypesCallback: function (MutatingScope $s, TypeSpecifierContext $context) use ($expr, $methodCall): SpecifiedTypes { + if ($context->null()) { + return $this->defaultNarrowingHelper->specifyDefaultTypes($expr, $context); + } + + $types = $this->typeSpecifier->specifyTypesInCondition( + $s, + new BooleanAnd( + new NotIdentical($expr->var, new ConstFetch(new Name('null'))), + $methodCall, + ), + $context, + )->setRootExpr($expr); + + $nullSafeTypes = $this->typeSpecifier->handleDefaultTruthyOrFalseyContext($context, $expr, $s); + return $context->true() ? $types->unionWith($nullSafeTypes) : $types->normalize($s)->intersectWith($nullSafeTypes->normalize($s)); + }, ); } diff --git a/src/Analyser/ExprHandler/NullsafePropertyFetchHandler.php b/src/Analyser/ExprHandler/NullsafePropertyFetchHandler.php index 26bd5d41224..d2efe1ea4f3 100644 --- a/src/Analyser/ExprHandler/NullsafePropertyFetchHandler.php +++ b/src/Analyser/ExprHandler/NullsafePropertyFetchHandler.php @@ -14,12 +14,12 @@ use PHPStan\Analyser\ExpressionResult; use PHPStan\Analyser\ExpressionResultFactory; use PHPStan\Analyser\ExpressionResultStorage; +use PHPStan\Analyser\ExprHandler; +use PHPStan\Analyser\ExprHandler\Helper\DefaultNarrowingHelper; use PHPStan\Analyser\ExprHandler\Helper\NonNullabilityHelper; use PHPStan\Analyser\MutatingScope; use PHPStan\Analyser\NodeScopeResolver; -use PHPStan\Analyser\Scope; use PHPStan\Analyser\SpecifiedTypes; -use PHPStan\Analyser\TypeResolvingExprHandler; use PHPStan\Analyser\TypeSpecifier; use PHPStan\Analyser\TypeSpecifierContext; use PHPStan\DependencyInjection\AutowiredService; @@ -30,15 +30,17 @@ use function array_merge; /** - * @implements TypeResolvingExprHandler + * @implements ExprHandler */ #[AutowiredService] -final class NullsafePropertyFetchHandler implements TypeResolvingExprHandler +final class NullsafePropertyFetchHandler implements ExprHandler { public function __construct( private NonNullabilityHelper $nonNullabilityHelper, private ExpressionResultFactory $expressionResultFactory, + private TypeSpecifier $typeSpecifier, + private DefaultNarrowingHelper $defaultNarrowingHelper, ) { } @@ -48,53 +50,18 @@ public function supports(Expr $expr): bool return $expr instanceof NullsafePropertyFetch; } - public function resolveType(MutatingScope $scope, Expr $expr): Type - { - $varType = $scope->getType($expr->var); - if ($varType->isNull()->yes()) { - return new NullType(); - } - if (!TypeCombinator::containsNull($varType)) { - return $scope->getType(new PropertyFetch($expr->var, $expr->name)); - } - - return TypeCombinator::union( - $scope->filterByTruthyValue(new NotIdentical($expr->var, new ConstFetch(new Name('null')))) - ->getType(new PropertyFetch($expr->var, $expr->name)), - new NullType(), - ); - } - - public function specifyTypes(TypeSpecifier $typeSpecifier, Scope $scope, Expr $expr, TypeSpecifierContext $context): SpecifiedTypes - { - if ($context->null()) { - return $typeSpecifier->specifyDefaultTypes($scope, $expr, $context); - } - - $types = $typeSpecifier->specifyTypesInCondition( - $scope, - new BooleanAnd( - new NotIdentical($expr->var, new ConstFetch(new Name('null'))), - new PropertyFetch($expr->var, $expr->name), - ), - $context, - )->setRootExpr($expr); - - $nullSafeTypes = $typeSpecifier->handleDefaultTruthyOrFalseyContext($context, $expr, $scope); - return $context->true() ? $types->unionWith($nullSafeTypes) : $types->normalize($scope)->intersectWith($nullSafeTypes->normalize($scope)); - } - public function processExpr(NodeScopeResolver $nodeScopeResolver, Stmt $stmt, Expr $expr, MutatingScope $scope, ExpressionResultStorage $storage, callable $nodeCallback, ExpressionContext $context): ExpressionResult { $beforeScope = $scope; $nonNullabilityResult = $this->nonNullabilityHelper->ensureShallowNonNullability($scope, $scope, $expr->var); $attributes = array_merge($expr->getAttributes(), ['virtualNullsafePropertyFetch' => true]); unset($attributes[ExprPrinter::ATTRIBUTE_CACHE_KEY]); - $exprResult = $nodeScopeResolver->processExprNode($stmt, new PropertyFetch( + $propertyFetch = new PropertyFetch( $expr->var, $expr->name, $attributes, - ), $nonNullabilityResult->getScope(), $storage, $nodeCallback, $context); + ); + $exprResult = $nodeScopeResolver->processExprNode($stmt, $propertyFetch, $nonNullabilityResult->getScope(), $storage, $nodeCallback, $context); $scope = $this->nonNullabilityHelper->revertNonNullability($exprResult->getScope(), $nonNullabilityResult->getSpecifiedExpressions()); return $this->expressionResultFactory->create( @@ -106,6 +73,38 @@ public function processExpr(NodeScopeResolver $nodeScopeResolver, Stmt $stmt, Ex throwPoints: $exprResult->getThrowPoints(), impurePoints: $exprResult->getImpurePoints(), containsNullsafe: true, + typeCallback: static function (MutatingScope $s) use ($expr, $exprResult): Type { + $varType = $s->getType($expr->var); + if ($varType->isNull()->yes()) { + return new NullType(); + } + if (!TypeCombinator::containsNull($varType)) { + return $exprResult->getTypeForScope($s); + } + + return TypeCombinator::union( + $s->filterByTruthyValue(new NotIdentical($expr->var, new ConstFetch(new Name('null')))) + ->getType(new PropertyFetch($expr->var, $expr->name)), + new NullType(), + ); + }, + specifyTypesCallback: function (MutatingScope $s, TypeSpecifierContext $context) use ($expr, $propertyFetch): SpecifiedTypes { + if ($context->null()) { + return $this->defaultNarrowingHelper->specifyDefaultTypes($expr, $context); + } + + $types = $this->typeSpecifier->specifyTypesInCondition( + $s, + new BooleanAnd( + new NotIdentical($expr->var, new ConstFetch(new Name('null'))), + $propertyFetch, + ), + $context, + )->setRootExpr($expr); + + $nullSafeTypes = $this->typeSpecifier->handleDefaultTruthyOrFalseyContext($context, $expr, $s); + return $context->true() ? $types->unionWith($nullSafeTypes) : $types->normalize($s)->intersectWith($nullSafeTypes->normalize($s)); + }, ); } From 31888e7536162d982ac89319fdebceb36730c839 Mon Sep 17 00:00:00 2001 From: Ondrej Mirtes Date: Wed, 17 Jun 2026 22:34:25 +0200 Subject: [PATCH 074/113] Move first-class callable type resolution into the *CallableNode handlers --- .../FirstClassCallableFuncCallHandler.php | 81 ------------------ .../FirstClassCallableMethodCallHandler.php | 82 ------------------- .../FirstClassCallableNewHandler.php | 67 --------------- .../FirstClassCallableStaticCallHandler.php | 66 --------------- src/Analyser/ExprHandler/PipeHandler.php | 18 ++-- .../Virtual/FunctionCallableNodeHandler.php | 29 ++++++- .../InstantiationCallableNodeHandler.php | 8 +- .../Virtual/MethodCallableNodeHandler.php | 30 ++++++- .../StaticMethodCallableNodeHandler.php | 8 +- src/Analyser/MutatingScope.php | 8 ++ src/Analyser/NodeScopeResolver.php | 3 + 11 files changed, 82 insertions(+), 318 deletions(-) delete mode 100644 src/Analyser/ExprHandler/FirstClassCallableFuncCallHandler.php delete mode 100644 src/Analyser/ExprHandler/FirstClassCallableMethodCallHandler.php delete mode 100644 src/Analyser/ExprHandler/FirstClassCallableNewHandler.php delete mode 100644 src/Analyser/ExprHandler/FirstClassCallableStaticCallHandler.php diff --git a/src/Analyser/ExprHandler/FirstClassCallableFuncCallHandler.php b/src/Analyser/ExprHandler/FirstClassCallableFuncCallHandler.php deleted file mode 100644 index 2fafbee4dbe..00000000000 --- a/src/Analyser/ExprHandler/FirstClassCallableFuncCallHandler.php +++ /dev/null @@ -1,81 +0,0 @@ - - */ -#[AutowiredService] -final class FirstClassCallableFuncCallHandler implements TypeResolvingExprHandler -{ - - public function __construct( - private InitializerExprTypeResolver $initializerExprTypeResolver, - ) - { - } - - public function supports(Expr $expr): bool - { - return $expr instanceof FuncCall && $expr->isFirstClassCallable(); - } - - public function processExpr( - NodeScopeResolver $nodeScopeResolver, - Stmt $stmt, - Expr $expr, - MutatingScope $scope, - ExpressionResultStorage $storage, - callable $nodeCallback, - ExpressionContext $context, - ): ExpressionResult - { - // handled in NodeScopeResolver before ExprHandlers are called - throw new ShouldNotHappenException(); - } - - public function resolveType(MutatingScope $scope, Expr $expr): Type - { - if ($expr->name instanceof Expr) { - $callableType = $scope->getType($expr->name); - if (!$callableType->isCallable()->yes()) { - return new ObjectType(Closure::class); - } - - return $this->initializerExprTypeResolver->createFirstClassCallable( - null, - $callableType->getCallableParametersAcceptors($scope), - $scope->nativeTypesPromoted, - ); - } - - return $this->initializerExprTypeResolver->getFirstClassCallableType($expr, InitializerExprContext::fromScope($scope), $scope->nativeTypesPromoted); - } - - public function specifyTypes(TypeSpecifier $typeSpecifier, Scope $scope, Expr $expr, TypeSpecifierContext $context): SpecifiedTypes - { - return (new SpecifiedTypes([], []))->setRootExpr($expr); - } - -} diff --git a/src/Analyser/ExprHandler/FirstClassCallableMethodCallHandler.php b/src/Analyser/ExprHandler/FirstClassCallableMethodCallHandler.php deleted file mode 100644 index e487de41fb6..00000000000 --- a/src/Analyser/ExprHandler/FirstClassCallableMethodCallHandler.php +++ /dev/null @@ -1,82 +0,0 @@ - - */ -#[AutowiredService] -final class FirstClassCallableMethodCallHandler implements TypeResolvingExprHandler -{ - - public function __construct( - private InitializerExprTypeResolver $initializerExprTypeResolver, - ) - { - } - - public function supports(Expr $expr): bool - { - return $expr instanceof MethodCall && $expr->isFirstClassCallable(); - } - - public function processExpr( - NodeScopeResolver $nodeScopeResolver, - Stmt $stmt, - Expr $expr, - MutatingScope $scope, - ExpressionResultStorage $storage, - callable $nodeCallback, - ExpressionContext $context, - ): ExpressionResult - { - // handled in NodeScopeResolver before ExprHandlers are called - throw new ShouldNotHappenException(); - } - - public function resolveType(MutatingScope $scope, Expr $expr): Type - { - if (!$expr->name instanceof Identifier) { - return new ObjectType(Closure::class); - } - - $varType = $scope->getType($expr->var); - $method = $scope->getMethodReflection($varType, $expr->name->toString()); - if ($method === null) { - return new ObjectType(Closure::class); - } - - return $this->initializerExprTypeResolver->createFirstClassCallable( - $method, - $method->getVariants(), - $scope->nativeTypesPromoted, - ); - } - - public function specifyTypes(TypeSpecifier $typeSpecifier, Scope $scope, Expr $expr, TypeSpecifierContext $context): SpecifiedTypes - { - return (new SpecifiedTypes([], []))->setRootExpr($expr); - } - -} diff --git a/src/Analyser/ExprHandler/FirstClassCallableNewHandler.php b/src/Analyser/ExprHandler/FirstClassCallableNewHandler.php deleted file mode 100644 index 9f5e05c198d..00000000000 --- a/src/Analyser/ExprHandler/FirstClassCallableNewHandler.php +++ /dev/null @@ -1,67 +0,0 @@ - - */ -#[AutowiredService] -final class FirstClassCallableNewHandler implements TypeResolvingExprHandler -{ - - public function __construct( - private InitializerExprTypeResolver $initializerExprTypeResolver, - ) - { - } - - public function supports(Expr $expr): bool - { - return $expr instanceof New_ && !$expr->class instanceof Class_ && $expr->isFirstClassCallable(); - } - - public function processExpr( - NodeScopeResolver $nodeScopeResolver, - Stmt $stmt, - Expr $expr, - MutatingScope $scope, - ExpressionResultStorage $storage, - callable $nodeCallback, - ExpressionContext $context, - ): ExpressionResult - { - // handled in NodeScopeResolver before ExprHandlers are called - throw new ShouldNotHappenException(); - } - - public function resolveType(MutatingScope $scope, Expr $expr): Type - { - return $this->initializerExprTypeResolver->getFirstClassCallableType($expr, InitializerExprContext::fromScope($scope), $scope->nativeTypesPromoted); - } - - public function specifyTypes(TypeSpecifier $typeSpecifier, Scope $scope, Expr $expr, TypeSpecifierContext $context): SpecifiedTypes - { - return (new SpecifiedTypes([], []))->setRootExpr($expr); - } - -} diff --git a/src/Analyser/ExprHandler/FirstClassCallableStaticCallHandler.php b/src/Analyser/ExprHandler/FirstClassCallableStaticCallHandler.php deleted file mode 100644 index 4a5c3cd0645..00000000000 --- a/src/Analyser/ExprHandler/FirstClassCallableStaticCallHandler.php +++ /dev/null @@ -1,66 +0,0 @@ - - */ -#[AutowiredService] -final class FirstClassCallableStaticCallHandler implements TypeResolvingExprHandler -{ - - public function __construct( - private InitializerExprTypeResolver $initializerExprTypeResolver, - ) - { - } - - public function supports(Expr $expr): bool - { - return $expr instanceof StaticCall && $expr->isFirstClassCallable(); - } - - public function processExpr( - NodeScopeResolver $nodeScopeResolver, - Stmt $stmt, - Expr $expr, - MutatingScope $scope, - ExpressionResultStorage $storage, - callable $nodeCallback, - ExpressionContext $context, - ): ExpressionResult - { - // handled in NodeScopeResolver before ExprHandlers are called - throw new ShouldNotHappenException(); - } - - public function resolveType(MutatingScope $scope, Expr $expr): Type - { - return $this->initializerExprTypeResolver->getFirstClassCallableType($expr, InitializerExprContext::fromScope($scope), $scope->nativeTypesPromoted); - } - - public function specifyTypes(TypeSpecifier $typeSpecifier, Scope $scope, Expr $expr, TypeSpecifierContext $context): SpecifiedTypes - { - return (new SpecifiedTypes([], []))->setRootExpr($expr); - } - -} diff --git a/src/Analyser/ExprHandler/PipeHandler.php b/src/Analyser/ExprHandler/PipeHandler.php index 6f27e615d52..fc55dd0b67b 100644 --- a/src/Analyser/ExprHandler/PipeHandler.php +++ b/src/Analyser/ExprHandler/PipeHandler.php @@ -20,7 +20,10 @@ use PHPStan\Analyser\SpecifiedTypes; use PHPStan\Analyser\TypeSpecifierContext; use PHPStan\DependencyInjection\AutowiredService; +use PHPStan\Node\FunctionCallableNode; +use PHPStan\Node\MethodCallableNode; use PHPStan\Node\Printer\ExprPrinter; +use PHPStan\Node\StaticMethodCallableNode; use PHPStan\Parser\ReversePipeTransformerVisitor; use PHPStan\Type\Type; use function array_merge; @@ -50,32 +53,34 @@ public function processExpr(NodeScopeResolver $nodeScopeResolver, Stmt $stmt, Ex unset($rightAttributes[ExprPrinter::ATTRIBUTE_CACHE_KEY]); $argAttributes = $expr->getAttribute(ReversePipeTransformerVisitor::ARG_ATTRIBUTES_NAME, []); - $isRightFirstClassCallable = false; + $firstClassCallableNode = null; if ($expr->right instanceof FuncCall && $expr->right->isFirstClassCallable()) { $callExpr = new FuncCall($expr->right->name, [ new Arg($expr->left, attributes: $argAttributes), ], $rightAttributes); - $isRightFirstClassCallable = true; + $firstClassCallableNode = new FunctionCallableNode($expr->right->name, $expr->right); } elseif ($expr->right instanceof MethodCall && $expr->right->isFirstClassCallable()) { $callExpr = new MethodCall($expr->right->var, $expr->right->name, [ new Arg($expr->left, attributes: $argAttributes), ], $rightAttributes); - $isRightFirstClassCallable = true; + $firstClassCallableNode = new MethodCallableNode($expr->right->var, $expr->right->name, $expr->right); } elseif ($expr->right instanceof StaticCall && $expr->right->isFirstClassCallable()) { $callExpr = new StaticCall($expr->right->class, $expr->right->name, [ new Arg($expr->left, attributes: $argAttributes), ], $rightAttributes); - $isRightFirstClassCallable = true; + $firstClassCallableNode = new StaticMethodCallableNode($expr->right->class, $expr->right->name, $expr->right); } else { $callExpr = new FuncCall($expr->right, [ new Arg($expr->left, attributes: $argAttributes), ], $rightAttributes); } - if ($isRightFirstClassCallable) { + if ($firstClassCallableNode !== null) { // the original first-class callable node is not processed through // processExprNode - store its result so that node callbacks asking - // about its type can be resumed + // about its type can be resumed. Its closure type lives on the + // matching *CallableNode, resolved on demand by its handler. + $callableNode = $firstClassCallableNode; $nodeScopeResolver->storeExpressionResult($storage, $expr->right, $this->expressionResultFactory->create( $scope, beforeScope: $scope, @@ -84,6 +89,7 @@ public function processExpr(NodeScopeResolver $nodeScopeResolver, Stmt $stmt, Ex isAlwaysTerminating: false, throwPoints: [], impurePoints: [], + typeCallback: static fn (MutatingScope $s): Type => $s->getType($callableNode), )); } diff --git a/src/Analyser/ExprHandler/Virtual/FunctionCallableNodeHandler.php b/src/Analyser/ExprHandler/Virtual/FunctionCallableNodeHandler.php index ae01f6be2b0..59da77e1335 100644 --- a/src/Analyser/ExprHandler/Virtual/FunctionCallableNodeHandler.php +++ b/src/Analyser/ExprHandler/Virtual/FunctionCallableNodeHandler.php @@ -2,6 +2,7 @@ namespace PHPStan\Analyser\ExprHandler\Virtual; +use Closure; use PhpParser\Node\Expr; use PhpParser\Node\Stmt; use PHPStan\Analyser\ExpressionContext; @@ -15,7 +16,9 @@ use PHPStan\Analyser\TypeSpecifierContext; use PHPStan\DependencyInjection\AutowiredService; use PHPStan\Node\FunctionCallableNode; -use PHPStan\Type\MixedType; +use PHPStan\Reflection\InitializerExprContext; +use PHPStan\Reflection\InitializerExprTypeResolver; +use PHPStan\Type\ObjectType; use PHPStan\Type\Type; /** @@ -28,6 +31,7 @@ final class FunctionCallableNodeHandler implements ExprHandler public function __construct( private ExpressionResultFactory $expressionResultFactory, private DefaultNarrowingHelper $defaultNarrowingHelper, + private InitializerExprTypeResolver $initializerExprTypeResolver, ) { } @@ -61,11 +65,28 @@ public function processExpr(NodeScopeResolver $nodeScopeResolver, Stmt $stmt, Ex isAlwaysTerminating: $isAlwaysTerminating, throwPoints: $throwPoints, impurePoints: $impurePoints, - // in practice the type of the first-class callable is resolved - // by FirstClassCallableFuncCallHandler - typeCallback: static fn (MutatingScope $scope): Type => new MixedType(), + typeCallback: fn (MutatingScope $scope): Type => $this->resolveType($scope, $expr), specifyTypesCallback: fn (MutatingScope $s, TypeSpecifierContext $context) => $this->defaultNarrowingHelper->specifyDefaultTypes($expr, $context), ); } + private function resolveType(MutatingScope $scope, FunctionCallableNode $expr): Type + { + $originalNode = $expr->getOriginalNode(); + if ($originalNode->name instanceof Expr) { + $callableType = $scope->getType($originalNode->name); + if (!$callableType->isCallable()->yes()) { + return new ObjectType(Closure::class); + } + + return $this->initializerExprTypeResolver->createFirstClassCallable( + null, + $callableType->getCallableParametersAcceptors($scope), + $scope->nativeTypesPromoted, + ); + } + + return $this->initializerExprTypeResolver->getFirstClassCallableType($originalNode, InitializerExprContext::fromScope($scope), $scope->nativeTypesPromoted); + } + } diff --git a/src/Analyser/ExprHandler/Virtual/InstantiationCallableNodeHandler.php b/src/Analyser/ExprHandler/Virtual/InstantiationCallableNodeHandler.php index 9fd33f592e6..c7ee4040a98 100644 --- a/src/Analyser/ExprHandler/Virtual/InstantiationCallableNodeHandler.php +++ b/src/Analyser/ExprHandler/Virtual/InstantiationCallableNodeHandler.php @@ -15,7 +15,8 @@ use PHPStan\Analyser\TypeSpecifierContext; use PHPStan\DependencyInjection\AutowiredService; use PHPStan\Node\InstantiationCallableNode; -use PHPStan\Type\MixedType; +use PHPStan\Reflection\InitializerExprContext; +use PHPStan\Reflection\InitializerExprTypeResolver; use PHPStan\Type\Type; /** @@ -28,6 +29,7 @@ final class InstantiationCallableNodeHandler implements ExprHandler public function __construct( private ExpressionResultFactory $expressionResultFactory, private DefaultNarrowingHelper $defaultNarrowingHelper, + private InitializerExprTypeResolver $initializerExprTypeResolver, ) { } @@ -61,9 +63,7 @@ public function processExpr(NodeScopeResolver $nodeScopeResolver, Stmt $stmt, Ex isAlwaysTerminating: $isAlwaysTerminating, throwPoints: $throwPoints, impurePoints: $impurePoints, - // in practice the type of the first-class callable is resolved - // by FirstClassCallableNewHandler - typeCallback: static fn (MutatingScope $scope): Type => new MixedType(), + typeCallback: fn (MutatingScope $scope): Type => $this->initializerExprTypeResolver->getFirstClassCallableType($expr->getOriginalNode(), InitializerExprContext::fromScope($scope), $scope->nativeTypesPromoted), specifyTypesCallback: fn (MutatingScope $s, TypeSpecifierContext $context) => $this->defaultNarrowingHelper->specifyDefaultTypes($expr, $context), ); } diff --git a/src/Analyser/ExprHandler/Virtual/MethodCallableNodeHandler.php b/src/Analyser/ExprHandler/Virtual/MethodCallableNodeHandler.php index 15aa475bbc9..175b0e8ebdb 100644 --- a/src/Analyser/ExprHandler/Virtual/MethodCallableNodeHandler.php +++ b/src/Analyser/ExprHandler/Virtual/MethodCallableNodeHandler.php @@ -2,7 +2,9 @@ namespace PHPStan\Analyser\ExprHandler\Virtual; +use Closure; use PhpParser\Node\Expr; +use PhpParser\Node\Identifier; use PhpParser\Node\Stmt; use PHPStan\Analyser\ExpressionContext; use PHPStan\Analyser\ExpressionResult; @@ -15,7 +17,8 @@ use PHPStan\Analyser\TypeSpecifierContext; use PHPStan\DependencyInjection\AutowiredService; use PHPStan\Node\MethodCallableNode; -use PHPStan\Type\MixedType; +use PHPStan\Reflection\InitializerExprTypeResolver; +use PHPStan\Type\ObjectType; use PHPStan\Type\Type; use function array_merge; @@ -29,6 +32,7 @@ final class MethodCallableNodeHandler implements ExprHandler public function __construct( private ExpressionResultFactory $expressionResultFactory, private DefaultNarrowingHelper $defaultNarrowingHelper, + private InitializerExprTypeResolver $initializerExprTypeResolver, ) { } @@ -64,11 +68,29 @@ public function processExpr(NodeScopeResolver $nodeScopeResolver, Stmt $stmt, Ex isAlwaysTerminating: $isAlwaysTerminating, throwPoints: $throwPoints, impurePoints: $impurePoints, - // in practice the type of the first-class callable is resolved - // by FirstClassCallableMethodCallHandler - typeCallback: static fn (MutatingScope $scope): Type => new MixedType(), + typeCallback: fn (MutatingScope $scope): Type => $this->resolveType($scope, $expr), specifyTypesCallback: fn (MutatingScope $s, TypeSpecifierContext $context) => $this->defaultNarrowingHelper->specifyDefaultTypes($expr, $context), ); } + private function resolveType(MutatingScope $scope, MethodCallableNode $expr): Type + { + $originalNode = $expr->getOriginalNode(); + if (!$originalNode->name instanceof Identifier) { + return new ObjectType(Closure::class); + } + + $varType = $scope->getType($originalNode->var); + $method = $scope->getMethodReflection($varType, $originalNode->name->toString()); + if ($method === null) { + return new ObjectType(Closure::class); + } + + return $this->initializerExprTypeResolver->createFirstClassCallable( + $method, + $method->getVariants(), + $scope->nativeTypesPromoted, + ); + } + } diff --git a/src/Analyser/ExprHandler/Virtual/StaticMethodCallableNodeHandler.php b/src/Analyser/ExprHandler/Virtual/StaticMethodCallableNodeHandler.php index 59a5a808985..33583ff0ec5 100644 --- a/src/Analyser/ExprHandler/Virtual/StaticMethodCallableNodeHandler.php +++ b/src/Analyser/ExprHandler/Virtual/StaticMethodCallableNodeHandler.php @@ -15,7 +15,8 @@ use PHPStan\Analyser\TypeSpecifierContext; use PHPStan\DependencyInjection\AutowiredService; use PHPStan\Node\StaticMethodCallableNode; -use PHPStan\Type\MixedType; +use PHPStan\Reflection\InitializerExprContext; +use PHPStan\Reflection\InitializerExprTypeResolver; use PHPStan\Type\Type; use function array_merge; @@ -29,6 +30,7 @@ final class StaticMethodCallableNodeHandler implements ExprHandler public function __construct( private ExpressionResultFactory $expressionResultFactory, private DefaultNarrowingHelper $defaultNarrowingHelper, + private InitializerExprTypeResolver $initializerExprTypeResolver, ) { } @@ -70,9 +72,7 @@ public function processExpr(NodeScopeResolver $nodeScopeResolver, Stmt $stmt, Ex isAlwaysTerminating: $isAlwaysTerminating, throwPoints: $throwPoints, impurePoints: $impurePoints, - // in practice the type of the first-class callable is resolved - // by FirstClassCallableStaticCallHandler - typeCallback: static fn (MutatingScope $scope): Type => new MixedType(), + typeCallback: fn (MutatingScope $scope): Type => $this->initializerExprTypeResolver->getFirstClassCallableType($expr->getOriginalNode(), InitializerExprContext::fromScope($scope), $scope->nativeTypesPromoted), specifyTypesCallback: fn (MutatingScope $s, TypeSpecifierContext $context) => $this->defaultNarrowingHelper->specifyDefaultTypes($expr, $context), ); } diff --git a/src/Analyser/MutatingScope.php b/src/Analyser/MutatingScope.php index 348ef238646..ea9feb70955 100644 --- a/src/Analyser/MutatingScope.php +++ b/src/Analyser/MutatingScope.php @@ -998,6 +998,14 @@ private function resolveType(string $exprString, Expr $node): Type return $this->expressionTypes[$exprString]->getType(); } + // NodeScopeResolver intercepts a first-class callable CallLike before the + // ExprHandler loop - no handler supports the original node, its closure + // type lives on the stored result's typeCallback (see the *CallableNode + // handlers), mirroring TypeSpecifier::specifyTypesInCondition(). + if ($node instanceof Expr\CallLike && $node->isFirstClassCallable()) { + return $this->resolveTypeOfNewWorldHandlerNode($node); + } + /** @var ExprHandler $exprHandler */ foreach ($this->container->getServicesByTag(ExprHandler::EXTENSION_TAG) as $exprHandler) { if (!$exprHandler->supports($node)) { diff --git a/src/Analyser/NodeScopeResolver.php b/src/Analyser/NodeScopeResolver.php index ec2472606d6..7a2b0475bf3 100644 --- a/src/Analyser/NodeScopeResolver.php +++ b/src/Analyser/NodeScopeResolver.php @@ -2938,6 +2938,9 @@ private function processExprNodeInternal( isAlwaysTerminating: $newExprResult->isAlwaysTerminating(), throwPoints: $newExprResult->getThrowPoints(), impurePoints: $newExprResult->getImpurePoints(), + // the first-class callable closure type lives on the *CallableNode + // result; delegate so getType() of the original CallLike answers from it + typeCallback: static fn (MutatingScope $s): Type => $newExprResult->getTypeForScope($s), ); $this->storeExpressionResult($storage, $expr, $expressionResult); return $expressionResult; From f3371dce224a447961621e4f2c318408d590408f Mon Sep 17 00:00:00 2001 From: Ondrej Mirtes Date: Wed, 17 Jun 2026 23:06:03 +0200 Subject: [PATCH 075/113] Price synthetic narrowing expressions on demand in getCurrentTypesOfSpecifiedExpr --- src/Analyser/MutatingScope.php | 23 +++++++++++++++++++---- 1 file changed, 19 insertions(+), 4 deletions(-) diff --git a/src/Analyser/MutatingScope.php b/src/Analyser/MutatingScope.php index ea9feb70955..31eab874540 100644 --- a/src/Analyser/MutatingScope.php +++ b/src/Analyser/MutatingScope.php @@ -1068,9 +1068,12 @@ private function resolveTypeOfNewWorldHandlerNode(Expr $node): Type * applySpecifiedTypes() needs to intersect with or subtract from but that * is not tracked in the scope. Old-world filterBySpecifiedTypes() asked * Scope::getType() here; pricing from the stored ExpressionResult answers - * through the typeCallback for converted handlers and keeps the legacy - * resolution as a bridge for the rest. Returns null for nodes the analysis - * in progress never processed (synthetic ones). + * through the typeCallback for converted handlers. A synthetic node the + * analysis never processed - e.g. the plain-chain variant a nullsafe + * narrowing emits ($a->b() alongside $a?->b()) - is priced on demand, + * mirroring resolveTypeOfNewWorldHandlerNode(); its real subnodes answer + * from stored results so the on-demand walk terminates. Returns null only + * when there is no analysis in progress to price against. * * @return array{Type, Type}|null */ @@ -1083,7 +1086,19 @@ private function getCurrentTypesOfSpecifiedExpr(Expr $expr): ?array $result = $storage->findExpressionResult($expr); if ($result === null) { - return null; + // a synthetic node - price it on demand, see + // resolveTypeOfNewWorldHandlerNode() + $scope = $this->toMutatingScope(); + $result = $this->container->getByType(NodeScopeResolver::class)->processExprOnDemand( + $expr, + $scope, + $storage->duplicate(), + ); + + return [ + $result->getTypeForScope($scope), + $result->getNativeTypeForScope($scope), + ]; } // re-evaluate on the asking scope, not the stored beforeScope: a handler From fad3c7b406adba17168fde9e888314e1f0ed3722 Mon Sep 17 00:00:00 2001 From: Ondrej Mirtes Date: Wed, 17 Jun 2026 23:06:03 +0200 Subject: [PATCH 076/113] BinaryOpHandler and BooleanNotHandler are no longer TypeResolvingExprHandler --- phpstan-baseline.neon | 6 - src/Analyser/ExprHandler/BinaryOpHandler.php | 827 +++++++++--------- .../ExprHandler/BooleanNotHandler.php | 49 +- 3 files changed, 459 insertions(+), 423 deletions(-) diff --git a/phpstan-baseline.neon b/phpstan-baseline.neon index 9b17fcbb52e..c805a89d7a3 100644 --- a/phpstan-baseline.neon +++ b/phpstan-baseline.neon @@ -36,12 +36,6 @@ parameters: count: 2 path: src/Analyser/ExprHandler/BinaryOpHandler.php - - - rawMessage: 'Doing instanceof PHPStan\Type\Constant\ConstantBooleanType is error-prone and deprecated. Use Type::isTrue() or Type::isFalse() instead.' - identifier: phpstanApi.instanceofType - count: 1 - path: src/Analyser/ExprHandler/BooleanNotHandler.php - - rawMessage: 'Doing instanceof PHPStan\Type\ConstantScalarType is error-prone and deprecated. Use Type::isConstantScalarValue() or Type::getConstantScalarTypes() or Type::getConstantScalarValues() instead.' identifier: phpstanApi.instanceofType diff --git a/src/Analyser/ExprHandler/BinaryOpHandler.php b/src/Analyser/ExprHandler/BinaryOpHandler.php index 516f54780a0..74046cba8ef 100644 --- a/src/Analyser/ExprHandler/BinaryOpHandler.php +++ b/src/Analyser/ExprHandler/BinaryOpHandler.php @@ -16,15 +16,15 @@ use PHPStan\Analyser\ExpressionResult; use PHPStan\Analyser\ExpressionResultFactory; use PHPStan\Analyser\ExpressionResultStorage; +use PHPStan\Analyser\ExprHandler; +use PHPStan\Analyser\ExprHandler\Helper\DefaultNarrowingHelper; use PHPStan\Analyser\ExprHandler\Helper\EqualityTypeSpecifyingHelper; use PHPStan\Analyser\ExprHandler\Helper\ImplicitToStringCallHelper; use PHPStan\Analyser\InternalThrowPoint; use PHPStan\Analyser\MutatingScope; use PHPStan\Analyser\NodeScopeResolver; use PHPStan\Analyser\RicherScopeGetTypeHelper; -use PHPStan\Analyser\Scope; use PHPStan\Analyser\SpecifiedTypes; -use PHPStan\Analyser\TypeResolvingExprHandler; use PHPStan\Analyser\TypeSpecifier; use PHPStan\Analyser\TypeSpecifierContext; use PHPStan\DependencyInjection\AutowiredService; @@ -54,10 +54,10 @@ use function strtolower; /** - * @implements TypeResolvingExprHandler + * @implements ExprHandler */ #[AutowiredService] -final class BinaryOpHandler implements TypeResolvingExprHandler +final class BinaryOpHandler implements ExprHandler { public function __construct( @@ -68,6 +68,8 @@ public function __construct( private ExprPrinter $exprPrinter, private EqualityTypeSpecifyingHelper $equalityTypeSpecifyingHelper, private ExpressionResultFactory $expressionResultFactory, + private TypeSpecifier $typeSpecifier, + private DefaultNarrowingHelper $defaultNarrowingHelper, ) { } @@ -112,466 +114,503 @@ public function processExpr(NodeScopeResolver $nodeScopeResolver, Stmt $stmt, Ex isAlwaysTerminating: $leftResult->isAlwaysTerminating() || $rightResult->isAlwaysTerminating(), throwPoints: $throwPoints, impurePoints: $impurePoints, - ); - } + typeCallback: function (MutatingScope $scope) use ($expr): Type { + $getType = static fn (Expr $expr): Type => $scope->getType($expr); - public function resolveType(MutatingScope $scope, Expr $expr): Type - { - $getType = static fn (Expr $expr): Type => $scope->getType($expr); + if ($expr instanceof BinaryOp\Smaller) { + return $scope->getType($expr->left)->isSmallerThan($scope->getType($expr->right), $this->phpVersion)->toBooleanType(); + } - if ($expr instanceof BinaryOp\Smaller) { - return $scope->getType($expr->left)->isSmallerThan($scope->getType($expr->right), $this->phpVersion)->toBooleanType(); - } + if ($expr instanceof BinaryOp\SmallerOrEqual) { + return $scope->getType($expr->left)->isSmallerThanOrEqual($scope->getType($expr->right), $this->phpVersion)->toBooleanType(); + } - if ($expr instanceof BinaryOp\SmallerOrEqual) { - return $scope->getType($expr->left)->isSmallerThanOrEqual($scope->getType($expr->right), $this->phpVersion)->toBooleanType(); - } + if ($expr instanceof BinaryOp\Greater) { + return $scope->getType($expr->right)->isSmallerThan($scope->getType($expr->left), $this->phpVersion)->toBooleanType(); + } - if ($expr instanceof BinaryOp\Greater) { - return $scope->getType($expr->right)->isSmallerThan($scope->getType($expr->left), $this->phpVersion)->toBooleanType(); - } + if ($expr instanceof BinaryOp\GreaterOrEqual) { + return $scope->getType($expr->right)->isSmallerThanOrEqual($scope->getType($expr->left), $this->phpVersion)->toBooleanType(); + } - if ($expr instanceof BinaryOp\GreaterOrEqual) { - return $scope->getType($expr->right)->isSmallerThanOrEqual($scope->getType($expr->left), $this->phpVersion)->toBooleanType(); - } + if ($expr instanceof BinaryOp\Equal) { + return $this->resolveEqualType($scope, $expr); + } - if ($expr instanceof BinaryOp\Equal) { - if ( - $expr->left instanceof Variable - && is_string($expr->left->name) - && $expr->right instanceof Variable - && is_string($expr->right->name) - && $expr->left->name === $expr->right->name - ) { - return new ConstantBooleanType(true); - } + if ($expr instanceof BinaryOp\NotEqual) { + // negation of the Equal result - direct computation avoids + // synthesizing a BooleanNot node (which would route through + // on-demand re-processing once BooleanNot is migrated) + $equalType = $this->resolveEqualType($scope, new BinaryOp\Equal($expr->left, $expr->right))->toBoolean(); + if ($equalType->isTrue()->yes()) { + return new ConstantBooleanType(false); + } + if ($equalType->isFalse()->yes()) { + return new ConstantBooleanType(true); + } - $leftType = $scope->getType($expr->left); - $rightType = $scope->getType($expr->right); + return new BooleanType(); + } - return $this->initializerExprTypeResolver->resolveEqualType($leftType, $rightType)->type; - } + if ($expr instanceof BinaryOp\Identical) { + return $this->richerScopeGetTypeHelper->getIdenticalResult($scope, $expr)->type; + } - if ($expr instanceof BinaryOp\NotEqual) { - return $scope->getType(new Expr\BooleanNot(new BinaryOp\Equal($expr->left, $expr->right))); - } + if ($expr instanceof BinaryOp\NotIdentical) { + return $this->richerScopeGetTypeHelper->getNotIdenticalResult($scope, $expr)->type; + } - if ($expr instanceof BinaryOp\Identical) { - return $this->richerScopeGetTypeHelper->getIdenticalResult($scope, $expr)->type; - } + if ($expr instanceof BinaryOp\LogicalXor) { + $leftBooleanType = $scope->getType($expr->left)->toBoolean(); + $rightBooleanType = $scope->getType($expr->right)->toBoolean(); - if ($expr instanceof BinaryOp\NotIdentical) { - return $this->richerScopeGetTypeHelper->getNotIdenticalResult($scope, $expr)->type; - } + if ( + $leftBooleanType instanceof ConstantBooleanType + && $rightBooleanType instanceof ConstantBooleanType + ) { + return new ConstantBooleanType( + $leftBooleanType->getValue() xor $rightBooleanType->getValue(), + ); + } - if ($expr instanceof BinaryOp\LogicalXor) { - $leftBooleanType = $scope->getType($expr->left)->toBoolean(); - $rightBooleanType = $scope->getType($expr->right)->toBoolean(); - - if ( - $leftBooleanType instanceof ConstantBooleanType - && $rightBooleanType instanceof ConstantBooleanType - ) { - return new ConstantBooleanType( - $leftBooleanType->getValue() xor $rightBooleanType->getValue(), - ); - } + return new BooleanType(); + } - return new BooleanType(); - } + if ($expr instanceof BinaryOp\Spaceship) { + return $this->initializerExprTypeResolver->getSpaceshipType($expr->left, $expr->right, $getType); + } - if ($expr instanceof BinaryOp\Spaceship) { - return $this->initializerExprTypeResolver->getSpaceshipType($expr->left, $expr->right, $getType); - } + if ($expr instanceof BinaryOp\Concat) { + return $this->initializerExprTypeResolver->getConcatType($expr->left, $expr->right, $getType); + } - if ($expr instanceof BinaryOp\Concat) { - return $this->initializerExprTypeResolver->getConcatType($expr->left, $expr->right, $getType); - } + if ($expr instanceof BinaryOp\BitwiseAnd) { + return $this->initializerExprTypeResolver->getBitwiseAndType($expr->left, $expr->right, $getType); + } - if ($expr instanceof BinaryOp\BitwiseAnd) { - return $this->initializerExprTypeResolver->getBitwiseAndType($expr->left, $expr->right, $getType); - } + if ($expr instanceof BinaryOp\BitwiseOr) { + return $this->initializerExprTypeResolver->getBitwiseOrType($expr->left, $expr->right, $getType); + } - if ($expr instanceof BinaryOp\BitwiseOr) { - return $this->initializerExprTypeResolver->getBitwiseOrType($expr->left, $expr->right, $getType); - } + if ($expr instanceof BinaryOp\BitwiseXor) { + return $this->initializerExprTypeResolver->getBitwiseXorType($expr->left, $expr->right, $getType); + } - if ($expr instanceof BinaryOp\BitwiseXor) { - return $this->initializerExprTypeResolver->getBitwiseXorType($expr->left, $expr->right, $getType); - } + if ($expr instanceof BinaryOp\Div) { + return $this->initializerExprTypeResolver->getDivType($expr->left, $expr->right, $getType); + } - if ($expr instanceof BinaryOp\Div) { - return $this->initializerExprTypeResolver->getDivType($expr->left, $expr->right, $getType); - } + if ($expr instanceof BinaryOp\Mod) { + return $this->initializerExprTypeResolver->getModType($expr->left, $expr->right, $getType); + } - if ($expr instanceof BinaryOp\Mod) { - return $this->initializerExprTypeResolver->getModType($expr->left, $expr->right, $getType); - } + if ($expr instanceof BinaryOp\Plus) { + return $this->initializerExprTypeResolver->getPlusType($expr->left, $expr->right, $getType); + } - if ($expr instanceof BinaryOp\Plus) { - return $this->initializerExprTypeResolver->getPlusType($expr->left, $expr->right, $getType); - } + if ($expr instanceof BinaryOp\Minus) { + return $this->initializerExprTypeResolver->getMinusType($expr->left, $expr->right, $getType); + } - if ($expr instanceof BinaryOp\Minus) { - return $this->initializerExprTypeResolver->getMinusType($expr->left, $expr->right, $getType); - } + if ($expr instanceof BinaryOp\Mul) { + return $this->initializerExprTypeResolver->getMulType($expr->left, $expr->right, $getType); + } - if ($expr instanceof BinaryOp\Mul) { - return $this->initializerExprTypeResolver->getMulType($expr->left, $expr->right, $getType); - } + if ($expr instanceof BinaryOp\Pow) { + return $this->initializerExprTypeResolver->getPowType($expr->left, $expr->right, $getType); + } - if ($expr instanceof BinaryOp\Pow) { - return $this->initializerExprTypeResolver->getPowType($expr->left, $expr->right, $getType); - } + if ($expr instanceof BinaryOp\ShiftLeft) { + return $this->initializerExprTypeResolver->getShiftLeftType($expr->left, $expr->right, $getType); + } - if ($expr instanceof BinaryOp\ShiftLeft) { - return $this->initializerExprTypeResolver->getShiftLeftType($expr->left, $expr->right, $getType); - } + if ($expr instanceof BinaryOp\ShiftRight) { + return $this->initializerExprTypeResolver->getShiftRightType($expr->left, $expr->right, $getType); + } - if ($expr instanceof BinaryOp\ShiftRight) { - return $this->initializerExprTypeResolver->getShiftRightType($expr->left, $expr->right, $getType); - } + throw new ShouldNotHappenException(sprintf('Unhandled %s', get_class($expr))); + }, + specifyTypesCallback: function (MutatingScope $scope, TypeSpecifierContext $context) use ($expr): SpecifiedTypes { + if ($expr instanceof BinaryOp\Identical) { + return $this->equalityTypeSpecifyingHelper->specifyTypesForIdentical($expr, $scope, $context); + } - throw new ShouldNotHappenException(sprintf('Unhandled %s', get_class($expr))); - } + if ($expr instanceof BinaryOp\NotIdentical) { + // negating the context is exactly what a BooleanNot around the + // Identical would do - direct computation avoids synthesizing a + // BooleanNot node (on-demand re-processing once it is migrated). + // A null context never negates (BooleanNot defaults on it too). + if ($context->null()) { + return $this->defaultNarrowingHelper->specifyDefaultTypes($expr, $context); + } - public function specifyTypes(TypeSpecifier $typeSpecifier, Scope $scope, Expr $expr, TypeSpecifierContext $context): SpecifiedTypes - { - if ($expr instanceof BinaryOp\Identical) { - return $this->equalityTypeSpecifyingHelper->specifyTypesForIdentical($expr, $scope, $context); - } + return $this->typeSpecifier->specifyTypesInCondition( + $scope, + new BinaryOp\Identical($expr->left, $expr->right), + $context->negate(), + )->setRootExpr($expr); + } - if ($expr instanceof BinaryOp\NotIdentical) { - return $typeSpecifier->specifyTypesInCondition( - $scope, - new Expr\BooleanNot(new BinaryOp\Identical($expr->left, $expr->right)), - $context, - )->setRootExpr($expr); - } + if ($expr instanceof BinaryOp\Equal) { + return $this->equalityTypeSpecifyingHelper->specifyTypesForEqual($expr, $scope, $context); + } - if ($expr instanceof BinaryOp\Equal) { - return $this->equalityTypeSpecifyingHelper->specifyTypesForEqual($expr, $scope, $context); - } + if ($expr instanceof BinaryOp\NotEqual) { + // see NotIdentical above + if ($context->null()) { + return $this->defaultNarrowingHelper->specifyDefaultTypes($expr, $context); + } - if ($expr instanceof BinaryOp\NotEqual) { - return $typeSpecifier->specifyTypesInCondition( - $scope, - new Expr\BooleanNot(new BinaryOp\Equal($expr->left, $expr->right)), - $context, - )->setRootExpr($expr); - } + return $this->typeSpecifier->specifyTypesInCondition( + $scope, + new BinaryOp\Equal($expr->left, $expr->right), + $context->negate(), + )->setRootExpr($expr); + } - if ($expr instanceof BinaryOp\Smaller || $expr instanceof BinaryOp\SmallerOrEqual) { - if ( - $expr->left instanceof Expr\FuncCall - && $expr->left->name instanceof Name - && !$expr->left->isFirstClassCallable() - && in_array(strtolower((string) $expr->left->name), ['count', 'sizeof', 'strlen', 'mb_strlen', 'preg_match'], true) - && count($expr->left->getArgs()) >= 1 - && ( - !$expr->right instanceof Expr\FuncCall - || !$expr->right->name instanceof Name - || !in_array(strtolower((string) $expr->right->name), ['count', 'sizeof', 'strlen', 'mb_strlen', 'preg_match'], true) - ) - ) { - $inverseOperator = $expr instanceof BinaryOp\Smaller - ? new BinaryOp\SmallerOrEqual($expr->right, $expr->left) - : new BinaryOp\Smaller($expr->right, $expr->left); - - return $typeSpecifier->specifyTypesInCondition( - $scope, - new Expr\BooleanNot($inverseOperator), - $context, - )->setRootExpr($expr); - } + if ($expr instanceof BinaryOp\Smaller || $expr instanceof BinaryOp\SmallerOrEqual) { + if ( + $expr->left instanceof Expr\FuncCall + && $expr->left->name instanceof Name + && !$expr->left->isFirstClassCallable() + && in_array(strtolower((string) $expr->left->name), ['count', 'sizeof', 'strlen', 'mb_strlen', 'preg_match'], true) + && count($expr->left->getArgs()) >= 1 + && ( + !$expr->right instanceof Expr\FuncCall + || !$expr->right->name instanceof Name + || !in_array(strtolower((string) $expr->right->name), ['count', 'sizeof', 'strlen', 'mb_strlen', 'preg_match'], true) + ) + ) { + $inverseOperator = $expr instanceof BinaryOp\Smaller + ? new BinaryOp\SmallerOrEqual($expr->right, $expr->left) + : new BinaryOp\Smaller($expr->right, $expr->left); + + // negating the context is exactly what a BooleanNot around the + // inverse operator would do - direct computation avoids + // synthesizing a BooleanNot node. A null context never negates + // (BooleanNot defaults on it too). + if ($context->null()) { + return $this->defaultNarrowingHelper->specifyDefaultTypes($expr, $context); + } - $orEqual = $expr instanceof BinaryOp\SmallerOrEqual; - $offset = $orEqual ? 0 : 1; - $leftType = $scope->getType($expr->left); - $result = (new SpecifiedTypes([], []))->setRootExpr($expr); - - if ( - !$context->null() - && $expr->right instanceof Expr\FuncCall - && $expr->right->name instanceof Name - && !$expr->right->isFirstClassCallable() - && in_array(strtolower((string) $expr->right->name), ['count', 'sizeof'], true) - && count($expr->right->getArgs()) >= 1 - && $leftType->isInteger()->yes() - ) { - $argType = $scope->getType($expr->right->getArgs()[0]->value); - - $sizeType = null; - if ($leftType instanceof ConstantIntegerType) { - if ($orEqual) { - $sizeType = IntegerRangeType::createAllGreaterThanOrEqualTo($leftType->getValue()); - } else { - $sizeType = IntegerRangeType::createAllGreaterThan($leftType->getValue()); + return $this->typeSpecifier->specifyTypesInCondition( + $scope, + $inverseOperator, + $context->negate(), + )->setRootExpr($expr); } - } elseif ($leftType instanceof IntegerRangeType) { - if ($context->falsey() && $leftType->getMax() !== null) { - if ($orEqual) { - $sizeType = IntegerRangeType::createAllGreaterThanOrEqualTo($leftType->getMax()); + + $orEqual = $expr instanceof BinaryOp\SmallerOrEqual; + $offset = $orEqual ? 0 : 1; + $leftType = $scope->getType($expr->left); + $result = (new SpecifiedTypes([], []))->setRootExpr($expr); + + if ( + !$context->null() + && $expr->right instanceof Expr\FuncCall + && $expr->right->name instanceof Name + && !$expr->right->isFirstClassCallable() + && in_array(strtolower((string) $expr->right->name), ['count', 'sizeof'], true) + && count($expr->right->getArgs()) >= 1 + && $leftType->isInteger()->yes() + ) { + $argType = $scope->getType($expr->right->getArgs()[0]->value); + + $sizeType = null; + if ($leftType instanceof ConstantIntegerType) { + if ($orEqual) { + $sizeType = IntegerRangeType::createAllGreaterThanOrEqualTo($leftType->getValue()); + } else { + $sizeType = IntegerRangeType::createAllGreaterThan($leftType->getValue()); + } + } elseif ($leftType instanceof IntegerRangeType) { + if ($context->falsey() && $leftType->getMax() !== null) { + if ($orEqual) { + $sizeType = IntegerRangeType::createAllGreaterThanOrEqualTo($leftType->getMax()); + } else { + $sizeType = IntegerRangeType::createAllGreaterThan($leftType->getMax()); + } + } elseif ($context->truthy() && $leftType->getMin() !== null) { + if ($orEqual) { + $sizeType = IntegerRangeType::createAllGreaterThanOrEqualTo($leftType->getMin()); + } else { + $sizeType = IntegerRangeType::createAllGreaterThan($leftType->getMin()); + } + } } else { - $sizeType = IntegerRangeType::createAllGreaterThan($leftType->getMax()); + $sizeType = $leftType; } - } elseif ($context->truthy() && $leftType->getMin() !== null) { - if ($orEqual) { - $sizeType = IntegerRangeType::createAllGreaterThanOrEqualTo($leftType->getMin()); - } else { - $sizeType = IntegerRangeType::createAllGreaterThan($leftType->getMin()); + + if ($sizeType !== null) { + $specifiedTypes = $this->typeSpecifier->specifyTypesForCountFuncCall($expr->right, $argType, $sizeType, $context, $scope, $expr); + if ($specifiedTypes !== null) { + $result = $result->unionWith($specifiedTypes); + } } - } - } else { - $sizeType = $leftType; - } - if ($sizeType !== null) { - $specifiedTypes = $typeSpecifier->specifyTypesForCountFuncCall($expr->right, $argType, $sizeType, $context, $scope, $expr); - if ($specifiedTypes !== null) { - $result = $result->unionWith($specifiedTypes); - } - } + if ( + $context->true() && (IntegerRangeType::createAllGreaterThanOrEqualTo(1 - $offset)->isSuperTypeOf($leftType)->yes()) + || ($context->false() && (new ConstantIntegerType(1 - $offset))->isSuperTypeOf($leftType)->yes()) + ) { + if ($context->truthy() && $argType->isArray()->maybe()) { + $countables = []; + if ($argType instanceof UnionType) { + $countableInterface = new ObjectType(Countable::class); + foreach ($argType->getTypes() as $innerType) { + if ($innerType->isArray()->yes()) { + $innerType = TypeCombinator::intersect(new NonEmptyArrayType(), $innerType); + $countables[] = $innerType; + } + + if (!$countableInterface->isSuperTypeOf($innerType)->yes()) { + continue; + } + + $countables[] = $innerType; + } + } - if ( - $context->true() && (IntegerRangeType::createAllGreaterThanOrEqualTo(1 - $offset)->isSuperTypeOf($leftType)->yes()) - || ($context->false() && (new ConstantIntegerType(1 - $offset))->isSuperTypeOf($leftType)->yes()) - ) { - if ($context->truthy() && $argType->isArray()->maybe()) { - $countables = []; - if ($argType instanceof UnionType) { - $countableInterface = new ObjectType(Countable::class); - foreach ($argType->getTypes() as $innerType) { - if ($innerType->isArray()->yes()) { - $innerType = TypeCombinator::intersect(new NonEmptyArrayType(), $innerType); - $countables[] = $innerType; + if (count($countables) > 0) { + $countableType = TypeCombinator::union(...$countables); + + return $this->typeSpecifier->create($expr->right->getArgs()[0]->value, $countableType, $context, $scope)->setRootExpr($expr); } + } - if (!$countableInterface->isSuperTypeOf($innerType)->yes()) { - continue; + if ($argType->isArray()->yes()) { + $newType = new NonEmptyArrayType(); + if ($context->true() && $argType->isList()->yes()) { + $newType = TypeCombinator::intersect($newType, new AccessoryArrayListType()); } - $countables[] = $innerType; + $result = $result->unionWith( + $this->typeSpecifier->create($expr->right->getArgs()[0]->value, $newType, $context, $scope)->setRootExpr($expr), + ); } } - if (count($countables) > 0) { - $countableType = TypeCombinator::union(...$countables); - - return $typeSpecifier->create($expr->right->getArgs()[0]->value, $countableType, $context, $scope)->setRootExpr($expr); + // infer $list[$index] after $index < count($list) + if ( + $context->true() + && !$orEqual + // constant offsets are handled via HasOffsetType/HasOffsetValueType + && !$leftType instanceof ConstantIntegerType + && $argType->isList()->yes() + && IntegerRangeType::fromInterval(0, null)->isSuperTypeOf($leftType)->yes() + ) { + $arrayArg = $expr->right->getArgs()[0]->value; + $dimFetch = new Expr\ArrayDimFetch($arrayArg, $expr->left); + $result = $result->unionWith( + $this->typeSpecifier->create($dimFetch, $argType->getIterableValueType(), TypeSpecifierContext::createTrue(), $scope)->setRootExpr($expr), + ); } } - if ($argType->isArray()->yes()) { - $newType = new NonEmptyArrayType(); - if ($context->true() && $argType->isList()->yes()) { - $newType = TypeCombinator::intersect($newType, new AccessoryArrayListType()); + // infer $list[$index] after $zeroOrMore < count($list) - N + // infer $list[$index] after $zeroOrMore <= count($list) - N + if ( + $context->true() + && $expr->right instanceof BinaryOp\Minus + && $expr->right->left instanceof Expr\FuncCall + && $expr->right->left->name instanceof Name + && !$expr->right->left->isFirstClassCallable() + && in_array(strtolower((string) $expr->right->left->name), ['count', 'sizeof'], true) + && count($expr->right->left->getArgs()) >= 1 + // constant offsets are handled via HasOffsetType/HasOffsetValueType + && !$leftType instanceof ConstantIntegerType + && $leftType->isInteger()->yes() + && IntegerRangeType::fromInterval(0, null)->isSuperTypeOf($leftType)->yes() + ) { + $countArgType = $scope->getType($expr->right->left->getArgs()[0]->value); + $subtractedType = $scope->getType($expr->right->right); + if ( + $countArgType->isList()->yes() + && $this->typeSpecifier->isNormalCountCall($expr->right->left, $countArgType, $scope)->yes() + && IntegerRangeType::fromInterval(1, null)->isSuperTypeOf($subtractedType)->yes() + ) { + $arrayArg = $expr->right->left->getArgs()[0]->value; + $dimFetch = new Expr\ArrayDimFetch($arrayArg, $expr->left); + $result = $result->unionWith( + $this->typeSpecifier->create($dimFetch, $countArgType->getIterableValueType(), TypeSpecifierContext::createTrue(), $scope)->setRootExpr($expr), + ); } + } - $result = $result->unionWith( - $typeSpecifier->create($expr->right->getArgs()[0]->value, $newType, $context, $scope)->setRootExpr($expr), - ); + if ( + !$context->null() + && $expr->right instanceof Expr\FuncCall + && $expr->right->name instanceof Name + && !$expr->right->isFirstClassCallable() + && in_array(strtolower((string) $expr->right->name), ['preg_match'], true) + && count($expr->right->getArgs()) >= 3 + && ( + IntegerRangeType::fromInterval(1, null)->isSuperTypeOf($leftType)->yes() + || ($expr instanceof BinaryOp\Smaller && IntegerRangeType::fromInterval(0, null)->isSuperTypeOf($leftType)->yes()) + ) + ) { + // 0 < preg_match or 1 <= preg_match becomes 1 === preg_match + $newExpr = new BinaryOp\Identical($expr->right, new Scalar\Int_(1)); + + return $this->typeSpecifier->specifyTypesInCondition($scope, $newExpr, $context)->setRootExpr($expr); } - } - // infer $list[$index] after $index < count($list) - if ( - $context->true() - && !$orEqual - // constant offsets are handled via HasOffsetType/HasOffsetValueType - && !$leftType instanceof ConstantIntegerType - && $argType->isList()->yes() - && IntegerRangeType::fromInterval(0, null)->isSuperTypeOf($leftType)->yes() - ) { - $arrayArg = $expr->right->getArgs()[0]->value; - $dimFetch = new Expr\ArrayDimFetch($arrayArg, $expr->left); - $result = $result->unionWith( - $typeSpecifier->create($dimFetch, $argType->getIterableValueType(), TypeSpecifierContext::createTrue(), $scope)->setRootExpr($expr), - ); - } - } + if ( + !$context->null() + && $expr->right instanceof Expr\FuncCall + && $expr->right->name instanceof Name + && !$expr->right->isFirstClassCallable() + && in_array(strtolower((string) $expr->right->name), ['strlen', 'mb_strlen'], true) + && count($expr->right->getArgs()) === 1 + && $leftType->isInteger()->yes() + ) { + if ( + $context->true() && (IntegerRangeType::createAllGreaterThanOrEqualTo(1 - $offset)->isSuperTypeOf($leftType)->yes()) + || ($context->false() && (new ConstantIntegerType(1 - $offset))->isSuperTypeOf($leftType)->yes()) + ) { + $argType = $scope->getType($expr->right->getArgs()[0]->value); + if ($argType->isString()->yes()) { + $accessory = new AccessoryNonEmptyStringType(); + + if (IntegerRangeType::createAllGreaterThanOrEqualTo(2 - $offset)->isSuperTypeOf($leftType)->yes()) { + $accessory = new AccessoryNonFalsyStringType(); + } - // infer $list[$index] after $zeroOrMore < count($list) - N - // infer $list[$index] after $zeroOrMore <= count($list) - N - if ( - $context->true() - && $expr->right instanceof BinaryOp\Minus - && $expr->right->left instanceof Expr\FuncCall - && $expr->right->left->name instanceof Name - && !$expr->right->left->isFirstClassCallable() - && in_array(strtolower((string) $expr->right->left->name), ['count', 'sizeof'], true) - && count($expr->right->left->getArgs()) >= 1 - // constant offsets are handled via HasOffsetType/HasOffsetValueType - && !$leftType instanceof ConstantIntegerType - && $leftType->isInteger()->yes() - && IntegerRangeType::fromInterval(0, null)->isSuperTypeOf($leftType)->yes() - ) { - $countArgType = $scope->getType($expr->right->left->getArgs()[0]->value); - $subtractedType = $scope->getType($expr->right->right); - if ( - $countArgType->isList()->yes() - && $typeSpecifier->isNormalCountCall($expr->right->left, $countArgType, $scope)->yes() - && IntegerRangeType::fromInterval(1, null)->isSuperTypeOf($subtractedType)->yes() - ) { - $arrayArg = $expr->right->left->getArgs()[0]->value; - $dimFetch = new Expr\ArrayDimFetch($arrayArg, $expr->left); - $result = $result->unionWith( - $typeSpecifier->create($dimFetch, $countArgType->getIterableValueType(), TypeSpecifierContext::createTrue(), $scope)->setRootExpr($expr), - ); - } - } + $result = $result->unionWith($this->typeSpecifier->create($expr->right->getArgs()[0]->value, $accessory, $context, $scope)->setRootExpr($expr)); + } + } + } - if ( - !$context->null() - && $expr->right instanceof Expr\FuncCall - && $expr->right->name instanceof Name - && !$expr->right->isFirstClassCallable() - && in_array(strtolower((string) $expr->right->name), ['preg_match'], true) - && count($expr->right->getArgs()) >= 3 - && ( - IntegerRangeType::fromInterval(1, null)->isSuperTypeOf($leftType)->yes() - || ($expr instanceof BinaryOp\Smaller && IntegerRangeType::fromInterval(0, null)->isSuperTypeOf($leftType)->yes()) - ) - ) { - // 0 < preg_match or 1 <= preg_match becomes 1 === preg_match - $newExpr = new BinaryOp\Identical($expr->right, new Scalar\Int_(1)); - - return $typeSpecifier->specifyTypesInCondition($scope, $newExpr, $context)->setRootExpr($expr); - } + if ($leftType instanceof ConstantIntegerType) { + if ($expr->right instanceof Expr\PostInc) { + $result = $result->unionWith($this->createRangeTypes( + $expr, + $expr->right->var, + IntegerRangeType::fromInterval($leftType->getValue(), null, $offset + 1), + $context, + )); + } elseif ($expr->right instanceof Expr\PostDec) { + $result = $result->unionWith($this->createRangeTypes( + $expr, + $expr->right->var, + IntegerRangeType::fromInterval($leftType->getValue(), null, $offset - 1), + $context, + )); + } elseif ($expr->right instanceof Expr\PreInc || $expr->right instanceof Expr\PreDec) { + $result = $result->unionWith($this->createRangeTypes( + $expr, + $expr->right->var, + IntegerRangeType::fromInterval($leftType->getValue(), null, $offset), + $context, + )); + } + } - if ( - !$context->null() - && $expr->right instanceof Expr\FuncCall - && $expr->right->name instanceof Name - && !$expr->right->isFirstClassCallable() - && in_array(strtolower((string) $expr->right->name), ['strlen', 'mb_strlen'], true) - && count($expr->right->getArgs()) === 1 - && $leftType->isInteger()->yes() - ) { - if ( - $context->true() && (IntegerRangeType::createAllGreaterThanOrEqualTo(1 - $offset)->isSuperTypeOf($leftType)->yes()) - || ($context->false() && (new ConstantIntegerType(1 - $offset))->isSuperTypeOf($leftType)->yes()) - ) { - $argType = $scope->getType($expr->right->getArgs()[0]->value); - if ($argType->isString()->yes()) { - $accessory = new AccessoryNonEmptyStringType(); - - if (IntegerRangeType::createAllGreaterThanOrEqualTo(2 - $offset)->isSuperTypeOf($leftType)->yes()) { - $accessory = new AccessoryNonFalsyStringType(); + $rightType = $scope->getType($expr->right); + if ($rightType instanceof ConstantIntegerType) { + if ($expr->left instanceof Expr\PostInc) { + $result = $result->unionWith($this->createRangeTypes( + $expr, + $expr->left->var, + IntegerRangeType::fromInterval(null, $rightType->getValue(), -$offset + 1), + $context, + )); + } elseif ($expr->left instanceof Expr\PostDec) { + $result = $result->unionWith($this->createRangeTypes( + $expr, + $expr->left->var, + IntegerRangeType::fromInterval(null, $rightType->getValue(), -$offset - 1), + $context, + )); + } elseif ($expr->left instanceof Expr\PreInc || $expr->left instanceof Expr\PreDec) { + $result = $result->unionWith($this->createRangeTypes( + $expr, + $expr->left->var, + IntegerRangeType::fromInterval(null, $rightType->getValue(), -$offset), + $context, + )); } + } - $result = $result->unionWith($typeSpecifier->create($expr->right->getArgs()[0]->value, $accessory, $context, $scope)->setRootExpr($expr)); + if ($context->true()) { + if (!$expr->left instanceof Scalar && !($expr->left instanceof Expr\UnaryMinus && $expr->left->expr instanceof Scalar)) { + $result = $result->unionWith( + $this->typeSpecifier->create( + $expr->left, + $orEqual ? $rightType->getSmallerOrEqualType($this->phpVersion) : $rightType->getSmallerType($this->phpVersion), + TypeSpecifierContext::createTruthy(), + $scope, + )->setRootExpr($expr), + ); + } + if (!$expr->right instanceof Scalar && !($expr->right instanceof Expr\UnaryMinus && $expr->right->expr instanceof Scalar)) { + $result = $result->unionWith( + $this->typeSpecifier->create( + $expr->right, + $orEqual ? $leftType->getGreaterOrEqualType($this->phpVersion) : $leftType->getGreaterType($this->phpVersion), + TypeSpecifierContext::createTruthy(), + $scope, + )->setRootExpr($expr), + ); + } + } elseif ($context->false()) { + if (!$expr->left instanceof Scalar && !($expr->left instanceof Expr\UnaryMinus && $expr->left->expr instanceof Scalar)) { + $result = $result->unionWith( + $this->typeSpecifier->create( + $expr->left, + $orEqual ? $rightType->getGreaterType($this->phpVersion) : $rightType->getGreaterOrEqualType($this->phpVersion), + TypeSpecifierContext::createTruthy(), + $scope, + )->setRootExpr($expr), + ); + } + if (!$expr->right instanceof Scalar && !($expr->right instanceof Expr\UnaryMinus && $expr->right->expr instanceof Scalar)) { + $result = $result->unionWith( + $this->typeSpecifier->create( + $expr->right, + $orEqual ? $leftType->getSmallerType($this->phpVersion) : $leftType->getSmallerOrEqualType($this->phpVersion), + TypeSpecifierContext::createTruthy(), + $scope, + )->setRootExpr($expr), + ); + } } - } - } - if ($leftType instanceof ConstantIntegerType) { - if ($expr->right instanceof Expr\PostInc) { - $result = $result->unionWith($this->createRangeTypes( - $expr, - $expr->right->var, - IntegerRangeType::fromInterval($leftType->getValue(), null, $offset + 1), - $context, - )); - } elseif ($expr->right instanceof Expr\PostDec) { - $result = $result->unionWith($this->createRangeTypes( - $expr, - $expr->right->var, - IntegerRangeType::fromInterval($leftType->getValue(), null, $offset - 1), - $context, - )); - } elseif ($expr->right instanceof Expr\PreInc || $expr->right instanceof Expr\PreDec) { - $result = $result->unionWith($this->createRangeTypes( - $expr, - $expr->right->var, - IntegerRangeType::fromInterval($leftType->getValue(), null, $offset), - $context, - )); + return $result; } - } - $rightType = $scope->getType($expr->right); - if ($rightType instanceof ConstantIntegerType) { - if ($expr->left instanceof Expr\PostInc) { - $result = $result->unionWith($this->createRangeTypes( - $expr, - $expr->left->var, - IntegerRangeType::fromInterval(null, $rightType->getValue(), -$offset + 1), - $context, - )); - } elseif ($expr->left instanceof Expr\PostDec) { - $result = $result->unionWith($this->createRangeTypes( - $expr, - $expr->left->var, - IntegerRangeType::fromInterval(null, $rightType->getValue(), -$offset - 1), - $context, - )); - } elseif ($expr->left instanceof Expr\PreInc || $expr->left instanceof Expr\PreDec) { - $result = $result->unionWith($this->createRangeTypes( - $expr, - $expr->left->var, - IntegerRangeType::fromInterval(null, $rightType->getValue(), -$offset), - $context, - )); + if ($expr instanceof BinaryOp\Greater) { + return $this->typeSpecifier->specifyTypesInCondition($scope, new BinaryOp\Smaller($expr->right, $expr->left), $context)->setRootExpr($expr); } - } - if ($context->true()) { - if (!$expr->left instanceof Scalar && !($expr->left instanceof Expr\UnaryMinus && $expr->left->expr instanceof Scalar)) { - $result = $result->unionWith( - $typeSpecifier->create( - $expr->left, - $orEqual ? $rightType->getSmallerOrEqualType($this->phpVersion) : $rightType->getSmallerType($this->phpVersion), - TypeSpecifierContext::createTruthy(), - $scope, - )->setRootExpr($expr), - ); - } - if (!$expr->right instanceof Scalar && !($expr->right instanceof Expr\UnaryMinus && $expr->right->expr instanceof Scalar)) { - $result = $result->unionWith( - $typeSpecifier->create( - $expr->right, - $orEqual ? $leftType->getGreaterOrEqualType($this->phpVersion) : $leftType->getGreaterType($this->phpVersion), - TypeSpecifierContext::createTruthy(), - $scope, - )->setRootExpr($expr), - ); - } - } elseif ($context->false()) { - if (!$expr->left instanceof Scalar && !($expr->left instanceof Expr\UnaryMinus && $expr->left->expr instanceof Scalar)) { - $result = $result->unionWith( - $typeSpecifier->create( - $expr->left, - $orEqual ? $rightType->getGreaterType($this->phpVersion) : $rightType->getGreaterOrEqualType($this->phpVersion), - TypeSpecifierContext::createTruthy(), - $scope, - )->setRootExpr($expr), - ); - } - if (!$expr->right instanceof Scalar && !($expr->right instanceof Expr\UnaryMinus && $expr->right->expr instanceof Scalar)) { - $result = $result->unionWith( - $typeSpecifier->create( - $expr->right, - $orEqual ? $leftType->getSmallerType($this->phpVersion) : $leftType->getSmallerOrEqualType($this->phpVersion), - TypeSpecifierContext::createTruthy(), - $scope, - )->setRootExpr($expr), - ); + if ($expr instanceof BinaryOp\GreaterOrEqual) { + return $this->typeSpecifier->specifyTypesInCondition($scope, new BinaryOp\SmallerOrEqual($expr->right, $expr->left), $context)->setRootExpr($expr); } - } - return $result; - } + return $this->defaultNarrowingHelper->specifyDefaultTypes($expr, $context); + }, + ); + } - if ($expr instanceof BinaryOp\Greater) { - return $typeSpecifier->specifyTypesInCondition($scope, new BinaryOp\Smaller($expr->right, $expr->left), $context)->setRootExpr($expr); + /** + * The boolean result of a `==` comparison, including the same-variable + * special case. Shared by the Equal and NotEqual type callbacks. + */ + private function resolveEqualType(MutatingScope $scope, BinaryOp\Equal $expr): Type + { + if ( + $expr->left instanceof Variable + && is_string($expr->left->name) + && $expr->right instanceof Variable + && is_string($expr->right->name) + && $expr->left->name === $expr->right->name + ) { + return new ConstantBooleanType(true); } - if ($expr instanceof BinaryOp\GreaterOrEqual) { - return $typeSpecifier->specifyTypesInCondition($scope, new BinaryOp\SmallerOrEqual($expr->right, $expr->left), $context)->setRootExpr($expr); - } + $leftType = $scope->getType($expr->left); + $rightType = $scope->getType($expr->right); - return $typeSpecifier->specifyDefaultTypes($scope, $expr, $context); + return $this->initializerExprTypeResolver->resolveEqualType($leftType, $rightType)->type; } private function createRangeTypes(?Expr $rootExpr, Expr $expr, Type $type, TypeSpecifierContext $context): SpecifiedTypes diff --git a/src/Analyser/ExprHandler/BooleanNotHandler.php b/src/Analyser/ExprHandler/BooleanNotHandler.php index fc4f263faee..09521ee26c9 100644 --- a/src/Analyser/ExprHandler/BooleanNotHandler.php +++ b/src/Analyser/ExprHandler/BooleanNotHandler.php @@ -9,11 +9,11 @@ use PHPStan\Analyser\ExpressionResult; use PHPStan\Analyser\ExpressionResultFactory; use PHPStan\Analyser\ExpressionResultStorage; +use PHPStan\Analyser\ExprHandler; +use PHPStan\Analyser\ExprHandler\Helper\DefaultNarrowingHelper; use PHPStan\Analyser\MutatingScope; use PHPStan\Analyser\NodeScopeResolver; -use PHPStan\Analyser\Scope; use PHPStan\Analyser\SpecifiedTypes; -use PHPStan\Analyser\TypeResolvingExprHandler; use PHPStan\Analyser\TypeSpecifier; use PHPStan\Analyser\TypeSpecifierContext; use PHPStan\DependencyInjection\AutowiredService; @@ -22,13 +22,17 @@ use PHPStan\Type\Type; /** - * @implements TypeResolvingExprHandler + * @implements ExprHandler */ #[AutowiredService] -final class BooleanNotHandler implements TypeResolvingExprHandler +final class BooleanNotHandler implements ExprHandler { - public function __construct(private ExpressionResultFactory $expressionResultFactory) + public function __construct( + private ExpressionResultFactory $expressionResultFactory, + private TypeSpecifier $typeSpecifier, + private DefaultNarrowingHelper $defaultNarrowingHelper, + ) { } @@ -51,26 +55,25 @@ public function processExpr(NodeScopeResolver $nodeScopeResolver, Stmt $stmt, Ex isAlwaysTerminating: $exprResult->isAlwaysTerminating(), throwPoints: $exprResult->getThrowPoints(), impurePoints: $exprResult->getImpurePoints(), - ); - } - - public function resolveType(MutatingScope $scope, Expr $expr): Type - { - $exprBooleanType = $scope->getType($expr->expr)->toBoolean(); - if ($exprBooleanType instanceof ConstantBooleanType) { - return new ConstantBooleanType(!$exprBooleanType->getValue()); - } + typeCallback: static function (MutatingScope $s) use ($exprResult): Type { + $exprBooleanType = $exprResult->getTypeForScope($s)->toBoolean(); + if ($exprBooleanType->isTrue()->yes()) { + return new ConstantBooleanType(false); + } + if ($exprBooleanType->isFalse()->yes()) { + return new ConstantBooleanType(true); + } - return new BooleanType(); - } - - public function specifyTypes(TypeSpecifier $typeSpecifier, Scope $scope, Expr $expr, TypeSpecifierContext $context): SpecifiedTypes - { - if ($context->null()) { - return $typeSpecifier->specifyDefaultTypes($scope, $expr, $context); - } + return new BooleanType(); + }, + specifyTypesCallback: function (MutatingScope $s, TypeSpecifierContext $context) use ($expr): SpecifiedTypes { + if ($context->null()) { + return $this->defaultNarrowingHelper->specifyDefaultTypes($expr, $context); + } - return $typeSpecifier->specifyTypesInCondition($scope, $expr->expr, $context->negate())->setRootExpr($expr); + return $this->typeSpecifier->specifyTypesInCondition($s, $expr->expr, $context->negate())->setRootExpr($expr); + }, + ); } } From 7ac47a94d4fd35bd6002694df5b8a4654fd43446 Mon Sep 17 00:00:00 2001 From: Ondrej Mirtes Date: Wed, 17 Jun 2026 23:28:39 +0200 Subject: [PATCH 077/113] Fix getIssetabilityDescriptor shadowed by descriptor-less assignment-target placeholders --- src/Analyser/ExpressionResult.php | 13 ------------- src/Analyser/MutatingScope.php | 14 ++++++++++++-- 2 files changed, 12 insertions(+), 15 deletions(-) diff --git a/src/Analyser/ExpressionResult.php b/src/Analyser/ExpressionResult.php index 5453896d5c2..9bc2dc335b5 100644 --- a/src/Analyser/ExpressionResult.php +++ b/src/Analyser/ExpressionResult.php @@ -130,19 +130,6 @@ public function issetCheckUndefined(MutatingScope $scope): ?bool return $this->issetabilityDescriptor?->checkUndefined($scope); } - /** Whether isset($expr) is definitely true/false (null = maybe). */ - public function isset(MutatingScope $scope): ?bool - { - return $this->issetCheck($scope, static function (Type $type): ?bool { - $isNull = $type->isNull(); - if ($isNull->maybe()) { - return null; - } - - return !$isNull->yes(); - }); - } - /** * Whether $expr is definitely set-and-non-falsey (i.e. the negation of * empty($expr)); null = maybe. EmptyHandler negates the result. diff --git a/src/Analyser/MutatingScope.php b/src/Analyser/MutatingScope.php index 31eab874540..9a0abf31be0 100644 --- a/src/Analyser/MutatingScope.php +++ b/src/Analyser/MutatingScope.php @@ -1172,17 +1172,27 @@ public function getIssetabilityDescriptor(Expr $expr): ?IssetabilityDescriptor { $scope = $this->toMutatingScope(); $storage = $this->expressionResultStorageStack->getCurrent(); + $onDemandStorage = $storage !== null ? $storage->duplicate() : new ExpressionResultStorage(); if ($storage !== null) { $result = $storage->findExpressionResult($expr); if ($result !== null) { - return $result->getIssetabilityDescriptor(); + $descriptor = $result->getIssetabilityDescriptor(); + if ($descriptor !== null) { + return $descriptor; + } + + // a placeholder result (e.g. the var of `$x['k'] ??= …`, stored + // as an assignment target) carries no descriptor; re-process on a + // fresh storage so the placeholder doesn't shadow the real one + // (processExprOnDemand returns stored results, incl. the placeholder) + $onDemandStorage = new ExpressionResultStorage(); } } $onDemandResult = $this->container->getByType(NodeScopeResolver::class)->processExprOnDemand( $expr, $scope, - $storage !== null ? $storage->duplicate() : new ExpressionResultStorage(), + $onDemandStorage, ); return $onDemandResult->getIssetabilityDescriptor(); From 00ca7a5d06c2533306b7df393ef4630baca17cfb Mon Sep 17 00:00:00 2001 From: Ondrej Mirtes Date: Thu, 18 Jun 2026 07:27:03 +0200 Subject: [PATCH 078/113] Eliminate the OriginalPropertyTypeExpr virtual node --- src/Analyser/ExprHandler/AssignHandler.php | 24 +++++- .../OriginalPropertyTypeExprHandler.php | 74 ------------------- .../SetExistingOffsetValueTypeExprHandler.php | 11 +-- .../Virtual/SetOffsetValueTypeExprHandler.php | 11 +-- src/Node/Expr/OriginalPropertyTypeExpr.php | 37 ---------- src/Node/Printer/Printer.php | 6 -- 6 files changed, 23 insertions(+), 140 deletions(-) delete mode 100644 src/Analyser/ExprHandler/Virtual/OriginalPropertyTypeExprHandler.php delete mode 100644 src/Node/Expr/OriginalPropertyTypeExpr.php diff --git a/src/Analyser/ExprHandler/AssignHandler.php b/src/Analyser/ExprHandler/AssignHandler.php index 181b7536332..36dc1ca6b92 100644 --- a/src/Analyser/ExprHandler/AssignHandler.php +++ b/src/Analyser/ExprHandler/AssignHandler.php @@ -43,7 +43,6 @@ use PHPStan\Node\Expr\ExistingArrayDimFetch; use PHPStan\Node\Expr\GetOffsetValueTypeExpr; use PHPStan\Node\Expr\IntertwinedVariableByReferenceWithExpr; -use PHPStan\Node\Expr\OriginalPropertyTypeExpr; use PHPStan\Node\Expr\SetExistingOffsetValueTypeExpr; use PHPStan\Node\Expr\SetOffsetValueTypeExpr; use PHPStan\Node\Expr\TypeExpr; @@ -53,6 +52,7 @@ use PHPStan\Node\VariableAssignNode; use PHPStan\Node\VirtualNode; use PHPStan\Php\PhpVersion; +use PHPStan\Rules\Properties\PropertyReflectionFinder; use PHPStan\ShouldNotHappenException; use PHPStan\TrinaryLogic; use PHPStan\Type\Accessory\AccessoryArrayListType; @@ -74,6 +74,7 @@ use PHPStan\Type\Type; use PHPStan\Type\TypeCombinator; use PHPStan\Type\TypeUtils; +use PHPStan\Type\UnionType; use TypeError; use function array_key_last; use function array_merge; @@ -99,6 +100,7 @@ public function __construct( private MatchHandler $matchHandler, private ExpressionResultFactory $expressionResultFactory, private DefaultNarrowingHelper $defaultNarrowingHelper, + private PropertyReflectionFinder $propertyReflectionFinder, ) { } @@ -567,7 +569,7 @@ public function processAssignVar( while ($var instanceof ArrayDimFetch) { $varForSetOffsetValue = $var->var; if ($varForSetOffsetValue instanceof PropertyFetch || $varForSetOffsetValue instanceof StaticPropertyFetch) { - $varForSetOffsetValue = new OriginalPropertyTypeExpr($varForSetOffsetValue); + $varForSetOffsetValue = new TypeExpr($this->getOriginalPropertyType($varForSetOffsetValue, $scope)); } if ( @@ -1003,7 +1005,7 @@ public function processAssignVar( while ($var instanceof ExistingArrayDimFetch) { $varForSetOffsetValue = $var->getVar(); if ($varForSetOffsetValue instanceof PropertyFetch || $varForSetOffsetValue instanceof StaticPropertyFetch) { - $varForSetOffsetValue = new OriginalPropertyTypeExpr($varForSetOffsetValue); + $varForSetOffsetValue = new TypeExpr($this->getOriginalPropertyType($varForSetOffsetValue, $scope)); } $assignedPropertyExpr = new SetExistingOffsetValueTypeExpr( $varForSetOffsetValue, @@ -1661,4 +1663,20 @@ private function isSameVariable(Expr $a, Expr $b): bool return false; } + /** + * Returns the property's readable (declared) type, filtered down to the union + * members that are not disjoint from the currently narrowed property type. + */ + private function getOriginalPropertyType(PropertyFetch|StaticPropertyFetch $propertyFetch, MutatingScope $scope): Type + { + $propertyReflection = $this->propertyReflectionFinder->findPropertyReflectionFromNode($propertyFetch, $scope); + $originalPropertyType = $propertyReflection !== null ? $propertyReflection->getReadableType() : new ErrorType(); + if ($originalPropertyType instanceof UnionType) { + $currentPropertyType = $scope->getType($propertyFetch); + $originalPropertyType = $originalPropertyType->filterTypes(static fn (Type $innerType) => !$innerType->isSuperTypeOf($currentPropertyType)->no()); + } + + return $originalPropertyType; + } + } diff --git a/src/Analyser/ExprHandler/Virtual/OriginalPropertyTypeExprHandler.php b/src/Analyser/ExprHandler/Virtual/OriginalPropertyTypeExprHandler.php deleted file mode 100644 index 0e346c921c5..00000000000 --- a/src/Analyser/ExprHandler/Virtual/OriginalPropertyTypeExprHandler.php +++ /dev/null @@ -1,74 +0,0 @@ - - */ -#[AutowiredService] -final class OriginalPropertyTypeExprHandler implements TypeResolvingExprHandler -{ - - public function __construct( - private PropertyReflectionFinder $propertyReflectionFinder, - private ExpressionResultFactory $expressionResultFactory, - ) - { - } - - public function supports(Expr $expr): bool - { - return $expr instanceof OriginalPropertyTypeExpr; - } - - public function processExpr(NodeScopeResolver $nodeScopeResolver, Stmt $stmt, Expr $expr, MutatingScope $scope, ExpressionResultStorage $storage, callable $nodeCallback, ExpressionContext $context): ExpressionResult - { - // because this is a virtual node handler, the caller will only be interested in the type - // we don't need to process the inner expr - - return $this->expressionResultFactory->create( - $scope, - beforeScope: $scope, - expr: $expr, - hasYield: false, - isAlwaysTerminating: false, - throwPoints: [], - impurePoints: [], - ); - } - - public function resolveType(MutatingScope $scope, Expr $expr): Type - { - $propertyReflection = $this->propertyReflectionFinder->findPropertyReflectionFromNode($expr->getPropertyFetch(), $scope); - if ($propertyReflection === null) { - return new ErrorType(); - } - - return $propertyReflection->getReadableType(); - } - - public function specifyTypes(TypeSpecifier $typeSpecifier, Scope $scope, Expr $expr, TypeSpecifierContext $context): SpecifiedTypes - { - return $typeSpecifier->specifyDefaultTypes($scope, $expr, $context); - } - -} diff --git a/src/Analyser/ExprHandler/Virtual/SetExistingOffsetValueTypeExprHandler.php b/src/Analyser/ExprHandler/Virtual/SetExistingOffsetValueTypeExprHandler.php index 56a154af6f8..7957ef0c797 100644 --- a/src/Analyser/ExprHandler/Virtual/SetExistingOffsetValueTypeExprHandler.php +++ b/src/Analyser/ExprHandler/Virtual/SetExistingOffsetValueTypeExprHandler.php @@ -16,10 +16,8 @@ use PHPStan\Analyser\TypeSpecifier; use PHPStan\Analyser\TypeSpecifierContext; use PHPStan\DependencyInjection\AutowiredService; -use PHPStan\Node\Expr\OriginalPropertyTypeExpr; use PHPStan\Node\Expr\SetExistingOffsetValueTypeExpr; use PHPStan\Type\Type; -use PHPStan\Type\UnionType; /** * @implements TypeResolvingExprHandler @@ -55,14 +53,7 @@ public function processExpr(NodeScopeResolver $nodeScopeResolver, Stmt $stmt, Ex public function resolveType(MutatingScope $scope, Expr $expr): Type { - $varNode = $expr->getVar(); - $varType = $scope->getType($varNode); - if ($varNode instanceof OriginalPropertyTypeExpr) { - $currentPropertyType = $scope->getType($varNode->getPropertyFetch()); - if ($varType instanceof UnionType) { - $varType = $varType->filterTypes(static fn (Type $innerType) => !$innerType->isSuperTypeOf($currentPropertyType)->no()); - } - } + $varType = $scope->getType($expr->getVar()); return $varType->setExistingOffsetValueType( $scope->getType($expr->getDim()), $scope->getType($expr->getValue()), diff --git a/src/Analyser/ExprHandler/Virtual/SetOffsetValueTypeExprHandler.php b/src/Analyser/ExprHandler/Virtual/SetOffsetValueTypeExprHandler.php index 08659b35101..e8871e56a07 100644 --- a/src/Analyser/ExprHandler/Virtual/SetOffsetValueTypeExprHandler.php +++ b/src/Analyser/ExprHandler/Virtual/SetOffsetValueTypeExprHandler.php @@ -16,10 +16,8 @@ use PHPStan\Analyser\TypeSpecifier; use PHPStan\Analyser\TypeSpecifierContext; use PHPStan\DependencyInjection\AutowiredService; -use PHPStan\Node\Expr\OriginalPropertyTypeExpr; use PHPStan\Node\Expr\SetOffsetValueTypeExpr; use PHPStan\Type\Type; -use PHPStan\Type\UnionType; /** * @implements TypeResolvingExprHandler @@ -55,14 +53,7 @@ public function processExpr(NodeScopeResolver $nodeScopeResolver, Stmt $stmt, Ex public function resolveType(MutatingScope $scope, Expr $expr): Type { - $varNode = $expr->getVar(); - $varType = $scope->getType($varNode); - if ($varNode instanceof OriginalPropertyTypeExpr) { - $currentPropertyType = $scope->getType($varNode->getPropertyFetch()); - if ($varType instanceof UnionType) { - $varType = $varType->filterTypes(static fn (Type $innerType) => !$innerType->isSuperTypeOf($currentPropertyType)->no()); - } - } + $varType = $scope->getType($expr->getVar()); return $varType->setOffsetValueType( $expr->getDim() !== null ? $scope->getType($expr->getDim()) : null, $scope->getType($expr->getValue()), diff --git a/src/Node/Expr/OriginalPropertyTypeExpr.php b/src/Node/Expr/OriginalPropertyTypeExpr.php deleted file mode 100644 index d662dbe488f..00000000000 --- a/src/Node/Expr/OriginalPropertyTypeExpr.php +++ /dev/null @@ -1,37 +0,0 @@ -propertyFetch; - } - - #[Override] - public function getType(): string - { - return 'PHPStan_Node_OriginalPropertyTypeExpr'; - } - - /** - * @return string[] - */ - #[Override] - public function getSubNodeNames(): array - { - return []; - } - -} diff --git a/src/Node/Printer/Printer.php b/src/Node/Printer/Printer.php index 7409140e8c2..ec447469639 100644 --- a/src/Node/Printer/Printer.php +++ b/src/Node/Printer/Printer.php @@ -17,7 +17,6 @@ use PHPStan\Node\Expr\NativeTypeExpr; use PHPStan\Node\Expr\OriginalForeachKeyExpr; use PHPStan\Node\Expr\OriginalForeachValueExpr; -use PHPStan\Node\Expr\OriginalPropertyTypeExpr; use PHPStan\Node\Expr\ParameterVariableOriginalValueExpr; use PHPStan\Node\Expr\PossiblyImpureCallExpr; use PHPStan\Node\Expr\PropertyInitializationExpr; @@ -75,11 +74,6 @@ protected function pPHPStan_Node_ExistingArrayDimFetch(ExistingArrayDimFetch $ex return sprintf('__phpstanExistingArrayDimFetch(%s, %s)', $this->p($expr->getVar()), $this->p($expr->getDim())); } - protected function pPHPStan_Node_OriginalPropertyTypeExpr(OriginalPropertyTypeExpr $expr): string // phpcs:ignore - { - return sprintf('__phpstanOriginalPropertyType(%s)', $this->p($expr->getPropertyFetch())); - } - protected function pPHPStan_Node_SetOffsetValueTypeExpr(SetOffsetValueTypeExpr $expr): string // phpcs:ignore { return sprintf('__phpstanSetOffsetValueType(%s, %s, %s)', $this->p($expr->getVar()), $expr->getDim() !== null ? $this->p($expr->getDim()) : 'null', $this->p($expr->getValue())); From d82266ba73c1ee4a7e004fe3657878eeef873888 Mon Sep 17 00:00:00 2001 From: Ondrej Mirtes Date: Thu, 18 Jun 2026 08:08:29 +0200 Subject: [PATCH 079/113] Eliminate the GetOffsetValueTypeExpr virtual node --- src/Analyser/ExprHandler/AssignHandler.php | 3 +- .../Virtual/GetOffsetValueTypeExprHandler.php | 64 ------------------- src/Node/Expr/GetOffsetValueTypeExpr.php | 42 ------------ src/Node/Printer/Printer.php | 6 -- src/Rules/PhpDoc/VarTagTypeRuleHelper.php | 4 +- 5 files changed, 3 insertions(+), 116 deletions(-) delete mode 100644 src/Analyser/ExprHandler/Virtual/GetOffsetValueTypeExprHandler.php delete mode 100644 src/Node/Expr/GetOffsetValueTypeExpr.php diff --git a/src/Analyser/ExprHandler/AssignHandler.php b/src/Analyser/ExprHandler/AssignHandler.php index 36dc1ca6b92..04412f8eaae 100644 --- a/src/Analyser/ExprHandler/AssignHandler.php +++ b/src/Analyser/ExprHandler/AssignHandler.php @@ -41,7 +41,6 @@ use PHPStan\Analyser\TypeSpecifierContext; use PHPStan\DependencyInjection\AutowiredService; use PHPStan\Node\Expr\ExistingArrayDimFetch; -use PHPStan\Node\Expr\GetOffsetValueTypeExpr; use PHPStan\Node\Expr\IntertwinedVariableByReferenceWithExpr; use PHPStan\Node\Expr\SetExistingOffsetValueTypeExpr; use PHPStan\Node\Expr\SetOffsetValueTypeExpr; @@ -980,7 +979,7 @@ public function processAssignVar( } else { $dimExpr = $arrayItem->key; } - $getOffsetValueTypeExpr = new GetOffsetValueTypeExpr($assignedExpr, $dimExpr); + $getOffsetValueTypeExpr = new TypeExpr($scope->getType($assignedExpr)->getOffsetValueType($scope->getType($dimExpr))); $result = $this->processAssignVar( $nodeScopeResolver, $scope, diff --git a/src/Analyser/ExprHandler/Virtual/GetOffsetValueTypeExprHandler.php b/src/Analyser/ExprHandler/Virtual/GetOffsetValueTypeExprHandler.php deleted file mode 100644 index ad70185384f..00000000000 --- a/src/Analyser/ExprHandler/Virtual/GetOffsetValueTypeExprHandler.php +++ /dev/null @@ -1,64 +0,0 @@ - - */ -#[AutowiredService] -final class GetOffsetValueTypeExprHandler implements TypeResolvingExprHandler -{ - - public function __construct(private ExpressionResultFactory $expressionResultFactory) - { - } - - public function supports(Expr $expr): bool - { - return $expr instanceof GetOffsetValueTypeExpr; - } - - public function processExpr(NodeScopeResolver $nodeScopeResolver, Stmt $stmt, Expr $expr, MutatingScope $scope, ExpressionResultStorage $storage, callable $nodeCallback, ExpressionContext $context): ExpressionResult - { - // because this is a virtual node handler, the caller will only be interested in the type - // we don't need to process the inner expr - - return $this->expressionResultFactory->create( - $scope, - beforeScope: $scope, - expr: $expr, - hasYield: false, - isAlwaysTerminating: false, - throwPoints: [], - impurePoints: [], - ); - } - - public function resolveType(MutatingScope $scope, Expr $expr): Type - { - return $scope->getType($expr->getVar())->getOffsetValueType($scope->getType($expr->getDim())); - } - - public function specifyTypes(TypeSpecifier $typeSpecifier, Scope $scope, Expr $expr, TypeSpecifierContext $context): SpecifiedTypes - { - return $typeSpecifier->specifyDefaultTypes($scope, $expr, $context); - } - -} diff --git a/src/Node/Expr/GetOffsetValueTypeExpr.php b/src/Node/Expr/GetOffsetValueTypeExpr.php deleted file mode 100644 index 38226855557..00000000000 --- a/src/Node/Expr/GetOffsetValueTypeExpr.php +++ /dev/null @@ -1,42 +0,0 @@ -var; - } - - public function getDim(): Expr - { - return $this->dim; - } - - #[Override] - public function getType(): string - { - return 'PHPStan_Node_GetOffsetValueTypeExpr'; - } - - /** - * @return string[] - */ - #[Override] - public function getSubNodeNames(): array - { - return []; - } - -} diff --git a/src/Node/Printer/Printer.php b/src/Node/Printer/Printer.php index ec447469639..d1bf761b14e 100644 --- a/src/Node/Printer/Printer.php +++ b/src/Node/Printer/Printer.php @@ -12,7 +12,6 @@ use PHPStan\Node\Expr\ForeachValueByRefExpr; use PHPStan\Node\Expr\GetIterableKeyTypeExpr; use PHPStan\Node\Expr\GetIterableValueTypeExpr; -use PHPStan\Node\Expr\GetOffsetValueTypeExpr; use PHPStan\Node\Expr\IntertwinedVariableByReferenceWithExpr; use PHPStan\Node\Expr\NativeTypeExpr; use PHPStan\Node\Expr\OriginalForeachKeyExpr; @@ -49,11 +48,6 @@ protected function pPHPStan_Node_NativeTypeExpr(NativeTypeExpr $expr): string // return sprintf('__phpstanNativeType(%s, %s)', $expr->getPhpDocType()->describe(VerbosityLevel::precise()), $expr->getNativeType()->describe(VerbosityLevel::precise())); } - protected function pPHPStan_Node_GetOffsetValueTypeExpr(GetOffsetValueTypeExpr $expr): string // phpcs:ignore - { - return sprintf('__phpstanGetOffsetValueType(%s, %s)', $this->p($expr->getVar()), $this->p($expr->getDim())); - } - protected function pPHPStan_Node_UnsetOffsetExpr(UnsetOffsetExpr $expr): string // phpcs:ignore { return sprintf('__phpstanUnsetOffset(%s, %s)', $this->p($expr->getVar()), $this->p($expr->getDim())); diff --git a/src/Rules/PhpDoc/VarTagTypeRuleHelper.php b/src/Rules/PhpDoc/VarTagTypeRuleHelper.php index 4a6e8421d91..127501aa58c 100644 --- a/src/Rules/PhpDoc/VarTagTypeRuleHelper.php +++ b/src/Rules/PhpDoc/VarTagTypeRuleHelper.php @@ -8,7 +8,7 @@ use PHPStan\Analyser\Scope; use PHPStan\DependencyInjection\AutowiredParameter; use PHPStan\DependencyInjection\AutowiredService; -use PHPStan\Node\Expr\GetOffsetValueTypeExpr; +use PHPStan\Node\Expr\TypeExpr; use PHPStan\PhpDoc\NameScopeAlreadyBeingCreatedException; use PHPStan\PhpDoc\Tag\VarTag; use PHPStan\PhpDoc\TypeNodeResolver; @@ -78,7 +78,7 @@ public function checkVarType(Scope $scope, Node\Expr $var, Node\Expr $expr, arra $dimExpr = $arrayItem->key; } - $itemErrors = $this->checkVarType($scope, $arrayItem->value, new GetOffsetValueTypeExpr($expr, $dimExpr), $varTags, $assignedVariables); + $itemErrors = $this->checkVarType($scope, $arrayItem->value, new TypeExpr($scope->getType($expr)->getOffsetValueType($scope->getType($dimExpr))), $varTags, $assignedVariables); foreach ($itemErrors as $error) { $errors[] = $error; } From 4b95a425f601ac7793d184bbe56c1c559d0e9099 Mon Sep 17 00:00:00 2001 From: Ondrej Mirtes Date: Thu, 18 Jun 2026 08:38:52 +0200 Subject: [PATCH 080/113] Eliminate the GetIterableKeyTypeExpr virtual node --- .../Virtual/GetIterableKeyTypeExprHandler.php | 64 ------------------- src/Analyser/MutatingScope.php | 7 +- src/Analyser/NodeScopeResolver.php | 13 +++- src/Node/Expr/GetIterableKeyTypeExpr.php | 37 ----------- src/Node/Printer/Printer.php | 6 -- src/Rules/Arrays/ArrayUnpackingRule.php | 7 +- .../PhpDoc/WrongVariableNameInVarTagRule.php | 8 ++- 7 files changed, 26 insertions(+), 116 deletions(-) delete mode 100644 src/Analyser/ExprHandler/Virtual/GetIterableKeyTypeExprHandler.php delete mode 100644 src/Node/Expr/GetIterableKeyTypeExpr.php diff --git a/src/Analyser/ExprHandler/Virtual/GetIterableKeyTypeExprHandler.php b/src/Analyser/ExprHandler/Virtual/GetIterableKeyTypeExprHandler.php deleted file mode 100644 index 6f32dd6c6ec..00000000000 --- a/src/Analyser/ExprHandler/Virtual/GetIterableKeyTypeExprHandler.php +++ /dev/null @@ -1,64 +0,0 @@ - - */ -#[AutowiredService] -final class GetIterableKeyTypeExprHandler implements TypeResolvingExprHandler -{ - - public function __construct(private ExpressionResultFactory $expressionResultFactory) - { - } - - public function supports(Expr $expr): bool - { - return $expr instanceof GetIterableKeyTypeExpr; - } - - public function processExpr(NodeScopeResolver $nodeScopeResolver, Stmt $stmt, Expr $expr, MutatingScope $scope, ExpressionResultStorage $storage, callable $nodeCallback, ExpressionContext $context): ExpressionResult - { - // because this is a virtual node handler, the caller will only be interested in the type - // we don't need to process the inner expr - - return $this->expressionResultFactory->create( - $scope, - beforeScope: $scope, - expr: $expr, - hasYield: false, - isAlwaysTerminating: false, - throwPoints: [], - impurePoints: [], - ); - } - - public function resolveType(MutatingScope $scope, Expr $expr): Type - { - return $scope->getIterableKeyType($scope->getType($expr->getExpr())); - } - - public function specifyTypes(TypeSpecifier $typeSpecifier, Scope $scope, Expr $expr, TypeSpecifierContext $context): SpecifiedTypes - { - return $typeSpecifier->specifyDefaultTypes($scope, $expr, $context); - } - -} diff --git a/src/Analyser/MutatingScope.php b/src/Analyser/MutatingScope.php index 9a0abf31be0..412df0116c3 100644 --- a/src/Analyser/MutatingScope.php +++ b/src/Analyser/MutatingScope.php @@ -28,8 +28,8 @@ use PHPStan\Node\EmitCollectedDataNode; use PHPStan\Node\Expr\AlwaysRememberedExpr; use PHPStan\Node\Expr\CloneReinitializationExpr; -use PHPStan\Node\Expr\GetIterableKeyTypeExpr; use PHPStan\Node\Expr\IntertwinedVariableByReferenceWithExpr; +use PHPStan\Node\Expr\NativeTypeExpr; use PHPStan\Node\Expr\OriginalForeachKeyExpr; use PHPStan\Node\Expr\OriginalForeachValueExpr; use PHPStan\Node\Expr\ParameterVariableOriginalValueExpr; @@ -2646,7 +2646,10 @@ public function enterForeach(self $originalScope, Expr $iteratee, string $valueN $scope = $scope->assignExpression( new IntertwinedVariableByReferenceWithExpr($valueName, $iteratee, new SetExistingOffsetValueTypeExpr( $iteratee, - new GetIterableKeyTypeExpr($iteratee), + new NativeTypeExpr( + $originalScope->getIterableKeyType($iterateeType), + $originalScope->getIterableKeyType($nativeIterateeType), + ), new Variable($valueName), )), $valueType, diff --git a/src/Analyser/NodeScopeResolver.php b/src/Analyser/NodeScopeResolver.php index 7a2b0475bf3..3232df23733 100644 --- a/src/Analyser/NodeScopeResolver.php +++ b/src/Analyser/NodeScopeResolver.php @@ -78,8 +78,8 @@ use PHPStan\Node\ExecutionEndNode; use PHPStan\Node\Expr\ExistingArrayDimFetch; use PHPStan\Node\Expr\ForeachValueByRefExpr; -use PHPStan\Node\Expr\GetIterableKeyTypeExpr; use PHPStan\Node\Expr\GetIterableValueTypeExpr; +use PHPStan\Node\Expr\NativeTypeExpr; use PHPStan\Node\Expr\OriginalForeachKeyExpr; use PHPStan\Node\Expr\OriginalForeachValueExpr; use PHPStan\Node\Expr\PropertyInitializationExpr; @@ -1541,7 +1541,11 @@ public function processStmtNode( $bodyScope = $scope; if ($stmt->keyVar instanceof Variable) { - $this->callNodeCallback($nodeCallback, new VariableAssignNode($stmt->keyVar, new GetIterableKeyTypeExpr($stmt->expr)), $originalScope, $storage); + $keyTypeExpr = new NativeTypeExpr( + $originalScope->getIterableKeyType($originalScope->getType($stmt->expr)), + $originalScope->getIterableKeyType($originalScope->getNativeType($stmt->expr)), + ); + $this->callNodeCallback($nodeCallback, new VariableAssignNode($stmt->keyVar, $keyTypeExpr), $originalScope, $storage); } if ($stmt->valueVar instanceof Variable) { @@ -4662,7 +4666,10 @@ private function enterForeach(MutatingScope $scope, ExpressionResultStorage $sto $storage, $stmt, $stmt->keyVar, - new GetIterableKeyTypeExpr($stmt->expr), + new NativeTypeExpr( + $originalScope->getIterableKeyType($iterateeType), + $originalScope->getIterableKeyType($originalScope->getNativeType($stmt->expr)), + ), $nodeCallback, )->getScope(); $vars = array_merge($vars, $this->getAssignedVariables($stmt->keyVar)); diff --git a/src/Node/Expr/GetIterableKeyTypeExpr.php b/src/Node/Expr/GetIterableKeyTypeExpr.php deleted file mode 100644 index 60731730539..00000000000 --- a/src/Node/Expr/GetIterableKeyTypeExpr.php +++ /dev/null @@ -1,37 +0,0 @@ -expr; - } - - #[Override] - public function getType(): string - { - return 'PHPStan_Node_GetIterableKeyTypeExpr'; - } - - /** - * @return string[] - */ - #[Override] - public function getSubNodeNames(): array - { - return []; - } - -} diff --git a/src/Node/Printer/Printer.php b/src/Node/Printer/Printer.php index d1bf761b14e..60e4b3d6e64 100644 --- a/src/Node/Printer/Printer.php +++ b/src/Node/Printer/Printer.php @@ -10,7 +10,6 @@ use PHPStan\Node\Expr\CloneReinitializationExpr; use PHPStan\Node\Expr\ExistingArrayDimFetch; use PHPStan\Node\Expr\ForeachValueByRefExpr; -use PHPStan\Node\Expr\GetIterableKeyTypeExpr; use PHPStan\Node\Expr\GetIterableValueTypeExpr; use PHPStan\Node\Expr\IntertwinedVariableByReferenceWithExpr; use PHPStan\Node\Expr\NativeTypeExpr; @@ -58,11 +57,6 @@ protected function pPHPStan_Node_GetIterableValueTypeExpr(GetIterableValueTypeEx return sprintf('__phpstanGetIterableValueType(%s)', $this->p($expr->getExpr())); } - protected function pPHPStan_Node_GetIterableKeyTypeExpr(GetIterableKeyTypeExpr $expr): string // phpcs:ignore - { - return sprintf('__phpstanGetIterableKeyType(%s)', $this->p($expr->getExpr())); - } - protected function pPHPStan_Node_ExistingArrayDimFetch(ExistingArrayDimFetch $expr): string // phpcs:ignore { return sprintf('__phpstanExistingArrayDimFetch(%s, %s)', $this->p($expr->getVar()), $this->p($expr->getDim())); diff --git a/src/Rules/Arrays/ArrayUnpackingRule.php b/src/Rules/Arrays/ArrayUnpackingRule.php index 4dc25092a9a..08cb915cad3 100644 --- a/src/Rules/Arrays/ArrayUnpackingRule.php +++ b/src/Rules/Arrays/ArrayUnpackingRule.php @@ -6,7 +6,7 @@ use PhpParser\Node\ArrayItem; use PHPStan\Analyser\Scope; use PHPStan\DependencyInjection\RegisteredRule; -use PHPStan\Node\Expr\GetIterableKeyTypeExpr; +use PHPStan\Node\Expr\NativeTypeExpr; use PHPStan\Php\PhpVersion; use PHPStan\Rules\Rule; use PHPStan\Rules\RuleErrorBuilder; @@ -40,7 +40,10 @@ public function processNode(Node $node, Scope $scope): array $typeResult = $this->ruleLevelHelper->findTypeToCheck( $scope, - new GetIterableKeyTypeExpr($node->value), + new NativeTypeExpr( + $scope->getIterableKeyType($scope->getType($node->value)), + $scope->getIterableKeyType($scope->getNativeType($node->value)), + ), '', static fn (Type $type): bool => $type->isString()->no(), ); diff --git a/src/Rules/PhpDoc/WrongVariableNameInVarTagRule.php b/src/Rules/PhpDoc/WrongVariableNameInVarTagRule.php index 6296e12ca6c..66b7ca6ce8f 100644 --- a/src/Rules/PhpDoc/WrongVariableNameInVarTagRule.php +++ b/src/Rules/PhpDoc/WrongVariableNameInVarTagRule.php @@ -7,8 +7,8 @@ use PhpParser\Node\Expr; use PHPStan\Analyser\Scope; use PHPStan\DependencyInjection\RegisteredRule; -use PHPStan\Node\Expr\GetIterableKeyTypeExpr; use PHPStan\Node\Expr\GetIterableValueTypeExpr; +use PHPStan\Node\Expr\NativeTypeExpr; use PHPStan\Node\InClassMethodNode; use PHPStan\Node\InClassNode; use PHPStan\Node\InFunctionNode; @@ -271,7 +271,11 @@ private function processForeach(Scope $scope, Node\Expr $iterateeExpr, ?Node\Exp $errors[] = $error; } if ($keyVar !== null) { - foreach ($this->varTagTypeRuleHelper->checkVarType($scope, $keyVar, new GetIterableKeyTypeExpr($iterateeExpr), $varTags, $variableNames) as $error) { + $keyTypeExpr = new NativeTypeExpr( + $scope->getIterableKeyType($scope->getScopeType($iterateeExpr)), + $scope->getIterableKeyType($scope->getScopeNativeType($iterateeExpr)), + ); + foreach ($this->varTagTypeRuleHelper->checkVarType($scope, $keyVar, $keyTypeExpr, $varTags, $variableNames) as $error) { $errors[] = $error; } } From 254d9ffb6f53d04ec6039479130155f1bd85541b Mon Sep 17 00:00:00 2001 From: Ondrej Mirtes Date: Thu, 18 Jun 2026 09:10:26 +0200 Subject: [PATCH 081/113] Eliminate the GetIterableValueTypeExpr virtual node --- .../GetIterableValueTypeExprHandler.php | 64 ------------------- src/Analyser/NodeScopeResolver.php | 17 +++-- src/Node/Expr/GetIterableValueTypeExpr.php | 37 ----------- src/Node/Printer/Printer.php | 6 -- .../PhpDoc/WrongVariableNameInVarTagRule.php | 7 +- 5 files changed, 18 insertions(+), 113 deletions(-) delete mode 100644 src/Analyser/ExprHandler/Virtual/GetIterableValueTypeExprHandler.php delete mode 100644 src/Node/Expr/GetIterableValueTypeExpr.php diff --git a/src/Analyser/ExprHandler/Virtual/GetIterableValueTypeExprHandler.php b/src/Analyser/ExprHandler/Virtual/GetIterableValueTypeExprHandler.php deleted file mode 100644 index 950d1025266..00000000000 --- a/src/Analyser/ExprHandler/Virtual/GetIterableValueTypeExprHandler.php +++ /dev/null @@ -1,64 +0,0 @@ - - */ -#[AutowiredService] -final class GetIterableValueTypeExprHandler implements TypeResolvingExprHandler -{ - - public function __construct(private ExpressionResultFactory $expressionResultFactory) - { - } - - public function supports(Expr $expr): bool - { - return $expr instanceof GetIterableValueTypeExpr; - } - - public function processExpr(NodeScopeResolver $nodeScopeResolver, Stmt $stmt, Expr $expr, MutatingScope $scope, ExpressionResultStorage $storage, callable $nodeCallback, ExpressionContext $context): ExpressionResult - { - // because this is a virtual node handler, the caller will only be interested in the type - // we don't need to process the inner expr - - return $this->expressionResultFactory->create( - $scope, - beforeScope: $scope, - expr: $expr, - hasYield: false, - isAlwaysTerminating: false, - throwPoints: [], - impurePoints: [], - ); - } - - public function resolveType(MutatingScope $scope, Expr $expr): Type - { - return $scope->getIterableValueType($scope->getType($expr->getExpr())); - } - - public function specifyTypes(TypeSpecifier $typeSpecifier, Scope $scope, Expr $expr, TypeSpecifierContext $context): SpecifiedTypes - { - return $typeSpecifier->specifyDefaultTypes($scope, $expr, $context); - } - -} diff --git a/src/Analyser/NodeScopeResolver.php b/src/Analyser/NodeScopeResolver.php index 3232df23733..74d69ec528e 100644 --- a/src/Analyser/NodeScopeResolver.php +++ b/src/Analyser/NodeScopeResolver.php @@ -78,7 +78,6 @@ use PHPStan\Node\ExecutionEndNode; use PHPStan\Node\Expr\ExistingArrayDimFetch; use PHPStan\Node\Expr\ForeachValueByRefExpr; -use PHPStan\Node\Expr\GetIterableValueTypeExpr; use PHPStan\Node\Expr\NativeTypeExpr; use PHPStan\Node\Expr\OriginalForeachKeyExpr; use PHPStan\Node\Expr\OriginalForeachValueExpr; @@ -1549,9 +1548,16 @@ public function processStmtNode( } if ($stmt->valueVar instanceof Variable) { - $this->callNodeCallback($nodeCallback, new VariableAssignNode($stmt->valueVar, new GetIterableValueTypeExpr($stmt->expr)), $originalScope, $storage); + $valueTypeExpr = new NativeTypeExpr( + $originalScope->getIterableValueType($originalScope->getType($stmt->expr)), + $originalScope->getIterableValueType($originalScope->getNativeType($stmt->expr)), + ); + $this->callNodeCallback($nodeCallback, new VariableAssignNode($stmt->valueVar, $valueTypeExpr), $originalScope, $storage); } elseif ($stmt->valueVar instanceof List_) { - $virtualAssign = new Assign($stmt->valueVar, new GetIterableValueTypeExpr($stmt->expr)); + $virtualAssign = new Assign($stmt->valueVar, new NativeTypeExpr( + $originalScope->getIterableValueType($originalScope->getType($stmt->expr)), + $originalScope->getIterableValueType($originalScope->getNativeType($stmt->expr)), + )); $virtualAssign->setAttributes($stmt->valueVar->getAttributes()); $this->callNodeCallback($nodeCallback, $virtualAssign, $scope, $storage); } @@ -4651,7 +4657,10 @@ private function enterForeach(MutatingScope $scope, ExpressionResultStorage $sto $storage, $stmt, $stmt->valueVar, - new GetIterableValueTypeExpr($stmt->expr), + new NativeTypeExpr( + $originalScope->getIterableValueType($iterateeType), + $originalScope->getIterableValueType($originalScope->getNativeType($stmt->expr)), + ), $nodeCallback, )->getScope(); $vars = $this->getAssignedVariables($stmt->valueVar); diff --git a/src/Node/Expr/GetIterableValueTypeExpr.php b/src/Node/Expr/GetIterableValueTypeExpr.php deleted file mode 100644 index 642eb4870ea..00000000000 --- a/src/Node/Expr/GetIterableValueTypeExpr.php +++ /dev/null @@ -1,37 +0,0 @@ -expr; - } - - #[Override] - public function getType(): string - { - return 'PHPStan_Node_GetIterableValueTypeExpr'; - } - - /** - * @return string[] - */ - #[Override] - public function getSubNodeNames(): array - { - return []; - } - -} diff --git a/src/Node/Printer/Printer.php b/src/Node/Printer/Printer.php index 60e4b3d6e64..d513a5f901d 100644 --- a/src/Node/Printer/Printer.php +++ b/src/Node/Printer/Printer.php @@ -10,7 +10,6 @@ use PHPStan\Node\Expr\CloneReinitializationExpr; use PHPStan\Node\Expr\ExistingArrayDimFetch; use PHPStan\Node\Expr\ForeachValueByRefExpr; -use PHPStan\Node\Expr\GetIterableValueTypeExpr; use PHPStan\Node\Expr\IntertwinedVariableByReferenceWithExpr; use PHPStan\Node\Expr\NativeTypeExpr; use PHPStan\Node\Expr\OriginalForeachKeyExpr; @@ -52,11 +51,6 @@ protected function pPHPStan_Node_UnsetOffsetExpr(UnsetOffsetExpr $expr): string return sprintf('__phpstanUnsetOffset(%s, %s)', $this->p($expr->getVar()), $this->p($expr->getDim())); } - protected function pPHPStan_Node_GetIterableValueTypeExpr(GetIterableValueTypeExpr $expr): string // phpcs:ignore - { - return sprintf('__phpstanGetIterableValueType(%s)', $this->p($expr->getExpr())); - } - protected function pPHPStan_Node_ExistingArrayDimFetch(ExistingArrayDimFetch $expr): string // phpcs:ignore { return sprintf('__phpstanExistingArrayDimFetch(%s, %s)', $this->p($expr->getVar()), $this->p($expr->getDim())); diff --git a/src/Rules/PhpDoc/WrongVariableNameInVarTagRule.php b/src/Rules/PhpDoc/WrongVariableNameInVarTagRule.php index 66b7ca6ce8f..fbd3f1c7bb2 100644 --- a/src/Rules/PhpDoc/WrongVariableNameInVarTagRule.php +++ b/src/Rules/PhpDoc/WrongVariableNameInVarTagRule.php @@ -7,7 +7,6 @@ use PhpParser\Node\Expr; use PHPStan\Analyser\Scope; use PHPStan\DependencyInjection\RegisteredRule; -use PHPStan\Node\Expr\GetIterableValueTypeExpr; use PHPStan\Node\Expr\NativeTypeExpr; use PHPStan\Node\InClassMethodNode; use PHPStan\Node\InClassNode; @@ -279,7 +278,11 @@ private function processForeach(Scope $scope, Node\Expr $iterateeExpr, ?Node\Exp $errors[] = $error; } } - foreach ($this->varTagTypeRuleHelper->checkVarType($scope, $valueVar, new GetIterableValueTypeExpr($iterateeExpr), $varTags, $variableNames) as $error) { + $valueTypeExpr = new NativeTypeExpr( + $scope->getIterableValueType($scope->getScopeType($iterateeExpr)), + $scope->getIterableValueType($scope->getScopeNativeType($iterateeExpr)), + ); + foreach ($this->varTagTypeRuleHelper->checkVarType($scope, $valueVar, $valueTypeExpr, $varTags, $variableNames) as $error) { $errors[] = $error; } From 0e0c23829e673ec507fa780ab74dae49075bae25 Mon Sep 17 00:00:00 2001 From: Ondrej Mirtes Date: Thu, 18 Jun 2026 11:23:52 +0200 Subject: [PATCH 082/113] ExistingArrayDimFetchHandler is no longer TypeResolvingExprHandler --- .../Virtual/ExistingArrayDimFetchHandler.php | 27 +++++-------------- 1 file changed, 7 insertions(+), 20 deletions(-) diff --git a/src/Analyser/ExprHandler/Virtual/ExistingArrayDimFetchHandler.php b/src/Analyser/ExprHandler/Virtual/ExistingArrayDimFetchHandler.php index 61b7884270d..47e33d64957 100644 --- a/src/Analyser/ExprHandler/Virtual/ExistingArrayDimFetchHandler.php +++ b/src/Analyser/ExprHandler/Virtual/ExistingArrayDimFetchHandler.php @@ -8,22 +8,18 @@ use PHPStan\Analyser\ExpressionResult; use PHPStan\Analyser\ExpressionResultFactory; use PHPStan\Analyser\ExpressionResultStorage; +use PHPStan\Analyser\ExprHandler; use PHPStan\Analyser\MutatingScope; use PHPStan\Analyser\NodeScopeResolver; -use PHPStan\Analyser\Scope; -use PHPStan\Analyser\SpecifiedTypes; -use PHPStan\Analyser\TypeResolvingExprHandler; -use PHPStan\Analyser\TypeSpecifier; -use PHPStan\Analyser\TypeSpecifierContext; use PHPStan\DependencyInjection\AutowiredService; use PHPStan\Node\Expr\ExistingArrayDimFetch; use PHPStan\Type\Type; /** - * @implements TypeResolvingExprHandler + * @implements ExprHandler */ #[AutowiredService] -final class ExistingArrayDimFetchHandler implements TypeResolvingExprHandler +final class ExistingArrayDimFetchHandler implements ExprHandler { public function __construct(private ExpressionResultFactory $expressionResultFactory) @@ -37,9 +33,9 @@ public function supports(Expr $expr): bool public function processExpr(NodeScopeResolver $nodeScopeResolver, Stmt $stmt, Expr $expr, MutatingScope $scope, ExpressionResultStorage $storage, callable $nodeCallback, ExpressionContext $context): ExpressionResult { - // because this is a virtual node handler, the caller will only be interested in the type - // we don't need to process the inner expr - + // virtual node: callers only read the type, computed lazily by the + // typeCallback. A null specifyTypesCallback falls back to default + // narrowing in TypeSpecifier, matching the old specifyDefaultTypes(). return $this->expressionResultFactory->create( $scope, beforeScope: $scope, @@ -48,17 +44,8 @@ public function processExpr(NodeScopeResolver $nodeScopeResolver, Stmt $stmt, Ex isAlwaysTerminating: false, throwPoints: [], impurePoints: [], + typeCallback: static fn (MutatingScope $s): Type => $s->getType(new Expr\ArrayDimFetch($expr->getVar(), $expr->getDim())), ); } - public function resolveType(MutatingScope $scope, Expr $expr): Type - { - return $scope->getType(new Expr\ArrayDimFetch($expr->getVar(), $expr->getDim())); - } - - public function specifyTypes(TypeSpecifier $typeSpecifier, Scope $scope, Expr $expr, TypeSpecifierContext $context): SpecifiedTypes - { - return $typeSpecifier->specifyDefaultTypes($scope, $expr, $context); - } - } From a877de74f34a5c0091648377ebf3af56aea4c905 Mon Sep 17 00:00:00 2001 From: Ondrej Mirtes Date: Thu, 18 Jun 2026 11:23:52 +0200 Subject: [PATCH 083/113] UnsetOffsetExprHandler is no longer TypeResolvingExprHandler --- .../Virtual/UnsetOffsetExprHandler.php | 27 +++++-------------- 1 file changed, 7 insertions(+), 20 deletions(-) diff --git a/src/Analyser/ExprHandler/Virtual/UnsetOffsetExprHandler.php b/src/Analyser/ExprHandler/Virtual/UnsetOffsetExprHandler.php index 9f5e4368b27..ad884abca1a 100644 --- a/src/Analyser/ExprHandler/Virtual/UnsetOffsetExprHandler.php +++ b/src/Analyser/ExprHandler/Virtual/UnsetOffsetExprHandler.php @@ -8,22 +8,18 @@ use PHPStan\Analyser\ExpressionResult; use PHPStan\Analyser\ExpressionResultFactory; use PHPStan\Analyser\ExpressionResultStorage; +use PHPStan\Analyser\ExprHandler; use PHPStan\Analyser\MutatingScope; use PHPStan\Analyser\NodeScopeResolver; -use PHPStan\Analyser\Scope; -use PHPStan\Analyser\SpecifiedTypes; -use PHPStan\Analyser\TypeResolvingExprHandler; -use PHPStan\Analyser\TypeSpecifier; -use PHPStan\Analyser\TypeSpecifierContext; use PHPStan\DependencyInjection\AutowiredService; use PHPStan\Node\Expr\UnsetOffsetExpr; use PHPStan\Type\Type; /** - * @implements TypeResolvingExprHandler + * @implements ExprHandler */ #[AutowiredService] -final class UnsetOffsetExprHandler implements TypeResolvingExprHandler +final class UnsetOffsetExprHandler implements ExprHandler { public function __construct(private ExpressionResultFactory $expressionResultFactory) @@ -37,9 +33,9 @@ public function supports(Expr $expr): bool public function processExpr(NodeScopeResolver $nodeScopeResolver, Stmt $stmt, Expr $expr, MutatingScope $scope, ExpressionResultStorage $storage, callable $nodeCallback, ExpressionContext $context): ExpressionResult { - // because this is a virtual node handler, the caller will only be interested in the type - // we don't need to process the inner expr - + // virtual node: callers only read the type, computed lazily by the + // typeCallback. A null specifyTypesCallback falls back to default + // narrowing in TypeSpecifier, matching the old specifyDefaultTypes(). return $this->expressionResultFactory->create( $scope, beforeScope: $scope, @@ -48,17 +44,8 @@ public function processExpr(NodeScopeResolver $nodeScopeResolver, Stmt $stmt, Ex isAlwaysTerminating: false, throwPoints: [], impurePoints: [], + typeCallback: static fn (MutatingScope $s): Type => $s->getType($expr->getVar())->unsetOffset($s->getType($expr->getDim())), ); } - public function resolveType(MutatingScope $scope, Expr $expr): Type - { - return $scope->getType($expr->getVar())->unsetOffset($scope->getType($expr->getDim())); - } - - public function specifyTypes(TypeSpecifier $typeSpecifier, Scope $scope, Expr $expr, TypeSpecifierContext $context): SpecifiedTypes - { - return $typeSpecifier->specifyDefaultTypes($scope, $expr, $context); - } - } From d869c72b7671468774f291359c9a76d3ee440a94 Mon Sep 17 00:00:00 2001 From: Ondrej Mirtes Date: Thu, 18 Jun 2026 11:23:52 +0200 Subject: [PATCH 084/113] SetOffsetValueTypeExprHandler is no longer TypeResolvingExprHandler --- .../Virtual/SetOffsetValueTypeExprHandler.php | 34 ++++++------------- 1 file changed, 10 insertions(+), 24 deletions(-) diff --git a/src/Analyser/ExprHandler/Virtual/SetOffsetValueTypeExprHandler.php b/src/Analyser/ExprHandler/Virtual/SetOffsetValueTypeExprHandler.php index e8871e56a07..7034d884562 100644 --- a/src/Analyser/ExprHandler/Virtual/SetOffsetValueTypeExprHandler.php +++ b/src/Analyser/ExprHandler/Virtual/SetOffsetValueTypeExprHandler.php @@ -8,22 +8,18 @@ use PHPStan\Analyser\ExpressionResult; use PHPStan\Analyser\ExpressionResultFactory; use PHPStan\Analyser\ExpressionResultStorage; +use PHPStan\Analyser\ExprHandler; use PHPStan\Analyser\MutatingScope; use PHPStan\Analyser\NodeScopeResolver; -use PHPStan\Analyser\Scope; -use PHPStan\Analyser\SpecifiedTypes; -use PHPStan\Analyser\TypeResolvingExprHandler; -use PHPStan\Analyser\TypeSpecifier; -use PHPStan\Analyser\TypeSpecifierContext; use PHPStan\DependencyInjection\AutowiredService; use PHPStan\Node\Expr\SetOffsetValueTypeExpr; use PHPStan\Type\Type; /** - * @implements TypeResolvingExprHandler + * @implements ExprHandler */ #[AutowiredService] -final class SetOffsetValueTypeExprHandler implements TypeResolvingExprHandler +final class SetOffsetValueTypeExprHandler implements ExprHandler { public function __construct(private ExpressionResultFactory $expressionResultFactory) @@ -37,9 +33,9 @@ public function supports(Expr $expr): bool public function processExpr(NodeScopeResolver $nodeScopeResolver, Stmt $stmt, Expr $expr, MutatingScope $scope, ExpressionResultStorage $storage, callable $nodeCallback, ExpressionContext $context): ExpressionResult { - // because this is a virtual node handler, the caller will only be interested in the type - // we don't need to process the inner expr - + // virtual node: callers only read the type, computed lazily by the + // typeCallback. A null specifyTypesCallback falls back to default + // narrowing in TypeSpecifier, matching the old specifyDefaultTypes(). return $this->expressionResultFactory->create( $scope, beforeScope: $scope, @@ -48,21 +44,11 @@ public function processExpr(NodeScopeResolver $nodeScopeResolver, Stmt $stmt, Ex isAlwaysTerminating: false, throwPoints: [], impurePoints: [], + typeCallback: static fn (MutatingScope $s): Type => $s->getType($expr->getVar())->setOffsetValueType( + $expr->getDim() !== null ? $s->getType($expr->getDim()) : null, + $s->getType($expr->getValue()), + ), ); } - public function resolveType(MutatingScope $scope, Expr $expr): Type - { - $varType = $scope->getType($expr->getVar()); - return $varType->setOffsetValueType( - $expr->getDim() !== null ? $scope->getType($expr->getDim()) : null, - $scope->getType($expr->getValue()), - ); - } - - public function specifyTypes(TypeSpecifier $typeSpecifier, Scope $scope, Expr $expr, TypeSpecifierContext $context): SpecifiedTypes - { - return $typeSpecifier->specifyDefaultTypes($scope, $expr, $context); - } - } From 9a6fadc83641e090f05c131baf88c9776846bf66 Mon Sep 17 00:00:00 2001 From: Ondrej Mirtes Date: Thu, 18 Jun 2026 11:23:52 +0200 Subject: [PATCH 085/113] SetExistingOffsetValueTypeExprHandler is no longer TypeResolvingExprHandler --- .../SetExistingOffsetValueTypeExprHandler.php | 34 ++++++------------- 1 file changed, 10 insertions(+), 24 deletions(-) diff --git a/src/Analyser/ExprHandler/Virtual/SetExistingOffsetValueTypeExprHandler.php b/src/Analyser/ExprHandler/Virtual/SetExistingOffsetValueTypeExprHandler.php index 7957ef0c797..46372f00f86 100644 --- a/src/Analyser/ExprHandler/Virtual/SetExistingOffsetValueTypeExprHandler.php +++ b/src/Analyser/ExprHandler/Virtual/SetExistingOffsetValueTypeExprHandler.php @@ -8,22 +8,18 @@ use PHPStan\Analyser\ExpressionResult; use PHPStan\Analyser\ExpressionResultFactory; use PHPStan\Analyser\ExpressionResultStorage; +use PHPStan\Analyser\ExprHandler; use PHPStan\Analyser\MutatingScope; use PHPStan\Analyser\NodeScopeResolver; -use PHPStan\Analyser\Scope; -use PHPStan\Analyser\SpecifiedTypes; -use PHPStan\Analyser\TypeResolvingExprHandler; -use PHPStan\Analyser\TypeSpecifier; -use PHPStan\Analyser\TypeSpecifierContext; use PHPStan\DependencyInjection\AutowiredService; use PHPStan\Node\Expr\SetExistingOffsetValueTypeExpr; use PHPStan\Type\Type; /** - * @implements TypeResolvingExprHandler + * @implements ExprHandler */ #[AutowiredService] -final class SetExistingOffsetValueTypeExprHandler implements TypeResolvingExprHandler +final class SetExistingOffsetValueTypeExprHandler implements ExprHandler { public function __construct(private ExpressionResultFactory $expressionResultFactory) @@ -37,9 +33,9 @@ public function supports(Expr $expr): bool public function processExpr(NodeScopeResolver $nodeScopeResolver, Stmt $stmt, Expr $expr, MutatingScope $scope, ExpressionResultStorage $storage, callable $nodeCallback, ExpressionContext $context): ExpressionResult { - // because this is a virtual node handler, the caller will only be interested in the type - // we don't need to process the inner expr - + // virtual node: callers only read the type, computed lazily by the + // typeCallback. A null specifyTypesCallback falls back to default + // narrowing in TypeSpecifier, matching the old specifyDefaultTypes(). return $this->expressionResultFactory->create( $scope, beforeScope: $scope, @@ -48,21 +44,11 @@ public function processExpr(NodeScopeResolver $nodeScopeResolver, Stmt $stmt, Ex isAlwaysTerminating: false, throwPoints: [], impurePoints: [], + typeCallback: static fn (MutatingScope $s): Type => $s->getType($expr->getVar())->setExistingOffsetValueType( + $s->getType($expr->getDim()), + $s->getType($expr->getValue()), + ), ); } - public function resolveType(MutatingScope $scope, Expr $expr): Type - { - $varType = $scope->getType($expr->getVar()); - return $varType->setExistingOffsetValueType( - $scope->getType($expr->getDim()), - $scope->getType($expr->getValue()), - ); - } - - public function specifyTypes(TypeSpecifier $typeSpecifier, Scope $scope, Expr $expr, TypeSpecifierContext $context): SpecifiedTypes - { - return $typeSpecifier->specifyDefaultTypes($scope, $expr, $context); - } - } From 6e617ebdb284d220fbf0fd1fddafa94337813f4a Mon Sep 17 00:00:00 2001 From: Ondrej Mirtes Date: Thu, 18 Jun 2026 14:57:08 +0200 Subject: [PATCH 086/113] Read operand types from ExpressionResults in Throw/BooleanAnd/Coalesce handlers --- src/Analyser/ExprHandler/BooleanAndHandler.php | 2 +- src/Analyser/ExprHandler/CoalesceHandler.php | 2 +- src/Analyser/ExprHandler/ThrowHandler.php | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/Analyser/ExprHandler/BooleanAndHandler.php b/src/Analyser/ExprHandler/BooleanAndHandler.php index 3e6854b175c..2c2e14dcb16 100644 --- a/src/Analyser/ExprHandler/BooleanAndHandler.php +++ b/src/Analyser/ExprHandler/BooleanAndHandler.php @@ -78,7 +78,7 @@ public function processExpr(NodeScopeResolver $nodeScopeResolver, Stmt $stmt, Ex $leftResult = $nodeScopeResolver->processExprNode($stmt, $expr->left, $scope, $storage, $nodeCallback, $context->enterDeep()); $leftTruthyScope = $leftResult->getTruthyScope(); $rightResult = $nodeScopeResolver->processExprNode($stmt, $expr->right, $leftTruthyScope, $storage, $nodeCallback, $context); - $rightExprType = $rightResult->getScope()->getType($expr->right); + $rightExprType = $rightResult->getTypeForScope($rightResult->getScope()); if ($rightExprType instanceof NeverType && $rightExprType->isExplicit()) { $leftMergedWithRightScope = $leftResult->getFalseyScope(); } else { diff --git a/src/Analyser/ExprHandler/CoalesceHandler.php b/src/Analyser/ExprHandler/CoalesceHandler.php index 0b496e8488a..3f600a64329 100644 --- a/src/Analyser/ExprHandler/CoalesceHandler.php +++ b/src/Analyser/ExprHandler/CoalesceHandler.php @@ -75,7 +75,7 @@ public function processExpr(NodeScopeResolver $nodeScopeResolver, Stmt $stmt, Ex // mid-processing would take the on-demand path and recurse $rightScope = $scope->applySpecifiedTypes($this->getFalseySpecifiedTypes($scope, $expr, $condResult, TypeSpecifierContext::createFalsey())); $rightResult = $nodeScopeResolver->processExprNode($stmt, $expr->right, $rightScope, $storage, $nodeCallback, $context->enterDeep()); - $rightExprType = $scope->getType($expr->right); + $rightExprType = $rightResult->getTypeForScope($scope); if ($rightExprType instanceof NeverType && $rightExprType->isExplicit()) { $scope = $scope->filterByTruthyValue(new Expr\Isset_([$expr->left])); } else { diff --git a/src/Analyser/ExprHandler/ThrowHandler.php b/src/Analyser/ExprHandler/ThrowHandler.php index 22bd0e1dc6e..89a3860f061 100644 --- a/src/Analyser/ExprHandler/ThrowHandler.php +++ b/src/Analyser/ExprHandler/ThrowHandler.php @@ -49,7 +49,7 @@ public function processExpr(NodeScopeResolver $nodeScopeResolver, Stmt $stmt, Ex expr: $expr, hasYield: false, isAlwaysTerminating: true, - throwPoints: array_merge($exprResult->getThrowPoints(), [InternalThrowPoint::createExplicit($scope, $scope->getType($expr->expr), $expr, false)]), + throwPoints: array_merge($exprResult->getThrowPoints(), [InternalThrowPoint::createExplicit($scope, $exprResult->getTypeForScope($scope), $expr, false)]), impurePoints: $exprResult->getImpurePoints(), typeCallback: static fn (MutatingScope $scope): Type => new NonAcceptingNeverType(), specifyTypesCallback: fn (MutatingScope $s, TypeSpecifierContext $context) => $this->defaultNarrowingHelper->specifyDefaultTypes($expr, $context), From 2f8015827f446812488fd6d2503c67a7799b4bbc Mon Sep 17 00:00:00 2001 From: Ondrej Mirtes Date: Thu, 18 Jun 2026 15:02:23 +0200 Subject: [PATCH 087/113] Read operand types from ExpressionResults in Ternary/ArrayDimFetch/PropertyFetch handlers --- src/Analyser/ExprHandler/ArrayDimFetchHandler.php | 2 +- src/Analyser/ExprHandler/PropertyFetchHandler.php | 2 +- src/Analyser/ExprHandler/TernaryHandler.php | 6 +++--- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/src/Analyser/ExprHandler/ArrayDimFetchHandler.php b/src/Analyser/ExprHandler/ArrayDimFetchHandler.php index efcb21cce2d..9d3461e7343 100644 --- a/src/Analyser/ExprHandler/ArrayDimFetchHandler.php +++ b/src/Analyser/ExprHandler/ArrayDimFetchHandler.php @@ -76,7 +76,7 @@ public function processExpr(NodeScopeResolver $nodeScopeResolver, Stmt $stmt, Ex $impurePoints = array_merge($dimResult->getImpurePoints(), $varResult->getImpurePoints()); $scope = $varResult->getScope(); - $varType = $scope->getType($expr->var); + $varType = $varResult->getTypeForScope($scope); if (!$varType->isArray()->yes() && !(new ObjectType(ArrayAccess::class))->isSuperTypeOf($varType)->no()) { $throwPoints = array_merge($throwPoints, $nodeScopeResolver->processExprNode( $stmt, diff --git a/src/Analyser/ExprHandler/PropertyFetchHandler.php b/src/Analyser/ExprHandler/PropertyFetchHandler.php index 69a39439697..fd1e43b5d09 100644 --- a/src/Analyser/ExprHandler/PropertyFetchHandler.php +++ b/src/Analyser/ExprHandler/PropertyFetchHandler.php @@ -65,7 +65,7 @@ public function processExpr(NodeScopeResolver $nodeScopeResolver, Stmt $stmt, Ex $nameResult = null; if ($expr->name instanceof Identifier) { $propertyName = $expr->name->toString(); - $propertyHolderType = $scopeBeforeVar->getType($expr->var); + $propertyHolderType = $varResult->getTypeForScope($scopeBeforeVar); $propertyReflection = $scopeBeforeVar->getInstancePropertyReflection($propertyHolderType, $propertyName); if ($propertyReflection !== null && $this->phpVersion->supportsPropertyHooks()) { $propertyDeclaringClass = $propertyReflection->getDeclaringClass(); diff --git a/src/Analyser/ExprHandler/TernaryHandler.php b/src/Analyser/ExprHandler/TernaryHandler.php index 79ece5c6c51..861429e4392 100644 --- a/src/Analyser/ExprHandler/TernaryHandler.php +++ b/src/Analyser/ExprHandler/TernaryHandler.php @@ -64,7 +64,7 @@ public function processExpr(NodeScopeResolver $nodeScopeResolver, Stmt $stmt, Ex $throwPoints = array_merge($throwPoints, $ifResult->getThrowPoints()); $impurePoints = array_merge($impurePoints, $ifResult->getImpurePoints()); $ifTrueScope = $ifResult->getScope(); - $ifTrueType = $ifTrueScope->getType($expr->if); + $ifTrueType = $ifResult->getTypeForScope($ifTrueScope); $elseResult = $nodeScopeResolver->processExprNode($stmt, $expr->else, $ifFalseScope, $storage, $nodeCallback, $context); $throwPoints = array_merge($throwPoints, $elseResult->getThrowPoints()); @@ -72,7 +72,7 @@ public function processExpr(NodeScopeResolver $nodeScopeResolver, Stmt $stmt, Ex $ifFalseScope = $elseResult->getScope(); } - $condType = $scope->getType($expr->cond); + $condType = $ternaryCondResult->getTypeForScope($scope); if ($condType->isTrue()->yes()) { $finalScope = $ifTrueScope; } elseif ($condType->isFalse()->yes()) { @@ -81,7 +81,7 @@ public function processExpr(NodeScopeResolver $nodeScopeResolver, Stmt $stmt, Ex if ($ifTrueType instanceof NeverType && $ifTrueType->isExplicit()) { $finalScope = $ifFalseScope; } else { - $ifFalseType = $ifFalseScope->getType($expr->else); + $ifFalseType = $elseResult->getTypeForScope($ifFalseScope); if ($ifFalseType instanceof NeverType && $ifFalseType->isExplicit()) { $finalScope = $ifTrueScope; From 6963841d9b43bafef35b5acde8d2485839452bc8 Mon Sep 17 00:00:00 2001 From: Ondrej Mirtes Date: Thu, 18 Jun 2026 15:15:10 +0200 Subject: [PATCH 088/113] Read sub-expression types from ExpressionResults in offset Virtual handlers --- .../Virtual/ExistingArrayDimFetchHandler.php | 11 ++++++++--- .../SetExistingOffsetValueTypeExprHandler.php | 15 +++++++++++---- .../Virtual/SetOffsetValueTypeExprHandler.php | 18 +++++++++++++----- .../Virtual/UnsetOffsetExprHandler.php | 10 ++++++++-- 4 files changed, 40 insertions(+), 14 deletions(-) diff --git a/src/Analyser/ExprHandler/Virtual/ExistingArrayDimFetchHandler.php b/src/Analyser/ExprHandler/Virtual/ExistingArrayDimFetchHandler.php index 47e33d64957..5645642ccfc 100644 --- a/src/Analyser/ExprHandler/Virtual/ExistingArrayDimFetchHandler.php +++ b/src/Analyser/ExprHandler/Virtual/ExistingArrayDimFetchHandler.php @@ -34,8 +34,13 @@ public function supports(Expr $expr): bool public function processExpr(NodeScopeResolver $nodeScopeResolver, Stmt $stmt, Expr $expr, MutatingScope $scope, ExpressionResultStorage $storage, callable $nodeCallback, ExpressionContext $context): ExpressionResult { // virtual node: callers only read the type, computed lazily by the - // typeCallback. A null specifyTypesCallback falls back to default - // narrowing in TypeSpecifier, matching the old specifyDefaultTypes(). + // typeCallback. The plain array dim fetch is processed here (its real + // leaves are already stored by on-demand time) so the typeCallback reads + // its ExpressionResult instead of Scope::getType(). A null + // specifyTypesCallback falls back to default narrowing in TypeSpecifier, + // matching the old specifyDefaultTypes(). + $arrayDimFetchResult = $nodeScopeResolver->processExprNode($stmt, new Expr\ArrayDimFetch($expr->getVar(), $expr->getDim()), $scope, $storage, $nodeCallback, $context); + return $this->expressionResultFactory->create( $scope, beforeScope: $scope, @@ -44,7 +49,7 @@ public function processExpr(NodeScopeResolver $nodeScopeResolver, Stmt $stmt, Ex isAlwaysTerminating: false, throwPoints: [], impurePoints: [], - typeCallback: static fn (MutatingScope $s): Type => $s->getType(new Expr\ArrayDimFetch($expr->getVar(), $expr->getDim())), + typeCallback: static fn (MutatingScope $s): Type => $arrayDimFetchResult->getTypeForScope($s), ); } diff --git a/src/Analyser/ExprHandler/Virtual/SetExistingOffsetValueTypeExprHandler.php b/src/Analyser/ExprHandler/Virtual/SetExistingOffsetValueTypeExprHandler.php index 46372f00f86..59ed410e1c9 100644 --- a/src/Analyser/ExprHandler/Virtual/SetExistingOffsetValueTypeExprHandler.php +++ b/src/Analyser/ExprHandler/Virtual/SetExistingOffsetValueTypeExprHandler.php @@ -34,8 +34,15 @@ public function supports(Expr $expr): bool public function processExpr(NodeScopeResolver $nodeScopeResolver, Stmt $stmt, Expr $expr, MutatingScope $scope, ExpressionResultStorage $storage, callable $nodeCallback, ExpressionContext $context): ExpressionResult { // virtual node: callers only read the type, computed lazily by the - // typeCallback. A null specifyTypesCallback falls back to default + // typeCallback. The (synthetic) sub-expressions are processed here - by + // on-demand time their real leaves are already stored, so this reads them + // back; the typeCallback then reads the ExpressionResults instead of + // Scope::getType(). A null specifyTypesCallback falls back to default // narrowing in TypeSpecifier, matching the old specifyDefaultTypes(). + $varResult = $nodeScopeResolver->processExprNode($stmt, $expr->getVar(), $scope, $storage, $nodeCallback, $context); + $dimResult = $nodeScopeResolver->processExprNode($stmt, $expr->getDim(), $scope, $storage, $nodeCallback, $context); + $valueResult = $nodeScopeResolver->processExprNode($stmt, $expr->getValue(), $scope, $storage, $nodeCallback, $context); + return $this->expressionResultFactory->create( $scope, beforeScope: $scope, @@ -44,9 +51,9 @@ public function processExpr(NodeScopeResolver $nodeScopeResolver, Stmt $stmt, Ex isAlwaysTerminating: false, throwPoints: [], impurePoints: [], - typeCallback: static fn (MutatingScope $s): Type => $s->getType($expr->getVar())->setExistingOffsetValueType( - $s->getType($expr->getDim()), - $s->getType($expr->getValue()), + typeCallback: static fn (MutatingScope $s): Type => $varResult->getTypeForScope($s)->setExistingOffsetValueType( + $dimResult->getTypeForScope($s), + $valueResult->getTypeForScope($s), ), ); } diff --git a/src/Analyser/ExprHandler/Virtual/SetOffsetValueTypeExprHandler.php b/src/Analyser/ExprHandler/Virtual/SetOffsetValueTypeExprHandler.php index 7034d884562..1f1ca8e113f 100644 --- a/src/Analyser/ExprHandler/Virtual/SetOffsetValueTypeExprHandler.php +++ b/src/Analyser/ExprHandler/Virtual/SetOffsetValueTypeExprHandler.php @@ -34,8 +34,16 @@ public function supports(Expr $expr): bool public function processExpr(NodeScopeResolver $nodeScopeResolver, Stmt $stmt, Expr $expr, MutatingScope $scope, ExpressionResultStorage $storage, callable $nodeCallback, ExpressionContext $context): ExpressionResult { // virtual node: callers only read the type, computed lazily by the - // typeCallback. A null specifyTypesCallback falls back to default - // narrowing in TypeSpecifier, matching the old specifyDefaultTypes(). + // typeCallback. The (synthetic) sub-expressions are processed here so the + // typeCallback reads their ExpressionResults instead of Scope::getType(). + // A null specifyTypesCallback falls back to default narrowing in + // TypeSpecifier, matching the old specifyDefaultTypes(). + $varResult = $nodeScopeResolver->processExprNode($stmt, $expr->getVar(), $scope, $storage, $nodeCallback, $context); + $dimResult = $expr->getDim() !== null + ? $nodeScopeResolver->processExprNode($stmt, $expr->getDim(), $scope, $storage, $nodeCallback, $context) + : null; + $valueResult = $nodeScopeResolver->processExprNode($stmt, $expr->getValue(), $scope, $storage, $nodeCallback, $context); + return $this->expressionResultFactory->create( $scope, beforeScope: $scope, @@ -44,9 +52,9 @@ public function processExpr(NodeScopeResolver $nodeScopeResolver, Stmt $stmt, Ex isAlwaysTerminating: false, throwPoints: [], impurePoints: [], - typeCallback: static fn (MutatingScope $s): Type => $s->getType($expr->getVar())->setOffsetValueType( - $expr->getDim() !== null ? $s->getType($expr->getDim()) : null, - $s->getType($expr->getValue()), + typeCallback: static fn (MutatingScope $s): Type => $varResult->getTypeForScope($s)->setOffsetValueType( + $dimResult !== null ? $dimResult->getTypeForScope($s) : null, + $valueResult->getTypeForScope($s), ), ); } diff --git a/src/Analyser/ExprHandler/Virtual/UnsetOffsetExprHandler.php b/src/Analyser/ExprHandler/Virtual/UnsetOffsetExprHandler.php index ad884abca1a..c81be133e7b 100644 --- a/src/Analyser/ExprHandler/Virtual/UnsetOffsetExprHandler.php +++ b/src/Analyser/ExprHandler/Virtual/UnsetOffsetExprHandler.php @@ -34,8 +34,14 @@ public function supports(Expr $expr): bool public function processExpr(NodeScopeResolver $nodeScopeResolver, Stmt $stmt, Expr $expr, MutatingScope $scope, ExpressionResultStorage $storage, callable $nodeCallback, ExpressionContext $context): ExpressionResult { // virtual node: callers only read the type, computed lazily by the - // typeCallback. A null specifyTypesCallback falls back to default + // typeCallback. The (synthetic) sub-expressions are processed here - by + // on-demand time their real leaves are already stored, so this reads them + // back; the typeCallback then reads the ExpressionResults instead of + // Scope::getType(). A null specifyTypesCallback falls back to default // narrowing in TypeSpecifier, matching the old specifyDefaultTypes(). + $varResult = $nodeScopeResolver->processExprNode($stmt, $expr->getVar(), $scope, $storage, $nodeCallback, $context); + $dimResult = $nodeScopeResolver->processExprNode($stmt, $expr->getDim(), $scope, $storage, $nodeCallback, $context); + return $this->expressionResultFactory->create( $scope, beforeScope: $scope, @@ -44,7 +50,7 @@ public function processExpr(NodeScopeResolver $nodeScopeResolver, Stmt $stmt, Ex isAlwaysTerminating: false, throwPoints: [], impurePoints: [], - typeCallback: static fn (MutatingScope $s): Type => $s->getType($expr->getVar())->unsetOffset($s->getType($expr->getDim())), + typeCallback: static fn (MutatingScope $s): Type => $varResult->getTypeForScope($s)->unsetOffset($dimResult->getTypeForScope($s)), ); } From 11c97c0efdcf5d4b6305a86be6bb07f414a6939a Mon Sep 17 00:00:00 2001 From: Ondrej Mirtes Date: Thu, 18 Jun 2026 15:51:58 +0200 Subject: [PATCH 089/113] Read child/narrowed types from results instead of Scope::getType in leaf handlers --- src/Analyser/ExprHandler/ArrayHandler.php | 5 ++++- .../ExprHandler/ClassConstFetchHandler.php | 11 ++++++++--- .../ExprHandler/InstanceofHandler.php | 19 +++++++++++++------ src/Analyser/ExprHandler/VariableHandler.php | 10 +++++++--- .../Virtual/FunctionCallableNodeHandler.php | 13 ++++++++++--- .../Virtual/MethodCallableNodeHandler.php | 8 +++++--- 6 files changed, 47 insertions(+), 19 deletions(-) diff --git a/src/Analyser/ExprHandler/ArrayHandler.php b/src/Analyser/ExprHandler/ArrayHandler.php index 35bcd555286..a0550ffad21 100644 --- a/src/Analyser/ExprHandler/ArrayHandler.php +++ b/src/Analyser/ExprHandler/ArrayHandler.php @@ -113,7 +113,10 @@ public function processExpr(NodeScopeResolver $nodeScopeResolver, Stmt $stmt, Ex ); if ( $s->hasExpressionType($isCallableCall)->yes() - && $s->getType($isCallableCall)->isTrue()->yes() + // read the narrowed type from expressionTypes directly (the + // synthetic is_callable() call was never processed as a child), + // mirroring ConstFetchHandler's narrowed-constant lookup + && $s->expressionTypes[$s->getNodeKey($isCallableCall)]->getType()->isTrue()->yes() ) { $type = TypeCombinator::intersect($type, new CallableType()); } diff --git a/src/Analyser/ExprHandler/ClassConstFetchHandler.php b/src/Analyser/ExprHandler/ClassConstFetchHandler.php index b0f2a1d48fe..2ce737b9db7 100644 --- a/src/Analyser/ExprHandler/ClassConstFetchHandler.php +++ b/src/Analyser/ExprHandler/ClassConstFetchHandler.php @@ -17,6 +17,7 @@ use PHPStan\Analyser\TypeSpecifierContext; use PHPStan\DependencyInjection\AutowiredService; use PHPStan\Reflection\InitializerExprTypeResolver; +use PHPStan\ShouldNotHappenException; use PHPStan\Type\MixedType; use PHPStan\Type\Type; use function array_merge; @@ -91,9 +92,13 @@ public function processExpr(NodeScopeResolver $nodeScopeResolver, Stmt $stmt, Ex $scope->isInClass() ? $scope->getClassReflection() : null, // getClassConstFetchTypeByReflection only invokes this for $expr->class // when it is an Expr, which is exactly when $classResult exists - static fn (Expr $e): Type => $classResult !== null && $e === $expr->class - ? $classResult->getTypeForScope($scope) - : $scope->getType($e), + static function (Expr $e) use ($classResult, $scope): Type { + if ($classResult === null) { + throw new ShouldNotHappenException(); + } + + return $classResult->getTypeForScope($scope); + }, ); }, specifyTypesCallback: fn (MutatingScope $s, TypeSpecifierContext $context) => $this->defaultNarrowingHelper->specifyDefaultTypes($expr, $context), diff --git a/src/Analyser/ExprHandler/InstanceofHandler.php b/src/Analyser/ExprHandler/InstanceofHandler.php index aaaa04fbed1..1ada2bd80b7 100644 --- a/src/Analyser/ExprHandler/InstanceofHandler.php +++ b/src/Analyser/ExprHandler/InstanceofHandler.php @@ -17,6 +17,7 @@ use PHPStan\Analyser\SpecifiedTypes; use PHPStan\Analyser\TypeSpecifierContext; use PHPStan\DependencyInjection\AutowiredService; +use PHPStan\ShouldNotHappenException; use PHPStan\Type\BooleanType; use PHPStan\Type\Constant\ConstantBooleanType; use PHPStan\Type\MixedType; @@ -103,9 +104,12 @@ public function processExpr(NodeScopeResolver $nodeScopeResolver, Stmt $stmt, Ex $classType = new ObjectType($className); } } else { - $classNameType = $classResult !== null - ? $classResult->getTypeForScope($s) - : $s->getType($expr->class); + // this branch is only reached when $expr->class is an Expr, + // which is exactly when $classResult was set in processExpr + if ($classResult === null) { + throw new ShouldNotHappenException(); + } + $classNameType = $classResult->getTypeForScope($s); $result = $classNameType->toObjectTypeForInstanceofCheck(); $classType = $result->type; $uncertainty = $result->uncertainty; @@ -149,9 +153,12 @@ public function processExpr(NodeScopeResolver $nodeScopeResolver, Stmt $stmt, Ex return $this->defaultNarrowingHelper->createSubjectTypes($s, $exprNode, $exprResult, $type, $context)->setRootExpr($expr); } - $classNameType = $classResult !== null - ? $classResult->getTypeForScope($s) - : $s->getType($expr->class); + // this branch is only reached when $expr->class is an Expr, + // which is exactly when $classResult was set in processExpr + if ($classResult === null) { + throw new ShouldNotHappenException(); + } + $classNameType = $classResult->getTypeForScope($s); $result = $classNameType->toObjectTypeForInstanceofCheck(); $type = $result->type; $uncertainty = $result->uncertainty; diff --git a/src/Analyser/ExprHandler/VariableHandler.php b/src/Analyser/ExprHandler/VariableHandler.php index 07ab8493bf3..984640d5241 100644 --- a/src/Analyser/ExprHandler/VariableHandler.php +++ b/src/Analyser/ExprHandler/VariableHandler.php @@ -22,6 +22,7 @@ use PHPStan\Analyser\SpecifiedTypes; use PHPStan\Analyser\TypeSpecifierContext; use PHPStan\DependencyInjection\AutowiredService; +use PHPStan\ShouldNotHappenException; use PHPStan\Type\ErrorType; use PHPStan\Type\MixedType; use PHPStan\Type\Type; @@ -69,9 +70,12 @@ public static function createTypeCallback(Variable $expr, ?ExpressionResult $nam return $s->getVariableType($expr->name); } - $nameType = $nameResult !== null - ? $nameResult->getTypeForScope($s) - : $s->getType($expr->name); + // this branch is only reached when $expr->name is an Expr, which is + // exactly when the caller (processExpr) set $nameResult + if ($nameResult === null) { + throw new ShouldNotHappenException(); + } + $nameType = $nameResult->getTypeForScope($s); if (count($nameType->getConstantStrings()) > 0) { $types = []; foreach ($nameType->getConstantStrings() as $constantString) { diff --git a/src/Analyser/ExprHandler/Virtual/FunctionCallableNodeHandler.php b/src/Analyser/ExprHandler/Virtual/FunctionCallableNodeHandler.php index 59da77e1335..80970e939f9 100644 --- a/src/Analyser/ExprHandler/Virtual/FunctionCallableNodeHandler.php +++ b/src/Analyser/ExprHandler/Virtual/FunctionCallableNodeHandler.php @@ -18,6 +18,7 @@ use PHPStan\Node\FunctionCallableNode; use PHPStan\Reflection\InitializerExprContext; use PHPStan\Reflection\InitializerExprTypeResolver; +use PHPStan\ShouldNotHappenException; use PHPStan\Type\ObjectType; use PHPStan\Type\Type; @@ -48,6 +49,7 @@ public function processExpr(NodeScopeResolver $nodeScopeResolver, Stmt $stmt, Ex $impurePoints = []; $hasYield = false; $isAlwaysTerminating = false; + $nameResult = null; if ($expr->getName() instanceof Expr) { $nameResult = $nodeScopeResolver->processExprNode($stmt, $expr->getName(), $scope, $storage, $nodeCallback, ExpressionContext::createDeep()); $scope = $nameResult->getScope(); @@ -65,16 +67,21 @@ public function processExpr(NodeScopeResolver $nodeScopeResolver, Stmt $stmt, Ex isAlwaysTerminating: $isAlwaysTerminating, throwPoints: $throwPoints, impurePoints: $impurePoints, - typeCallback: fn (MutatingScope $scope): Type => $this->resolveType($scope, $expr), + typeCallback: fn (MutatingScope $scope): Type => $this->resolveType($scope, $expr, $nameResult), specifyTypesCallback: fn (MutatingScope $s, TypeSpecifierContext $context) => $this->defaultNarrowingHelper->specifyDefaultTypes($expr, $context), ); } - private function resolveType(MutatingScope $scope, FunctionCallableNode $expr): Type + private function resolveType(MutatingScope $scope, FunctionCallableNode $expr, ?ExpressionResult $nameResult): Type { $originalNode = $expr->getOriginalNode(); if ($originalNode->name instanceof Expr) { - $callableType = $scope->getType($originalNode->name); + // $originalNode->name is the same node as $expr->getName(), processed + // in processExpr exactly in this branch - read its ExpressionResult + if ($nameResult === null) { + throw new ShouldNotHappenException(); + } + $callableType = $nameResult->getTypeForScope($scope); if (!$callableType->isCallable()->yes()) { return new ObjectType(Closure::class); } diff --git a/src/Analyser/ExprHandler/Virtual/MethodCallableNodeHandler.php b/src/Analyser/ExprHandler/Virtual/MethodCallableNodeHandler.php index 175b0e8ebdb..947d299200c 100644 --- a/src/Analyser/ExprHandler/Virtual/MethodCallableNodeHandler.php +++ b/src/Analyser/ExprHandler/Virtual/MethodCallableNodeHandler.php @@ -68,19 +68,21 @@ public function processExpr(NodeScopeResolver $nodeScopeResolver, Stmt $stmt, Ex isAlwaysTerminating: $isAlwaysTerminating, throwPoints: $throwPoints, impurePoints: $impurePoints, - typeCallback: fn (MutatingScope $scope): Type => $this->resolveType($scope, $expr), + typeCallback: fn (MutatingScope $scope): Type => $this->resolveType($scope, $expr, $varResult), specifyTypesCallback: fn (MutatingScope $s, TypeSpecifierContext $context) => $this->defaultNarrowingHelper->specifyDefaultTypes($expr, $context), ); } - private function resolveType(MutatingScope $scope, MethodCallableNode $expr): Type + private function resolveType(MutatingScope $scope, MethodCallableNode $expr, ExpressionResult $varResult): Type { $originalNode = $expr->getOriginalNode(); if (!$originalNode->name instanceof Identifier) { return new ObjectType(Closure::class); } - $varType = $scope->getType($originalNode->var); + // $originalNode->var is the same node as $expr->getVar(), processed in + // processExpr - read its ExpressionResult instead of Scope::getType() + $varType = $varResult->getTypeForScope($scope); $method = $scope->getMethodReflection($varType, $originalNode->name->toString()); if ($method === null) { return new ObjectType(Closure::class); From d2e2cde033694410d39b3363d6a5653b42d5632a Mon Sep 17 00:00:00 2001 From: Ondrej Mirtes Date: Thu, 18 Jun 2026 16:02:46 +0200 Subject: [PATCH 090/113] Process synthetic offsetGet/callable nodes in processExpr, read results in callbacks --- .../ExprHandler/ArrayDimFetchHandler.php | 25 +++++++++++-------- src/Analyser/ExprHandler/PipeHandler.php | 12 ++++----- 2 files changed, 20 insertions(+), 17 deletions(-) diff --git a/src/Analyser/ExprHandler/ArrayDimFetchHandler.php b/src/Analyser/ExprHandler/ArrayDimFetchHandler.php index 9d3461e7343..9ee5588a8e8 100644 --- a/src/Analyser/ExprHandler/ArrayDimFetchHandler.php +++ b/src/Analyser/ExprHandler/ArrayDimFetchHandler.php @@ -77,6 +77,7 @@ public function processExpr(NodeScopeResolver $nodeScopeResolver, Stmt $stmt, Ex $scope = $varResult->getScope(); $varType = $varResult->getTypeForScope($scope); + $offsetGetResult = null; if (!$varType->isArray()->yes() && !(new ObjectType(ArrayAccess::class))->isSuperTypeOf($varType)->no()) { $throwPoints = array_merge($throwPoints, $nodeScopeResolver->processExprNode( $stmt, @@ -86,6 +87,15 @@ public function processExpr(NodeScopeResolver $nodeScopeResolver, Stmt $stmt, Ex new NoopNodeCallback(), $context, )->getThrowPoints()); + // process the offsetGet here (storage is available, so the result is + // captured - not the storage - avoiding a reference cycle) so the + // typeCallback reads its result instead of Scope::getType(). Gated by + // the same maybe-ArrayAccess condition, so plain arrays never reach it. + $offsetGetResult = $nodeScopeResolver->processExprOnDemand( + new MethodCall($expr->var, new Identifier('offsetGet'), [new Arg($expr->dim)]), + $scope, + $storage, + ); } return $this->expressionResultFactory->create( @@ -98,25 +108,18 @@ public function processExpr(NodeScopeResolver $nodeScopeResolver, Stmt $stmt, Ex impurePoints: $impurePoints, containsNullsafe: $varResult->containsNullsafe(), issetabilityDescriptor: IssetabilityDescriptor::offset($varResult, $dimResult), - typeCallback: function (MutatingScope $s) use ($expr, $varResult, $dimResult): Type { + typeCallback: static function (MutatingScope $s) use ($varResult, $dimResult, $offsetGetResult): Type { $offsetAccessibleType = $varResult->getTypeForScope($s); $shortCircuit = static fn (Type $type): Type => $varResult->containsNullsafe() && TypeCombinator::containsNull($offsetAccessibleType) ? TypeCombinator::addNull($type) : $type; if ( - !$offsetAccessibleType->isArray()->yes() + $offsetGetResult !== null + && !$offsetAccessibleType->isArray()->yes() && (new ObjectType(ArrayAccess::class))->isSuperTypeOf($offsetAccessibleType)->yes() ) { - return $shortCircuit($s->getType( - new MethodCall( - $expr->var, - new Identifier('offsetGet'), - [ - new Arg($expr->dim), - ], - ), - )); + return $shortCircuit($offsetGetResult->getTypeForScope($s)); } return $shortCircuit($offsetAccessibleType->getOffsetValueType($dimResult->getTypeForScope($s))); diff --git a/src/Analyser/ExprHandler/PipeHandler.php b/src/Analyser/ExprHandler/PipeHandler.php index fc55dd0b67b..54073bc043b 100644 --- a/src/Analyser/ExprHandler/PipeHandler.php +++ b/src/Analyser/ExprHandler/PipeHandler.php @@ -76,11 +76,11 @@ public function processExpr(NodeScopeResolver $nodeScopeResolver, Stmt $stmt, Ex } if ($firstClassCallableNode !== null) { - // the original first-class callable node is not processed through - // processExprNode - store its result so that node callbacks asking - // about its type can be resumed. Its closure type lives on the - // matching *CallableNode, resolved on demand by its handler. - $callableNode = $firstClassCallableNode; + // store a result for $expr->right so node callbacks asking about its + // type can be resumed. Its closure type lives on the matching + // *CallableNode, processed here (storage is available, so the result - + // not the storage - is captured) and read back in the typeCallback. + $callableNodeResult = $nodeScopeResolver->processExprOnDemand($firstClassCallableNode, $scope, $storage); $nodeScopeResolver->storeExpressionResult($storage, $expr->right, $this->expressionResultFactory->create( $scope, beforeScope: $scope, @@ -89,7 +89,7 @@ public function processExpr(NodeScopeResolver $nodeScopeResolver, Stmt $stmt, Ex isAlwaysTerminating: false, throwPoints: [], impurePoints: [], - typeCallback: static fn (MutatingScope $s): Type => $s->getType($callableNode), + typeCallback: static fn (MutatingScope $s): Type => $callableNodeResult->getTypeForScope($s), )); } From b043f8494016a7fc9b109bc69274b2cf75eb913a Mon Sep 17 00:00:00 2001 From: Ondrej Mirtes Date: Thu, 18 Jun 2026 16:22:37 +0200 Subject: [PATCH 091/113] Add readStoredOrPriceOnDemand/priceSyntheticOnDemand and use them instead of Scope::getType in property/nullsafe/boolean/assignop handlers --- src/Analyser/ExprHandler/AssignOpHandler.php | 18 +++++++--- src/Analyser/ExprHandler/BooleanOrHandler.php | 14 ++++---- .../ExprHandler/NullsafeMethodCallHandler.php | 16 ++++++--- .../NullsafePropertyFetchHandler.php | 12 ++++--- .../ExprHandler/PropertyFetchHandler.php | 21 ++++++++---- .../StaticPropertyFetchHandler.php | 23 +++++++++---- src/Analyser/MutatingScope.php | 16 +++++++++ src/Analyser/NodeScopeResolver.php | 34 +++++++++++++++++++ 8 files changed, 121 insertions(+), 33 deletions(-) diff --git a/src/Analyser/ExprHandler/AssignOpHandler.php b/src/Analyser/ExprHandler/AssignOpHandler.php index cfefb0d0fa7..01c7b33adf3 100644 --- a/src/Analyser/ExprHandler/AssignOpHandler.php +++ b/src/Analyser/ExprHandler/AssignOpHandler.php @@ -57,11 +57,19 @@ public function processExpr(NodeScopeResolver $nodeScopeResolver, Stmt $stmt, Ex { $beforeScope = $scope; - $typeCallback = function (MutatingScope $s) use ($expr): Type { - $getType = static fn (Expr $e): Type => $s->getType($e); + $typeCallback = function (MutatingScope $s) use ($expr, $nodeScopeResolver): Type { + // $expr->var and $expr->expr were processed during this handler's + // processExpr (the var as the assignment target, the value expr by the + // inner closure below), so their ExpressionResults are stored - read + // them instead of re-walking via Scope::getType(). + $getType = static fn (Expr $e): Type => $nodeScopeResolver->readStoredOrPriceOnDemand($e, $s); if ($expr instanceof Expr\AssignOp\Coalesce) { - return $s->getType(new BinaryOp\Coalesce($expr->var, $expr->expr, $expr->getAttributes())); + // the coalesce is synthetic - price it on demand against the + // current storage, mirroring resolveTypeOfNewWorldHandlerNode(). + $coalesce = new BinaryOp\Coalesce($expr->var, $expr->expr, $expr->getAttributes()); + + return $nodeScopeResolver->priceSyntheticOnDemand($coalesce, $s); } if ($expr instanceof Expr\AssignOp\Concat) { @@ -150,7 +158,7 @@ function (MutatingScope $scope) use ($stmt, $expr, $nodeCallback, $context, $sto $exprResult = $nodeScopeResolver->processExprNode($stmt, $expr->expr, $scope, $storage, $nodeCallback, $context->enterDeep()); if ($expr instanceof Expr\AssignOp\Coalesce) { - $isAlwaysTerminating = $exprResult->isAlwaysTerminating() && $originalScope->getType($expr->var)->isNull()->yes(); + $isAlwaysTerminating = $exprResult->isAlwaysTerminating() && $nodeScopeResolver->readStoredOrPriceOnDemand($expr->var, $originalScope)->isNull()->yes(); return $this->expressionResultFactory->create( $exprResult->getScope()->mergeWith($originalScope), $originalScope, @@ -171,7 +179,7 @@ function (MutatingScope $scope) use ($stmt, $expr, $nodeCallback, $context, $sto $impurePoints = $assignResult->getImpurePoints(); if ( ($expr instanceof Expr\AssignOp\Div || $expr instanceof Expr\AssignOp\Mod) && - !$scope->getType($expr->expr)->toNumber()->isSuperTypeOf(new ConstantIntegerType(0))->no() + !$nodeScopeResolver->readStoredOrPriceOnDemand($expr->expr, $scope)->toNumber()->isSuperTypeOf(new ConstantIntegerType(0))->no() ) { $throwPoints[] = InternalThrowPoint::createExplicit($scope, new ObjectType(DivisionByZeroError::class), $expr, false); } diff --git a/src/Analyser/ExprHandler/BooleanOrHandler.php b/src/Analyser/ExprHandler/BooleanOrHandler.php index 6e9857ce6ba..f6745ae6c60 100644 --- a/src/Analyser/ExprHandler/BooleanOrHandler.php +++ b/src/Analyser/ExprHandler/BooleanOrHandler.php @@ -65,7 +65,7 @@ public function supports(Expr $expr): bool * skipped: in the OR-truthy scope the arm that didn't narrow could still be * the truthy one, so the sound result is the original (unnarrowed) type. */ - private function augmentBooleanOrTruthyWithConditionalHolders(MutatingScope $scope, MutatingScope $rightScope, BooleanOr|LogicalOr $expr, SpecifiedTypes $types): SpecifiedTypes + private function augmentBooleanOrTruthyWithConditionalHolders(NodeScopeResolver $nodeScopeResolver, MutatingScope $scope, MutatingScope $rightScope, BooleanOr|LogicalOr $expr, SpecifiedTypes $types): SpecifiedTypes { $leftTruthyScope = $scope->filterByTruthyValue($expr->left); $rightTruthyScope = $rightScope->filterByTruthyValue($expr->right); @@ -97,9 +97,9 @@ private function augmentBooleanOrTruthyWithConditionalHolders(MutatingScope $sco continue; } - $origType = $scope->getType($targetExpr); - $leftType = $leftTruthyScope->getType($targetExpr); - $rightType = $rightTruthyScope->getType($targetExpr); + $origType = $nodeScopeResolver->readStoredOrPriceOnDemand($targetExpr, $scope); + $leftType = $nodeScopeResolver->readStoredOrPriceOnDemand($targetExpr, $leftTruthyScope); + $rightType = $nodeScopeResolver->readStoredOrPriceOnDemand($targetExpr, $rightTruthyScope); $leftNarrowed = !$leftType->equals($origType) && $origType->isSuperTypeOf($leftType)->yes(); $rightNarrowed = !$rightType->equals($origType) && $origType->isSuperTypeOf($rightType)->yes(); @@ -127,7 +127,7 @@ public function processExpr(NodeScopeResolver $nodeScopeResolver, Stmt $stmt, Ex $leftResult = $nodeScopeResolver->processExprNode($stmt, $expr->left, $scope, $storage, $nodeCallback, $context->enterDeep()); $leftFalseyScope = $leftResult->getFalseyScope(); $rightResult = $nodeScopeResolver->processExprNode($stmt, $expr->right, $leftFalseyScope, $storage, $nodeCallback, $context); - $rightExprType = $rightResult->getScope()->getType($expr->right); + $rightExprType = $rightResult->getTypeForScope($rightResult->getScope()); if ($rightExprType instanceof NeverType && $rightExprType->isExplicit()) { $leftMergedWithRightScope = $leftResult->getTruthyScope(); } else { @@ -170,7 +170,7 @@ public function processExpr(NodeScopeResolver $nodeScopeResolver, Stmt $stmt, Ex return new BooleanType(); }, - specifyTypesCallback: function (MutatingScope $s, TypeSpecifierContext $context) use ($expr, $leftResult, $rightResult): SpecifiedTypes { + specifyTypesCallback: function (MutatingScope $s, TypeSpecifierContext $context) use ($expr, $leftResult, $rightResult, $nodeScopeResolver): SpecifiedTypes { $leftTypes = $this->defaultNarrowingHelper->getChildSpecifiedTypes($s, $expr->left, $leftResult, $context)->setRootExpr($expr); $rightScope = $s->filterByFalseyValue($expr->left); $rightTypes = $this->defaultNarrowingHelper->getChildSpecifiedTypes($rightScope, $expr->right, $rightResult, $context)->setRootExpr($expr); @@ -189,7 +189,7 @@ public function processExpr(NodeScopeResolver $nodeScopeResolver, Stmt $stmt, Ex $leftNormalized = $leftTypes->normalize($s); $rightNormalized = $rightTypes->normalize($rightScope); $types = $leftNormalized->intersectWith($rightNormalized); - $types = $this->augmentBooleanOrTruthyWithConditionalHolders($s, $rightScope, $expr, $types); + $types = $this->augmentBooleanOrTruthyWithConditionalHolders($nodeScopeResolver, $s, $rightScope, $expr, $types); $types = $this->conditionalExpressionHolderHelper->augmentDisjunctionTypes($s, $rightScope, $leftNormalized, $rightNormalized, $expr->left, $expr->right, true, $types); } } else { diff --git a/src/Analyser/ExprHandler/NullsafeMethodCallHandler.php b/src/Analyser/ExprHandler/NullsafeMethodCallHandler.php index fd3d7c61c67..360200ecf02 100644 --- a/src/Analyser/ExprHandler/NullsafeMethodCallHandler.php +++ b/src/Analyser/ExprHandler/NullsafeMethodCallHandler.php @@ -54,7 +54,6 @@ public function processExpr(NodeScopeResolver $nodeScopeResolver, Stmt $stmt, Ex { $beforeScope = $scope; $scopeBeforeNullsafe = $scope; - $varType = $scope->getType($expr->var); $nonNullabilityResult = $this->nonNullabilityHelper->ensureShallowNonNullability($scope, $scope, $expr->var); $attributes = array_merge($expr->getAttributes(), ['virtualNullsafeMethodCall' => true]); @@ -75,6 +74,10 @@ public function processExpr(NodeScopeResolver $nodeScopeResolver, Stmt $stmt, Ex ); $scope = $this->nonNullabilityHelper->revertNonNullability($exprResult->getScope(), $nonNullabilityResult->getSpecifiedExpressions()); + // the var was processed above as the receiver of $methodCall; read its + // stored result on the original scope instead of re-walking via getType(). + $varType = $nodeScopeResolver->readStoredOrPriceOnDemand($expr->var, $scopeBeforeNullsafe); + $varIsNull = $varType->isNull(); if ($varIsNull->yes()) { // Arguments are never evaluated when the var is always null. @@ -94,8 +97,9 @@ public function processExpr(NodeScopeResolver $nodeScopeResolver, Stmt $stmt, Ex throwPoints: $exprResult->getThrowPoints(), impurePoints: $exprResult->getImpurePoints(), containsNullsafe: true, - typeCallback: static function (MutatingScope $s) use ($expr, $exprResult): Type { - $varType = $s->getType($expr->var); + typeCallback: static function (MutatingScope $s) use ($expr, $exprResult, $nodeScopeResolver): Type { + // the var was processed above as the receiver of $methodCall. + $varType = $nodeScopeResolver->readStoredOrPriceOnDemand($expr->var, $s); if ($varType->isNull()->yes()) { return new NullType(); } @@ -103,9 +107,11 @@ public function processExpr(NodeScopeResolver $nodeScopeResolver, Stmt $stmt, Ex return $exprResult->getTypeForScope($s); } + // the plain method call on the null-removed scope is synthetic. + $truthyScope = $s->filterByTruthyValue(new NotIdentical($expr->var, new ConstFetch(new Name('null')))); + return TypeCombinator::union( - $s->filterByTruthyValue(new NotIdentical($expr->var, new ConstFetch(new Name('null')))) - ->getType(new MethodCall($expr->var, $expr->name, $expr->args)), + $nodeScopeResolver->priceSyntheticOnDemand(new MethodCall($expr->var, $expr->name, $expr->args), $truthyScope), new NullType(), ); }, diff --git a/src/Analyser/ExprHandler/NullsafePropertyFetchHandler.php b/src/Analyser/ExprHandler/NullsafePropertyFetchHandler.php index d2efe1ea4f3..5085c6c9637 100644 --- a/src/Analyser/ExprHandler/NullsafePropertyFetchHandler.php +++ b/src/Analyser/ExprHandler/NullsafePropertyFetchHandler.php @@ -73,8 +73,10 @@ public function processExpr(NodeScopeResolver $nodeScopeResolver, Stmt $stmt, Ex throwPoints: $exprResult->getThrowPoints(), impurePoints: $exprResult->getImpurePoints(), containsNullsafe: true, - typeCallback: static function (MutatingScope $s) use ($expr, $exprResult): Type { - $varType = $s->getType($expr->var); + typeCallback: static function (MutatingScope $s) use ($expr, $exprResult, $nodeScopeResolver): Type { + // the var was processed above as the receiver of $propertyFetch - + // read its stored result instead of re-walking via Scope::getType(). + $varType = $nodeScopeResolver->readStoredOrPriceOnDemand($expr->var, $s); if ($varType->isNull()->yes()) { return new NullType(); } @@ -82,9 +84,11 @@ public function processExpr(NodeScopeResolver $nodeScopeResolver, Stmt $stmt, Ex return $exprResult->getTypeForScope($s); } + // the plain property fetch on the null-removed scope is synthetic. + $truthyScope = $s->filterByTruthyValue(new NotIdentical($expr->var, new ConstFetch(new Name('null')))); + return TypeCombinator::union( - $s->filterByTruthyValue(new NotIdentical($expr->var, new ConstFetch(new Name('null')))) - ->getType(new PropertyFetch($expr->var, $expr->name)), + $nodeScopeResolver->priceSyntheticOnDemand(new PropertyFetch($expr->var, $expr->name), $truthyScope), new NullType(), ); }, diff --git a/src/Analyser/ExprHandler/PropertyFetchHandler.php b/src/Analyser/ExprHandler/PropertyFetchHandler.php index fd1e43b5d09..bf7069449c0 100644 --- a/src/Analyser/ExprHandler/PropertyFetchHandler.php +++ b/src/Analyser/ExprHandler/PropertyFetchHandler.php @@ -96,7 +96,7 @@ public function processExpr(NodeScopeResolver $nodeScopeResolver, Stmt $stmt, Ex impurePoints: $impurePoints, containsNullsafe: $varResult->containsNullsafe(), issetabilityDescriptor: IssetabilityDescriptor::property($varResult, fn (MutatingScope $s): ?FoundPropertyReflection => $this->propertyReflectionFinder->findPropertyReflectionFromNode($expr, $s), $expr), - typeCallback: function (MutatingScope $s) use ($expr, $varResult, $nameResult): Type { + typeCallback: function (MutatingScope $s) use ($expr, $varResult, $nameResult, $nodeScopeResolver): Type { // a fetch on a nullsafe chain whose receiver is currently nullable // short-circuits to null - the receiver result carries whether the // chain contains a ?-> (a plain nullable receiver does not propagate) @@ -131,14 +131,23 @@ public function processExpr(NodeScopeResolver $nodeScopeResolver, Stmt $stmt, Ex return $shortCircuit($returnType); } - $nameType = $nameResult !== null ? $nameResult->getTypeForScope($s) : $s->getType($expr->name); + $nameType = $nameResult !== null ? $nameResult->getTypeForScope($s) : $nodeScopeResolver->readStoredOrPriceOnDemand($expr->name, $s); if (count($nameType->getConstantStrings()) > 0) { return TypeCombinator::union( - ...array_map(static fn ($constantString) => $constantString->getValue() === '' ? new ErrorType() : $s - ->filterByTruthyValue(new Expr\BinaryOp\Identical($expr->name, new String_($constantString->getValue()))) - ->getType( + ...array_map(static function ($constantString) use ($expr, $s, $nodeScopeResolver): Type { + if ($constantString->getValue() === '') { + return new ErrorType(); + } + + // a property fetch with a concrete name on the + // name-pinned scope is synthetic. + $truthyScope = $s->filterByTruthyValue(new Expr\BinaryOp\Identical($expr->name, new String_($constantString->getValue()))); + + return $nodeScopeResolver->priceSyntheticOnDemand( new PropertyFetch($expr->var, new Identifier($constantString->getValue())), - ), $nameType->getConstantStrings()), + $truthyScope, + ); + }, $nameType->getConstantStrings()), ); } diff --git a/src/Analyser/ExprHandler/StaticPropertyFetchHandler.php b/src/Analyser/ExprHandler/StaticPropertyFetchHandler.php index 607ad2df610..8454caea4be 100644 --- a/src/Analyser/ExprHandler/StaticPropertyFetchHandler.php +++ b/src/Analyser/ExprHandler/StaticPropertyFetchHandler.php @@ -96,7 +96,7 @@ public function processExpr(NodeScopeResolver $nodeScopeResolver, Stmt $stmt, Ex impurePoints: $impurePoints, containsNullsafe: $classResult !== null && $classResult->containsNullsafe(), issetabilityDescriptor: IssetabilityDescriptor::property($classResult, fn (MutatingScope $s): ?FoundPropertyReflection => $this->propertyReflectionFinder->findPropertyReflectionFromNode($expr, $s), $expr), - typeCallback: function (MutatingScope $s) use ($expr, $classResult, $nameResult): Type { + typeCallback: function (MutatingScope $s) use ($expr, $classResult, $nameResult, $nodeScopeResolver): Type { $shortCircuit = static fn (Type $type): Type => $classResult !== null && $classResult->containsNullsafe() && TypeCombinator::containsNull($classResult->getTypeForScope($s)) ? TypeCombinator::addNull($type) : $type; @@ -117,7 +117,7 @@ public function processExpr(NodeScopeResolver $nodeScopeResolver, Stmt $stmt, Ex if ($expr->class instanceof Name) { $staticPropertyFetchedOnType = $s->resolveTypeByName($expr->class); } else { - $classType = $classResult !== null ? $classResult->getTypeForScope($s) : $s->getType($expr->class); + $classType = $classResult !== null ? $classResult->getTypeForScope($s) : $nodeScopeResolver->readStoredOrPriceOnDemand($expr->class, $s); $staticPropertyFetchedOnType = TypeCombinator::removeNull($classType)->getObjectTypeOrClassStringObjectType(); } @@ -134,12 +134,23 @@ public function processExpr(NodeScopeResolver $nodeScopeResolver, Stmt $stmt, Ex return $shortCircuit($fetchType); } - $nameType = $nameResult !== null ? $nameResult->getTypeForScope($s) : $s->getType($expr->name); + $nameType = $nameResult !== null ? $nameResult->getTypeForScope($s) : $nodeScopeResolver->readStoredOrPriceOnDemand($expr->name, $s); if (count($nameType->getConstantStrings()) > 0) { return TypeCombinator::union( - ...array_map(static fn ($constantString) => $constantString->getValue() === '' ? new ErrorType() : $s - ->filterByTruthyValue(new Identical($expr->name, new String_($constantString->getValue()))) - ->getType(new Expr\StaticPropertyFetch($expr->class, new VarLikeIdentifier($constantString->getValue()))), $nameType->getConstantStrings()), + ...array_map(static function ($constantString) use ($expr, $s, $nodeScopeResolver): Type { + if ($constantString->getValue() === '') { + return new ErrorType(); + } + + // a static property fetch with a concrete name on the + // name-pinned scope is synthetic. + $truthyScope = $s->filterByTruthyValue(new Identical($expr->name, new String_($constantString->getValue()))); + + return $nodeScopeResolver->priceSyntheticOnDemand( + new Expr\StaticPropertyFetch($expr->class, new VarLikeIdentifier($constantString->getValue())), + $truthyScope, + ); + }, $nameType->getConstantStrings()), ); } diff --git a/src/Analyser/MutatingScope.php b/src/Analyser/MutatingScope.php index 412df0116c3..29c93ee2ed0 100644 --- a/src/Analyser/MutatingScope.php +++ b/src/Analyser/MutatingScope.php @@ -1159,6 +1159,22 @@ public function popExpressionResultStorage(): void $this->expressionResultStorageStack->pop(); } + /** + * The ExpressionResultStorage of the analysis currently in progress, the one + * resolveTypeOfNewWorldHandlerNode() prices synthetic nodes against. A handler + * pricing a synthetic node from a lazily-invoked typeCallback must use this + * (not a storage captured at processExpr() time): a later re-evaluation + * (e.g. findEarlyTerminatingExpr()) runs under a different current storage, + * and the captured one would resolve the synthetic node's real subnodes from + * stale stored results. + * + * @internal + */ + public function getCurrentExpressionResultStorage(): ?ExpressionResultStorage + { + return $this->expressionResultStorageStack->getCurrent(); + } + /** * The isset/empty/?? chain descriptor PHPStan\Rules\IssetCheck folds. Reads * it from the current expression-result storage; when the rule asks before diff --git a/src/Analyser/NodeScopeResolver.php b/src/Analyser/NodeScopeResolver.php index 74d69ec528e..25114e5e74b 100644 --- a/src/Analyser/NodeScopeResolver.php +++ b/src/Analyser/NodeScopeResolver.php @@ -2878,6 +2878,40 @@ public function processExprOnDemand(Expr $expr, MutatingScope $scope, Expression } } + /** + * Reads the type, on the given scope, of a node an ExprHandler already + * processed (its ExpressionResult is in the storage of the analysis in + * progress). Used from lazily-invoked typeCallbacks instead of + * Scope::getType(): it reads the stored result rather than re-walking, and + * does not allocate a throwaway duplicate storage. Falls back to pricing the + * node as synthetic when it is not stored (e.g. a re-evaluation reached this + * before the original processing did). + */ + public function readStoredOrPriceOnDemand(Expr $expr, MutatingScope $scope): Type + { + $current = $scope->getCurrentExpressionResultStorage(); + $result = $current?->findExpressionResult($expr); + if ($result !== null) { + return $result->getTypeForScope($scope); + } + + return $this->priceSyntheticOnDemand($expr, $scope); + } + + /** + * Prices a synthetic node (one an ExprHandler built itself) on a duplicate of + * the storage of the analysis currently in progress, mirroring + * MutatingScope::resolveTypeOfNewWorldHandlerNode(): the duplicate isolates + * the synthetic node's own stored result from the live storage while its real + * subnodes still resolve from the fallback. + */ + public function priceSyntheticOnDemand(Expr $expr, MutatingScope $scope): Type + { + $current = $scope->getCurrentExpressionResultStorage() ?? new ExpressionResultStorage(); + + return $this->processExprOnDemand($expr, $scope, $current->duplicate())->getTypeForScope($scope); + } + /** * @param callable(Node $node, Scope $scope): void $nodeCallback */ From 414974e8fcc5bdf61ab10f324ab7a137d2f985ba Mon Sep 17 00:00:00 2001 From: Ondrej Mirtes Date: Thu, 18 Jun 2026 16:26:01 +0200 Subject: [PATCH 092/113] Price synthetic unary-minus operand on demand instead of Scope::getType --- src/Analyser/ExprHandler/UnaryMinusHandler.php | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/Analyser/ExprHandler/UnaryMinusHandler.php b/src/Analyser/ExprHandler/UnaryMinusHandler.php index b2826e29cb4..ff6dd498e1a 100644 --- a/src/Analyser/ExprHandler/UnaryMinusHandler.php +++ b/src/Analyser/ExprHandler/UnaryMinusHandler.php @@ -50,14 +50,14 @@ public function processExpr(NodeScopeResolver $nodeScopeResolver, Stmt $stmt, Ex isAlwaysTerminating: $exprResult->isAlwaysTerminating(), throwPoints: $exprResult->getThrowPoints(), impurePoints: $exprResult->getImpurePoints(), - typeCallback: fn (MutatingScope $scope) => $this->initializerExprTypeResolver->getUnaryMinusType($expr->expr, static function (Expr $e) use ($scope, $expr, $exprResult): Type { + typeCallback: fn (MutatingScope $scope) => $this->initializerExprTypeResolver->getUnaryMinusType($expr->expr, static function (Expr $e) use ($scope, $expr, $exprResult, $nodeScopeResolver): Type { if ($e === $expr->expr) { return $exprResult->getTypeForScope($scope); } // a synthetic node ($expr->expr * -1, derived for an IntegerRangeType - // operand) - not a child result, resolved on demand - return $scope->getType($e); + // operand) created inside getUnaryMinusType - priced on demand + return $nodeScopeResolver->priceSyntheticOnDemand($e, $scope); }), specifyTypesCallback: fn (MutatingScope $s, TypeSpecifierContext $context) => $this->defaultNarrowingHelper->specifyDefaultTypes($expr, $context), ); From 66c8df299e16bdc9bd58486121dcba14cfc4d99f Mon Sep 17 00:00:00 2001 From: Ondrej Mirtes Date: Thu, 18 Jun 2026 16:49:01 +0200 Subject: [PATCH 093/113] Read child/synthetic types via result or helpers in Match/BinaryOp/Equality instead of Scope::getType --- src/Analyser/ExprHandler/AssignHandler.php | 5 +- src/Analyser/ExprHandler/BinaryOpHandler.php | 58 +++++++++------- .../Helper/EqualityTypeSpecifyingHelper.php | 68 +++++++++++-------- src/Analyser/ExprHandler/MatchHandler.php | 40 +++++++---- 4 files changed, 107 insertions(+), 64 deletions(-) diff --git a/src/Analyser/ExprHandler/AssignHandler.php b/src/Analyser/ExprHandler/AssignHandler.php index 04412f8eaae..359308ce2d9 100644 --- a/src/Analyser/ExprHandler/AssignHandler.php +++ b/src/Analyser/ExprHandler/AssignHandler.php @@ -493,7 +493,7 @@ public function processAssignVar( if ($assignedExpr instanceof Match_) { $conditionalExpressions = $this->mergeConditionalExpressions( $conditionalExpressions, - $this->processMatchForConditionalExpressionsAfterAssign($scopeBeforeAssignEval, $var->name, $assignedExpr), + $this->processMatchForConditionalExpressionsAfterAssign($nodeScopeResolver, $scopeBeforeAssignEval, $var->name, $assignedExpr), ); } @@ -1262,12 +1262,13 @@ private function mergeConditionalExpressions(array $conditionalExpressions, arra * @return array */ private function processMatchForConditionalExpressionsAfterAssign( + NodeScopeResolver $nodeScopeResolver, MutatingScope $scope, string $variableName, Match_ $expr, ): array { - $armScopesAndTypes = $this->matchHandler->getArmScopesAndTypes($scope, $expr); + $armScopesAndTypes = $this->matchHandler->getArmScopesAndTypes($nodeScopeResolver, $scope, $expr); if (count($armScopesAndTypes) < 2) { return []; } diff --git a/src/Analyser/ExprHandler/BinaryOpHandler.php b/src/Analyser/ExprHandler/BinaryOpHandler.php index 74046cba8ef..6130d65193f 100644 --- a/src/Analyser/ExprHandler/BinaryOpHandler.php +++ b/src/Analyser/ExprHandler/BinaryOpHandler.php @@ -94,7 +94,9 @@ public function processExpr(NodeScopeResolver $nodeScopeResolver, Stmt $stmt, Ex $impurePoints = array_merge($leftResult->getImpurePoints(), $rightResult->getImpurePoints()); if ( ($expr instanceof BinaryOp\Div || $expr instanceof BinaryOp\Mod) && - !$leftResult->getScope()->getType($expr->right)->toNumber()->isSuperTypeOf(new ConstantIntegerType(0))->no() + // the right operand was just processed on $leftResult's scope; read its + // result instead of re-walking via Scope::getType(). + !$rightResult->getTypeForScope($leftResult->getScope())->toNumber()->isSuperTypeOf(new ConstantIntegerType(0))->no() ) { $throwPoints[] = InternalThrowPoint::createExplicit($leftResult->getScope(), new ObjectType(DivisionByZeroError::class), $expr, false); } @@ -114,34 +116,38 @@ public function processExpr(NodeScopeResolver $nodeScopeResolver, Stmt $stmt, Ex isAlwaysTerminating: $leftResult->isAlwaysTerminating() || $rightResult->isAlwaysTerminating(), throwPoints: $throwPoints, impurePoints: $impurePoints, - typeCallback: function (MutatingScope $scope) use ($expr): Type { - $getType = static fn (Expr $expr): Type => $scope->getType($expr); + typeCallback: function (MutatingScope $scope) use ($expr, $nodeScopeResolver): Type { + // the operands were processed during processExpr; read their stored + // results instead of re-walking via Scope::getType(). Synthetic + // nodes the resolver builds (e.g. getDivType's Mod) are priced on + // demand by the same helper. + $getType = static fn (Expr $e): Type => $nodeScopeResolver->readStoredOrPriceOnDemand($e, $scope); if ($expr instanceof BinaryOp\Smaller) { - return $scope->getType($expr->left)->isSmallerThan($scope->getType($expr->right), $this->phpVersion)->toBooleanType(); + return $getType($expr->left)->isSmallerThan($getType($expr->right), $this->phpVersion)->toBooleanType(); } if ($expr instanceof BinaryOp\SmallerOrEqual) { - return $scope->getType($expr->left)->isSmallerThanOrEqual($scope->getType($expr->right), $this->phpVersion)->toBooleanType(); + return $getType($expr->left)->isSmallerThanOrEqual($getType($expr->right), $this->phpVersion)->toBooleanType(); } if ($expr instanceof BinaryOp\Greater) { - return $scope->getType($expr->right)->isSmallerThan($scope->getType($expr->left), $this->phpVersion)->toBooleanType(); + return $getType($expr->right)->isSmallerThan($getType($expr->left), $this->phpVersion)->toBooleanType(); } if ($expr instanceof BinaryOp\GreaterOrEqual) { - return $scope->getType($expr->right)->isSmallerThanOrEqual($scope->getType($expr->left), $this->phpVersion)->toBooleanType(); + return $getType($expr->right)->isSmallerThanOrEqual($getType($expr->left), $this->phpVersion)->toBooleanType(); } if ($expr instanceof BinaryOp\Equal) { - return $this->resolveEqualType($scope, $expr); + return $this->resolveEqualType($nodeScopeResolver, $scope, $expr); } if ($expr instanceof BinaryOp\NotEqual) { // negation of the Equal result - direct computation avoids // synthesizing a BooleanNot node (which would route through // on-demand re-processing once BooleanNot is migrated) - $equalType = $this->resolveEqualType($scope, new BinaryOp\Equal($expr->left, $expr->right))->toBoolean(); + $equalType = $this->resolveEqualType($nodeScopeResolver, $scope, new BinaryOp\Equal($expr->left, $expr->right))->toBoolean(); if ($equalType->isTrue()->yes()) { return new ConstantBooleanType(false); } @@ -161,8 +167,8 @@ public function processExpr(NodeScopeResolver $nodeScopeResolver, Stmt $stmt, Ex } if ($expr instanceof BinaryOp\LogicalXor) { - $leftBooleanType = $scope->getType($expr->left)->toBoolean(); - $rightBooleanType = $scope->getType($expr->right)->toBoolean(); + $leftBooleanType = $getType($expr->left)->toBoolean(); + $rightBooleanType = $getType($expr->right)->toBoolean(); if ( $leftBooleanType instanceof ConstantBooleanType @@ -230,9 +236,9 @@ public function processExpr(NodeScopeResolver $nodeScopeResolver, Stmt $stmt, Ex throw new ShouldNotHappenException(sprintf('Unhandled %s', get_class($expr))); }, - specifyTypesCallback: function (MutatingScope $scope, TypeSpecifierContext $context) use ($expr): SpecifiedTypes { + specifyTypesCallback: function (MutatingScope $scope, TypeSpecifierContext $context) use ($expr, $nodeScopeResolver): SpecifiedTypes { if ($expr instanceof BinaryOp\Identical) { - return $this->equalityTypeSpecifyingHelper->specifyTypesForIdentical($expr, $scope, $context); + return $this->equalityTypeSpecifyingHelper->specifyTypesForIdentical($nodeScopeResolver, $expr, $scope, $context); } if ($expr instanceof BinaryOp\NotIdentical) { @@ -252,7 +258,7 @@ public function processExpr(NodeScopeResolver $nodeScopeResolver, Stmt $stmt, Ex } if ($expr instanceof BinaryOp\Equal) { - return $this->equalityTypeSpecifyingHelper->specifyTypesForEqual($expr, $scope, $context); + return $this->equalityTypeSpecifyingHelper->specifyTypesForEqual($nodeScopeResolver, $expr, $scope, $context); } if ($expr instanceof BinaryOp\NotEqual) { @@ -302,7 +308,11 @@ public function processExpr(NodeScopeResolver $nodeScopeResolver, Stmt $stmt, Ex $orEqual = $expr instanceof BinaryOp\SmallerOrEqual; $offset = $orEqual ? 0 : 1; - $leftType = $scope->getType($expr->left); + // the operands and their subexpressions were processed during + // processExpr; read their stored results instead of re-walking + // via Scope::getType(). + $getType = static fn (Expr $e): Type => $nodeScopeResolver->readStoredOrPriceOnDemand($e, $scope); + $leftType = $getType($expr->left); $result = (new SpecifiedTypes([], []))->setRootExpr($expr); if ( @@ -314,7 +324,7 @@ public function processExpr(NodeScopeResolver $nodeScopeResolver, Stmt $stmt, Ex && count($expr->right->getArgs()) >= 1 && $leftType->isInteger()->yes() ) { - $argType = $scope->getType($expr->right->getArgs()[0]->value); + $argType = $getType($expr->right->getArgs()[0]->value); $sizeType = null; if ($leftType instanceof ConstantIntegerType) { @@ -421,8 +431,8 @@ public function processExpr(NodeScopeResolver $nodeScopeResolver, Stmt $stmt, Ex && $leftType->isInteger()->yes() && IntegerRangeType::fromInterval(0, null)->isSuperTypeOf($leftType)->yes() ) { - $countArgType = $scope->getType($expr->right->left->getArgs()[0]->value); - $subtractedType = $scope->getType($expr->right->right); + $countArgType = $getType($expr->right->left->getArgs()[0]->value); + $subtractedType = $getType($expr->right->right); if ( $countArgType->isList()->yes() && $this->typeSpecifier->isNormalCountCall($expr->right->left, $countArgType, $scope)->yes() @@ -467,7 +477,7 @@ public function processExpr(NodeScopeResolver $nodeScopeResolver, Stmt $stmt, Ex $context->true() && (IntegerRangeType::createAllGreaterThanOrEqualTo(1 - $offset)->isSuperTypeOf($leftType)->yes()) || ($context->false() && (new ConstantIntegerType(1 - $offset))->isSuperTypeOf($leftType)->yes()) ) { - $argType = $scope->getType($expr->right->getArgs()[0]->value); + $argType = $getType($expr->right->getArgs()[0]->value); if ($argType->isString()->yes()) { $accessory = new AccessoryNonEmptyStringType(); @@ -505,7 +515,7 @@ public function processExpr(NodeScopeResolver $nodeScopeResolver, Stmt $stmt, Ex } } - $rightType = $scope->getType($expr->right); + $rightType = $getType($expr->right); if ($rightType instanceof ConstantIntegerType) { if ($expr->left instanceof Expr\PostInc) { $result = $result->unionWith($this->createRangeTypes( @@ -595,7 +605,7 @@ public function processExpr(NodeScopeResolver $nodeScopeResolver, Stmt $stmt, Ex * The boolean result of a `==` comparison, including the same-variable * special case. Shared by the Equal and NotEqual type callbacks. */ - private function resolveEqualType(MutatingScope $scope, BinaryOp\Equal $expr): Type + private function resolveEqualType(NodeScopeResolver $nodeScopeResolver, MutatingScope $scope, BinaryOp\Equal $expr): Type { if ( $expr->left instanceof Variable @@ -607,8 +617,10 @@ private function resolveEqualType(MutatingScope $scope, BinaryOp\Equal $expr): T return new ConstantBooleanType(true); } - $leftType = $scope->getType($expr->left); - $rightType = $scope->getType($expr->right); + // the operands were processed during processExpr; read their stored + // results instead of re-walking via Scope::getType(). + $leftType = $nodeScopeResolver->readStoredOrPriceOnDemand($expr->left, $scope); + $rightType = $nodeScopeResolver->readStoredOrPriceOnDemand($expr->right, $scope); return $this->initializerExprTypeResolver->resolveEqualType($leftType, $rightType)->type; } diff --git a/src/Analyser/ExprHandler/Helper/EqualityTypeSpecifyingHelper.php b/src/Analyser/ExprHandler/Helper/EqualityTypeSpecifyingHelper.php index c5162b0830e..cd02477548d 100644 --- a/src/Analyser/ExprHandler/Helper/EqualityTypeSpecifyingHelper.php +++ b/src/Analyser/ExprHandler/Helper/EqualityTypeSpecifyingHelper.php @@ -10,6 +10,7 @@ use PhpParser\Node\Expr\FuncCall; use PhpParser\Node\Expr\Instanceof_; use PhpParser\Node\Name; +use PHPStan\Analyser\NodeScopeResolver; use PHPStan\Analyser\Scope; use PHPStan\Analyser\SpecifiedTypes; use PHPStan\Analyser\TypeSpecifier; @@ -67,9 +68,9 @@ public function __construct( { } - public function specifyTypesForEqual(Expr\BinaryOp\Equal $expr, Scope $scope, TypeSpecifierContext $context): SpecifiedTypes + public function specifyTypesForEqual(NodeScopeResolver $nodeScopeResolver, Expr\BinaryOp\Equal $expr, Scope $scope, TypeSpecifierContext $context): SpecifiedTypes { - $expressions = $this->findTypeExpressionsFromBinaryOperation($scope, $expr); + $expressions = $this->findTypeExpressionsFromBinaryOperation($nodeScopeResolver, $scope, $expr); if ($expressions !== null) { $exprNode = $expressions[0]; $constantType = $expressions[1]; @@ -181,8 +182,10 @@ public function specifyTypesForEqual(Expr\BinaryOp\Equal $expr, Scope $scope, Ty } } - $leftType = $scope->getType($expr->left); - $rightType = $scope->getType($expr->right); + // the operands were processed during processExpr; read their stored results + // instead of re-walking via Scope::getType(). + $leftType = $nodeScopeResolver->readStoredOrPriceOnDemand($expr->left, $scope->toMutatingScope()); + $rightType = $nodeScopeResolver->readStoredOrPriceOnDemand($expr->right, $scope->toMutatingScope()); $leftBooleanType = $leftType->toBoolean(); if ($leftBooleanType instanceof ConstantBooleanType && $rightType->isBoolean()->yes()) { @@ -249,19 +252,19 @@ public function specifyTypesForEqual(Expr\BinaryOp\Equal $expr, Scope $scope, Ty : $leftTypes->normalize($scope)->intersectWith($rightTypes->normalize($scope)); } - public function specifyTypesForIdentical(Expr\BinaryOp\Identical $expr, Scope $scope, TypeSpecifierContext $context): SpecifiedTypes + public function specifyTypesForIdentical(NodeScopeResolver $nodeScopeResolver, Expr\BinaryOp\Identical $expr, Scope $scope, TypeSpecifierContext $context): SpecifiedTypes { $leftExpr = $expr->left; $rightExpr = $expr->right; // Normalize to: fn() === expr if ($rightExpr instanceof FuncCall && !$leftExpr instanceof FuncCall) { - $specifiedTypes = $this->specifyTypesForNormalizedIdentical(new Expr\BinaryOp\Identical( + $specifiedTypes = $this->specifyTypesForNormalizedIdentical($nodeScopeResolver, new Expr\BinaryOp\Identical( $rightExpr, $leftExpr, ), $scope, $context); } else { - $specifiedTypes = $this->specifyTypesForNormalizedIdentical(new Expr\BinaryOp\Identical( + $specifiedTypes = $this->specifyTypesForNormalizedIdentical($nodeScopeResolver, new Expr\BinaryOp\Identical( $leftExpr, $rightExpr, ), $scope, $context); @@ -270,7 +273,7 @@ public function specifyTypesForIdentical(Expr\BinaryOp\Identical $expr, Scope $s // merge result of fn1() === fn2() and fn2() === fn1() if ($rightExpr instanceof FuncCall && $leftExpr instanceof FuncCall) { return $specifiedTypes->unionWith( - $this->specifyTypesForNormalizedIdentical(new Expr\BinaryOp\Identical( + $this->specifyTypesForNormalizedIdentical($nodeScopeResolver, new Expr\BinaryOp\Identical( $rightExpr, $leftExpr, ), $scope, $context), @@ -280,7 +283,7 @@ public function specifyTypesForIdentical(Expr\BinaryOp\Identical $expr, Scope $s return $specifiedTypes; } - private function specifyTypesForNormalizedIdentical(Expr\BinaryOp\Identical $expr, Scope $scope, TypeSpecifierContext $context): SpecifiedTypes + private function specifyTypesForNormalizedIdentical(NodeScopeResolver $nodeScopeResolver, Expr\BinaryOp\Identical $expr, Scope $scope, TypeSpecifierContext $context): SpecifiedTypes { $leftExpr = $expr->left; $rightExpr = $expr->right; @@ -294,7 +297,11 @@ private function specifyTypesForNormalizedIdentical(Expr\BinaryOp\Identical $exp $unwrappedRightExpr = $rightExpr->getExpr(); } - $rightType = $scope->getType($rightExpr); + // the operands and their subexpressions were processed during processExpr; + // read their stored results instead of re-walking via Scope::getType(). + $getType = static fn (Expr $e): Type => $nodeScopeResolver->readStoredOrPriceOnDemand($e, $scope->toMutatingScope()); + + $rightType = $getType($rightExpr); // (count($a) === $expr) if ( @@ -315,16 +322,16 @@ private function specifyTypesForNormalizedIdentical(Expr\BinaryOp\Identical $exp && in_array($unwrappedRightExpr->name->toLowerString(), ['count', 'sizeof'], true) && count($unwrappedRightExpr->getArgs()) >= 1 ) { - $argType = $scope->getType($unwrappedRightExpr->getArgs()[0]->value); - $sizeType = $scope->getType($leftExpr); + $argType = $getType($unwrappedRightExpr->getArgs()[0]->value); + $sizeType = $getType($leftExpr); $specifiedTypes = $this->typeSpecifier->specifyTypesForCountFuncCall($unwrappedRightExpr, $argType, $sizeType, $context, $scope, $expr); if ($specifiedTypes !== null) { return $specifiedTypes; } - $leftArrayType = $scope->getType($unwrappedLeftExpr->getArgs()[0]->value); - $rightArrayType = $scope->getType($unwrappedRightExpr->getArgs()[0]->value); + $leftArrayType = $getType($unwrappedLeftExpr->getArgs()[0]->value); + $rightArrayType = $getType($unwrappedRightExpr->getArgs()[0]->value); if ( $leftArrayType->isArray()->yes() && $rightArrayType->isArray()->yes() @@ -342,7 +349,7 @@ private function specifyTypesForNormalizedIdentical(Expr\BinaryOp\Identical $exp return $this->typeSpecifier->create($unwrappedLeftExpr->getArgs()[0]->value, new NeverType(), $context, $scope)->setRootExpr($expr); } - $argType = $scope->getType($unwrappedLeftExpr->getArgs()[0]->value); + $argType = $getType($unwrappedLeftExpr->getArgs()[0]->value); $isZero = (new ConstantIntegerType(0))->isSuperTypeOf($rightType); if ($isZero->yes()) { $funcTypes = $this->typeSpecifier->create($leftExpr, $rightType, $context, $scope)->setRootExpr($expr); @@ -405,7 +412,7 @@ private function specifyTypesForNormalizedIdentical(Expr\BinaryOp\Identical $exp } if ($context->truthy() && IntegerRangeType::fromInterval(1, null)->isSuperTypeOf($rightType)->yes()) { - $argType = $scope->getType($unwrappedLeftExpr->getArgs()[0]->value); + $argType = $getType($unwrappedLeftExpr->getArgs()[0]->value); if ($argType->isString()->yes()) { $funcTypes = $this->typeSpecifier->create($leftExpr, $rightType, $context, $scope)->setRootExpr($expr); @@ -435,7 +442,7 @@ private function specifyTypesForNormalizedIdentical(Expr\BinaryOp\Identical $exp $notNullOnly = $funcName === 'array_find_key'; if ($bothDirections || $notNullOnly) { $args = $unwrappedLeftExpr->getArgs(); - $argType = $scope->getType($args[0]->value); + $argType = $getType($args[0]->value); if ($argType->isArray()->yes()) { if ($bothDirections) { return $this->typeSpecifier->create($args[0]->value, new NonEmptyArrayType(), $context->negate(), $scope)->setRootExpr($expr); @@ -503,7 +510,7 @@ private function specifyTypesForNormalizedIdentical(Expr\BinaryOp\Identical $exp && isset($unwrappedLeftExpr->getArgs()[0]) && $rightType->isNonEmptyString()->yes() ) { - $argType = $scope->getType($unwrappedLeftExpr->getArgs()[0]->value); + $argType = $getType($unwrappedLeftExpr->getArgs()[0]->value); if ($argType->isString()->yes()) { $specifiedTypes = new SpecifiedTypes(); @@ -545,7 +552,7 @@ private function specifyTypesForNormalizedIdentical(Expr\BinaryOp\Identical $exp if ($rightType->isString()->yes()) { $types = null; foreach ($rightType->getConstantStrings() as $constantString) { - $specifiedType = $this->specifyTypesForConstantStringBinaryExpression($unwrappedLeftExpr, $constantString, $context, $scope, $expr); + $specifiedType = $this->specifyTypesForConstantStringBinaryExpression($nodeScopeResolver, $unwrappedLeftExpr, $constantString, $context, $scope, $expr); if ($specifiedType === null) { continue; @@ -566,7 +573,7 @@ private function specifyTypesForNormalizedIdentical(Expr\BinaryOp\Identical $exp } } - $expressions = $this->findTypeExpressionsFromBinaryOperation($scope, $expr); + $expressions = $this->findTypeExpressionsFromBinaryOperation($nodeScopeResolver, $scope, $expr); if ($expressions !== null) { $exprNode = $expressions[0]; $constantType = $expressions[1]; @@ -617,7 +624,7 @@ private function specifyTypesForNormalizedIdentical(Expr\BinaryOp\Identical $exp } } - $leftType = $scope->getType($leftExpr); + $leftType = $getType($leftExpr); // 'Foo' === $a::class if ( @@ -651,7 +658,7 @@ private function specifyTypesForNormalizedIdentical(Expr\BinaryOp\Identical $exp } if ($context->false()) { - $identicalType = $scope->getType($expr); + $identicalType = $getType($expr); if ($identicalType instanceof ConstantBooleanType) { $never = new NeverType(); $contextForTypes = $identicalType->getValue() ? $context->negate() : $context; @@ -760,10 +767,12 @@ private function specifyTypesForNormalizedIdentical(Expr\BinaryOp\Identical $exp /** * @return array{Expr, ConstantScalarType, Type}|null */ - private function findTypeExpressionsFromBinaryOperation(Scope $scope, Node\Expr\BinaryOp $binaryOperation): ?array + private function findTypeExpressionsFromBinaryOperation(NodeScopeResolver $nodeScopeResolver, Scope $scope, Node\Expr\BinaryOp $binaryOperation): ?array { - $leftType = $scope->getType($binaryOperation->left); - $rightType = $scope->getType($binaryOperation->right); + // the operands were processed during processExpr; read their stored results + // instead of re-walking via Scope::getType(). + $leftType = $nodeScopeResolver->readStoredOrPriceOnDemand($binaryOperation->left, $scope->toMutatingScope()); + $rightType = $nodeScopeResolver->readStoredOrPriceOnDemand($binaryOperation->right, $scope->toMutatingScope()); $rightExpr = $binaryOperation->right; if ($rightExpr instanceof AlwaysRememberedExpr) { @@ -828,6 +837,7 @@ private function specifyTypesForConstantBinaryExpression( } private function specifyTypesForConstantStringBinaryExpression( + NodeScopeResolver $nodeScopeResolver, Expr $exprNode, Type $constantType, TypeSpecifierContext $context, @@ -889,7 +899,9 @@ private function specifyTypesForConstantStringBinaryExpression( && strtolower((string) $exprNode->name) === 'get_parent_class' && isset($exprNode->getArgs()[0]) ) { - $argType = $scope->getType($exprNode->getArgs()[0]->value); + // the argument was processed during processExpr; read its stored result + // instead of re-walking via Scope::getType(). + $argType = $nodeScopeResolver->readStoredOrPriceOnDemand($exprNode->getArgs()[0]->value, $scope->toMutatingScope()); $objectType = new ObjectType($constantStringValue); $classStringType = new GenericClassStringType($objectType); @@ -932,7 +944,9 @@ private function specifyTypesForConstantStringBinaryExpression( && $constantStringValue === '' ) { $argValue = $exprNode->getArgs()[0]->value; - $argType = $scope->getType($argValue); + // the argument was processed during processExpr; read its stored result + // instead of re-walking via Scope::getType(). + $argType = $nodeScopeResolver->readStoredOrPriceOnDemand($argValue, $scope->toMutatingScope()); if ($argType->isString()->yes()) { return $this->typeSpecifier->create( $argValue, diff --git a/src/Analyser/ExprHandler/MatchHandler.php b/src/Analyser/ExprHandler/MatchHandler.php index c44ca579f91..7f9fcf7c026 100644 --- a/src/Analyser/ExprHandler/MatchHandler.php +++ b/src/Analyser/ExprHandler/MatchHandler.php @@ -77,10 +77,12 @@ public function supports(Expr $expr): bool * * @return list */ - public function getArmScopesAndTypes(MutatingScope $scope, Match_ $expr): array + public function getArmScopesAndTypes(NodeScopeResolver $nodeScopeResolver, MutatingScope $scope, Match_ $expr): array { $cond = $expr->cond; - $condType = $scope->getType($cond); + // the subject was processed before this shadow walk runs; read its stored + // result on the incoming scope instead of re-walking via Scope::getType(). + $condType = $nodeScopeResolver->readStoredOrPriceOnDemand($cond, $scope); $armScopesAndTypes = []; $matchScope = $scope; @@ -142,7 +144,10 @@ public function getArmScopesAndTypes(MutatingScope $scope, Match_ $expr): array $cond, $conditionCaseType, ); - $armScopesAndTypes[] = [$armScope, $armScope->getType($arm->body)]; + // the arm body is read on the subject-narrowed scope this shadow + // walk built; that (body, narrowed-scope) pair is not stored, so + // price the body on demand against the current storage. + $armScopesAndTypes[] = [$armScope, $nodeScopeResolver->priceSyntheticOnDemand($arm->body, $armScope)]; unset($arms[$i]); } @@ -171,7 +176,7 @@ public function getArmScopesAndTypes(MutatingScope $scope, Match_ $expr): array if ($expr->hasAttribute(MutatingScope::KEEP_VOID_ATTRIBUTE_NAME)) { $arm->body->setAttribute(MutatingScope::KEEP_VOID_ATTRIBUTE_NAME, $expr->getAttribute(MutatingScope::KEEP_VOID_ATTRIBUTE_NAME)); } - $armScopesAndTypes[] = [$matchScope, $matchScope->getType($arm->body)]; + $armScopesAndTypes[] = [$matchScope, $nodeScopeResolver->priceSyntheticOnDemand($arm->body, $matchScope)]; continue; } @@ -181,14 +186,16 @@ public function getArmScopesAndTypes(MutatingScope $scope, Match_ $expr): array $filteringExpr = $this->getFilteringExprForMatchArm($expr, $arm->conds); - $filteringExprType = $matchScope->getType($filteringExpr); + // the filtering expression is synthetic - price it on demand against the + // current storage instead of re-walking via Scope::getType(). + $filteringExprType = $nodeScopeResolver->priceSyntheticOnDemand($filteringExpr, $matchScope); if (!$filteringExprType->isFalse()->yes()) { $truthyScope = $matchScope->filterByTruthyValue($filteringExpr); if ($expr->hasAttribute(MutatingScope::KEEP_VOID_ATTRIBUTE_NAME)) { $arm->body->setAttribute(MutatingScope::KEEP_VOID_ATTRIBUTE_NAME, $expr->getAttribute(MutatingScope::KEEP_VOID_ATTRIBUTE_NAME)); } - $armScopesAndTypes[] = [$truthyScope, $truthyScope->getType($arm->body)]; + $armScopesAndTypes[] = [$truthyScope, $nodeScopeResolver->priceSyntheticOnDemand($arm->body, $truthyScope)]; } $matchScope = $matchScope->filterByFalseyValue($filteringExpr); @@ -201,9 +208,11 @@ public function processExpr(NodeScopeResolver $nodeScopeResolver, Stmt $stmt, Ex { $beforeScope = $scope; $deepContext = $context->enterDeep(); - $condType = $scope->getType($expr->cond); - $condNativeType = $scope->getNativeType($expr->cond); $condResult = $nodeScopeResolver->processExprNode($stmt, $expr->cond, $scope, $storage, $nodeCallback, $deepContext); + // the subject was just processed on this scope; read its result instead of + // re-walking via Scope::getType(). + $condType = $condResult->getTypeForScope($scope); + $condNativeType = $condResult->getNativeTypeForScope($scope); $scope = $condResult->getScope(); $hasYield = $condResult->hasYield(); $throwPoints = $condResult->getThrowPoints(); @@ -429,7 +438,11 @@ public function processExpr(NodeScopeResolver $nodeScopeResolver, Stmt $stmt, Ex $impurePoints = array_merge($impurePoints, $armCondResult->getImpurePoints()); $armCondExpr = new BinaryOp\Identical($expr->cond, $armCond); $armCondResultScope = $armCondResult->getScope(); - $armCondType = $this->treatPhpDocTypesAsCertain ? $armCondResultScope->getType($armCondExpr) : $armCondResultScope->getNativeType($armCondExpr); + // the `subject === cond` comparison is synthetic - price it on demand + // against the current storage instead of re-walking via Scope::getType(). + $armCondType = $this->treatPhpDocTypesAsCertain + ? $nodeScopeResolver->priceSyntheticOnDemand($armCondExpr, $armCondResultScope) + : $nodeScopeResolver->priceSyntheticOnDemand($armCondExpr, $armCondResultScope->doNotTreatPhpDocTypesAsCertain()); if ($armCondType->isTrue()->yes()) { $hasAlwaysTrueCond = true; } @@ -464,8 +477,9 @@ public function processExpr(NodeScopeResolver $nodeScopeResolver, Stmt $stmt, Ex $impurePoints = array_merge($impurePoints, $armResult->getImpurePoints()); // Mirror getArmScopesAndTypes: an arm whose filtering expression is // always false is unreachable and does not contribute to the result - // type. - $filteringExprType = $matchScope->getType($filteringExpr); + // type. The filtering expression is synthetic - price it on demand + // against the current storage instead of re-walking via Scope::getType(). + $filteringExprType = $nodeScopeResolver->priceSyntheticOnDemand($filteringExpr, $matchScope); if (!$filteringExprType->isFalse()->yes()) { $armTypeResults[] = [$armResult, $bodyScope, $arm->body]; } @@ -483,7 +497,9 @@ public function processExpr(NodeScopeResolver $nodeScopeResolver, Stmt $stmt, Ex $isExhaustive = $hasDefaultCond || $hasAlwaysTrueCond; if (!$isExhaustive) { - $remainingType = $matchScope->getType($expr->cond); + // the subject was processed above; read its stored result on the + // arm-narrowed scope instead of re-walking via Scope::getType(). + $remainingType = $nodeScopeResolver->readStoredOrPriceOnDemand($expr->cond, $matchScope); if ($remainingType instanceof NeverType) { $isExhaustive = true; } From d422c46b81666461cf03260f015261c54c7bd024 Mon Sep 17 00:00:00 2001 From: Ondrej Mirtes Date: Thu, 18 Jun 2026 17:02:22 +0200 Subject: [PATCH 094/113] Thread NodeScopeResolver into narrowing/throw-point helpers to avoid Scope::getType --- src/Analyser/ExprHandler/AssignOpHandler.php | 2 +- src/Analyser/ExprHandler/BinaryOpHandler.php | 4 +-- .../ExprHandler/BooleanAndHandler.php | 12 ++++----- src/Analyser/ExprHandler/BooleanOrHandler.php | 10 ++++---- .../ExprHandler/CastStringHandler.php | 2 +- src/Analyser/ExprHandler/CoalesceHandler.php | 2 +- src/Analyser/ExprHandler/EmptyHandler.php | 2 +- .../ConditionalExpressionHolderHelper.php | 19 ++++++++------ .../Helper/ImplicitToStringCallHelper.php | 13 +++++++--- .../Helper/MethodThrowPointHelper.php | 14 ++++++++--- .../Helper/NonNullabilityHelper.php | 25 +++++++++++-------- .../ExprHandler/InterpolatedStringHandler.php | 2 +- src/Analyser/ExprHandler/IssetHandler.php | 2 +- .../ExprHandler/MethodCallHandler.php | 2 +- .../ExprHandler/NullsafeMethodCallHandler.php | 2 +- .../NullsafePropertyFetchHandler.php | 2 +- src/Analyser/ExprHandler/PrintHandler.php | 2 +- .../ExprHandler/StaticCallHandler.php | 2 +- src/Analyser/NodeScopeResolver.php | 2 +- 19 files changed, 70 insertions(+), 51 deletions(-) diff --git a/src/Analyser/ExprHandler/AssignOpHandler.php b/src/Analyser/ExprHandler/AssignOpHandler.php index 01c7b33adf3..76f9ac93b2e 100644 --- a/src/Analyser/ExprHandler/AssignOpHandler.php +++ b/src/Analyser/ExprHandler/AssignOpHandler.php @@ -184,7 +184,7 @@ function (MutatingScope $scope) use ($stmt, $expr, $nodeCallback, $context, $sto $throwPoints[] = InternalThrowPoint::createExplicit($scope, new ObjectType(DivisionByZeroError::class), $expr, false); } if ($expr instanceof Expr\AssignOp\Concat) { - $toStringResult = $this->implicitToStringCallHelper->processImplicitToStringCall($expr->expr, $scope); + $toStringResult = $this->implicitToStringCallHelper->processImplicitToStringCall($nodeScopeResolver, $expr->expr, $scope); $throwPoints = array_merge($throwPoints, $toStringResult->getThrowPoints()); $impurePoints = array_merge($impurePoints, $toStringResult->getImpurePoints()); } diff --git a/src/Analyser/ExprHandler/BinaryOpHandler.php b/src/Analyser/ExprHandler/BinaryOpHandler.php index 6130d65193f..3e740ee9de4 100644 --- a/src/Analyser/ExprHandler/BinaryOpHandler.php +++ b/src/Analyser/ExprHandler/BinaryOpHandler.php @@ -101,8 +101,8 @@ public function processExpr(NodeScopeResolver $nodeScopeResolver, Stmt $stmt, Ex $throwPoints[] = InternalThrowPoint::createExplicit($leftResult->getScope(), new ObjectType(DivisionByZeroError::class), $expr, false); } if ($expr instanceof BinaryOp\Concat) { - $leftToStringResult = $this->implicitToStringCallHelper->processImplicitToStringCall($expr->left, $scope); - $rightToStringResult = $this->implicitToStringCallHelper->processImplicitToStringCall($expr->right, $leftResult->getScope()); + $leftToStringResult = $this->implicitToStringCallHelper->processImplicitToStringCall($nodeScopeResolver, $expr->left, $scope); + $rightToStringResult = $this->implicitToStringCallHelper->processImplicitToStringCall($nodeScopeResolver, $expr->right, $leftResult->getScope()); $throwPoints = array_merge($throwPoints, $leftToStringResult->getThrowPoints(), $rightToStringResult->getThrowPoints()); $impurePoints = array_merge($impurePoints, $leftToStringResult->getImpurePoints(), $rightToStringResult->getImpurePoints()); } diff --git a/src/Analyser/ExprHandler/BooleanAndHandler.php b/src/Analyser/ExprHandler/BooleanAndHandler.php index 2c2e14dcb16..863b6b5c91b 100644 --- a/src/Analyser/ExprHandler/BooleanAndHandler.php +++ b/src/Analyser/ExprHandler/BooleanAndHandler.php @@ -121,7 +121,7 @@ public function processExpr(NodeScopeResolver $nodeScopeResolver, Stmt $stmt, Ex return new BooleanType(); }, - specifyTypesCallback: function (MutatingScope $s, TypeSpecifierContext $context) use ($expr, $leftResult, $rightResult): SpecifiedTypes { + specifyTypesCallback: function (MutatingScope $s, TypeSpecifierContext $context) use ($expr, $leftResult, $rightResult, $nodeScopeResolver): SpecifiedTypes { $leftTypes = $this->defaultNarrowingHelper->getChildSpecifiedTypes($s, $expr->left, $leftResult, $context)->setRootExpr($expr); $rightScope = $s->filterByTruthyValue($expr->left); $rightTypes = $this->defaultNarrowingHelper->getChildSpecifiedTypes($rightScope, $expr->right, $rightResult, $context)->setRootExpr($expr); @@ -131,7 +131,7 @@ public function processExpr(NodeScopeResolver $nodeScopeResolver, Stmt $stmt, Ex $leftNormalized = $leftTypes->normalize($s); $rightNormalized = $rightTypes->normalize($rightScope); $types = $leftNormalized->intersectWith($rightNormalized); - $types = $this->conditionalExpressionHolderHelper->augmentDisjunctionTypes($s, $rightScope, $leftNormalized, $rightNormalized, $expr->left, $expr->right, false, $types); + $types = $this->conditionalExpressionHolderHelper->augmentDisjunctionTypes($nodeScopeResolver, $s, $rightScope, $leftNormalized, $rightNormalized, $expr->left, $expr->right, false, $types); } if ($context->false()) { // Consequent (holder) narrowings projected by each holder: these must be @@ -177,10 +177,10 @@ public function processExpr(NodeScopeResolver $nodeScopeResolver, Stmt $stmt, Ex $result = $result->setAlwaysOverwriteTypes(); } return $result->setNewConditionalExpressionHolders($this->conditionalExpressionHolderHelper->mergeConditionalHolders([ - $this->conditionalExpressionHolderHelper->processBooleanConditionalTypes($s, $leftCondTypes, $rightHolderTypes, false, true, $rightScope, $expr->right), - $this->conditionalExpressionHolderHelper->processBooleanConditionalTypes($s, $rightCondTypes, $leftHolderTypes, false, true, $s, $expr->left), - $this->conditionalExpressionHolderHelper->processBooleanConditionalTypes($s, $leftCondTypes, $rightHolderTypes, true, true, $rightScope, $expr->right), - $this->conditionalExpressionHolderHelper->processBooleanConditionalTypes($s, $rightCondTypes, $leftHolderTypes, true, true, $s, $expr->left), + $this->conditionalExpressionHolderHelper->processBooleanConditionalTypes($nodeScopeResolver, $s, $leftCondTypes, $rightHolderTypes, false, true, $rightScope, $expr->right), + $this->conditionalExpressionHolderHelper->processBooleanConditionalTypes($nodeScopeResolver, $s, $rightCondTypes, $leftHolderTypes, false, true, $s, $expr->left), + $this->conditionalExpressionHolderHelper->processBooleanConditionalTypes($nodeScopeResolver, $s, $leftCondTypes, $rightHolderTypes, true, true, $rightScope, $expr->right), + $this->conditionalExpressionHolderHelper->processBooleanConditionalTypes($nodeScopeResolver, $s, $rightCondTypes, $leftHolderTypes, true, true, $s, $expr->left), ]))->setRootExpr($expr); } diff --git a/src/Analyser/ExprHandler/BooleanOrHandler.php b/src/Analyser/ExprHandler/BooleanOrHandler.php index f6745ae6c60..184fbf9becc 100644 --- a/src/Analyser/ExprHandler/BooleanOrHandler.php +++ b/src/Analyser/ExprHandler/BooleanOrHandler.php @@ -190,7 +190,7 @@ public function processExpr(NodeScopeResolver $nodeScopeResolver, Stmt $stmt, Ex $rightNormalized = $rightTypes->normalize($rightScope); $types = $leftNormalized->intersectWith($rightNormalized); $types = $this->augmentBooleanOrTruthyWithConditionalHolders($nodeScopeResolver, $s, $rightScope, $expr, $types); - $types = $this->conditionalExpressionHolderHelper->augmentDisjunctionTypes($s, $rightScope, $leftNormalized, $rightNormalized, $expr->left, $expr->right, true, $types); + $types = $this->conditionalExpressionHolderHelper->augmentDisjunctionTypes($nodeScopeResolver, $s, $rightScope, $leftNormalized, $rightNormalized, $expr->left, $expr->right, true, $types); } } else { $types = $leftTypes->unionWith($rightTypes); @@ -205,10 +205,10 @@ public function processExpr(NodeScopeResolver $nodeScopeResolver, Stmt $stmt, Ex $result = $result->setAlwaysOverwriteTypes(); } return $result->setNewConditionalExpressionHolders($this->conditionalExpressionHolderHelper->mergeConditionalHolders([ - $this->conditionalExpressionHolderHelper->processBooleanConditionalTypes($s, $leftTypes, $rightTypes, false, false, $rightScope, $expr->right), - $this->conditionalExpressionHolderHelper->processBooleanConditionalTypes($s, $rightTypes, $leftTypes, false, false, $s, $expr->left), - $this->conditionalExpressionHolderHelper->processBooleanConditionalTypes($s, $leftTypes, $rightTypes, true, false, $rightScope, $expr->right), - $this->conditionalExpressionHolderHelper->processBooleanConditionalTypes($s, $rightTypes, $leftTypes, true, false, $s, $expr->left), + $this->conditionalExpressionHolderHelper->processBooleanConditionalTypes($nodeScopeResolver, $s, $leftTypes, $rightTypes, false, false, $rightScope, $expr->right), + $this->conditionalExpressionHolderHelper->processBooleanConditionalTypes($nodeScopeResolver, $s, $rightTypes, $leftTypes, false, false, $s, $expr->left), + $this->conditionalExpressionHolderHelper->processBooleanConditionalTypes($nodeScopeResolver, $s, $leftTypes, $rightTypes, true, false, $rightScope, $expr->right), + $this->conditionalExpressionHolderHelper->processBooleanConditionalTypes($nodeScopeResolver, $s, $rightTypes, $leftTypes, true, false, $s, $expr->left), ]))->setRootExpr($expr); } diff --git a/src/Analyser/ExprHandler/CastStringHandler.php b/src/Analyser/ExprHandler/CastStringHandler.php index 6f3d36f59c9..0b6d874814d 100644 --- a/src/Analyser/ExprHandler/CastStringHandler.php +++ b/src/Analyser/ExprHandler/CastStringHandler.php @@ -52,7 +52,7 @@ public function processExpr(NodeScopeResolver $nodeScopeResolver, Stmt $stmt, Ex $impurePoints = $exprResult->getImpurePoints(); $throwPoints = $exprResult->getThrowPoints(); - $toStringResult = $this->implicitToStringCallHelper->processImplicitToStringCall($expr->expr, $scope); + $toStringResult = $this->implicitToStringCallHelper->processImplicitToStringCall($nodeScopeResolver, $expr->expr, $scope); $throwPoints = array_merge($throwPoints, $toStringResult->getThrowPoints()); $impurePoints = array_merge($impurePoints, $toStringResult->getImpurePoints()); diff --git a/src/Analyser/ExprHandler/CoalesceHandler.php b/src/Analyser/ExprHandler/CoalesceHandler.php index 3f600a64329..8faff93160b 100644 --- a/src/Analyser/ExprHandler/CoalesceHandler.php +++ b/src/Analyser/ExprHandler/CoalesceHandler.php @@ -65,7 +65,7 @@ private function getFalseySpecifiedTypes(MutatingScope $s, Expr $expr, Expressio public function processExpr(NodeScopeResolver $nodeScopeResolver, Stmt $stmt, Expr $expr, MutatingScope $scope, ExpressionResultStorage $storage, callable $nodeCallback, ExpressionContext $context): ExpressionResult { $beforeScope = $scope; - $nonNullabilityResult = $this->nonNullabilityHelper->ensureNonNullability($scope, $expr->left); + $nonNullabilityResult = $this->nonNullabilityHelper->ensureNonNullability($nodeScopeResolver, $scope, $expr->left); $condScope = $nodeScopeResolver->lookForSetAllowedUndefinedExpressions($nonNullabilityResult->getScope(), $expr->left); $condResult = $nodeScopeResolver->processExprNode($stmt, $expr->left, $condScope, $storage, $nodeCallback, $context->enterDeep()); $scope = $this->nonNullabilityHelper->revertNonNullability($condResult->getScope(), $nonNullabilityResult->getSpecifiedExpressions()); diff --git a/src/Analyser/ExprHandler/EmptyHandler.php b/src/Analyser/ExprHandler/EmptyHandler.php index 55f32fde61f..4f31028040f 100644 --- a/src/Analyser/ExprHandler/EmptyHandler.php +++ b/src/Analyser/ExprHandler/EmptyHandler.php @@ -45,7 +45,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 { $beforeScope = $scope; - $nonNullabilityResult = $this->nonNullabilityHelper->ensureNonNullability($scope, $expr->expr); + $nonNullabilityResult = $this->nonNullabilityHelper->ensureNonNullability($nodeScopeResolver, $scope, $expr->expr); $scope = $nodeScopeResolver->lookForSetAllowedUndefinedExpressions($nonNullabilityResult->getScope(), $expr->expr); $exprResult = $nodeScopeResolver->processExprNode($stmt, $expr->expr, $scope, $storage, $nodeCallback, $context->enterDeep()); $scope = $exprResult->getScope(); diff --git a/src/Analyser/ExprHandler/Helper/ConditionalExpressionHolderHelper.php b/src/Analyser/ExprHandler/Helper/ConditionalExpressionHolderHelper.php index e8c1008c183..6c1bcfb85f7 100644 --- a/src/Analyser/ExprHandler/Helper/ConditionalExpressionHolderHelper.php +++ b/src/Analyser/ExprHandler/Helper/ConditionalExpressionHolderHelper.php @@ -10,7 +10,7 @@ use PHPStan\Analyser\ConditionalExpressionHolder; use PHPStan\Analyser\ExpressionTypeHolder; use PHPStan\Analyser\MutatingScope; -use PHPStan\Analyser\Scope; +use PHPStan\Analyser\NodeScopeResolver; use PHPStan\Analyser\SpecifiedTypes; use PHPStan\Analyser\TypeSpecifier; use PHPStan\Analyser\TypeSpecifierContext; @@ -37,6 +37,7 @@ public function __construct( } public function augmentDisjunctionTypes( + NodeScopeResolver $nodeScopeResolver, MutatingScope $scope, MutatingScope $rightScope, SpecifiedTypes $leftNormalized, @@ -88,9 +89,11 @@ public function augmentDisjunctionTypes( continue; } - $originalType = $scope->getType($targetExpr); - $leftType = $leftFilteredScope->getType($targetExpr); - $rightType = $rightFilteredScope->getType($targetExpr); + // the operands were processed during processExpr; read their stored + // results on these filtered scopes instead of re-walking via getType(). + $originalType = $nodeScopeResolver->readStoredOrPriceOnDemand($targetExpr, $scope); + $leftType = $nodeScopeResolver->readStoredOrPriceOnDemand($targetExpr, $leftFilteredScope); + $rightType = $nodeScopeResolver->readStoredOrPriceOnDemand($targetExpr, $rightFilteredScope); if ($leftType->equals($originalType) || !$originalType->isSuperTypeOf($leftType)->yes()) { continue; @@ -141,7 +144,7 @@ public function mergeConditionalHolders(array $holderLists): array /** * @return array */ - public function processBooleanConditionalTypes(Scope $scope, SpecifiedTypes $conditionSpecifiedTypes, SpecifiedTypes $holderSpecifiedTypes, bool $holdersFromSureTypes, bool $holderSideIsNegated, Scope $rightScope, ?Expr $holderSideExpr = null): array + public function processBooleanConditionalTypes(NodeScopeResolver $nodeScopeResolver, MutatingScope $scope, SpecifiedTypes $conditionSpecifiedTypes, SpecifiedTypes $holderSpecifiedTypes, bool $holdersFromSureTypes, bool $holderSideIsNegated, MutatingScope $rightScope, ?Expr $holderSideExpr = null): array { // The condition side asserts that its sub-expression evaluates truthy. // When that sub-expression is itself a compound boolean (e.g. `$a && $b`), @@ -156,7 +159,7 @@ public function processBooleanConditionalTypes(Scope $scope, SpecifiedTypes $con continue; } - $scopeType = $scope->getType($expr); + $scopeType = $nodeScopeResolver->readStoredOrPriceOnDemand($expr, $scope); $conditionType = TypeCombinator::remove($scopeType, $type); if ($scopeType->equals($conditionType)) { continue; @@ -174,7 +177,7 @@ public function processBooleanConditionalTypes(Scope $scope, SpecifiedTypes $con $conditionExpressionTypes[$exprString] = ExpressionTypeHolder::createYes( $expr, - TypeCombinator::intersect($scope->getType($expr), $type), + TypeCombinator::intersect($nodeScopeResolver->readStoredOrPriceOnDemand($expr, $scope), $type), ); } @@ -213,7 +216,7 @@ public function processBooleanConditionalTypes(Scope $scope, SpecifiedTypes $con } $targetScope = $expr instanceof Expr\Variable ? $scope : $rightScope; - $targetType = $targetScope->getType($expr); + $targetType = $nodeScopeResolver->readStoredOrPriceOnDemand($expr, $targetScope); $holderType = $holdersFromSureTypes ? TypeCombinator::intersect($targetType, $type) : TypeCombinator::remove($targetType, $type); diff --git a/src/Analyser/ExprHandler/Helper/ImplicitToStringCallHelper.php b/src/Analyser/ExprHandler/Helper/ImplicitToStringCallHelper.php index 58ed6c39ac6..d98ff8b4c49 100644 --- a/src/Analyser/ExprHandler/Helper/ImplicitToStringCallHelper.php +++ b/src/Analyser/ExprHandler/Helper/ImplicitToStringCallHelper.php @@ -9,6 +9,7 @@ use PHPStan\Analyser\ExpressionResultFactory; use PHPStan\Analyser\ImpurePoint; use PHPStan\Analyser\MutatingScope; +use PHPStan\Analyser\NodeScopeResolver; use PHPStan\DependencyInjection\AutowiredService; use PHPStan\Php\PhpVersion; use function sprintf; @@ -25,12 +26,14 @@ public function __construct( { } - public function processImplicitToStringCall(Expr $expr, MutatingScope $scope): ExpressionResult + public function processImplicitToStringCall(NodeScopeResolver $nodeScopeResolver, Expr $expr, MutatingScope $scope): ExpressionResult { $throwPoints = []; $impurePoints = []; - $exprType = $scope->getType($expr); + // the expression was processed before this call; read its stored result + // or price it on demand instead of re-walking via Scope::getType(). + $exprType = $nodeScopeResolver->readStoredOrPriceOnDemand($expr, $scope); $toStringMethod = null; if (!$exprType->isObject()->no()) { @@ -59,12 +62,16 @@ public function processImplicitToStringCall(Expr $expr, MutatingScope $scope): E } if ($this->phpVersion->throwsOnStringCast()) { + // the __toString() call is a synthetic node - price it on demand to + // resolve its return type instead of re-walking via Scope::getType(). + $toStringCall = new Expr\MethodCall($expr, new Identifier('__toString')); $throwPoint = $this->methodThrowPointHelper->getThrowPoint( $toStringMethod, $toStringMethod->getOnlyVariant(), - new Expr\MethodCall($expr, new Identifier('__toString')), + $toStringCall, $scope, ExpressionContext::createDeep(), + $nodeScopeResolver->priceSyntheticOnDemand($toStringCall, $scope), ); if ($throwPoint !== null) { $throwPoints[] = $throwPoint; diff --git a/src/Analyser/ExprHandler/Helper/MethodThrowPointHelper.php b/src/Analyser/ExprHandler/Helper/MethodThrowPointHelper.php index 08ff8735587..a786153f2d0 100644 --- a/src/Analyser/ExprHandler/Helper/MethodThrowPointHelper.php +++ b/src/Analyser/ExprHandler/Helper/MethodThrowPointHelper.php @@ -14,6 +14,7 @@ use PHPStan\Reflection\ParametersAcceptor; use PHPStan\Type\NeverType; use PHPStan\Type\ObjectType; +use PHPStan\Type\Type; use ReflectionFunction; use ReflectionMethod; use Throwable; @@ -31,12 +32,19 @@ public function __construct( { } + /** + * @param Type $methodCallReturnType the resolved return type of $normalizedMethodCall; + * passed in by the caller so this helper never asks Scope::getType() itself + * (the old-world call handlers resolve it directly, the new-world toString + * path prices the synthetic call on demand) + */ public function getThrowPoint( MethodReflection $methodReflection, ParametersAcceptor $parametersAcceptor, MethodCall|StaticCall $normalizedMethodCall, MutatingScope $scope, ExpressionContext $context, + Type $methodCallReturnType, ): ?InternalThrowPoint { if ($normalizedMethodCall instanceof MethodCall) { @@ -77,8 +85,7 @@ public function getThrowPoint( $throwType = $methodReflection->getThrowType(); if ($throwType === null) { - $returnType = $scope->getType($normalizedMethodCall); - if ($returnType instanceof NeverType && $returnType->isExplicit()) { + if ($methodCallReturnType instanceof NeverType && $methodCallReturnType->isExplicit()) { $throwType = new ObjectType(Throwable::class); } } @@ -88,8 +95,7 @@ public function getThrowPoint( return InternalThrowPoint::createExplicit($scope, $throwType, $normalizedMethodCall, true); } } elseif ($this->implicitThrows) { - $methodReturnedType = $scope->getType($normalizedMethodCall); - if (!$context->isInThrow() || !(new ObjectType(Throwable::class))->isSuperTypeOf($methodReturnedType)->yes()) { + if (!$context->isInThrow() || !(new ObjectType(Throwable::class))->isSuperTypeOf($methodCallReturnType)->yes()) { return InternalThrowPoint::createImplicit($scope, $normalizedMethodCall); } } diff --git a/src/Analyser/ExprHandler/Helper/NonNullabilityHelper.php b/src/Analyser/ExprHandler/Helper/NonNullabilityHelper.php index 79f5fe0b67c..c3eb36a1778 100644 --- a/src/Analyser/ExprHandler/Helper/NonNullabilityHelper.php +++ b/src/Analyser/ExprHandler/Helper/NonNullabilityHelper.php @@ -11,7 +11,7 @@ use PHPStan\Analyser\EnsuredNonNullabilityResult; use PHPStan\Analyser\EnsuredNonNullabilityResultExpression; use PHPStan\Analyser\MutatingScope; -use PHPStan\Analyser\Scope; +use PHPStan\Analyser\NodeScopeResolver; use PHPStan\DependencyInjection\AutowiredService; use PHPStan\TrinaryLogic; use PHPStan\Type\TypeCombinator; @@ -20,9 +20,12 @@ final class NonNullabilityHelper { - public function ensureShallowNonNullability(MutatingScope $scope, Scope $originalScope, Expr $exprToSpecify): EnsuredNonNullabilityResult + public function ensureShallowNonNullability(NodeScopeResolver $nodeScopeResolver, MutatingScope $scope, MutatingScope $originalScope, Expr $exprToSpecify): EnsuredNonNullabilityResult { - $exprType = $scope->getType($exprToSpecify); + // the expression has not been processed into the storage yet (this runs + // before processExprNode), so read its type from the stored result or + // price it on demand instead of re-walking via Scope::getType(). + $exprType = $nodeScopeResolver->readStoredOrPriceOnDemand($exprToSpecify, $scope); $isNull = $exprType->isNull(); if ($isNull->yes()) { return new EnsuredNonNullabilityResult($scope, []); @@ -32,9 +35,9 @@ public function ensureShallowNonNullability(MutatingScope $scope, Scope $origina $exprTypeWithoutNull = TypeCombinator::removeNull($exprType); if ($exprType->equals($exprTypeWithoutNull)) { - $originalExprType = $originalScope->getType($exprToSpecify); + $originalExprType = $nodeScopeResolver->readStoredOrPriceOnDemand($exprToSpecify, $originalScope); if (!$originalExprType->equals($exprTypeWithoutNull)) { - $originalNativeType = $originalScope->getNativeType($exprToSpecify); + $originalNativeType = $nodeScopeResolver->readStoredOrPriceOnDemand($exprToSpecify, $originalScope->doNotTreatPhpDocTypesAsCertain()); return new EnsuredNonNullabilityResult($scope, [ new EnsuredNonNullabilityResultExpression($exprToSpecify, $originalExprType, $originalNativeType, $hasExpressionType), @@ -52,8 +55,8 @@ public function ensureShallowNonNullability(MutatingScope $scope, Scope $origina $parentExpr = $exprToSpecify->var; $specifiedExpressions[] = new EnsuredNonNullabilityResultExpression( $parentExpr, - $scope->getType($parentExpr), - $scope->getNativeType($parentExpr), + $nodeScopeResolver->readStoredOrPriceOnDemand($parentExpr, $scope), + $nodeScopeResolver->readStoredOrPriceOnDemand($parentExpr, $scope->doNotTreatPhpDocTypesAsCertain()), $originalScope->hasExpressionType($parentExpr), ); } @@ -64,7 +67,7 @@ public function ensureShallowNonNullability(MutatingScope $scope, Scope $origina $certainty = $hasExpressionType; } - $nativeType = $scope->getNativeType($exprToSpecify); + $nativeType = $nodeScopeResolver->readStoredOrPriceOnDemand($exprToSpecify, $scope->doNotTreatPhpDocTypesAsCertain()); $specifiedExpressions[] = new EnsuredNonNullabilityResultExpression($exprToSpecify, $exprType, $nativeType, $certainty); $scope = $scope->specifyExpressionType( $exprToSpecify, @@ -79,12 +82,12 @@ public function ensureShallowNonNullability(MutatingScope $scope, Scope $origina ); } - public function ensureNonNullability(MutatingScope $scope, Expr $expr): EnsuredNonNullabilityResult + public function ensureNonNullability(NodeScopeResolver $nodeScopeResolver, MutatingScope $scope, Expr $expr): EnsuredNonNullabilityResult { $specifiedExpressions = []; $originalScope = $scope; - $scope = $this->lookForExpressionCallback($scope, $expr, function ($scope, $expr) use (&$specifiedExpressions, $originalScope) { - $result = $this->ensureShallowNonNullability($scope, $originalScope, $expr); + $scope = $this->lookForExpressionCallback($scope, $expr, function ($scope, $expr) use (&$specifiedExpressions, $originalScope, $nodeScopeResolver) { + $result = $this->ensureShallowNonNullability($nodeScopeResolver, $scope, $originalScope, $expr); foreach ($result->getSpecifiedExpressions() as $specifiedExpression) { $specifiedExpressions[] = $specifiedExpression; } diff --git a/src/Analyser/ExprHandler/InterpolatedStringHandler.php b/src/Analyser/ExprHandler/InterpolatedStringHandler.php index cf7975d713f..98d5d7868cb 100644 --- a/src/Analyser/ExprHandler/InterpolatedStringHandler.php +++ b/src/Analyser/ExprHandler/InterpolatedStringHandler.php @@ -63,7 +63,7 @@ public function processExpr(NodeScopeResolver $nodeScopeResolver, Stmt $stmt, Ex $throwPoints = array_merge($throwPoints, $partResult->getThrowPoints()); $impurePoints = array_merge($impurePoints, $partResult->getImpurePoints()); - $toStringResult = $this->implicitToStringCallHelper->processImplicitToStringCall($part, $scope); + $toStringResult = $this->implicitToStringCallHelper->processImplicitToStringCall($nodeScopeResolver, $part, $scope); $throwPoints = array_merge($throwPoints, $toStringResult->getThrowPoints()); $impurePoints = array_merge($impurePoints, $toStringResult->getImpurePoints()); diff --git a/src/Analyser/ExprHandler/IssetHandler.php b/src/Analyser/ExprHandler/IssetHandler.php index 7d379b16556..89589a74bfb 100644 --- a/src/Analyser/ExprHandler/IssetHandler.php +++ b/src/Analyser/ExprHandler/IssetHandler.php @@ -353,7 +353,7 @@ public function processExpr(NodeScopeResolver $nodeScopeResolver, Stmt $stmt, Ex $nonNullabilityResults = []; $isAlwaysTerminating = false; foreach ($expr->vars as $var) { - $nonNullabilityResult = $this->nonNullabilityHelper->ensureNonNullability($scope, $var); + $nonNullabilityResult = $this->nonNullabilityHelper->ensureNonNullability($nodeScopeResolver, $scope, $var); $scope = $nodeScopeResolver->lookForSetAllowedUndefinedExpressions($nonNullabilityResult->getScope(), $var); $varResult = $nodeScopeResolver->processExprNode($stmt, $var, $scope, $storage, $nodeCallback, $context->enterDeep()); $scope = $varResult->getScope(); diff --git a/src/Analyser/ExprHandler/MethodCallHandler.php b/src/Analyser/ExprHandler/MethodCallHandler.php index d538a176625..171f3f825bf 100644 --- a/src/Analyser/ExprHandler/MethodCallHandler.php +++ b/src/Analyser/ExprHandler/MethodCallHandler.php @@ -154,7 +154,7 @@ public function processExpr(NodeScopeResolver $nodeScopeResolver, Stmt $stmt, Ex $nodeScopeResolver->processDroppedArgs($stmt, $expr, $normalizedExpr, $scope, $storage, $context); if ($methodReflection !== null) { - $methodThrowPoint = $this->methodThrowPointHelper->getThrowPoint($methodReflection, $parametersAcceptor, $normalizedExpr, $scope, $context); + $methodThrowPoint = $this->methodThrowPointHelper->getThrowPoint($methodReflection, $parametersAcceptor, $normalizedExpr, $scope, $context, $scope->getType($normalizedExpr)); if ($methodThrowPoint !== null) { $throwPoints[] = $methodThrowPoint; } diff --git a/src/Analyser/ExprHandler/NullsafeMethodCallHandler.php b/src/Analyser/ExprHandler/NullsafeMethodCallHandler.php index 360200ecf02..5f7f089a314 100644 --- a/src/Analyser/ExprHandler/NullsafeMethodCallHandler.php +++ b/src/Analyser/ExprHandler/NullsafeMethodCallHandler.php @@ -55,7 +55,7 @@ public function processExpr(NodeScopeResolver $nodeScopeResolver, Stmt $stmt, Ex $beforeScope = $scope; $scopeBeforeNullsafe = $scope; - $nonNullabilityResult = $this->nonNullabilityHelper->ensureShallowNonNullability($scope, $scope, $expr->var); + $nonNullabilityResult = $this->nonNullabilityHelper->ensureShallowNonNullability($nodeScopeResolver, $scope, $scope, $expr->var); $attributes = array_merge($expr->getAttributes(), ['virtualNullsafeMethodCall' => true]); unset($attributes[ExprPrinter::ATTRIBUTE_CACHE_KEY]); $methodCall = new MethodCall( diff --git a/src/Analyser/ExprHandler/NullsafePropertyFetchHandler.php b/src/Analyser/ExprHandler/NullsafePropertyFetchHandler.php index 5085c6c9637..1a3dd160d41 100644 --- a/src/Analyser/ExprHandler/NullsafePropertyFetchHandler.php +++ b/src/Analyser/ExprHandler/NullsafePropertyFetchHandler.php @@ -53,7 +53,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 { $beforeScope = $scope; - $nonNullabilityResult = $this->nonNullabilityHelper->ensureShallowNonNullability($scope, $scope, $expr->var); + $nonNullabilityResult = $this->nonNullabilityHelper->ensureShallowNonNullability($nodeScopeResolver, $scope, $scope, $expr->var); $attributes = array_merge($expr->getAttributes(), ['virtualNullsafePropertyFetch' => true]); unset($attributes[ExprPrinter::ATTRIBUTE_CACHE_KEY]); $propertyFetch = new PropertyFetch( diff --git a/src/Analyser/ExprHandler/PrintHandler.php b/src/Analyser/ExprHandler/PrintHandler.php index 37172802e83..95b8b2dc76d 100644 --- a/src/Analyser/ExprHandler/PrintHandler.php +++ b/src/Analyser/ExprHandler/PrintHandler.php @@ -48,7 +48,7 @@ public function processExpr(NodeScopeResolver $nodeScopeResolver, Stmt $stmt, Ex $throwPoints = $exprResult->getThrowPoints(); $impurePoints = $exprResult->getImpurePoints(); - $toStringResult = $this->implicitToStringCallHelper->processImplicitToStringCall($expr->expr, $scope); + $toStringResult = $this->implicitToStringCallHelper->processImplicitToStringCall($nodeScopeResolver, $expr->expr, $scope); $throwPoints = array_merge($throwPoints, $toStringResult->getThrowPoints()); $impurePoints = array_merge($impurePoints, $toStringResult->getImpurePoints()); diff --git a/src/Analyser/ExprHandler/StaticCallHandler.php b/src/Analyser/ExprHandler/StaticCallHandler.php index b99e49f9e93..7bc5282cdd4 100644 --- a/src/Analyser/ExprHandler/StaticCallHandler.php +++ b/src/Analyser/ExprHandler/StaticCallHandler.php @@ -223,7 +223,7 @@ public function processExpr(NodeScopeResolver $nodeScopeResolver, Stmt $stmt, Ex $scopeFunction = $scope->getFunction(); if ($methodReflection !== null) { - $methodThrowPoint = $this->methodThrowPointHelper->getThrowPoint($methodReflection, $parametersAcceptor, $normalizedExpr, $scope, $context); + $methodThrowPoint = $this->methodThrowPointHelper->getThrowPoint($methodReflection, $parametersAcceptor, $normalizedExpr, $scope, $context, $scope->getType($normalizedExpr)); if ($methodThrowPoint !== null) { $throwPoints[] = $methodThrowPoint; } diff --git a/src/Analyser/NodeScopeResolver.php b/src/Analyser/NodeScopeResolver.php index 25114e5e74b..1af57d9f7cf 100644 --- a/src/Analyser/NodeScopeResolver.php +++ b/src/Analyser/NodeScopeResolver.php @@ -1153,7 +1153,7 @@ public function processStmtNode( $result = $this->processExprNode($stmt, $echoExpr, $scope, $storage, $nodeCallback, ExpressionContext::createDeep()); $throwPoints = array_merge($throwPoints, $result->getThrowPoints()); $impurePoints = array_merge($impurePoints, $result->getImpurePoints()); - $toStringResult = $this->implicitToStringCallHelper->processImplicitToStringCall($echoExpr, $scope); + $toStringResult = $this->implicitToStringCallHelper->processImplicitToStringCall($this, $echoExpr, $scope); $throwPoints = array_merge($throwPoints, $toStringResult->getThrowPoints()); $impurePoints = array_merge($impurePoints, $toStringResult->getImpurePoints()); $scope = $result->getScope(); From 214869a3e8cdaeff6474dc03932aeeed622fd6bb Mon Sep 17 00:00:00 2001 From: Ondrej Mirtes Date: Thu, 18 Jun 2026 18:41:23 +0200 Subject: [PATCH 095/113] Read child/synthetic types via results or helpers in AssignHandler's assign machinery --- src/Analyser/ExprHandler/AssignHandler.php | 179 +++++++++++---------- src/Analyser/NodeScopeResolver.php | 20 +++ 2 files changed, 113 insertions(+), 86 deletions(-) diff --git a/src/Analyser/ExprHandler/AssignHandler.php b/src/Analyser/ExprHandler/AssignHandler.php index 359308ce2d9..6b0379bf755 100644 --- a/src/Analyser/ExprHandler/AssignHandler.php +++ b/src/Analyser/ExprHandler/AssignHandler.php @@ -187,8 +187,8 @@ function (MutatingScope $scope) use ($stmt, $expr, $nodeCallback, $context, $sto ) { $varName = $expr->var->name; $refName = $expr->expr->name; - $type = $scope->getType($expr->var); - $nativeType = $scope->getNativeType($expr->var); + $type = $nodeScopeResolver->readStoredOrPriceOnDemand($expr->var, $scope); + $nativeType = $nodeScopeResolver->readStoredOrPriceOnDemandNative($expr->var, $scope); // When $varName is assigned, update $refName $scope = $scope->assignExpression( @@ -222,8 +222,8 @@ function (MutatingScope $scope) use ($stmt, $expr, $nodeCallback, $context, $sto isAlwaysTerminating: $result->isAlwaysTerminating(), throwPoints: $result->getThrowPoints(), impurePoints: $result->getImpurePoints(), - typeCallback: static fn (MutatingScope $s): Type => $assignedExprResult !== null ? $assignedExprResult->getTypeForScope($s) : $s->getType($expr->expr), - specifyTypesCallback: $expr instanceof Assign ? $this->createSpecifyTypesCallback($expr, $assignedExprResult) : null, + typeCallback: static fn (MutatingScope $s): Type => $assignedExprResult !== null ? $assignedExprResult->getTypeForScope($s) : $nodeScopeResolver->readStoredOrPriceOnDemand($expr->expr, $s), + specifyTypesCallback: $expr instanceof Assign ? $this->createSpecifyTypesCallback($nodeScopeResolver, $expr, $assignedExprResult) : null, createTypesCallback: $expr instanceof Assign ? $this->createCreateTypesCallback($expr, $assignedExprResult) : null, ); } @@ -256,9 +256,9 @@ private function createCreateTypesCallback(Assign $expr, ?ExpressionResult $assi * * @return Closure(MutatingScope, TypeSpecifierContext): SpecifiedTypes */ - private function createSpecifyTypesCallback(Assign $expr, ?ExpressionResult $assignedExprResult): Closure + private function createSpecifyTypesCallback(NodeScopeResolver $nodeScopeResolver, Assign $expr, ?ExpressionResult $assignedExprResult): Closure { - return function (MutatingScope $s, TypeSpecifierContext $context) use ($expr, $assignedExprResult): SpecifiedTypes { + return function (MutatingScope $s, TypeSpecifierContext $context) use ($nodeScopeResolver, $expr, $assignedExprResult): SpecifiedTypes { if ($context->null()) { $specifiedTypes = $this->defaultNarrowingHelper->getChildSpecifiedTypes($s->exitFirstLevelStatements(), $expr->expr, $assignedExprResult, $context)->setRootExpr($expr); $specifiedTypes = $specifiedTypes->removeExpr($this->exprPrinter->printExpr($expr->var)); @@ -275,7 +275,7 @@ private function createSpecifyTypesCallback(Assign $expr, ?ExpressionResult $ass && count($expr->expr->getArgs()) >= 1 ) { $arrayArg = $expr->expr->getArgs()[0]->value; - $arrayType = $s->getType($arrayArg); + $arrayType = $nodeScopeResolver->readStoredOrPriceOnDemand($arrayArg, $s); if ($arrayType->isArray()->yes()) { if ($context->true()) { @@ -293,7 +293,7 @@ private function createSpecifyTypesCallback(Assign $expr, ?ExpressionResult $ass $this->defaultNarrowingHelper->createSubjectTypes($s, $dimFetch, null, $arrayType->getIterableValueType(), TypeSpecifierContext::createTrue()), ); } elseif ($expr->var instanceof Variable && is_string($expr->var->name)) { - $keyType = $s->getType($expr->expr); + $keyType = $nodeScopeResolver->readStoredOrPriceOnDemand($expr->expr, $s); $nonNullKeyType = TypeCombinator::removeNull($keyType); if (!$nonNullKeyType instanceof NeverType) { $specifiedTypes = $specifiedTypes->unionWith( @@ -319,14 +319,14 @@ private function createSpecifyTypesCallback(Assign $expr, ?ExpressionResult $ass if ($funcName === 'array_search') { $arrayArg = $expr->expr->getArgs()[1]->value; $sentinelType = new ConstantBooleanType(false); - $isStrictArraySearch = count($expr->expr->getArgs()) >= 3 && $s->getType($expr->expr->getArgs()[2]->value)->isTrue()->yes(); + $isStrictArraySearch = count($expr->expr->getArgs()) >= 3 && $nodeScopeResolver->readStoredOrPriceOnDemand($expr->expr->getArgs()[2]->value, $s)->isTrue()->yes(); } elseif ($funcName === 'array_find_key') { $arrayArg = $expr->expr->getArgs()[0]->value; $sentinelType = new NullType(); } if ($arrayArg !== null) { - $arrayType = $s->getType($arrayArg); + $arrayType = $nodeScopeResolver->readStoredOrPriceOnDemand($arrayArg, $s); if ($arrayType->isArray()->yes()) { if ($context->true()) { @@ -337,7 +337,7 @@ private function createSpecifyTypesCallback(Assign $expr, ?ExpressionResult $ass $dimFetch = new ArrayDimFetch($arrayArg, $expr->var); if ($isStrictArraySearch) { - $needleType = $s->getType($expr->expr->getArgs()[0]->value); + $needleType = $nodeScopeResolver->readStoredOrPriceOnDemand($expr->expr->getArgs()[0]->value, $s); $dimFetchType = TypeCombinator::intersect($needleType, $arrayType->getIterableValueType()); } else { $dimFetchType = $arrayType->getIterableValueType(); @@ -347,11 +347,11 @@ private function createSpecifyTypesCallback(Assign $expr, ?ExpressionResult $ass $this->defaultNarrowingHelper->createSubjectTypes($s, $dimFetch, null, $dimFetchType, TypeSpecifierContext::createTrue()), ); } elseif ($expr->var instanceof Variable && is_string($expr->var->name)) { - $keyType = $s->getType($expr->expr); + $keyType = $nodeScopeResolver->readStoredOrPriceOnDemand($expr->expr, $s); $narrowedKeyType = TypeCombinator::remove($keyType, $sentinelType); if (!$narrowedKeyType instanceof NeverType) { if ($isStrictArraySearch) { - $needleType = $s->getType($expr->expr->getArgs()[0]->value); + $needleType = $nodeScopeResolver->readStoredOrPriceOnDemand($expr->expr->getArgs()[0]->value, $s); $dimFetchType = TypeCombinator::intersect($needleType, $arrayType->getIterableValueType()); } else { $dimFetchType = $arrayType->getIterableValueType(); @@ -381,12 +381,12 @@ private function createSpecifyTypesCallback(Assign $expr, ?ExpressionResult $ass $numArg = $args[1]->value; } $one = new ConstantIntegerType(1); - $arrayType = $s->getType($arrayArg); + $arrayType = $nodeScopeResolver->readStoredOrPriceOnDemand($arrayArg, $s); if ( $arrayType->isArray()->yes() && $arrayType->isIterableAtLeastOnce()->yes() - && ($numArg === null || $one->isSuperTypeOf($s->getType($numArg))->yes()) + && ($numArg === null || $one->isSuperTypeOf($nodeScopeResolver->readStoredOrPriceOnDemand($numArg, $s))->yes()) ) { $dimFetch = new ArrayDimFetch($arrayArg, $expr->var); @@ -408,7 +408,7 @@ private function createSpecifyTypesCallback(Assign $expr, ?ExpressionResult $ass && count($expr->expr->left->getArgs()) >= 1 ) { $arrayArg = $expr->expr->left->getArgs()[0]->value; - $arrayType = $s->getType($arrayArg); + $arrayType = $nodeScopeResolver->readStoredOrPriceOnDemand($arrayArg, $s); if ( $arrayType->isList()->yes() && $arrayType->isIterableAtLeastOnce()->yes() @@ -463,7 +463,7 @@ public function processAssignVar( $impurePoints[] = new ImpurePoint($scopeBeforeAssignEval, $var, 'superglobal', 'assign to superglobal variable', true); } $assignedExpr = $this->unwrapAssign($assignedExpr); - $type = $scopeBeforeAssignEval->getType($assignedExpr); + $type = $nodeScopeResolver->readStoredOrPriceOnDemand($assignedExpr, $scopeBeforeAssignEval); $conditionalExpressions = []; if ($assignedExpr instanceof Ternary) { @@ -476,17 +476,17 @@ public function processAssignVar( $falseySpecifiedTypes = $this->typeSpecifier->specifyTypesInCondition($condScope, $assignedExpr->cond, TypeSpecifierContext::createFalsey()); $truthyScope = $condScope->filterBySpecifiedTypes($truthySpecifiedTypes); $falsyScope = $condScope->filterBySpecifiedTypes($falseySpecifiedTypes); - $truthyType = $truthyScope->getType($if); - $falseyType = $falsyScope->getType($assignedExpr->else); + $truthyType = $nodeScopeResolver->readStoredOrPriceOnDemand($if, $truthyScope); + $falseyType = $nodeScopeResolver->readStoredOrPriceOnDemand($assignedExpr->else, $falsyScope); if ( $truthyType->isSuperTypeOf($falseyType)->no() && $falseyType->isSuperTypeOf($truthyType)->no() ) { - $conditionalExpressions = $this->processSureTypesForConditionalExpressionsAfterAssign($condScope, $var->name, $conditionalExpressions, $truthySpecifiedTypes, $truthyType, $impurePoints, $assignedExpr); - $conditionalExpressions = $this->processSureNotTypesForConditionalExpressionsAfterAssign($condScope, $var->name, $conditionalExpressions, $truthySpecifiedTypes, $truthyType, $impurePoints, $assignedExpr); - $conditionalExpressions = $this->processSureTypesForConditionalExpressionsAfterAssign($condScope, $var->name, $conditionalExpressions, $falseySpecifiedTypes, $falseyType, $impurePoints, $assignedExpr); - $conditionalExpressions = $this->processSureNotTypesForConditionalExpressionsAfterAssign($condScope, $var->name, $conditionalExpressions, $falseySpecifiedTypes, $falseyType, $impurePoints, $assignedExpr); + $conditionalExpressions = $this->processSureTypesForConditionalExpressionsAfterAssign($nodeScopeResolver, $condScope, $var->name, $conditionalExpressions, $truthySpecifiedTypes, $truthyType, $impurePoints, $assignedExpr); + $conditionalExpressions = $this->processSureNotTypesForConditionalExpressionsAfterAssign($nodeScopeResolver, $condScope, $var->name, $conditionalExpressions, $truthySpecifiedTypes, $truthyType, $impurePoints, $assignedExpr); + $conditionalExpressions = $this->processSureTypesForConditionalExpressionsAfterAssign($nodeScopeResolver, $condScope, $var->name, $conditionalExpressions, $falseySpecifiedTypes, $falseyType, $impurePoints, $assignedExpr); + $conditionalExpressions = $this->processSureNotTypesForConditionalExpressionsAfterAssign($nodeScopeResolver, $condScope, $var->name, $conditionalExpressions, $falseySpecifiedTypes, $falseyType, $impurePoints, $assignedExpr); } } @@ -500,13 +500,13 @@ public function processAssignVar( $truthyType = TypeCombinator::removeFalsey($type); if ($truthyType !== $type) { $truthySpecifiedTypes = $this->typeSpecifier->specifyTypesInCondition($scope, $assignedExpr, TypeSpecifierContext::createTruthy()); - $conditionalExpressions = $this->processSureTypesForConditionalExpressionsAfterAssign($scope, $var->name, $conditionalExpressions, $truthySpecifiedTypes, $truthyType, $impurePoints, $assignedExpr); - $conditionalExpressions = $this->processSureNotTypesForConditionalExpressionsAfterAssign($scope, $var->name, $conditionalExpressions, $truthySpecifiedTypes, $truthyType, $impurePoints, $assignedExpr); + $conditionalExpressions = $this->processSureTypesForConditionalExpressionsAfterAssign($nodeScopeResolver, $scope, $var->name, $conditionalExpressions, $truthySpecifiedTypes, $truthyType, $impurePoints, $assignedExpr); + $conditionalExpressions = $this->processSureNotTypesForConditionalExpressionsAfterAssign($nodeScopeResolver, $scope, $var->name, $conditionalExpressions, $truthySpecifiedTypes, $truthyType, $impurePoints, $assignedExpr); $falseyType = TypeCombinator::intersect($type, StaticTypeFactory::falsey()); $falseySpecifiedTypes = $this->typeSpecifier->specifyTypesInCondition($scope, $assignedExpr, TypeSpecifierContext::createFalsey()); - $conditionalExpressions = $this->processSureTypesForConditionalExpressionsAfterAssign($scope, $var->name, $conditionalExpressions, $falseySpecifiedTypes, $falseyType, $impurePoints, $assignedExpr); - $conditionalExpressions = $this->processSureNotTypesForConditionalExpressionsAfterAssign($scope, $var->name, $conditionalExpressions, $falseySpecifiedTypes, $falseyType, $impurePoints, $assignedExpr); + $conditionalExpressions = $this->processSureTypesForConditionalExpressionsAfterAssign($nodeScopeResolver, $scope, $var->name, $conditionalExpressions, $falseySpecifiedTypes, $falseyType, $impurePoints, $assignedExpr); + $conditionalExpressions = $this->processSureNotTypesForConditionalExpressionsAfterAssign($nodeScopeResolver, $scope, $var->name, $conditionalExpressions, $falseySpecifiedTypes, $falseyType, $impurePoints, $assignedExpr); } foreach ([null, false, 0, 0.0, '', '0', []] as $falseyScalar) { @@ -535,23 +535,23 @@ public function processAssignVar( $notIdenticalConditionExpr = new Expr\BinaryOp\NotIdentical($assignedExpr, $astNode); $notIdenticalSpecifiedTypes = $this->typeSpecifier->specifyTypesInCondition($scope, $notIdenticalConditionExpr, TypeSpecifierContext::createTrue()); - $conditionalExpressions = $this->processSureTypesForConditionalExpressionsAfterAssign($scope, $var->name, $conditionalExpressions, $notIdenticalSpecifiedTypes, $withoutFalseyType, $impurePoints, $assignedExpr); - $conditionalExpressions = $this->processSureNotTypesForConditionalExpressionsAfterAssign($scope, $var->name, $conditionalExpressions, $notIdenticalSpecifiedTypes, $withoutFalseyType, $impurePoints, $assignedExpr); + $conditionalExpressions = $this->processSureTypesForConditionalExpressionsAfterAssign($nodeScopeResolver, $scope, $var->name, $conditionalExpressions, $notIdenticalSpecifiedTypes, $withoutFalseyType, $impurePoints, $assignedExpr); + $conditionalExpressions = $this->processSureNotTypesForConditionalExpressionsAfterAssign($nodeScopeResolver, $scope, $var->name, $conditionalExpressions, $notIdenticalSpecifiedTypes, $withoutFalseyType, $impurePoints, $assignedExpr); $identicalConditionExpr = new Expr\BinaryOp\Identical($assignedExpr, $astNode); $identicalSpecifiedTypes = $this->typeSpecifier->specifyTypesInCondition($scope, $identicalConditionExpr, TypeSpecifierContext::createTrue()); - $conditionalExpressions = $this->processSureTypesForConditionalExpressionsAfterAssign($scope, $var->name, $conditionalExpressions, $identicalSpecifiedTypes, $falseyType, $impurePoints, $assignedExpr); - $conditionalExpressions = $this->processSureNotTypesForConditionalExpressionsAfterAssign($scope, $var->name, $conditionalExpressions, $identicalSpecifiedTypes, $falseyType, $impurePoints, $assignedExpr); + $conditionalExpressions = $this->processSureTypesForConditionalExpressionsAfterAssign($nodeScopeResolver, $scope, $var->name, $conditionalExpressions, $identicalSpecifiedTypes, $falseyType, $impurePoints, $assignedExpr); + $conditionalExpressions = $this->processSureNotTypesForConditionalExpressionsAfterAssign($nodeScopeResolver, $scope, $var->name, $conditionalExpressions, $identicalSpecifiedTypes, $falseyType, $impurePoints, $assignedExpr); } $nodeScopeResolver->callNodeCallback($nodeCallback, new VariableAssignNode($var, $assignedExpr), $scopeBeforeAssignEval, $storage); - $scope = $scope->assignVariable($var->name, $type, $scope->getNativeType($assignedExpr), TrinaryLogic::createYes()); + $scope = $scope->assignVariable($var->name, $type, $nodeScopeResolver->readStoredOrPriceOnDemandNative($assignedExpr, $scope), TrinaryLogic::createYes()); foreach ($conditionalExpressions as $exprString => $holders) { $scope = $scope->addConditionalExpressions((string) $exprString, $holders); } if ($assignedExpr instanceof Expr\Array_) { - $scope = $this->processArrayByRefItems($scope, $var->name, $assignedExpr, new Variable($var->name)); + $scope = $this->processArrayByRefItems($nodeScopeResolver, $scope, $var->name, $assignedExpr, new Variable($var->name)); } } else { $nameExprResult = $nodeScopeResolver->processExprNode($stmt, $var->name, $scope, $storage, $nodeCallback, $context); @@ -568,7 +568,7 @@ public function processAssignVar( while ($var instanceof ArrayDimFetch) { $varForSetOffsetValue = $var->var; if ($varForSetOffsetValue instanceof PropertyFetch || $varForSetOffsetValue instanceof StaticPropertyFetch) { - $varForSetOffsetValue = new TypeExpr($this->getOriginalPropertyType($varForSetOffsetValue, $scope)); + $varForSetOffsetValue = new TypeExpr($this->getOriginalPropertyType($nodeScopeResolver, $varForSetOffsetValue, $scope)); } if ( @@ -634,8 +634,8 @@ public function processAssignVar( )); } else { - $offsetTypes[] = [$scope->getType($dimExpr), $dimFetch]; - $offsetNativeTypes[] = [$scope->getNativeType($dimExpr), $dimFetch]; + $offsetTypes[] = [$nodeScopeResolver->readStoredOrPriceOnDemand($dimExpr, $scope), $dimFetch]; + $offsetNativeTypes[] = [$nodeScopeResolver->readStoredOrPriceOnDemandNative($dimExpr, $scope), $dimFetch]; if ($enterExpressionAssign) { $scope->enterExpressionAssign($dimExpr); @@ -648,7 +648,7 @@ public function processAssignVar( isAlwaysTerminating: false, throwPoints: [], impurePoints: [], - typeCallback: static fn (MutatingScope $s): Type => $s->getType($dimFetch->var)->getOffsetValueType($s->getType($dimExpr)), + typeCallback: static fn (MutatingScope $s): Type => $nodeScopeResolver->readStoredOrPriceOnDemand($dimFetch->var, $s)->getOffsetValueType($nodeScopeResolver->readStoredOrPriceOnDemand($dimExpr, $s)), )); $result = $nodeScopeResolver->processExprNode($stmt, $dimExpr, $scope, $storage, $nodeCallback, $context->enterDeep()); $hasYield = $hasYield || $result->hasYield(); @@ -661,6 +661,13 @@ public function processAssignVar( } } + // SKIPPED (single-pass inside-out invariant): these two reads must stay as + // Scope::getType()/getNativeType(). Unlike the non-caching helpers, + // Scope::getType() memoises the assigned expression's sub-expression types + // onto $scope (e.g. hasExpressionType() for the array-dim-fetch being + // written). produceArrayDimFetchAssignValueToWrite() below relies on that + // memoised state to keep a freshly-coalesced offset optional - replacing + // these with the helpers regresses bug-13623 ($x[...] ??= [] chains). $valueToWrite = $scope->getType($assignedExpr); $nativeValueToWrite = $scope->getNativeType($assignedExpr); $scopeBeforeAssignEval = $scope; @@ -673,8 +680,8 @@ public function processAssignVar( $isAlwaysTerminating = $isAlwaysTerminating || $result->isAlwaysTerminating(); $scope = $result->getScope(); - $varType = $scope->getType($var); - $varNativeType = $scope->getNativeType($var); + $varType = $nodeScopeResolver->readStoredOrPriceOnDemand($var, $scope); + $varNativeType = $nodeScopeResolver->readStoredOrPriceOnDemandNative($var, $scope); // 4. compose types $isImplicitArrayCreation = $this->isImplicitArrayCreation($dimFetchStack, $scope); @@ -685,10 +692,10 @@ public function processAssignVar( $offsetValueType = $varType; $offsetNativeValueType = $varNativeType; - [$valueToWrite, $additionalExpressions] = $this->produceArrayDimFetchAssignValueToWrite($dimFetchStack, $offsetTypes, $offsetValueType, $valueToWrite, $scope); + [$valueToWrite, $additionalExpressions] = $this->produceArrayDimFetchAssignValueToWrite($nodeScopeResolver, $dimFetchStack, $offsetTypes, $offsetValueType, $valueToWrite, $scope); if (!$offsetValueType->equals($offsetNativeValueType) || !$valueToWrite->equals($nativeValueToWrite)) { - [$nativeValueToWrite, $additionalNativeExpressions] = $this->produceArrayDimFetchAssignValueToWrite($dimFetchStack, $offsetNativeTypes, $offsetNativeValueType, $nativeValueToWrite, $scope); + [$nativeValueToWrite, $additionalNativeExpressions] = $this->produceArrayDimFetchAssignValueToWrite($nodeScopeResolver, $dimFetchStack, $offsetNativeTypes, $offsetNativeValueType, $nativeValueToWrite, $scope); } else { $rewritten = false; foreach ($offsetTypes as $i => [$offsetType]) { @@ -707,7 +714,7 @@ public function processAssignVar( continue; } - [$nativeValueToWrite] = $this->produceArrayDimFetchAssignValueToWrite($dimFetchStack, $offsetNativeTypes, $offsetNativeValueType, $nativeValueToWrite, $scope); + [$nativeValueToWrite] = $this->produceArrayDimFetchAssignValueToWrite($nodeScopeResolver, $dimFetchStack, $offsetNativeTypes, $offsetNativeValueType, $nativeValueToWrite, $scope); $rewritten = true; break; } @@ -725,7 +732,7 @@ public function processAssignVar( if ($var instanceof PropertyFetch || $var instanceof StaticPropertyFetch) { $nodeScopeResolver->callNodeCallback($nodeCallback, new PropertyAssignNode($var, $assignedPropertyExpr, $isAssignOp), $scopeBeforeAssignEval, $storage); if ($var instanceof PropertyFetch && $var->name instanceof Node\Identifier && !$isAssignOp) { - $scope = $scope->assignInitializedProperty($scope->getType($var->var), $var->name->toString()); + $scope = $scope->assignInitializedProperty($nodeScopeResolver->readStoredOrPriceOnDemand($var->var, $scope), $var->name->toString()); } } $scope = $scope->assignExpression( @@ -740,7 +747,7 @@ public function processAssignVar( } elseif ($var instanceof PropertyFetch || $var instanceof StaticPropertyFetch) { $nodeScopeResolver->callNodeCallback($nodeCallback, new PropertyAssignNode($var, $assignedPropertyExpr, $isAssignOp), $scopeBeforeAssignEval, $storage); if ($var instanceof PropertyFetch && $var->name instanceof Node\Identifier && !$isAssignOp) { - $scope = $scope->assignInitializedProperty($scope->getType($var->var), $var->name->toString()); + $scope = $scope->assignInitializedProperty($nodeScopeResolver->readStoredOrPriceOnDemand($var->var, $scope), $var->name->toString()); } } } @@ -755,7 +762,7 @@ public function processAssignVar( $scope = $scope->assignExpression($expr, $type, $nativeType); } - $setVarType = $scope->getType($originalVar->var); + $setVarType = $nodeScopeResolver->readStoredOrPriceOnDemand($originalVar->var, $scope); if ( !$setVarType instanceof ErrorType && !$setVarType->isArray()->yes() @@ -802,10 +809,10 @@ public function processAssignVar( $throwPoints[] = InternalThrowPoint::createImplicit($scope, $var); } - $propertyHolderType = $scope->getType($var->var); + $propertyHolderType = $nodeScopeResolver->readStoredOrPriceOnDemand($var->var, $scope); if ($propertyName !== null && $propertyHolderType->hasInstanceProperty($propertyName)->yes()) { $propertyReflection = $propertyHolderType->getInstanceProperty($propertyName, $scope); - $assignedExprType = $scope->getType($assignedExpr); + $assignedExprType = $nodeScopeResolver->readStoredOrPriceOnDemand($assignedExpr, $scope); $nodeScopeResolver->callNodeCallback($nodeCallback, new PropertyAssignNode($var, $assignedExpr, $isAssignOp), $scopeBeforeAssignEval, $storage); if ($propertyReflection->canChangeTypeAfterAssignment()) { if ($propertyReflection->hasNativeType()) { @@ -822,16 +829,16 @@ public function processAssignVar( } if ($assignedTypeIsCompatible) { - $scope = $scope->assignExpression($var, $assignedExprType, $scope->getNativeType($assignedExpr)); + $scope = $scope->assignExpression($var, $assignedExprType, $nodeScopeResolver->readStoredOrPriceOnDemandNative($assignedExpr, $scope)); } else { $scope = $scope->assignExpression( $var, TypeCombinator::intersect($assignedExprType->toCoercedArgumentType($scope->isDeclareStrictTypes()), $propertyNativeType), - TypeCombinator::intersect($scope->getNativeType($assignedExpr)->toCoercedArgumentType($scope->isDeclareStrictTypes()), $propertyNativeType), + TypeCombinator::intersect($nodeScopeResolver->readStoredOrPriceOnDemandNative($assignedExpr, $scope)->toCoercedArgumentType($scope->isDeclareStrictTypes()), $propertyNativeType), ); } } else { - $scope = $scope->assignExpression($var, $assignedExprType, $scope->getNativeType($assignedExpr)); + $scope = $scope->assignExpression($var, $assignedExprType, $nodeScopeResolver->readStoredOrPriceOnDemandNative($assignedExpr, $scope)); } } $declaringClass = $propertyReflection->getDeclaringClass(); @@ -866,9 +873,9 @@ public function processAssignVar( } } else { // fallback - $assignedExprType = $scope->getType($assignedExpr); + $assignedExprType = $nodeScopeResolver->readStoredOrPriceOnDemand($assignedExpr, $scope); $nodeScopeResolver->callNodeCallback($nodeCallback, new PropertyAssignNode($var, $assignedExpr, $isAssignOp), $scopeBeforeAssignEval, $storage); - $scope = $scope->assignExpression($var, $assignedExprType, $scope->getNativeType($assignedExpr)); + $scope = $scope->assignExpression($var, $assignedExprType, $nodeScopeResolver->readStoredOrPriceOnDemandNative($assignedExpr, $scope)); // simulate dynamic property assign by __set to get throw points if (!$propertyHolderType->hasMethod('__set')->no()) { $throwPoints = array_merge($throwPoints, $nodeScopeResolver->processExprNode( @@ -887,7 +894,7 @@ public function processAssignVar( $propertyHolderType = $scope->resolveTypeByName($var->class); } else { $nodeScopeResolver->processExprNode($stmt, $var->class, $scope, $storage, $nodeCallback, $context); - $propertyHolderType = $scope->getType($var->class); + $propertyHolderType = $nodeScopeResolver->readStoredOrPriceOnDemand($var->class, $scope); } $propertyName = null; @@ -912,7 +919,7 @@ public function processAssignVar( if ($propertyName !== null) { $propertyReflection = $scope->getStaticPropertyReflection($propertyHolderType, $propertyName); - $assignedExprType = $scope->getType($assignedExpr); + $assignedExprType = $nodeScopeResolver->readStoredOrPriceOnDemand($assignedExpr, $scope); $nodeScopeResolver->callNodeCallback($nodeCallback, new PropertyAssignNode($var, $assignedExpr, $isAssignOp), $scopeBeforeAssignEval, $storage); if ($propertyReflection !== null && $propertyReflection->canChangeTypeAfterAssignment()) { if ($propertyReflection->hasNativeType()) { @@ -929,23 +936,23 @@ public function processAssignVar( } if ($assignedTypeIsCompatible) { - $scope = $scope->assignExpression($var, $assignedExprType, $scope->getNativeType($assignedExpr)); + $scope = $scope->assignExpression($var, $assignedExprType, $nodeScopeResolver->readStoredOrPriceOnDemandNative($assignedExpr, $scope)); } else { $scope = $scope->assignExpression( $var, TypeCombinator::intersect($assignedExprType->toCoercedArgumentType($scope->isDeclareStrictTypes()), $propertyNativeType), - TypeCombinator::intersect($scope->getNativeType($assignedExpr)->toCoercedArgumentType($scope->isDeclareStrictTypes()), $propertyNativeType), + TypeCombinator::intersect($nodeScopeResolver->readStoredOrPriceOnDemandNative($assignedExpr, $scope)->toCoercedArgumentType($scope->isDeclareStrictTypes()), $propertyNativeType), ); } } else { - $scope = $scope->assignExpression($var, $assignedExprType, $scope->getNativeType($assignedExpr)); + $scope = $scope->assignExpression($var, $assignedExprType, $nodeScopeResolver->readStoredOrPriceOnDemandNative($assignedExpr, $scope)); } } } else { // fallback - $assignedExprType = $scope->getType($assignedExpr); + $assignedExprType = $nodeScopeResolver->readStoredOrPriceOnDemand($assignedExpr, $scope); $nodeScopeResolver->callNodeCallback($nodeCallback, new PropertyAssignNode($var, $assignedExpr, $isAssignOp), $scopeBeforeAssignEval, $storage); - $scope = $scope->assignExpression($var, $assignedExprType, $scope->getNativeType($assignedExpr)); + $scope = $scope->assignExpression($var, $assignedExprType, $nodeScopeResolver->readStoredOrPriceOnDemandNative($assignedExpr, $scope)); } } elseif ($var instanceof List_) { $result = $processExprCallback($scope); @@ -979,7 +986,7 @@ public function processAssignVar( } else { $dimExpr = $arrayItem->key; } - $getOffsetValueTypeExpr = new TypeExpr($scope->getType($assignedExpr)->getOffsetValueType($scope->getType($dimExpr))); + $getOffsetValueTypeExpr = new TypeExpr($nodeScopeResolver->readStoredOrPriceOnDemand($assignedExpr, $scope)->getOffsetValueType($nodeScopeResolver->readStoredOrPriceOnDemand($dimExpr, $scope))); $result = $this->processAssignVar( $nodeScopeResolver, $scope, @@ -1004,7 +1011,7 @@ public function processAssignVar( while ($var instanceof ExistingArrayDimFetch) { $varForSetOffsetValue = $var->getVar(); if ($varForSetOffsetValue instanceof PropertyFetch || $varForSetOffsetValue instanceof StaticPropertyFetch) { - $varForSetOffsetValue = new TypeExpr($this->getOriginalPropertyType($varForSetOffsetValue, $scope)); + $varForSetOffsetValue = new TypeExpr($this->getOriginalPropertyType($nodeScopeResolver, $varForSetOffsetValue, $scope)); } $assignedPropertyExpr = new SetExistingOffsetValueTypeExpr( $varForSetOffsetValue, @@ -1025,14 +1032,14 @@ public function processAssignVar( foreach (array_reverse($dimFetchStack) as $dimFetch) { $dimExpr = $dimFetch->getDim(); $nodeScopeResolver->processExprNode($stmt, $dimExpr, $scope, $storage, new NoopNodeCallback(), $context->enterDeep()); - $offsetTypes[] = [$scope->getType($dimExpr), $dimFetch]; - $offsetNativeTypes[] = [$scope->getNativeType($dimExpr), $dimFetch]; + $offsetTypes[] = [$nodeScopeResolver->readStoredOrPriceOnDemand($dimExpr, $scope), $dimFetch]; + $offsetNativeTypes[] = [$nodeScopeResolver->readStoredOrPriceOnDemandNative($dimExpr, $scope), $dimFetch]; } - $valueToWrite = $scope->getType($assignedExpr); - $nativeValueToWrite = $scope->getNativeType($assignedExpr); - $varType = $scope->getType($var); - $varNativeType = $scope->getNativeType($var); + $valueToWrite = $nodeScopeResolver->readStoredOrPriceOnDemand($assignedExpr, $scope); + $nativeValueToWrite = $nodeScopeResolver->readStoredOrPriceOnDemandNative($assignedExpr, $scope); + $varType = $nodeScopeResolver->readStoredOrPriceOnDemand($var, $scope); + $varNativeType = $nodeScopeResolver->readStoredOrPriceOnDemandNative($var, $scope); $offsetValueType = $varType; $offsetNativeValueType = $varNativeType; @@ -1125,7 +1132,7 @@ private function unwrapAssign(Expr $expr): Expr * @param ImpurePoint[] $rhsImpurePoints * @return array */ - private function processSureTypesForConditionalExpressionsAfterAssign(Scope $scope, string $variableName, array $conditionalExpressions, SpecifiedTypes $specifiedTypes, Type $variableType, array $rhsImpurePoints, Expr $assignedExpr): array + private function processSureTypesForConditionalExpressionsAfterAssign(NodeScopeResolver $nodeScopeResolver, MutatingScope $scope, string $variableName, array $conditionalExpressions, SpecifiedTypes $specifiedTypes, Type $variableType, array $rhsImpurePoints, Expr $assignedExpr): array { foreach ($specifiedTypes->getSureTypes() as $exprString => [$expr, $exprType]) { if (!$this->isExprSafeToProjectThroughVariable($expr, $variableName, $rhsImpurePoints, $assignedExpr)) { @@ -1140,7 +1147,7 @@ private function processSureTypesForConditionalExpressionsAfterAssign(Scope $sco $variableType, $innerExpr, $this->exprPrinter->printExpr($innerExpr), - $scope->getType($innerExpr), + $nodeScopeResolver->readStoredOrPriceOnDemand($innerExpr, $scope), TrinaryLogic::createMaybe(), ); continue; @@ -1154,7 +1161,7 @@ private function processSureTypesForConditionalExpressionsAfterAssign(Scope $sco $variableType, $expr, $exprString, - TypeCombinator::intersect($scope->getType($expr), $exprType), + TypeCombinator::intersect($nodeScopeResolver->readStoredOrPriceOnDemand($expr, $scope), $exprType), TrinaryLogic::createYes(), ); } @@ -1167,7 +1174,7 @@ private function processSureTypesForConditionalExpressionsAfterAssign(Scope $sco * @param ImpurePoint[] $rhsImpurePoints * @return array */ - private function processSureNotTypesForConditionalExpressionsAfterAssign(Scope $scope, string $variableName, array $conditionalExpressions, SpecifiedTypes $specifiedTypes, Type $variableType, array $rhsImpurePoints, Expr $assignedExpr): array + private function processSureNotTypesForConditionalExpressionsAfterAssign(NodeScopeResolver $nodeScopeResolver, MutatingScope $scope, string $variableName, array $conditionalExpressions, SpecifiedTypes $specifiedTypes, Type $variableType, array $rhsImpurePoints, Expr $assignedExpr): array { foreach ($specifiedTypes->getSureNotTypes() as $exprString => [$expr, $exprType]) { if (!$this->isExprSafeToProjectThroughVariable($expr, $variableName, $rhsImpurePoints, $assignedExpr)) { @@ -1196,7 +1203,7 @@ private function processSureNotTypesForConditionalExpressionsAfterAssign(Scope $ $variableType, $expr, $exprString, - TypeCombinator::remove($scope->getType($expr), $exprType), + TypeCombinator::remove($nodeScopeResolver->readStoredOrPriceOnDemand($expr, $scope), $exprType), TrinaryLogic::createYes(), ); } @@ -1397,12 +1404,12 @@ private function isImplicitArrayCreation(array $dimFetchStack, Scope $scope): Tr return $scope->hasVariableType($varNode->name)->negate(); } - private function processArrayByRefItems(MutatingScope $scope, string $rootVarName, Expr\Array_ $arrayExpr, Expr $parentExpr): MutatingScope + private function processArrayByRefItems(NodeScopeResolver $nodeScopeResolver, MutatingScope $scope, string $rootVarName, Expr\Array_ $arrayExpr, Expr $parentExpr): MutatingScope { $implicitIndex = 0; foreach ($arrayExpr->items as $arrayItem) { if ($arrayItem->key !== null) { - $keyType = $scope->getType($arrayItem->key)->toArrayKey(); + $keyType = $nodeScopeResolver->readStoredOrPriceOnDemand($arrayItem->key, $scope)->toArrayKey(); if ($implicitIndex !== null) { $keyValues = $keyType->getConstantScalarValues(); @@ -1428,7 +1435,7 @@ private function processArrayByRefItems(MutatingScope $scope, string $rootVarNam if ($arrayItem->value instanceof Expr\Array_) { $dimFetchExpr = new ArrayDimFetch($parentExpr, $dimExpr); - $scope = $this->processArrayByRefItems($scope, $rootVarName, $arrayItem->value, $dimFetchExpr); + $scope = $this->processArrayByRefItems($nodeScopeResolver, $scope, $rootVarName, $arrayItem->value, $dimFetchExpr); } if (!$arrayItem->byRef || !$arrayItem->value instanceof Variable || !is_string($arrayItem->value->name)) { @@ -1437,8 +1444,8 @@ private function processArrayByRefItems(MutatingScope $scope, string $rootVarNam $refVarName = $arrayItem->value->name; $dimFetchExpr = new ArrayDimFetch($parentExpr, $dimExpr); - $refType = $scope->getType(new Variable($refVarName)); - $refNativeType = $scope->getNativeType(new Variable($refVarName)); + $refType = $nodeScopeResolver->readStoredOrPriceOnDemand(new Variable($refVarName), $scope); + $refNativeType = $nodeScopeResolver->readStoredOrPriceOnDemandNative(new Variable($refVarName), $scope); // When $rootVarName's array key changes, update $refVarName $scope = $scope->assignExpression( @@ -1466,7 +1473,7 @@ private function processArrayByRefItems(MutatingScope $scope, string $rootVarNam * * @return array{Type, list} */ - private function produceArrayDimFetchAssignValueToWrite(array $dimFetchStack, array $offsetTypes, Type $offsetValueType, Type $valueToWrite, Scope $scope): array + private function produceArrayDimFetchAssignValueToWrite(NodeScopeResolver $nodeScopeResolver, array $dimFetchStack, array $offsetTypes, Type $offsetValueType, Type $valueToWrite, MutatingScope $scope): array { $originalValueToWrite = $valueToWrite; @@ -1485,14 +1492,14 @@ private function produceArrayDimFetchAssignValueToWrite(array $dimFetchStack, ar $has = $offsetValueType->hasOffsetValueType($offsetType); if ($has->yes()) { if ($scope->hasExpressionType($dimFetch)->yes()) { - $offsetValueType = $scope->getType($dimFetch); + $offsetValueType = $nodeScopeResolver->readStoredOrPriceOnDemand($dimFetch, $scope); } else { $offsetValueType = $offsetValueType->getOffsetValueType($offsetType); } } elseif ($has->maybe()) { if ($scope->hasExpressionType($dimFetch)->yes()) { $generalizeOnWrite = false; - $offsetValueType = $scope->getType($dimFetch); + $offsetValueType = $nodeScopeResolver->readStoredOrPriceOnDemand($dimFetch, $scope); } else { $offsetValueType = TypeCombinator::union($offsetValueType->getOffsetValueType($offsetType), new ConstantArrayType([], [])); } @@ -1570,7 +1577,7 @@ private function produceArrayDimFetchAssignValueToWrite(array $dimFetchStack, ar $valueToWrite = $offsetValueType->setOffsetValueType($offsetType, $valueToWrite, $unionValues); } - if ($arrayDimFetch !== null && $offsetValueType->isList()->yes() && $this->shouldKeepList($arrayDimFetch, $scope, $offsetValueType)) { + if ($arrayDimFetch !== null && $offsetValueType->isList()->yes() && $this->shouldKeepList($nodeScopeResolver, $arrayDimFetch, $scope, $offsetValueType)) { $valueToWrite = TypeCombinator::intersect($valueToWrite, new AccessoryArrayListType()); } @@ -1593,7 +1600,7 @@ private function produceArrayDimFetchAssignValueToWrite(array $dimFetchStack, ar } elseif (isset($computedContainerValues[$key])) { $additionalValueType = $computedContainerValues[$key]; } else { - $offsetType = $scope->getType($dimFetch->dim); + $offsetType = $nodeScopeResolver->readStoredOrPriceOnDemand($dimFetch->dim, $scope); $additionalValueType = $valueToWrite->getOffsetValueType($offsetType); } @@ -1603,7 +1610,7 @@ private function produceArrayDimFetchAssignValueToWrite(array $dimFetchStack, ar return [$valueToWrite, $additionalExpressions]; } - private function shouldKeepList(ArrayDimFetch $arrayDimFetch, Scope $scope, Type $offsetValueType): bool + private function shouldKeepList(NodeScopeResolver $nodeScopeResolver, ArrayDimFetch $arrayDimFetch, MutatingScope $scope, Type $offsetValueType): bool { if ($arrayDimFetch->dim instanceof Expr\BinaryOp\Plus) { if ( // keep list for $list[$index + 1] assignments @@ -1629,7 +1636,7 @@ private function shouldKeepList(ArrayDimFetch $arrayDimFetch, Scope $scope, Type && in_array($arrayDimFetch->dim->left->name->toLowerString(), ['count', 'sizeof'], true) && count($arrayDimFetch->dim->left->getArgs()) === 1 // could support COUNT_RECURSIVE, COUNT_NORMAL && $this->isSameVariable($arrayDimFetch->var, $arrayDimFetch->dim->left->getArgs()[0]->value) - && IntegerRangeType::fromInterval(0, null)->isSuperTypeOf($scope->getType($arrayDimFetch->dim))->yes() + && IntegerRangeType::fromInterval(0, null)->isSuperTypeOf($nodeScopeResolver->readStoredOrPriceOnDemand($arrayDimFetch->dim, $scope))->yes() && $offsetValueType->isIterableAtLeastOnce()->yes() ) { return true; @@ -1667,12 +1674,12 @@ private function isSameVariable(Expr $a, Expr $b): bool * Returns the property's readable (declared) type, filtered down to the union * members that are not disjoint from the currently narrowed property type. */ - private function getOriginalPropertyType(PropertyFetch|StaticPropertyFetch $propertyFetch, MutatingScope $scope): Type + private function getOriginalPropertyType(NodeScopeResolver $nodeScopeResolver, PropertyFetch|StaticPropertyFetch $propertyFetch, MutatingScope $scope): Type { $propertyReflection = $this->propertyReflectionFinder->findPropertyReflectionFromNode($propertyFetch, $scope); $originalPropertyType = $propertyReflection !== null ? $propertyReflection->getReadableType() : new ErrorType(); if ($originalPropertyType instanceof UnionType) { - $currentPropertyType = $scope->getType($propertyFetch); + $currentPropertyType = $nodeScopeResolver->readStoredOrPriceOnDemand($propertyFetch, $scope); $originalPropertyType = $originalPropertyType->filterTypes(static fn (Type $innerType) => !$innerType->isSuperTypeOf($currentPropertyType)->no()); } diff --git a/src/Analyser/NodeScopeResolver.php b/src/Analyser/NodeScopeResolver.php index 1af57d9f7cf..5e0680c1b28 100644 --- a/src/Analyser/NodeScopeResolver.php +++ b/src/Analyser/NodeScopeResolver.php @@ -2912,6 +2912,26 @@ public function priceSyntheticOnDemand(Expr $expr, MutatingScope $scope): Type return $this->processExprOnDemand($expr, $scope, $current->duplicate())->getTypeForScope($scope); } + /** Native counterpart of readStoredOrPriceOnDemand(). */ + public function readStoredOrPriceOnDemandNative(Expr $expr, MutatingScope $scope): Type + { + $current = $scope->getCurrentExpressionResultStorage(); + $result = $current?->findExpressionResult($expr); + if ($result !== null) { + return $result->getNativeTypeForScope($scope); + } + + return $this->priceSyntheticOnDemandNative($expr, $scope); + } + + /** Native counterpart of priceSyntheticOnDemand(). */ + public function priceSyntheticOnDemandNative(Expr $expr, MutatingScope $scope): Type + { + $current = $scope->getCurrentExpressionResultStorage() ?? new ExpressionResultStorage(); + + return $this->processExprOnDemand($expr, $scope, $current->duplicate())->getNativeTypeForScope($scope); + } + /** * @param callable(Node $node, Scope $scope): void $nodeCallback */ From 46a53f8bed66b8016608eebfcc27f8283df338a8 Mon Sep 17 00:00:00 2001 From: Ondrej Mirtes Date: Thu, 18 Jun 2026 18:51:50 +0200 Subject: [PATCH 096/113] Read expr types via results or helpers in NodeScopeResolver instead of Scope::getType --- src/Analyser/NodeScopeResolver.php | 156 +++++++++++++++++------------ 1 file changed, 91 insertions(+), 65 deletions(-) diff --git a/src/Analyser/NodeScopeResolver.php b/src/Analyser/NodeScopeResolver.php index 5e0680c1b28..3255aaf979a 100644 --- a/src/Analyser/NodeScopeResolver.php +++ b/src/Analyser/NodeScopeResolver.php @@ -51,6 +51,7 @@ use PhpParser\NodeTraverser; use PhpParser\NodeVisitorAbstract; use PHPStan\Analyser\ExprHandler\AssignHandler; +use PHPStan\Analyser\ExprHandler\Helper\ClosureTypeResolver; use PHPStan\Analyser\ExprHandler\Helper\ImplicitToStringCallHelper; use PHPStan\BetterReflection\Reflection\Adapter\ReflectionClass; use PHPStan\BetterReflection\Reflection\ReflectionEnum; @@ -1052,7 +1053,7 @@ public function processStmtNode( $gatheredYieldStatements = []; $executionEnds = []; $methodImpurePoints = []; - $statementResult = $this->processStmtNodesInternal($stmt, $stmt->stmts, $methodScope, $storage, static function (Node $node, Scope $scope) use ($nodeCallback, $methodScope, &$gatheredReturnStatements, &$gatheredYieldStatements, &$executionEnds, &$methodImpurePoints): void { + $statementResult = $this->processStmtNodesInternal($stmt, $stmt->stmts, $methodScope, $storage, function (Node $node, Scope $scope) use ($nodeCallback, $methodScope, &$gatheredReturnStatements, &$gatheredYieldStatements, &$executionEnds, &$methodImpurePoints): void { $nodeCallback($node, $scope); if ($scope->getFunction() !== $methodScope->getFunction()) { return; @@ -1066,7 +1067,7 @@ public function processStmtNode( && $scope->getFunction() instanceof PhpMethodFromParserNodeReflection && $scope->getFunction()->getDeclaringClass()->hasConstructor() && $scope->getFunction()->getDeclaringClass()->getConstructor()->getName() === $scope->getFunction()->getName() - && TypeUtils::findThisType($scope->getType($node->getPropertyFetch()->var)) !== null + && TypeUtils::findThisType($this->readStoredOrPriceOnDemand($node->getPropertyFetch()->var, $scope->toMutatingScope())) !== null ) { return; } @@ -1431,8 +1432,8 @@ public function processStmtNode( $condScope = $scope; foreach ($stmt->elseifs as $elseif) { $this->callNodeCallback($nodeCallback, $elseif, $scope, $storage); - $elseIfConditionType = ($this->treatPhpDocTypesAsCertain ? $condScope->getType($elseif->cond) : $scope->getNativeType($elseif->cond))->toBoolean(); $condResult = $this->processExprNode($stmt, $elseif->cond, $condScope, $storage, $nodeCallback, ExpressionContext::createDeep()); + $elseIfConditionType = ($this->treatPhpDocTypesAsCertain ? $condResult->getTypeForScope($condScope) : $condResult->getNativeTypeForScope($scope))->toBoolean(); $throwPoints = array_merge($throwPoints, $condResult->getThrowPoints()); $impurePoints = array_merge($impurePoints, $condResult->getImpurePoints()); $branchScopeStatementResult = $this->processStmtNodesInternal($elseif, $elseif->stmts, $condResult->getTruthyScope(), $storage, $nodeCallback, $context); @@ -1539,24 +1540,27 @@ public function processStmtNode( $originalScope = $scope; $bodyScope = $scope; + $foreachIterateeType = $condResult->getTypeForScope($originalScope); + $foreachNativeIterateeType = $condResult->getNativeTypeForScope($originalScope); + if ($stmt->keyVar instanceof Variable) { $keyTypeExpr = new NativeTypeExpr( - $originalScope->getIterableKeyType($originalScope->getType($stmt->expr)), - $originalScope->getIterableKeyType($originalScope->getNativeType($stmt->expr)), + $originalScope->getIterableKeyType($foreachIterateeType), + $originalScope->getIterableKeyType($foreachNativeIterateeType), ); $this->callNodeCallback($nodeCallback, new VariableAssignNode($stmt->keyVar, $keyTypeExpr), $originalScope, $storage); } if ($stmt->valueVar instanceof Variable) { $valueTypeExpr = new NativeTypeExpr( - $originalScope->getIterableValueType($originalScope->getType($stmt->expr)), - $originalScope->getIterableValueType($originalScope->getNativeType($stmt->expr)), + $originalScope->getIterableValueType($foreachIterateeType), + $originalScope->getIterableValueType($foreachNativeIterateeType), ); $this->callNodeCallback($nodeCallback, new VariableAssignNode($stmt->valueVar, $valueTypeExpr), $originalScope, $storage); } elseif ($stmt->valueVar instanceof List_) { $virtualAssign = new Assign($stmt->valueVar, new NativeTypeExpr( - $originalScope->getIterableValueType($originalScope->getType($stmt->expr)), - $originalScope->getIterableValueType($originalScope->getNativeType($stmt->expr)), + $originalScope->getIterableValueType($foreachIterateeType), + $originalScope->getIterableValueType($foreachNativeIterateeType), )); $virtualAssign->setAttributes($stmt->valueVar->getAttributes()); $this->callNodeCallback($nodeCallback, $virtualAssign, $scope, $storage); @@ -1569,19 +1573,21 @@ public function processStmtNode( $storage = $originalStorage->duplicate(); $originalScope = $this->polluteScopeWithAlwaysIterableForeach ? $scope->filterByTruthyValue($arrayComparisonExpr) : $scope; - $unrolledResult = $this->tryProcessUnrolledConstantArrayForeach($stmt, $originalScope, $originalStorage, $context); + $foreachIterateeType = $condResult->getTypeForScope($originalScope); + $foreachNativeIterateeType = $condResult->getNativeTypeForScope($originalScope); + $unrolledResult = $this->tryProcessUnrolledConstantArrayForeach($stmt, $originalScope, $originalStorage, $context, $foreachIterateeType, $foreachNativeIterateeType); if ($unrolledResult !== null) { $bodyScope = $unrolledResult['bodyScope']; $unrolledEndScope = $unrolledResult['endScope']; $unrolledTotalKeys = $unrolledResult['totalKeys']; } else { - $bodyScope = $this->enterForeach($originalScope, $storage, $originalScope, $stmt, $nodeCallback); + $bodyScope = $this->enterForeach($originalScope, $storage, $originalScope, $stmt, $foreachIterateeType, $foreachNativeIterateeType, $nodeCallback); $count = 0; do { $prevScope = $bodyScope; $bodyScope = $bodyScope->mergeWith($this->polluteScopeWithAlwaysIterableForeach ? $scope->filterByTruthyValue($arrayComparisonExpr) : $scope); $storage = $originalStorage->duplicate(); - $bodyScope = $this->enterForeach($bodyScope, $storage, $originalScope, $stmt, $nodeCallback); + $bodyScope = $this->enterForeach($bodyScope, $storage, $originalScope, $stmt, $foreachIterateeType, $foreachNativeIterateeType, $nodeCallback); $bodyScopeResult = $this->processStmtNodesInternal($stmt, $stmt->stmts, $bodyScope, $storage, new NoopNodeCallback(), $context->enterDeep())->filterOutLoopExitPoints(); $bodyScope = $bodyScopeResult->getScope(); foreach ($bodyScopeResult->getExitPointsByType(Continue_::class) as $continueExitPoint) { @@ -1601,7 +1607,7 @@ public function processStmtNode( $bodyScope = $bodyScope->mergeWith($this->polluteScopeWithAlwaysIterableForeach ? $scope->filterByTruthyValue($arrayComparisonExpr) : $scope); $storage = $originalStorage; - $bodyScope = $this->enterForeach($bodyScope, $storage, $originalScope, $stmt, $nodeCallback); + $bodyScope = $this->enterForeach($bodyScope, $storage, $originalScope, $stmt, $foreachIterateeType, $foreachNativeIterateeType, $nodeCallback); $finalPassContext = $unrolledTotalKeys !== null ? $context->enterUnrolledForeach($unrolledTotalKeys) : $context; $finalScopeResult = $this->processStmtNodesInternal($stmt, $stmt->stmts, $bodyScope, $storage, $nodeCallback, $finalPassContext)->filterOutLoopExitPoints(); $finalScope = $finalScopeResult->getScope(); @@ -1651,7 +1657,7 @@ public function processStmtNode( $finalScope = $unrolledEndScope; } - $exprType = $scope->getType($stmt->expr); + $exprType = $condResult->getTypeForScope($scope); $hasExpr = $scope->hasExpressionType($stmt->expr); if ( count($breakExitPoints) === 0 @@ -1669,8 +1675,8 @@ public function processStmtNode( foreach ($scopesWithIterableValueType as $scopeWithIterableValueType) { if ($keyVarExpr !== null) { $arrayExprDimFetch = new ArrayDimFetch($stmt->expr, $keyVarExpr); - $dimFetchType = $scopeWithIterableValueType->getType($arrayExprDimFetch); - $dimFetchNativeType = $scopeWithIterableValueType->getNativeType($arrayExprDimFetch); + $dimFetchType = $this->priceSyntheticOnDemand($arrayExprDimFetch, $scopeWithIterableValueType); + $dimFetchNativeType = $this->priceSyntheticOnDemand($arrayExprDimFetch, $scopeWithIterableValueType->doNotTreatPhpDocTypesAsCertain()); // Condition-based narrowings like `is_string($type)` apply to the value // variable but not automatically to the array dim fetch, even though the // two describe the same element for a given iteration. If the value var @@ -1678,21 +1684,21 @@ public function processStmtNode( // the narrowed value-var type in place of the broader dim fetch type so // the loop's final array rewrite below picks up the sharper element type. if ($originalValueExpr !== null && $scopeWithIterableValueType->hasExpressionType($originalValueExpr)->yes()) { - $valueVarType = $scopeWithIterableValueType->getType($stmt->valueVar); + $valueVarType = $this->readStoredOrPriceOnDemand($stmt->valueVar, $scopeWithIterableValueType); if ($dimFetchType->isSuperTypeOf($valueVarType)->yes()) { $dimFetchType = $valueVarType; } - $valueVarNativeType = $scopeWithIterableValueType->getNativeType($stmt->valueVar); + $valueVarNativeType = $this->readStoredOrPriceOnDemand($stmt->valueVar, $scopeWithIterableValueType->doNotTreatPhpDocTypesAsCertain()); if ($dimFetchNativeType->isSuperTypeOf($valueVarNativeType)->yes()) { $dimFetchNativeType = $valueVarNativeType; } } - $keyLoopTypes[] = $scopeWithIterableValueType->getType($keyVarExpr); - $keyLoopNativeTypes[] = $scopeWithIterableValueType->getType($keyVarExpr); + $keyLoopTypes[] = $this->readStoredOrPriceOnDemand($keyVarExpr, $scopeWithIterableValueType); + $keyLoopNativeTypes[] = $this->readStoredOrPriceOnDemand($keyVarExpr, $scopeWithIterableValueType); } else { // No key variable: the narrowed value var is the array element type directly. - $dimFetchType = $scopeWithIterableValueType->getType($stmt->valueVar); - $dimFetchNativeType = $scopeWithIterableValueType->getNativeType($stmt->valueVar); + $dimFetchType = $this->readStoredOrPriceOnDemand($stmt->valueVar, $scopeWithIterableValueType); + $dimFetchNativeType = $this->readStoredOrPriceOnDemand($stmt->valueVar, $scopeWithIterableValueType->doNotTreatPhpDocTypesAsCertain()); } $arrayDimFetchLoopTypes[] = $dimFetchType; $arrayDimFetchLoopNativeTypes[] = $dimFetchNativeType; @@ -1704,7 +1710,7 @@ public function processStmtNode( $valueTypeChanged = !$arrayDimFetchLoopType->equals($exprType->getIterableValueType()); $keyTypeChanged = false; $keyLoopType = $exprType->getIterableKeyType(); - $keyLoopNativeType = $scope->getNativeType($stmt->expr)->getIterableKeyType(); + $keyLoopNativeType = $condResult->getNativeTypeForScope($scope)->getIterableKeyType(); if ($keyVarExpr !== null) { $keyLoopType = TypeCombinator::union(...$keyLoopTypes); $keyLoopNativeType = TypeCombinator::union(...$keyLoopNativeTypes); @@ -1720,7 +1726,7 @@ public function processStmtNode( $newExprType = $newExprType->mapKeyType(static fn (Type $type): Type => $keyLoopType); } - $nativeExprType = $scope->getNativeType($stmt->expr); + $nativeExprType = $condResult->getNativeTypeForScope($scope); $newExprNativeType = $nativeExprType; if ($valueTypeChanged) { $newExprNativeType = $newExprNativeType->mapValueType(static fn (Type $type): Type => $arrayDimFetchLoopNativeType); @@ -1768,7 +1774,7 @@ public function processStmtNode( $throwPoints = array_merge($throwPoints, $finalScopeResult->getThrowPoints()); $impurePoints = array_merge($impurePoints, $finalScopeResult->getImpurePoints()); } - $traversableThrowPoint = $this->getTraversableForeachThrowPoint($scope, $stmt->expr); + $traversableThrowPoint = $this->getTraversableForeachThrowPoint($scope, $stmt->expr, $exprType); if ($traversableThrowPoint !== null) { $throwPoints[] = $traversableThrowPoint; } @@ -1788,7 +1794,7 @@ public function processStmtNode( $originalStorage = $storage; $storage = $originalStorage->duplicate(); $condResult = $this->processExprNode($stmt, $stmt->cond, $scope, $storage, new NoopNodeCallback(), ExpressionContext::createDeep()); - $beforeCondBooleanType = ($this->treatPhpDocTypesAsCertain ? $scope->getType($stmt->cond) : $scope->getNativeType($stmt->cond))->toBoolean(); + $beforeCondBooleanType = ($this->treatPhpDocTypesAsCertain ? $condResult->getTypeForScope($scope) : $condResult->getNativeTypeForScope($scope))->toBoolean(); $condScope = $condResult->getFalseyScope(); if (!$context->isTopLevel() && $beforeCondBooleanType->isFalse()->yes()) { if (!$this->polluteScopeWithLoopInitialAssignments) { @@ -1839,7 +1845,7 @@ public function processStmtNode( $alwaysIterates = false; $neverIterates = false; if ($context->isTopLevel()) { - $condBooleanType = ($this->treatPhpDocTypesAsCertain ? $bodyScopeMaybeRan->getType($stmt->cond) : $bodyScopeMaybeRan->getNativeType($stmt->cond))->toBoolean(); + $condBooleanType = ($this->treatPhpDocTypesAsCertain ? $this->readStoredOrPriceOnDemand($stmt->cond, $bodyScopeMaybeRan) : $this->readStoredOrPriceOnDemand($stmt->cond, $bodyScopeMaybeRan->doNotTreatPhpDocTypesAsCertain()))->toBoolean(); $alwaysIterates = $condBooleanType->isTrue()->yes(); $neverIterates = $condBooleanType->isFalse()->yes(); } @@ -1937,7 +1943,7 @@ public function processStmtNode( $alwaysIterates = false; if ($context->isTopLevel()) { - $condBooleanType = ($this->treatPhpDocTypesAsCertain ? $bodyScope->getType($stmt->cond) : $bodyScope->getNativeType($stmt->cond))->toBoolean(); + $condBooleanType = ($this->treatPhpDocTypesAsCertain ? $this->readStoredOrPriceOnDemand($stmt->cond, $bodyScope) : $this->readStoredOrPriceOnDemand($stmt->cond, $bodyScope->doNotTreatPhpDocTypesAsCertain()))->toBoolean(); $alwaysIterates = $condBooleanType->isTrue()->yes(); } @@ -2008,7 +2014,7 @@ public function processStmtNode( // 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 = ($this->treatPhpDocTypesAsCertain ? $condResultScope->getType($condExpr) : $condResultScope->getNativeType($condExpr))->toBoolean(); + $condTruthiness = ($this->treatPhpDocTypesAsCertain ? $condResult->getTypeForScope($condResultScope) : $condResult->getNativeTypeForScope($condResultScope))->toBoolean(); $isIterableAtLeastOnce = $isIterableAtLeastOnce->and($condTruthiness->isTrue()); } @@ -2058,7 +2064,7 @@ public function processStmtNode( $alwaysIterates = TrinaryLogic::createFromBoolean($context->isTopLevel()); if ($lastCondExpr !== null) { - $alwaysIterates = $alwaysIterates->and($bodyScope->getType($lastCondExpr)->toBoolean()->isTrue()); + $alwaysIterates = $alwaysIterates->and($this->readStoredOrPriceOnDemand($lastCondExpr, $bodyScope)->toBoolean()->isTrue()); $bodyScope = $this->processExprNode($stmt, $lastCondExpr, $bodyScope, $storage, $nodeCallback, ExpressionContext::createDeep())->getTruthyScope(); $bodyScope = $this->inferForLoopExpressions($stmt, $lastCondExpr, $bodyScope); } @@ -2182,7 +2188,7 @@ public function processStmtNode( } } - $exhaustive = $scopeForBranches->getType($stmt->cond) instanceof NeverType; + $exhaustive = $condResult->getTypeForScope($scopeForBranches) instanceof NeverType; if (!$hasDefaultCase && !$exhaustive) { $alwaysTerminating = false; @@ -2435,7 +2441,7 @@ public function processStmtNode( $throwPoints = array_merge($throwPoints, $exprResult->getThrowPoints()); $impurePoints = array_merge($impurePoints, $exprResult->getImpurePoints()); if ($var instanceof ArrayDimFetch && $var->dim !== null) { - $varType = $scope->getType($var->var); + $varType = $this->readStoredOrPriceOnDemand($var->var, $scope); if (!$varType->isArray()->yes() && !(new ObjectType(ArrayAccess::class))->isSuperTypeOf($varType)->no()) { $throwPoints = array_merge($throwPoints, $this->processExprNode( $stmt, @@ -2566,7 +2572,7 @@ public function leaveNode(Node $node): ?ExistingArrayDimFetch } else { $constantName = new Name\FullyQualified($const->name->toString()); } - $scope = $scope->assignExpression(new ConstFetch($constantName), $scope->getType($const->value), $scope->getNativeType($const->value)); + $scope = $scope->assignExpression(new ConstFetch($constantName), $constResult->getTypeForScope($scope), $constResult->getNativeTypeForScope($scope)); } } elseif ($stmt instanceof Node\Stmt\ClassConst) { $hasYield = false; @@ -2582,8 +2588,8 @@ public function leaveNode(Node $node): ?ExistingArrayDimFetch } $scope = $scope->assignExpression( new Expr\ClassConstFetch(new Name\FullyQualified($scope->getClassReflection()->getName()), $const->name), - $scope->getType($const->value), - $scope->getNativeType($const->value), + $constResult->getTypeForScope($scope), + $constResult->getNativeTypeForScope($scope), ); } } elseif ($stmt instanceof Node\Stmt\EnumCase) { @@ -2807,12 +2813,12 @@ private function findEarlyTerminatingExpr(Expr $expr, Scope $scope): ?Expr if (($expr instanceof MethodCall || $expr instanceof Expr\StaticCall) && $expr->name instanceof Node\Identifier) { if (array_key_exists($expr->name->toLowerString(), $this->earlyTerminatingMethodNames)) { if ($expr instanceof MethodCall) { - $methodCalledOnType = $scope->getType($expr->var); + $methodCalledOnType = $this->readStoredOrPriceOnDemand($expr->var, $scope->toMutatingScope()); } else { if ($expr->class instanceof Name) { $methodCalledOnType = $scope->resolveTypeByName($expr->class); } else { - $methodCalledOnType = $scope->getType($expr->class); + $methodCalledOnType = $this->readStoredOrPriceOnDemand($expr->class, $scope->toMutatingScope()); } } @@ -2845,6 +2851,10 @@ private function findEarlyTerminatingExpr(Expr $expr, Scope $scope): ?Expr return $expr; } + // Scope::getType() here memoises the expression's sub-expression types onto + // $scope (e.g. the array-dim-fetches of a `$x[...] ??= []` chain); the ??= + // offset detection relies on that memoised state, so the side-effect-free + // helpers would regress bug-13623. Kept as Scope::getType deliberately. $exprType = $scope->getType($expr); if ($exprType instanceof NeverType && $exprType->isExplicit()) { return $expr; @@ -3220,7 +3230,7 @@ private function processClosureNodeInternal( $inAssignRightSideVariableName === $use->var->name && $inAssignRightSideExpr !== null ) { - $inAssignRightSideType = $scope->getType($inAssignRightSideExpr); + $inAssignRightSideType = $this->resolveCallableTypeForScope($inAssignRightSideExpr, $scope); if ($inAssignRightSideType instanceof ClosureType) { $variableType = $inAssignRightSideType; } else { @@ -3231,7 +3241,7 @@ private function processClosureNodeInternal( $variableType = TypeCombinator::union($scope->getVariableType($inAssignRightSideVariableName), $inAssignRightSideType); } } - $inAssignRightSideNativeType = $scope->getNativeType($inAssignRightSideExpr); + $inAssignRightSideNativeType = $this->resolveCallableTypeForScope($inAssignRightSideExpr, $scope->doNotTreatPhpDocTypesAsCertain()); if ($inAssignRightSideNativeType instanceof ClosureType) { $variableNativeType = $inAssignRightSideNativeType; } else { @@ -3432,26 +3442,44 @@ public function processArrowFunctionNode( * @param Node\Arg[]|null $args * @return ParameterReflection[]|null */ - public function createCallableParameters(Scope $scope, Expr $closureExpr, ?array $args, ?Type $passedToType): ?array + public function createCallableParameters(MutatingScope $scope, Expr $closureExpr, ?array $args, ?Type $passedToType): ?array { - return $this->doCreateCallableParameters($scope, $closureExpr, $args, $passedToType, static fn (Scope $s, Expr $e) => $s->getType($e)); + 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(Scope $scope, Expr $closureExpr, ?array $args, ?Type $nativePassedToType): ?array + 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 + * through its TypeResolvingExprHandler (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. + */ + private function resolveCallableTypeForScope(Expr $expr, MutatingScope $scope): Type { - return $this->doCreateCallableParameters($scope, $closureExpr, $args, $nativePassedToType, static fn (Scope $s, Expr $e) => $s->getNativeType($e)); + if ($expr instanceof Expr\Closure || $expr instanceof Expr\ArrowFunction) { + return $this->container->getByType(ClosureTypeResolver::class)->getClosureType($scope, $expr); + } + + return $this->readStoredOrPriceOnDemand($expr, $scope); } /** * @param Node\Arg[]|null $args - * @param Closure(Scope, Expr): Type $typeGetter + * @param Closure(MutatingScope, Expr): Type $typeGetter * @return ParameterReflection[]|null */ - private function doCreateCallableParameters(Scope $scope, Expr $closureExpr, ?array $args, ?Type $passedToType, Closure $typeGetter): ?array + private function doCreateCallableParameters(MutatingScope $scope, Expr $closureExpr, ?array $args, ?Type $passedToType, Closure $typeGetter): ?array { $callableParameters = null; if ($args !== null) { @@ -3999,7 +4027,7 @@ public function processArgs( } $this->storeExpressionResult($storage, $arg->value, $arrowFunctionResult); } else { - $exprType = $scope->getType($arg->value); + $exprType = $this->readStoredOrPriceOnDemand($arg->value, $scope); $enterExpressionAssignForByRef = $assignByReference && $arg->value instanceof ArrayDimFetch && $arg->value->dim === null; if ($enterExpressionAssignForByRef) { $scopeToPass = $scopeToPass->enterExpressionAssign($arg->value); @@ -4107,7 +4135,7 @@ public function processArgs( $scope = $this->lookForUnsetAllowedUndefinedExpressions($scope, $argValue); } } elseif ($calleeReflection !== null && $calleeReflection->hasSideEffects()->yes()) { - $argType = $scope->getType($arg->value); + $argType = $this->readStoredOrPriceOnDemand($arg->value, $scope); if (!$argType->isObject()->no()) { $nakedReturnType = null; if ($nakedMethodReflection !== null) { @@ -4376,14 +4404,14 @@ public function processStmtVarAnnotation(MutatingScope $scope, ExpressionResultS $scope = $scope->assignVariable( $name, $varTag->getType(), - $scope->getNativeType($variableNode), + $this->priceSyntheticOnDemand($variableNode, $scope->doNotTreatPhpDocTypesAsCertain()), $certainty, ); } } if (count($variableLessTags) === 1 && $defaultExpr !== null) { - $originalType = $scope->getType($defaultExpr); + $originalType = $this->readStoredOrPriceOnDemand($defaultExpr, $scope); $varTag = $variableLessTags[0]; if (!$originalType->equals($varTag->getType())) { $this->callNodeCallback($nodeCallback, new VarTagChangedExpressionTypeNode($varTag, $defaultExpr), $scope, $storage); @@ -4449,6 +4477,8 @@ private function tryProcessUnrolledConstantArrayForeach( MutatingScope $originalScope, ExpressionResultStorage $originalStorage, StatementContext $context, + Type $iterateeType, + Type $nativeIterateeType, ): ?array { if ($stmt->byRef) { @@ -4461,7 +4491,6 @@ private function tryProcessUnrolledConstantArrayForeach( return null; } - $iterateeType = $originalScope->getType($stmt->expr); if (!$iterateeType->isConstantArray()->yes()) { return null; } @@ -4487,7 +4516,6 @@ private function tryProcessUnrolledConstantArrayForeach( return null; } - $nativeIterateeType = $originalScope->getNativeType($stmt->expr); $nativeConstantArrays = $nativeIterateeType->getConstantArrays(); $matchedNativeArrays = count($nativeConstantArrays) === count($constantArrays) ? $nativeConstantArrays : null; @@ -4623,7 +4651,7 @@ private function tryProcessUnrolledConstantArrayForeach( $prevLoopScope = $loopScope; $iterStorage = $originalStorage->duplicate(); $iterBodyScope = $loopScope->mergeWith($endScope); - $iterBodyScope = $this->enterForeach($iterBodyScope, $iterStorage, $originalScope, $stmt, new NoopNodeCallback()); + $iterBodyScope = $this->enterForeach($iterBodyScope, $iterStorage, $originalScope, $stmt, $iterateeType, $nativeIterateeType, new NoopNodeCallback()); $iterBodyScopeResult = $this->processStmtNodesInternal($stmt, $stmt->stmts, $iterBodyScope, $iterStorage, new NoopNodeCallback(), $context->enterDeep())->filterOutLoopExitPoints(); $loopScope = $iterBodyScopeResult->getScope(); foreach ($iterBodyScopeResult->getExitPointsByType(Continue_::class) as $continueExitPoint) { @@ -4648,9 +4676,8 @@ private function tryProcessUnrolledConstantArrayForeach( return ['bodyScope' => $bodyScope, 'endScope' => $endScope, 'totalKeys' => $totalKeys]; } - private function getTraversableForeachThrowPoint(MutatingScope $scope, Expr $iteratee): ?InternalThrowPoint + private function getTraversableForeachThrowPoint(MutatingScope $scope, Expr $iteratee, Type $exprType): ?InternalThrowPoint { - $exprType = $scope->getType($iteratee); $traversableType = new ObjectType(Traversable::class); if ($traversableType->isSuperTypeOf($exprType)->no()) { @@ -4682,13 +4709,12 @@ private function getTraversableForeachThrowPoint(MutatingScope $scope, Expr $ite /** * @param callable(Node $node, Scope $scope): void $nodeCallback */ - private function enterForeach(MutatingScope $scope, ExpressionResultStorage $storage, MutatingScope $originalScope, Foreach_ $stmt, callable $nodeCallback): MutatingScope + private function enterForeach(MutatingScope $scope, ExpressionResultStorage $storage, MutatingScope $originalScope, Foreach_ $stmt, Type $iterateeType, Type $nativeIterateeType, callable $nodeCallback): MutatingScope { if ($stmt->expr instanceof Variable && is_string($stmt->expr->name)) { $scope = $this->processVarAnnotation($scope, [$stmt->expr->name], $stmt); } - $iterateeType = $originalScope->getType($stmt->expr); if ( ($stmt->valueVar instanceof Variable && is_string($stmt->valueVar->name)) && ($stmt->keyVar === null || ($stmt->keyVar instanceof Variable && is_string($stmt->keyVar->name))) @@ -4713,7 +4739,7 @@ private function enterForeach(MutatingScope $scope, ExpressionResultStorage $sto $stmt->valueVar, new NativeTypeExpr( $originalScope->getIterableValueType($iterateeType), - $originalScope->getIterableValueType($originalScope->getNativeType($stmt->expr)), + $originalScope->getIterableValueType($nativeIterateeType), ), $nodeCallback, )->getScope(); @@ -4731,7 +4757,7 @@ private function enterForeach(MutatingScope $scope, ExpressionResultStorage $sto $stmt->keyVar, new NativeTypeExpr( $originalScope->getIterableKeyType($iterateeType), - $originalScope->getIterableKeyType($originalScope->getNativeType($stmt->expr)), + $originalScope->getIterableKeyType($nativeIterateeType), ), $nodeCallback, )->getScope(); @@ -4795,8 +4821,8 @@ private function enterForeach(MutatingScope $scope, ExpressionResultStorage $sto $arrayArg = $args[0]->value; $scope = $scope->assignExpression( new ArrayDimFetch($arrayArg, $stmt->valueVar), - $scope->getType($arrayArg)->getIterableValueType(), - $scope->getNativeType($arrayArg)->getIterableValueType(), + $this->readStoredOrPriceOnDemand($arrayArg, $scope)->getIterableValueType(), + $this->readStoredOrPriceOnDemand($arrayArg, $scope->doNotTreatPhpDocTypesAsCertain())->getIterableValueType(), ); } } @@ -5094,7 +5120,7 @@ public function processCalledMethod(MethodReflection $methodReflection): ?Mutati $statementResult = $executionEnd->getStatementResult(); $endNode = $executionEnd->getNode(); if ($endNode instanceof Node\Stmt\Expression) { - $exprType = $statementResult->getScope()->getType($endNode->expr); + $exprType = $this->readStoredOrPriceOnDemand($endNode->expr, $statementResult->getScope()->toMutatingScope()); if ($exprType instanceof NeverType && $exprType->isExplicit()) { continue; } @@ -5479,12 +5505,12 @@ private function inferForLoopExpressions(For_ $stmt, Expr $lastCondExpr, Mutatin && $stmt->init[0]->var->name === $lastCondExpr->left->name ) { $arrayArg = $lastCondExpr->right->getArgs()[0]->value; - $arrayType = $bodyScope->getType($arrayArg); + $arrayType = $this->readStoredOrPriceOnDemand($arrayArg, $bodyScope); if ($arrayType->isList()->yes()) { $bodyScope = $bodyScope->assignExpression( new ArrayDimFetch($lastCondExpr->right->getArgs()[0]->value, $lastCondExpr->left), $arrayType->getIterableValueType(), - $bodyScope->getNativeType($arrayArg)->getIterableValueType(), + $this->readStoredOrPriceOnDemand($arrayArg, $bodyScope->doNotTreatPhpDocTypesAsCertain())->getIterableValueType(), ); } } @@ -5504,12 +5530,12 @@ private function inferForLoopExpressions(For_ $stmt, Expr $lastCondExpr, Mutatin && $stmt->init[0]->var->name === $lastCondExpr->right->name ) { $arrayArg = $lastCondExpr->left->getArgs()[0]->value; - $arrayType = $bodyScope->getType($arrayArg); + $arrayType = $this->readStoredOrPriceOnDemand($arrayArg, $bodyScope); if ($arrayType->isList()->yes()) { $bodyScope = $bodyScope->assignExpression( new ArrayDimFetch($lastCondExpr->left->getArgs()[0]->value, $lastCondExpr->right), $arrayType->getIterableValueType(), - $bodyScope->getNativeType($arrayArg)->getIterableValueType(), + $this->readStoredOrPriceOnDemand($arrayArg, $bodyScope->doNotTreatPhpDocTypesAsCertain())->getIterableValueType(), ); } } From bcd0e020c1d3de0031d4e299910a4215760cb3fb Mon Sep 17 00:00:00 2001 From: Ondrej Mirtes Date: Thu, 18 Jun 2026 19:29:19 +0200 Subject: [PATCH 097/113] Correct the explanation of the two load-bearing Scope::getType() exceptions --- src/Analyser/ExprHandler/AssignHandler.php | 14 ++++++++------ src/Analyser/NodeScopeResolver.php | 10 ++++++---- 2 files changed, 14 insertions(+), 10 deletions(-) diff --git a/src/Analyser/ExprHandler/AssignHandler.php b/src/Analyser/ExprHandler/AssignHandler.php index 6b0379bf755..38d8b615d4b 100644 --- a/src/Analyser/ExprHandler/AssignHandler.php +++ b/src/Analyser/ExprHandler/AssignHandler.php @@ -662,12 +662,14 @@ public function processAssignVar( } // SKIPPED (single-pass inside-out invariant): these two reads must stay as - // Scope::getType()/getNativeType(). Unlike the non-caching helpers, - // Scope::getType() memoises the assigned expression's sub-expression types - // onto $scope (e.g. hasExpressionType() for the array-dim-fetch being - // written). produceArrayDimFetchAssignValueToWrite() below relies on that - // memoised state to keep a freshly-coalesced offset optional - replacing - // these with the helpers regresses bug-13623 ($x[...] ??= [] chains). + // Scope::getType()/getNativeType(). This is NOT a scope-state side effect + // (assignExpression cannot reproduce it): getType() returns its cached + // resolvedTypes value, computed during loop convergence when a `$x[...] ??= + // []` left side was still maybe-set, so the coalesced value keeps its + // optional array{} branch. The side-effect-free helpers re-price on the + // converged (definitely-set) scope, where CoalesceHandler drops the array{} + // branch (issetCheck === true) - which regresses bug-13623. The optionality + // lives in the loop history the converged scope no longer carries. $valueToWrite = $scope->getType($assignedExpr); $nativeValueToWrite = $scope->getNativeType($assignedExpr); $scopeBeforeAssignEval = $scope; diff --git a/src/Analyser/NodeScopeResolver.php b/src/Analyser/NodeScopeResolver.php index 3255aaf979a..66c29f1279b 100644 --- a/src/Analyser/NodeScopeResolver.php +++ b/src/Analyser/NodeScopeResolver.php @@ -2851,10 +2851,12 @@ private function findEarlyTerminatingExpr(Expr $expr, Scope $scope): ?Expr return $expr; } - // Scope::getType() here memoises the expression's sub-expression types onto - // $scope (e.g. the array-dim-fetches of a `$x[...] ??= []` chain); the ??= - // offset detection relies on that memoised state, so the side-effect-free - // helpers would regress bug-13623. Kept as Scope::getType deliberately. + // Scope::getType() must stay here (not a scope-state side effect): for a + // `$x[...] ??= []` expression it returns getType()'s cached resolvedTypes + // value, computed during loop convergence when the left side was maybe-set + // (so the coalesced value keeps its optional array{} branch). The + // side-effect-free helpers re-price on the converged scope and drop that + // branch, regressing bug-13623. See AssignHandler::processAssignVar. $exprType = $scope->getType($expr); if ($exprType instanceof NeverType && $exprType->isExplicit()) { return $expr; From 53e4fa3ea2b62e67bd6e191684853be7ee1b4034 Mon Sep 17 00:00:00 2001 From: Ondrej Mirtes Date: Thu, 18 Jun 2026 19:34:37 +0200 Subject: [PATCH 098/113] Read Identical operand types from results in RicherScopeGetTypeHelper when called inside-out --- src/Analyser/ExprHandler/BinaryOpHandler.php | 4 ++-- src/Analyser/RicherScopeGetTypeHelper.php | 17 ++++++++++++----- 2 files changed, 14 insertions(+), 7 deletions(-) diff --git a/src/Analyser/ExprHandler/BinaryOpHandler.php b/src/Analyser/ExprHandler/BinaryOpHandler.php index 3e740ee9de4..297cbb8bb4f 100644 --- a/src/Analyser/ExprHandler/BinaryOpHandler.php +++ b/src/Analyser/ExprHandler/BinaryOpHandler.php @@ -159,11 +159,11 @@ public function processExpr(NodeScopeResolver $nodeScopeResolver, Stmt $stmt, Ex } if ($expr instanceof BinaryOp\Identical) { - return $this->richerScopeGetTypeHelper->getIdenticalResult($scope, $expr)->type; + return $this->richerScopeGetTypeHelper->getIdenticalResult($scope, $expr, $nodeScopeResolver)->type; } if ($expr instanceof BinaryOp\NotIdentical) { - return $this->richerScopeGetTypeHelper->getNotIdenticalResult($scope, $expr)->type; + return $this->richerScopeGetTypeHelper->getNotIdenticalResult($scope, $expr, $nodeScopeResolver)->type; } if ($expr instanceof BinaryOp\LogicalXor) { diff --git a/src/Analyser/RicherScopeGetTypeHelper.php b/src/Analyser/RicherScopeGetTypeHelper.php index 132c1875809..557f4949469 100644 --- a/src/Analyser/RicherScopeGetTypeHelper.php +++ b/src/Analyser/RicherScopeGetTypeHelper.php @@ -27,7 +27,7 @@ public function __construct( /** * @return TypeResult */ - public function getIdenticalResult(Scope $scope, Identical $expr): TypeResult + public function getIdenticalResult(Scope $scope, Identical $expr, ?NodeScopeResolver $nodeScopeResolver = null): TypeResult { if ( $expr->left instanceof Variable @@ -39,8 +39,15 @@ public function getIdenticalResult(Scope $scope, Identical $expr): TypeResult return new TypeResult(new ConstantBooleanType(true), []); } - $leftType = $scope->getType($expr->left); - $rightType = $scope->getType($expr->right); + // $nodeScopeResolver is passed from inside-out callbacks (e.g. BinaryOp's + // typeCallback) so the operands are read from their ExpressionResults + // instead of Scope::getType(); rules call this without it (BC). + $leftType = $nodeScopeResolver !== null + ? $nodeScopeResolver->readStoredOrPriceOnDemand($expr->left, $scope->toMutatingScope()) + : $scope->getType($expr->left); + $rightType = $nodeScopeResolver !== null + ? $nodeScopeResolver->readStoredOrPriceOnDemand($expr->right, $scope->toMutatingScope()) + : $scope->getType($expr->right); if ( ( @@ -78,9 +85,9 @@ public function getIdenticalResult(Scope $scope, Identical $expr): TypeResult /** * @return TypeResult */ - public function getNotIdenticalResult(Scope $scope, Node\Expr\BinaryOp\NotIdentical $expr): TypeResult + public function getNotIdenticalResult(Scope $scope, Node\Expr\BinaryOp\NotIdentical $expr, ?NodeScopeResolver $nodeScopeResolver = null): TypeResult { - $identicalResult = $this->getIdenticalResult($scope, new Identical($expr->left, $expr->right)); + $identicalResult = $this->getIdenticalResult($scope, new Identical($expr->left, $expr->right), $nodeScopeResolver); $identicalType = $identicalResult->type; if ($identicalType instanceof ConstantBooleanType) { return new TypeResult(new ConstantBooleanType(!$identicalType->getValue()), $identicalResult->reasons); From 259d4c19afebee36075b4dd8901e8e2890ecf335 Mon Sep 17 00:00:00 2001 From: Ondrej Mirtes Date: Thu, 18 Jun 2026 19:40:32 +0200 Subject: [PATCH 099/113] Read expr types from results in SpecifiedTypes::normalize when called inside-out --- src/Analyser/ExprHandler/BooleanAndHandler.php | 4 ++-- src/Analyser/ExprHandler/BooleanOrHandler.php | 8 ++++---- .../ExprHandler/Helper/EqualityTypeSpecifyingHelper.php | 6 +++--- src/Analyser/ExprHandler/NullsafeMethodCallHandler.php | 4 ++-- .../ExprHandler/NullsafePropertyFetchHandler.php | 4 ++-- src/Analyser/SpecifiedTypes.php | 9 +++++++-- 6 files changed, 20 insertions(+), 15 deletions(-) diff --git a/src/Analyser/ExprHandler/BooleanAndHandler.php b/src/Analyser/ExprHandler/BooleanAndHandler.php index 863b6b5c91b..b06378b06c6 100644 --- a/src/Analyser/ExprHandler/BooleanAndHandler.php +++ b/src/Analyser/ExprHandler/BooleanAndHandler.php @@ -128,8 +128,8 @@ public function processExpr(NodeScopeResolver $nodeScopeResolver, Stmt $stmt, Ex if ($context->true()) { $types = $leftTypes->unionWith($rightTypes); } else { - $leftNormalized = $leftTypes->normalize($s); - $rightNormalized = $rightTypes->normalize($rightScope); + $leftNormalized = $leftTypes->normalize($s, $nodeScopeResolver); + $rightNormalized = $rightTypes->normalize($rightScope, $nodeScopeResolver); $types = $leftNormalized->intersectWith($rightNormalized); $types = $this->conditionalExpressionHolderHelper->augmentDisjunctionTypes($nodeScopeResolver, $s, $rightScope, $leftNormalized, $rightNormalized, $expr->left, $expr->right, false, $types); } diff --git a/src/Analyser/ExprHandler/BooleanOrHandler.php b/src/Analyser/ExprHandler/BooleanOrHandler.php index 184fbf9becc..2e1fea4502f 100644 --- a/src/Analyser/ExprHandler/BooleanOrHandler.php +++ b/src/Analyser/ExprHandler/BooleanOrHandler.php @@ -179,15 +179,15 @@ public function processExpr(NodeScopeResolver $nodeScopeResolver, Stmt $stmt, Ex if ( $leftResult->getTypeForScope($s)->toBoolean()->isFalse()->yes() ) { - $types = $rightTypes->normalize($rightScope); + $types = $rightTypes->normalize($rightScope, $nodeScopeResolver); } elseif ( $leftResult->getTypeForScope($s)->toBoolean()->isTrue()->yes() || $rightResult->getTypeForScope($s)->toBoolean()->isFalse()->yes() ) { - $types = $leftTypes->normalize($s); + $types = $leftTypes->normalize($s, $nodeScopeResolver); } else { - $leftNormalized = $leftTypes->normalize($s); - $rightNormalized = $rightTypes->normalize($rightScope); + $leftNormalized = $leftTypes->normalize($s, $nodeScopeResolver); + $rightNormalized = $rightTypes->normalize($rightScope, $nodeScopeResolver); $types = $leftNormalized->intersectWith($rightNormalized); $types = $this->augmentBooleanOrTruthyWithConditionalHolders($nodeScopeResolver, $s, $rightScope, $expr, $types); $types = $this->conditionalExpressionHolderHelper->augmentDisjunctionTypes($nodeScopeResolver, $s, $rightScope, $leftNormalized, $rightNormalized, $expr->left, $expr->right, true, $types); diff --git a/src/Analyser/ExprHandler/Helper/EqualityTypeSpecifyingHelper.php b/src/Analyser/ExprHandler/Helper/EqualityTypeSpecifyingHelper.php index cd02477548d..ee541152646 100644 --- a/src/Analyser/ExprHandler/Helper/EqualityTypeSpecifyingHelper.php +++ b/src/Analyser/ExprHandler/Helper/EqualityTypeSpecifyingHelper.php @@ -249,7 +249,7 @@ public function specifyTypesForEqual(NodeScopeResolver $nodeScopeResolver, Expr\ return $context->true() ? $leftTypes->unionWith($rightTypes) - : $leftTypes->normalize($scope)->intersectWith($rightTypes->normalize($scope)); + : $leftTypes->normalize($scope, $nodeScopeResolver)->intersectWith($rightTypes->normalize($scope, $nodeScopeResolver)); } public function specifyTypesForIdentical(NodeScopeResolver $nodeScopeResolver, Expr\BinaryOp\Identical $expr, Scope $scope, TypeSpecifierContext $context): SpecifiedTypes @@ -757,8 +757,8 @@ private function specifyTypesForNormalizedIdentical(NodeScopeResolver $nodeScope } return $leftTypes->unionWith($rightTypes); } elseif ($context->false()) { - return $this->typeSpecifier->create($leftExpr, $leftType, $context, $scope)->setRootExpr($expr)->normalize($scope) - ->intersectWith($this->typeSpecifier->create($rightExpr, $rightType, $context, $scope)->setRootExpr($expr)->normalize($scope)); + return $this->typeSpecifier->create($leftExpr, $leftType, $context, $scope)->setRootExpr($expr)->normalize($scope, $nodeScopeResolver) + ->intersectWith($this->typeSpecifier->create($rightExpr, $rightType, $context, $scope)->setRootExpr($expr)->normalize($scope, $nodeScopeResolver)); } return (new SpecifiedTypes([], []))->setRootExpr($expr); diff --git a/src/Analyser/ExprHandler/NullsafeMethodCallHandler.php b/src/Analyser/ExprHandler/NullsafeMethodCallHandler.php index 5f7f089a314..e0de6903c97 100644 --- a/src/Analyser/ExprHandler/NullsafeMethodCallHandler.php +++ b/src/Analyser/ExprHandler/NullsafeMethodCallHandler.php @@ -115,7 +115,7 @@ public function processExpr(NodeScopeResolver $nodeScopeResolver, Stmt $stmt, Ex new NullType(), ); }, - specifyTypesCallback: function (MutatingScope $s, TypeSpecifierContext $context) use ($expr, $methodCall): SpecifiedTypes { + specifyTypesCallback: function (MutatingScope $s, TypeSpecifierContext $context) use ($expr, $methodCall, $nodeScopeResolver): SpecifiedTypes { if ($context->null()) { return $this->defaultNarrowingHelper->specifyDefaultTypes($expr, $context); } @@ -130,7 +130,7 @@ public function processExpr(NodeScopeResolver $nodeScopeResolver, Stmt $stmt, Ex )->setRootExpr($expr); $nullSafeTypes = $this->typeSpecifier->handleDefaultTruthyOrFalseyContext($context, $expr, $s); - return $context->true() ? $types->unionWith($nullSafeTypes) : $types->normalize($s)->intersectWith($nullSafeTypes->normalize($s)); + return $context->true() ? $types->unionWith($nullSafeTypes) : $types->normalize($s, $nodeScopeResolver)->intersectWith($nullSafeTypes->normalize($s, $nodeScopeResolver)); }, ); } diff --git a/src/Analyser/ExprHandler/NullsafePropertyFetchHandler.php b/src/Analyser/ExprHandler/NullsafePropertyFetchHandler.php index 1a3dd160d41..d7b94e6a7b6 100644 --- a/src/Analyser/ExprHandler/NullsafePropertyFetchHandler.php +++ b/src/Analyser/ExprHandler/NullsafePropertyFetchHandler.php @@ -92,7 +92,7 @@ public function processExpr(NodeScopeResolver $nodeScopeResolver, Stmt $stmt, Ex new NullType(), ); }, - specifyTypesCallback: function (MutatingScope $s, TypeSpecifierContext $context) use ($expr, $propertyFetch): SpecifiedTypes { + specifyTypesCallback: function (MutatingScope $s, TypeSpecifierContext $context) use ($expr, $propertyFetch, $nodeScopeResolver): SpecifiedTypes { if ($context->null()) { return $this->defaultNarrowingHelper->specifyDefaultTypes($expr, $context); } @@ -107,7 +107,7 @@ public function processExpr(NodeScopeResolver $nodeScopeResolver, Stmt $stmt, Ex )->setRootExpr($expr); $nullSafeTypes = $this->typeSpecifier->handleDefaultTruthyOrFalseyContext($context, $expr, $s); - return $context->true() ? $types->unionWith($nullSafeTypes) : $types->normalize($s)->intersectWith($nullSafeTypes->normalize($s)); + return $context->true() ? $types->unionWith($nullSafeTypes) : $types->normalize($s, $nodeScopeResolver)->intersectWith($nullSafeTypes->normalize($s, $nodeScopeResolver)); }, ); } diff --git a/src/Analyser/SpecifiedTypes.php b/src/Analyser/SpecifiedTypes.php index 5cfa65dc53b..ad213042f35 100644 --- a/src/Analyser/SpecifiedTypes.php +++ b/src/Analyser/SpecifiedTypes.php @@ -218,13 +218,18 @@ public function unionWith(SpecifiedTypes $other): self return $result->setRootExpr($rootExpr); } - public function normalize(Scope $scope): self + public function normalize(Scope $scope, ?NodeScopeResolver $nodeScopeResolver = null): self { $sureTypes = $this->sureTypes; foreach ($this->sureNotTypes as $exprString => [$exprNode, $sureNotType]) { if (!isset($sureTypes[$exprString])) { - $sureTypes[$exprString] = [$exprNode, TypeCombinator::remove($scope->getType($exprNode), $sureNotType)]; + // $nodeScopeResolver is passed from inside-out callbacks so the expr + // type is read from its ExpressionResult instead of Scope::getType(). + $exprType = $nodeScopeResolver !== null + ? $nodeScopeResolver->readStoredOrPriceOnDemand($exprNode, $scope->toMutatingScope()) + : $scope->getType($exprNode); + $sureTypes[$exprString] = [$exprNode, TypeCombinator::remove($exprType, $sureNotType)]; continue; } From 3dd027bea557ab533d8730a765892be676f3cd46 Mon Sep 17 00:00:00 2001 From: Ondrej Mirtes Date: Thu, 18 Jun 2026 20:31:05 +0200 Subject: [PATCH 100/113] Read assign-target sub-expression types from their results instead of the on-demand helpers in AssignHandler --- src/Analyser/ExprHandler/AssignHandler.php | 34 +++++++++++----------- 1 file changed, 17 insertions(+), 17 deletions(-) diff --git a/src/Analyser/ExprHandler/AssignHandler.php b/src/Analyser/ExprHandler/AssignHandler.php index 38d8b615d4b..726ea56810a 100644 --- a/src/Analyser/ExprHandler/AssignHandler.php +++ b/src/Analyser/ExprHandler/AssignHandler.php @@ -596,12 +596,12 @@ public function processAssignVar( if ($enterExpressionAssign) { $scope = $scope->enterExpressionAssign($var); } - $result = $nodeScopeResolver->processExprNode($stmt, $var, $scope, $storage, $nodeCallback, $context->enterDeep()); - $hasYield = $result->hasYield(); - $throwPoints = $result->getThrowPoints(); - $impurePoints = $result->getImpurePoints(); - $isAlwaysTerminating = $result->isAlwaysTerminating(); - $scope = $result->getScope(); + $varResult = $nodeScopeResolver->processExprNode($stmt, $var, $scope, $storage, $nodeCallback, $context->enterDeep()); + $hasYield = $varResult->hasYield(); + $throwPoints = $varResult->getThrowPoints(); + $impurePoints = $varResult->getImpurePoints(); + $isAlwaysTerminating = $varResult->isAlwaysTerminating(); + $scope = $varResult->getScope(); if ($enterExpressionAssign) { $scope = $scope->exitExpressionAssign($var); } @@ -682,8 +682,8 @@ public function processAssignVar( $isAlwaysTerminating = $isAlwaysTerminating || $result->isAlwaysTerminating(); $scope = $result->getScope(); - $varType = $nodeScopeResolver->readStoredOrPriceOnDemand($var, $scope); - $varNativeType = $nodeScopeResolver->readStoredOrPriceOnDemandNative($var, $scope); + $varType = $varResult->getTypeForScope($scope); + $varNativeType = $varResult->getNativeTypeForScope($scope); // 4. compose types $isImplicitArrayCreation = $this->isImplicitArrayCreation($dimFetchStack, $scope); @@ -811,7 +811,7 @@ public function processAssignVar( $throwPoints[] = InternalThrowPoint::createImplicit($scope, $var); } - $propertyHolderType = $nodeScopeResolver->readStoredOrPriceOnDemand($var->var, $scope); + $propertyHolderType = $objectResult->getTypeForScope($scope); if ($propertyName !== null && $propertyHolderType->hasInstanceProperty($propertyName)->yes()) { $propertyReflection = $propertyHolderType->getInstanceProperty($propertyName, $scope); $assignedExprType = $nodeScopeResolver->readStoredOrPriceOnDemand($assignedExpr, $scope); @@ -895,8 +895,8 @@ public function processAssignVar( if ($var->class instanceof Node\Name) { $propertyHolderType = $scope->resolveTypeByName($var->class); } else { - $nodeScopeResolver->processExprNode($stmt, $var->class, $scope, $storage, $nodeCallback, $context); - $propertyHolderType = $nodeScopeResolver->readStoredOrPriceOnDemand($var->class, $scope); + $classResult = $nodeScopeResolver->processExprNode($stmt, $var->class, $scope, $storage, $nodeCallback, $context); + $propertyHolderType = $classResult->getTypeForScope($scope); } $propertyName = null; @@ -1027,21 +1027,21 @@ public function processAssignVar( // the chain is usually a clone of AST nodes already processed elsewhere // (see Unset_ handling) - process it with a noop callback so that // results for its nodes are stored without invoking rules twice - $nodeScopeResolver->processExprNode($stmt, $var, $scope, $storage, new NoopNodeCallback(), $context->enterDeep()); + $varResult = $nodeScopeResolver->processExprNode($stmt, $var, $scope, $storage, new NoopNodeCallback(), $context->enterDeep()); $offsetTypes = []; $offsetNativeTypes = []; foreach (array_reverse($dimFetchStack) as $dimFetch) { $dimExpr = $dimFetch->getDim(); - $nodeScopeResolver->processExprNode($stmt, $dimExpr, $scope, $storage, new NoopNodeCallback(), $context->enterDeep()); - $offsetTypes[] = [$nodeScopeResolver->readStoredOrPriceOnDemand($dimExpr, $scope), $dimFetch]; - $offsetNativeTypes[] = [$nodeScopeResolver->readStoredOrPriceOnDemandNative($dimExpr, $scope), $dimFetch]; + $dimResult = $nodeScopeResolver->processExprNode($stmt, $dimExpr, $scope, $storage, new NoopNodeCallback(), $context->enterDeep()); + $offsetTypes[] = [$dimResult->getTypeForScope($scope), $dimFetch]; + $offsetNativeTypes[] = [$dimResult->getNativeTypeForScope($scope), $dimFetch]; } $valueToWrite = $nodeScopeResolver->readStoredOrPriceOnDemand($assignedExpr, $scope); $nativeValueToWrite = $nodeScopeResolver->readStoredOrPriceOnDemandNative($assignedExpr, $scope); - $varType = $nodeScopeResolver->readStoredOrPriceOnDemand($var, $scope); - $varNativeType = $nodeScopeResolver->readStoredOrPriceOnDemandNative($var, $scope); + $varType = $varResult->getTypeForScope($scope); + $varNativeType = $varResult->getNativeTypeForScope($scope); $offsetValueType = $varType; $offsetNativeValueType = $varNativeType; From 9f0a479ff8566b1326651e73fdbecacf7b42b56d Mon Sep 17 00:00:00 2001 From: Ondrej Mirtes Date: Thu, 18 Jun 2026 20:34:00 +0200 Subject: [PATCH 101/113] Read while-loop condition type from its result instead of the on-demand helper --- src/Analyser/NodeScopeResolver.php | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/Analyser/NodeScopeResolver.php b/src/Analyser/NodeScopeResolver.php index 66c29f1279b..dcea2df0edc 100644 --- a/src/Analyser/NodeScopeResolver.php +++ b/src/Analyser/NodeScopeResolver.php @@ -1838,14 +1838,15 @@ public function processStmtNode( $bodyScope = $bodyScope->mergeWith($scope); $bodyScopeMaybeRan = $bodyScope; $storage = $originalStorage; - $bodyScope = $this->processExprNode($stmt, $stmt->cond, $bodyScope, $storage, $nodeCallback, ExpressionContext::createDeep())->getTruthyScope(); + $bodyCondResult = $this->processExprNode($stmt, $stmt->cond, $bodyScope, $storage, $nodeCallback, ExpressionContext::createDeep()); + $bodyScope = $bodyCondResult->getTruthyScope(); $finalScopeResult = $this->processStmtNodesInternal($stmt, $stmt->stmts, $bodyScope, $storage, $nodeCallback, $context)->filterOutLoopExitPoints(); $finalScope = $finalScopeResult->getScope()->filterByFalseyValue($stmt->cond); $alwaysIterates = false; $neverIterates = false; if ($context->isTopLevel()) { - $condBooleanType = ($this->treatPhpDocTypesAsCertain ? $this->readStoredOrPriceOnDemand($stmt->cond, $bodyScopeMaybeRan) : $this->readStoredOrPriceOnDemand($stmt->cond, $bodyScopeMaybeRan->doNotTreatPhpDocTypesAsCertain()))->toBoolean(); + $condBooleanType = ($this->treatPhpDocTypesAsCertain ? $bodyCondResult->getTypeForScope($bodyScopeMaybeRan) : $bodyCondResult->getTypeForScope($bodyScopeMaybeRan->doNotTreatPhpDocTypesAsCertain()))->toBoolean(); $alwaysIterates = $condBooleanType->isTrue()->yes(); $neverIterates = $condBooleanType->isFalse()->yes(); } From 376ea07d5aaaf6259a5ddf3ed7df3b89042b8a49 Mon Sep 17 00:00:00 2001 From: Ondrej Mirtes Date: Thu, 18 Jun 2026 20:51:16 +0200 Subject: [PATCH 102/113] Pass already-computed results into ImplicitToStringCallHelper and read operand types directly in BinaryOp/Match --- src/Analyser/ExprHandler/BinaryOpHandler.php | 45 +++++++++++++------ .../ExprHandler/CastStringHandler.php | 2 +- .../Helper/ImplicitToStringCallHelper.php | 15 +++++-- .../ExprHandler/InterpolatedStringHandler.php | 2 +- src/Analyser/ExprHandler/MatchHandler.php | 4 +- src/Analyser/ExprHandler/PrintHandler.php | 2 +- src/Analyser/NodeScopeResolver.php | 2 +- 7 files changed, 49 insertions(+), 23 deletions(-) diff --git a/src/Analyser/ExprHandler/BinaryOpHandler.php b/src/Analyser/ExprHandler/BinaryOpHandler.php index 297cbb8bb4f..e48728343e1 100644 --- a/src/Analyser/ExprHandler/BinaryOpHandler.php +++ b/src/Analyser/ExprHandler/BinaryOpHandler.php @@ -101,8 +101,8 @@ public function processExpr(NodeScopeResolver $nodeScopeResolver, Stmt $stmt, Ex $throwPoints[] = InternalThrowPoint::createExplicit($leftResult->getScope(), new ObjectType(DivisionByZeroError::class), $expr, false); } if ($expr instanceof BinaryOp\Concat) { - $leftToStringResult = $this->implicitToStringCallHelper->processImplicitToStringCall($nodeScopeResolver, $expr->left, $scope); - $rightToStringResult = $this->implicitToStringCallHelper->processImplicitToStringCall($nodeScopeResolver, $expr->right, $leftResult->getScope()); + $leftToStringResult = $this->implicitToStringCallHelper->processImplicitToStringCall($nodeScopeResolver, $expr->left, $scope, $leftResult); + $rightToStringResult = $this->implicitToStringCallHelper->processImplicitToStringCall($nodeScopeResolver, $expr->right, $leftResult->getScope(), $rightResult); $throwPoints = array_merge($throwPoints, $leftToStringResult->getThrowPoints(), $rightToStringResult->getThrowPoints()); $impurePoints = array_merge($impurePoints, $leftToStringResult->getImpurePoints(), $rightToStringResult->getImpurePoints()); } @@ -116,12 +116,21 @@ public function processExpr(NodeScopeResolver $nodeScopeResolver, Stmt $stmt, Ex isAlwaysTerminating: $leftResult->isAlwaysTerminating() || $rightResult->isAlwaysTerminating(), throwPoints: $throwPoints, impurePoints: $impurePoints, - typeCallback: function (MutatingScope $scope) use ($expr, $nodeScopeResolver): Type { - // the operands were processed during processExpr; read their stored - // results instead of re-walking via Scope::getType(). Synthetic - // nodes the resolver builds (e.g. getDivType's Mod) are priced on - // demand by the same helper. - $getType = static fn (Expr $e): Type => $nodeScopeResolver->readStoredOrPriceOnDemand($e, $scope); + typeCallback: function (MutatingScope $scope) use ($expr, $leftResult, $rightResult, $nodeScopeResolver): Type { + // the operands were processed during processExpr; read their already + // computed results instead of re-walking via Scope::getType(). + // Synthetic nodes the resolver builds (e.g. getDivType's Mod) are + // priced on demand by the same helper. + $getType = static function (Expr $e) use ($expr, $leftResult, $rightResult, $scope, $nodeScopeResolver): Type { + if ($e === $expr->left) { + return $leftResult->getTypeForScope($scope); + } + if ($e === $expr->right) { + return $rightResult->getTypeForScope($scope); + } + + return $nodeScopeResolver->readStoredOrPriceOnDemand($e, $scope); + }; if ($expr instanceof BinaryOp\Smaller) { return $getType($expr->left)->isSmallerThan($getType($expr->right), $this->phpVersion)->toBooleanType(); @@ -236,7 +245,7 @@ public function processExpr(NodeScopeResolver $nodeScopeResolver, Stmt $stmt, Ex throw new ShouldNotHappenException(sprintf('Unhandled %s', get_class($expr))); }, - specifyTypesCallback: function (MutatingScope $scope, TypeSpecifierContext $context) use ($expr, $nodeScopeResolver): SpecifiedTypes { + specifyTypesCallback: function (MutatingScope $scope, TypeSpecifierContext $context) use ($expr, $leftResult, $rightResult, $nodeScopeResolver): SpecifiedTypes { if ($expr instanceof BinaryOp\Identical) { return $this->equalityTypeSpecifyingHelper->specifyTypesForIdentical($nodeScopeResolver, $expr, $scope, $context); } @@ -308,10 +317,20 @@ public function processExpr(NodeScopeResolver $nodeScopeResolver, Stmt $stmt, Ex $orEqual = $expr instanceof BinaryOp\SmallerOrEqual; $offset = $orEqual ? 0 : 1; - // the operands and their subexpressions were processed during - // processExpr; read their stored results instead of re-walking - // via Scope::getType(). - $getType = static fn (Expr $e): Type => $nodeScopeResolver->readStoredOrPriceOnDemand($e, $scope); + // the operands were processed during processExpr; read their + // already computed results instead of re-walking via + // Scope::getType(). Their subexpressions (e.g. count() arguments) + // were also processed and are read from the stored result. + $getType = static function (Expr $e) use ($expr, $leftResult, $rightResult, $scope, $nodeScopeResolver): Type { + if ($e === $expr->left) { + return $leftResult->getTypeForScope($scope); + } + if ($e === $expr->right) { + return $rightResult->getTypeForScope($scope); + } + + return $nodeScopeResolver->readStoredOrPriceOnDemand($e, $scope); + }; $leftType = $getType($expr->left); $result = (new SpecifiedTypes([], []))->setRootExpr($expr); diff --git a/src/Analyser/ExprHandler/CastStringHandler.php b/src/Analyser/ExprHandler/CastStringHandler.php index 0b6d874814d..785458350e4 100644 --- a/src/Analyser/ExprHandler/CastStringHandler.php +++ b/src/Analyser/ExprHandler/CastStringHandler.php @@ -52,7 +52,7 @@ public function processExpr(NodeScopeResolver $nodeScopeResolver, Stmt $stmt, Ex $impurePoints = $exprResult->getImpurePoints(); $throwPoints = $exprResult->getThrowPoints(); - $toStringResult = $this->implicitToStringCallHelper->processImplicitToStringCall($nodeScopeResolver, $expr->expr, $scope); + $toStringResult = $this->implicitToStringCallHelper->processImplicitToStringCall($nodeScopeResolver, $expr->expr, $scope, $exprResult); $throwPoints = array_merge($throwPoints, $toStringResult->getThrowPoints()); $impurePoints = array_merge($impurePoints, $toStringResult->getImpurePoints()); diff --git a/src/Analyser/ExprHandler/Helper/ImplicitToStringCallHelper.php b/src/Analyser/ExprHandler/Helper/ImplicitToStringCallHelper.php index d98ff8b4c49..5a2a1ee138b 100644 --- a/src/Analyser/ExprHandler/Helper/ImplicitToStringCallHelper.php +++ b/src/Analyser/ExprHandler/Helper/ImplicitToStringCallHelper.php @@ -26,14 +26,21 @@ public function __construct( { } - public function processImplicitToStringCall(NodeScopeResolver $nodeScopeResolver, Expr $expr, MutatingScope $scope): ExpressionResult + /** + * @param ExpressionResult|null $exprResult the already-computed result of $expr, + * passed by callers that processed it on $scope so this helper reads its type + * directly instead of re-walking via Scope::getType(); callers that do not + * hold the result (only the Expr) pass null and the type is read from the + * stored result or priced on demand + */ + public function processImplicitToStringCall(NodeScopeResolver $nodeScopeResolver, Expr $expr, MutatingScope $scope, ?ExpressionResult $exprResult = null): ExpressionResult { $throwPoints = []; $impurePoints = []; - // the expression was processed before this call; read its stored result - // or price it on demand instead of re-walking via Scope::getType(). - $exprType = $nodeScopeResolver->readStoredOrPriceOnDemand($expr, $scope); + $exprType = $exprResult !== null + ? $exprResult->getTypeForScope($scope) + : $nodeScopeResolver->readStoredOrPriceOnDemand($expr, $scope); $toStringMethod = null; if (!$exprType->isObject()->no()) { diff --git a/src/Analyser/ExprHandler/InterpolatedStringHandler.php b/src/Analyser/ExprHandler/InterpolatedStringHandler.php index 98d5d7868cb..577938afa3f 100644 --- a/src/Analyser/ExprHandler/InterpolatedStringHandler.php +++ b/src/Analyser/ExprHandler/InterpolatedStringHandler.php @@ -63,7 +63,7 @@ public function processExpr(NodeScopeResolver $nodeScopeResolver, Stmt $stmt, Ex $throwPoints = array_merge($throwPoints, $partResult->getThrowPoints()); $impurePoints = array_merge($impurePoints, $partResult->getImpurePoints()); - $toStringResult = $this->implicitToStringCallHelper->processImplicitToStringCall($nodeScopeResolver, $part, $scope); + $toStringResult = $this->implicitToStringCallHelper->processImplicitToStringCall($nodeScopeResolver, $part, $scope, $partResult); $throwPoints = array_merge($throwPoints, $toStringResult->getThrowPoints()); $impurePoints = array_merge($impurePoints, $toStringResult->getImpurePoints()); diff --git a/src/Analyser/ExprHandler/MatchHandler.php b/src/Analyser/ExprHandler/MatchHandler.php index 7f9fcf7c026..047d0acc6cf 100644 --- a/src/Analyser/ExprHandler/MatchHandler.php +++ b/src/Analyser/ExprHandler/MatchHandler.php @@ -497,9 +497,9 @@ public function processExpr(NodeScopeResolver $nodeScopeResolver, Stmt $stmt, Ex $isExhaustive = $hasDefaultCond || $hasAlwaysTrueCond; if (!$isExhaustive) { - // the subject was processed above; read its stored result on the + // the subject was processed above ($condResult); read its type on the // arm-narrowed scope instead of re-walking via Scope::getType(). - $remainingType = $nodeScopeResolver->readStoredOrPriceOnDemand($expr->cond, $matchScope); + $remainingType = $condResult->getTypeForScope($matchScope); if ($remainingType instanceof NeverType) { $isExhaustive = true; } diff --git a/src/Analyser/ExprHandler/PrintHandler.php b/src/Analyser/ExprHandler/PrintHandler.php index 95b8b2dc76d..8bee2ebdb2d 100644 --- a/src/Analyser/ExprHandler/PrintHandler.php +++ b/src/Analyser/ExprHandler/PrintHandler.php @@ -48,7 +48,7 @@ public function processExpr(NodeScopeResolver $nodeScopeResolver, Stmt $stmt, Ex $throwPoints = $exprResult->getThrowPoints(); $impurePoints = $exprResult->getImpurePoints(); - $toStringResult = $this->implicitToStringCallHelper->processImplicitToStringCall($nodeScopeResolver, $expr->expr, $scope); + $toStringResult = $this->implicitToStringCallHelper->processImplicitToStringCall($nodeScopeResolver, $expr->expr, $scope, $exprResult); $throwPoints = array_merge($throwPoints, $toStringResult->getThrowPoints()); $impurePoints = array_merge($impurePoints, $toStringResult->getImpurePoints()); diff --git a/src/Analyser/NodeScopeResolver.php b/src/Analyser/NodeScopeResolver.php index dcea2df0edc..71ba8a973b0 100644 --- a/src/Analyser/NodeScopeResolver.php +++ b/src/Analyser/NodeScopeResolver.php @@ -1154,7 +1154,7 @@ public function processStmtNode( $result = $this->processExprNode($stmt, $echoExpr, $scope, $storage, $nodeCallback, ExpressionContext::createDeep()); $throwPoints = array_merge($throwPoints, $result->getThrowPoints()); $impurePoints = array_merge($impurePoints, $result->getImpurePoints()); - $toStringResult = $this->implicitToStringCallHelper->processImplicitToStringCall($this, $echoExpr, $scope); + $toStringResult = $this->implicitToStringCallHelper->processImplicitToStringCall($this, $echoExpr, $scope, $result); $throwPoints = array_merge($throwPoints, $toStringResult->getThrowPoints()); $impurePoints = array_merge($impurePoints, $toStringResult->getImpurePoints()); $scope = $result->getScope(); From 5a1230a03af380018cc9894d1c89e893b4013494 Mon Sep 17 00:00:00 2001 From: Ondrej Mirtes Date: Thu, 18 Jun 2026 21:04:30 +0200 Subject: [PATCH 103/113] NSRT test for precise Scope --- .../nsrt/precise-scope-select-from-args.php | 28 +++++++++++++++++++ 1 file changed, 28 insertions(+) create mode 100644 tests/PHPStan/Analyser/nsrt/precise-scope-select-from-args.php diff --git a/tests/PHPStan/Analyser/nsrt/precise-scope-select-from-args.php b/tests/PHPStan/Analyser/nsrt/precise-scope-select-from-args.php new file mode 100644 index 00000000000..989c34a46b3 --- /dev/null +++ b/tests/PHPStan/Analyser/nsrt/precise-scope-select-from-args.php @@ -0,0 +1,28 @@ + Date: Thu, 18 Jun 2026 22:18:47 +0200 Subject: [PATCH 104/113] Extract intrinsic argument parameter overrides from selectFromArgs into a pluggable applyIntrinsicArgOverrides --- src/Reflection/ParametersAcceptorSelector.php | 276 ++++++++++-------- 1 file changed, 158 insertions(+), 118 deletions(-) diff --git a/src/Reflection/ParametersAcceptorSelector.php b/src/Reflection/ParametersAcceptorSelector.php index d918b512f22..8220386c5c0 100644 --- a/src/Reflection/ParametersAcceptorSelector.php +++ b/src/Reflection/ParametersAcceptorSelector.php @@ -4,6 +4,7 @@ use Closure; use PhpParser\Node; +use PhpParser\Node\Expr; use PHPStan\Analyser\ArgumentsNormalizer; use PHPStan\Analyser\MutatingScope; use PHPStan\Analyser\Scope; @@ -80,6 +81,124 @@ public static function selectFromArgs( { $types = []; $unpack = false; + $parametersAcceptors = self::applyIntrinsicArgOverrides( + $args, + $parametersAcceptors, + $namedArgumentsVariants, + $scope, + static fn (Expr $e): Type => $scope->getType($e), + static fn (Expr $e): Type => $scope->getNativeType($e), + static fn (Type $t): Type => $scope->getIterableValueType($t), + static fn (Type $t): Type => $scope->getIterableKeyType($t), + ); + + if (count($parametersAcceptors) === 1) { + $acceptor = $parametersAcceptors[0]; + if (!self::hasAcceptorTemplateOrLateResolvableType($acceptor)) { + return $acceptor; + } + } + + $reorderedArgs = $args; + $parameters = null; + $singleParametersAcceptor = null; + if (count($parametersAcceptors) === 1) { + if (!array_is_list($args)) { + // actually $args parameter should be typed to list but we can't atm, + // because its a BC break. + $args = array_values($args); + } + $reorderedArgs = ArgumentsNormalizer::reorderArgs($parametersAcceptors[0], $args); + $singleParametersAcceptor = $parametersAcceptors[0]; + } + + $hasName = false; + foreach ($reorderedArgs ?? $args as $i => $arg) { + $originalArg = $arg->getAttribute(ArgumentsNormalizer::ORIGINAL_ARG_ATTRIBUTE) ?? $arg; + $parameter = null; + if ($singleParametersAcceptor !== null) { + $parameters = $singleParametersAcceptor->getParameters(); + if (isset($parameters[$i])) { + $parameter = $parameters[$i]; + } elseif (count($parameters) > 0 && $singleParametersAcceptor->isVariadic()) { + $parameter = array_last($parameters); + } + } + + if ($parameter !== null && $scope instanceof MutatingScope) { + $rememberTypes = !$originalArg->value instanceof Node\Expr\Closure && !$originalArg->value instanceof Node\Expr\ArrowFunction; + $scope = $scope->pushInFunctionCall(null, $parameter, $rememberTypes); + } + + $type = $scope->getType($originalArg->value); + + if ($parameter !== null && $scope instanceof MutatingScope) { + $scope = $scope->popInFunctionCall(); + } + + 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; + } + } + + if ($hasName && $namedArgumentsVariants !== null) { + return self::selectFromTypes($types, $namedArgumentsVariants, $unpack); + } + + return self::selectFromTypes($types, $parametersAcceptors, $unpack); + } + + /** + * @internal + * @param Node\Arg[] $args + * @param ParametersAcceptor[] $parametersAcceptors + * @param ParametersAcceptor[]|null $namedArgumentsVariants + * @param Closure(Expr): Type $typeGetter + * @param Closure(Expr): Type $nativeTypeGetter + * @param Closure(Type): Type $iterableValueTypeGetter + * @param Closure(Type): Type $iterableKeyTypeGetter + * @return ParametersAcceptor[] + */ + public static function applyIntrinsicArgOverrides( + array $args, + array $parametersAcceptors, + ?array $namedArgumentsVariants, + Scope $scope, + Closure $typeGetter, + Closure $nativeTypeGetter, + Closure $iterableValueTypeGetter, + Closure $iterableKeyTypeGetter, + ): array + { if ( count($args) > 0 && count($parametersAcceptors) > 0 @@ -89,15 +208,15 @@ public static function selectFromArgs( $callbackParameters = []; $nativeCallbackParameters = []; foreach ($arrayMapArgs as $arg) { - $argType = $scope->getType($arg->value); - $nativeArgType = $scope->getNativeType($arg->value); + $argType = ($typeGetter)($arg->value); + $nativeArgType = ($nativeTypeGetter)($arg->value); if ($arg->unpack) { $constantArrays = $argType->getConstantArrays(); if (count($constantArrays) > 0) { foreach ($constantArrays as $constantArray) { $valueTypes = $constantArray->getValueTypes(); foreach ($valueTypes as $valueType) { - $callbackParameters[] = new DummyParameter('item', $scope->getIterableValueType($valueType), optional: false, passedByReference: PassedByReference::createNo(), variadic: false, defaultValue: null); + $callbackParameters[] = new DummyParameter('item', ($iterableValueTypeGetter)($valueType), optional: false, passedByReference: PassedByReference::createNo(), variadic: false, defaultValue: null); } } } @@ -106,13 +225,13 @@ public static function selectFromArgs( foreach ($nativeConstantArrays as $constantArray) { $valueTypes = $constantArray->getValueTypes(); foreach ($valueTypes as $valueType) { - $nativeCallbackParameters[] = new DummyParameter('item', $scope->getIterableValueType($valueType), optional: false, passedByReference: PassedByReference::createNo(), variadic: false, defaultValue: null); + $nativeCallbackParameters[] = new DummyParameter('item', ($iterableValueTypeGetter)($valueType), optional: false, passedByReference: PassedByReference::createNo(), variadic: false, defaultValue: null); } } } } else { - $callbackParameters[] = new DummyParameter('item', $scope->getIterableValueType($argType), optional: false, passedByReference: PassedByReference::createNo(), variadic: false, defaultValue: null); - $nativeCallbackParameters[] = new DummyParameter('item', $scope->getIterableValueType($nativeArgType), optional: false, passedByReference: PassedByReference::createNo(), variadic: false, defaultValue: null); + $callbackParameters[] = new DummyParameter('item', ($iterableValueTypeGetter)($argType), optional: false, passedByReference: PassedByReference::createNo(), variadic: false, defaultValue: null); + $nativeCallbackParameters[] = new DummyParameter('item', ($iterableValueTypeGetter)($nativeArgType), optional: false, passedByReference: PassedByReference::createNo(), variadic: false, defaultValue: null); } } @@ -133,7 +252,7 @@ public static function selectFromArgs( } if (count($args) >= 3 && (bool) $args[0]->getAttribute(CurlSetOptArgVisitor::ATTRIBUTE_NAME)) { - $optType = $scope->getType($args[1]->value); + $optType = ($typeGetter)($args[1]->value); $valueTypes = []; foreach ($optType->getConstantScalarValues() as $scalarValue) { @@ -177,7 +296,7 @@ public static function selectFromArgs( } if (count($args) >= 2 && (bool) $args[1]->getAttribute(CurlSetOptArrayArgVisitor::ATTRIBUTE_NAME)) { - $optArrayType = $scope->getType($args[1]->value); + $optArrayType = ($typeGetter)($args[1]->value); $hasTypes = false; $builder = ConstantArrayTypeBuilder::createEmpty(); @@ -232,23 +351,23 @@ public static function selectFromArgs( $arrayFilterParameters = null; $nativeArrayFilterParameters = null; if (isset($args[2])) { - $mode = $scope->getType($args[2]->value); + $mode = ($typeGetter)($args[2]->value); if ($mode instanceof ConstantIntegerType) { if ($mode->getValue() === ARRAY_FILTER_USE_KEY) { $arrayFilterParameters = [ - new DummyParameter('key', $scope->getIterableKeyType($scope->getType($args[0]->value)), optional: false, passedByReference: PassedByReference::createNo(), variadic: false, defaultValue: null), + new DummyParameter('key', ($iterableKeyTypeGetter)(($typeGetter)($args[0]->value)), optional: false, passedByReference: PassedByReference::createNo(), variadic: false, defaultValue: null), ]; $nativeArrayFilterParameters = [ - new DummyParameter('key', $scope->getIterableKeyType($scope->getNativeType($args[0]->value)), optional: false, passedByReference: PassedByReference::createNo(), variadic: false, defaultValue: null), + new DummyParameter('key', ($iterableKeyTypeGetter)(($nativeTypeGetter)($args[0]->value)), optional: false, passedByReference: PassedByReference::createNo(), variadic: false, defaultValue: null), ]; } elseif ($mode->getValue() === ARRAY_FILTER_USE_BOTH) { $arrayFilterParameters = [ - new DummyParameter('item', $scope->getIterableValueType($scope->getType($args[0]->value)), optional: false, passedByReference: PassedByReference::createNo(), variadic: false, defaultValue: null), - new DummyParameter('key', $scope->getIterableKeyType($scope->getType($args[0]->value)), optional: false, passedByReference: PassedByReference::createNo(), variadic: false, defaultValue: null), + new DummyParameter('item', ($iterableValueTypeGetter)(($typeGetter)($args[0]->value)), optional: false, passedByReference: PassedByReference::createNo(), variadic: false, defaultValue: null), + new DummyParameter('key', ($iterableKeyTypeGetter)(($typeGetter)($args[0]->value)), optional: false, passedByReference: PassedByReference::createNo(), variadic: false, defaultValue: null), ]; $nativeArrayFilterParameters = [ - new DummyParameter('item', $scope->getIterableValueType($scope->getNativeType($args[0]->value)), optional: false, passedByReference: PassedByReference::createNo(), variadic: false, defaultValue: null), - new DummyParameter('key', $scope->getIterableKeyType($scope->getNativeType($args[0]->value)), optional: false, passedByReference: PassedByReference::createNo(), variadic: false, defaultValue: null), + new DummyParameter('item', ($iterableValueTypeGetter)(($nativeTypeGetter)($args[0]->value)), optional: false, passedByReference: PassedByReference::createNo(), variadic: false, defaultValue: null), + new DummyParameter('key', ($iterableKeyTypeGetter)(($nativeTypeGetter)($args[0]->value)), optional: false, passedByReference: PassedByReference::createNo(), variadic: false, defaultValue: null), ]; } } @@ -257,22 +376,22 @@ public static function selectFromArgs( $acceptor = $parametersAcceptors[0]; $parameters = $acceptor->getParameters(); if (isset($parameters[1])) { - $arrayArgType = $scope->getType($args[0]->value); + $arrayArgType = ($typeGetter)($args[0]->value); $callableType = new UnionType([ new CallableType( $arrayFilterParameters ?? [ - new DummyParameter('item', $scope->getIterableValueType($arrayArgType), optional: false, passedByReference: PassedByReference::createNo(), variadic: false, defaultValue: null), + new DummyParameter('item', ($iterableValueTypeGetter)($arrayArgType), optional: false, passedByReference: PassedByReference::createNo(), variadic: false, defaultValue: null), ], new BooleanType(), false, ), new NullType(), ]); - $nativeArrayArgType = $scope->getNativeType($args[0]->value); + $nativeArrayArgType = ($nativeTypeGetter)($args[0]->value); $nativeCallableType = new UnionType([ new CallableType( $nativeArrayFilterParameters ?? [ - new DummyParameter('item', $scope->getIterableValueType($nativeArrayArgType), optional: false, passedByReference: PassedByReference::createNo(), variadic: false, defaultValue: null), + new DummyParameter('item', ($iterableValueTypeGetter)($nativeArrayArgType), optional: false, passedByReference: PassedByReference::createNo(), variadic: false, defaultValue: null), ], new BooleanType(), false, @@ -314,19 +433,19 @@ public static function selectFromArgs( } if ((bool) $args[0]->getAttribute(ArrayWalkArgVisitor::ATTRIBUTE_NAME)) { - $arrayArgType = $scope->getType($args[0]->value); - $nativeArrayArgType = $scope->getNativeType($args[0]->value); + $arrayArgType = ($typeGetter)($args[0]->value); + $nativeArrayArgType = ($nativeTypeGetter)($args[0]->value); $arrayWalkParameters = [ - new DummyParameter('item', $scope->getIterableValueType($arrayArgType), optional: false, passedByReference: PassedByReference::createReadsArgument(), variadic: false, defaultValue: null), - new DummyParameter('key', $scope->getIterableKeyType($arrayArgType), optional: false, passedByReference: PassedByReference::createNo(), variadic: false, defaultValue: null), + new DummyParameter('item', ($iterableValueTypeGetter)($arrayArgType), optional: false, passedByReference: PassedByReference::createReadsArgument(), variadic: false, defaultValue: null), + new DummyParameter('key', ($iterableKeyTypeGetter)($arrayArgType), optional: false, passedByReference: PassedByReference::createNo(), variadic: false, defaultValue: null), ]; $nativeArrayWalkParameters = [ - new DummyParameter('item', $scope->getIterableValueType($nativeArrayArgType), optional: false, passedByReference: PassedByReference::createReadsArgument(), variadic: false, defaultValue: null), - new DummyParameter('key', $scope->getIterableKeyType($nativeArrayArgType), optional: false, passedByReference: PassedByReference::createNo(), variadic: false, defaultValue: null), + new DummyParameter('item', ($iterableValueTypeGetter)($nativeArrayArgType), optional: false, passedByReference: PassedByReference::createReadsArgument(), variadic: false, defaultValue: null), + new DummyParameter('key', ($iterableKeyTypeGetter)($nativeArrayArgType), optional: false, passedByReference: PassedByReference::createNo(), variadic: false, defaultValue: null), ]; if (isset($args[2])) { - $arrayWalkParameters[] = new DummyParameter('arg', $scope->getType($args[2]->value), optional: false, passedByReference: PassedByReference::createNo(), variadic: false, defaultValue: null); - $nativeArrayWalkParameters[] = new DummyParameter('arg', $scope->getNativeType($args[2]->value), optional: false, passedByReference: PassedByReference::createNo(), variadic: false, defaultValue: null); + $arrayWalkParameters[] = new DummyParameter('arg', ($typeGetter)($args[2]->value), optional: false, passedByReference: PassedByReference::createNo(), variadic: false, defaultValue: null); + $nativeArrayWalkParameters[] = new DummyParameter('arg', ($nativeTypeGetter)($args[2]->value), optional: false, passedByReference: PassedByReference::createNo(), variadic: false, defaultValue: null); } $acceptor = $parametersAcceptors[0]; @@ -343,20 +462,20 @@ public static function selectFromArgs( $acceptor = $parametersAcceptors[0]; $parameters = $acceptor->getParameters(); if (isset($parameters[1])) { - $argType = $scope->getType($args[0]->value); + $argType = ($typeGetter)($args[0]->value); $callableType = new CallableType( [ - new DummyParameter('value', $scope->getIterableValueType($argType), optional: false, passedByReference: PassedByReference::createNo(), variadic: false, defaultValue: null), - new DummyParameter('key', $scope->getIterableKeyType($argType), optional: false, passedByReference: PassedByReference::createNo(), variadic: false, defaultValue: null), + new DummyParameter('value', ($iterableValueTypeGetter)($argType), optional: false, passedByReference: PassedByReference::createNo(), variadic: false, defaultValue: null), + new DummyParameter('key', ($iterableKeyTypeGetter)($argType), optional: false, passedByReference: PassedByReference::createNo(), variadic: false, defaultValue: null), ], new BooleanType(), false, ); - $nativeArgType = $scope->getNativeType($args[0]->value); + $nativeArgType = ($nativeTypeGetter)($args[0]->value); $nativeCallableType = new CallableType( [ - new DummyParameter('value', $scope->getIterableValueType($nativeArgType), optional: false, passedByReference: PassedByReference::createNo(), variadic: false, defaultValue: null), - new DummyParameter('key', $scope->getIterableKeyType($nativeArgType), optional: false, passedByReference: PassedByReference::createNo(), variadic: false, defaultValue: null), + new DummyParameter('value', ($iterableValueTypeGetter)($nativeArgType), optional: false, passedByReference: PassedByReference::createNo(), variadic: false, defaultValue: null), + new DummyParameter('key', ($iterableKeyTypeGetter)($nativeArgType), optional: false, passedByReference: PassedByReference::createNo(), variadic: false, defaultValue: null), ], new BooleanType(), false, @@ -371,7 +490,7 @@ public static function selectFromArgs( $closureBindToVar instanceof Node\Expr\Variable && is_string($closureBindToVar->name) ) { - $varType = $scope->getType($closureBindToVar); + $varType = ($typeGetter)($closureBindToVar); if ((new ObjectType(Closure::class))->isSuperTypeOf($varType)->yes()) { $inFunction = $scope->getFunction(); if ($inFunction !== null) { @@ -458,92 +577,13 @@ public static function selectFromArgs( } } - if (count($parametersAcceptors) === 1) { - $acceptor = $parametersAcceptors[0]; - if (!self::hasAcceptorTemplateOrLateResolvableType($acceptor)) { - return $acceptor; - } - } - - $reorderedArgs = $args; - $parameters = null; - $singleParametersAcceptor = null; - if (count($parametersAcceptors) === 1) { - if (!array_is_list($args)) { - // actually $args parameter should be typed to list but we can't atm, - // because its a BC break. - $args = array_values($args); - } - $reorderedArgs = ArgumentsNormalizer::reorderArgs($parametersAcceptors[0], $args); - $singleParametersAcceptor = $parametersAcceptors[0]; - } - - $hasName = false; - foreach ($reorderedArgs ?? $args as $i => $arg) { - $originalArg = $arg->getAttribute(ArgumentsNormalizer::ORIGINAL_ARG_ATTRIBUTE) ?? $arg; - $parameter = null; - if ($singleParametersAcceptor !== null) { - $parameters = $singleParametersAcceptor->getParameters(); - if (isset($parameters[$i])) { - $parameter = $parameters[$i]; - } elseif (count($parameters) > 0 && $singleParametersAcceptor->isVariadic()) { - $parameter = array_last($parameters); - } - } - - if ($parameter !== null && $scope instanceof MutatingScope) { - $rememberTypes = !$originalArg->value instanceof Node\Expr\Closure && !$originalArg->value instanceof Node\Expr\ArrowFunction; - $scope = $scope->pushInFunctionCall(null, $parameter, $rememberTypes); - } - - $type = $scope->getType($originalArg->value); - - if ($parameter !== null && $scope instanceof MutatingScope) { - $scope = $scope->popInFunctionCall(); - } - - 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; - } - } - - if ($hasName && $namedArgumentsVariants !== null) { - return self::selectFromTypes($types, $namedArgumentsVariants, $unpack); - } - - return self::selectFromTypes($types, $parametersAcceptors, $unpack); + return $parametersAcceptors; } - private static function hasAcceptorTemplateOrLateResolvableType(ParametersAcceptor $acceptor): bool + /** + * @internal + */ + public static function hasAcceptorTemplateOrLateResolvableType(ParametersAcceptor $acceptor): bool { if ($acceptor->getReturnType()->hasTemplateOrLateResolvableType()) { return true; From bc198cf1ca7ef0772a6303df779c8b9ef26c3e8e Mon Sep 17 00:00:00 2001 From: Ondrej Mirtes Date: Fri, 19 Jun 2026 08:15:33 +0200 Subject: [PATCH 105/113] Resolve argument types on an arg-to-arg evolving scope and select the parameters acceptor via selectFromTypes in processArgs --- src/Analyser/ArgsResult.php | 61 ++++ src/Analyser/ExprHandler/FuncCallHandler.php | 35 ++- .../Helper/MethodCallReturnTypeHelper.php | 4 +- .../ExprHandler/MethodCallHandler.php | 33 ++- src/Analyser/ExprHandler/NewHandler.php | 31 ++- .../ExprHandler/StaticCallHandler.php | 16 +- src/Analyser/MutatingScope.php | 14 + src/Analyser/NodeScopeResolver.php | 262 +++++++++++++++--- src/Reflection/ParametersAcceptorSelector.php | 51 ++++ 9 files changed, 449 insertions(+), 58 deletions(-) create mode 100644 src/Analyser/ArgsResult.php diff --git a/src/Analyser/ArgsResult.php b/src/Analyser/ArgsResult.php new file mode 100644 index 00000000000..217cccecf5d --- /dev/null +++ b/src/Analyser/ArgsResult.php @@ -0,0 +1,61 @@ +expressionResult->getScope(); + } + + public function hasYield(): bool + { + return $this->expressionResult->hasYield(); + } + + public function isAlwaysTerminating(): bool + { + return $this->expressionResult->isAlwaysTerminating(); + } + + /** + * @return InternalThrowPoint[] + */ + public function getThrowPoints(): array + { + return $this->expressionResult->getThrowPoints(); + } + + /** + * @return ImpurePoint[] + */ + public function getImpurePoints(): array + { + return $this->expressionResult->getImpurePoints(); + } + + public function getResolvedParametersAcceptor(): ?ParametersAcceptor + { + return $this->resolvedParametersAcceptor; + } + +} diff --git a/src/Analyser/ExprHandler/FuncCallHandler.php b/src/Analyser/ExprHandler/FuncCallHandler.php index 07b5198172b..edda233fa02 100644 --- a/src/Analyser/ExprHandler/FuncCallHandler.php +++ b/src/Analyser/ExprHandler/FuncCallHandler.php @@ -110,6 +110,8 @@ public function processExpr(NodeScopeResolver $nodeScopeResolver, Stmt $stmt, Ex { $beforeScope = $scope; $parametersAcceptor = null; + $variants = []; + $namedArgumentsVariants = null; $functionReflection = null; $throwPoints = []; $impurePoints = []; @@ -117,10 +119,11 @@ public function processExpr(NodeScopeResolver $nodeScopeResolver, Stmt $stmt, Ex if ($expr->name instanceof Expr) { $nameType = $scope->getType($expr->name); if (!$nameType->isCallable()->no()) { + $variants = $nameType->getCallableParametersAcceptors($scope); $parametersAcceptor = ParametersAcceptorSelector::selectFromArgs( $scope, $expr->getArgs(), - $nameType->getCallableParametersAcceptors($scope), + $variants, null, ); } @@ -148,11 +151,13 @@ public function processExpr(NodeScopeResolver $nodeScopeResolver, Stmt $stmt, Ex } } elseif ($this->reflectionProvider->hasFunction($expr->name, $scope)) { $functionReflection = $this->reflectionProvider->getFunction($expr->name, $scope); + $variants = $functionReflection->getVariants(); + $namedArgumentsVariants = $functionReflection->getNamedArgumentsVariants(); $parametersAcceptor = ParametersAcceptorSelector::selectFromArgs( $scope, $expr->getArgs(), - $functionReflection->getVariants(), - $functionReflection->getNamedArgumentsVariants(), + $variants, + $namedArgumentsVariants, ); $impurePoint = SimpleImpurePoint::createFromVariant($functionReflection, $parametersAcceptor, $scope, $expr->getArgs()); if ($impurePoint !== null) { @@ -276,7 +281,8 @@ public function processExpr(NodeScopeResolver $nodeScopeResolver, Stmt $stmt, Ex } $scopeBeforeArgs = $scope; - $argsResult = $nodeScopeResolver->processArgs($stmt, $functionReflection, null, $parametersAcceptor, $normalizedExpr, $scope, $storage, $nodeCallbackForArgs, $context); + $argsResult = $nodeScopeResolver->processArgs($stmt, $functionReflection, null, $variants, $namedArgumentsVariants, $normalizedExpr, $scope, $storage, $nodeCallbackForArgs, $context); + $resolvedParametersAcceptor = $argsResult->getResolvedParametersAcceptor(); $scope = $argsResult->getScope(); $nodeScopeResolver->processDroppedArgs($stmt, $expr, $normalizedExpr, $scope, $storage, $context); $hasYield = $argsResult->hasYield(); @@ -582,6 +588,9 @@ public function processExpr(NodeScopeResolver $nodeScopeResolver, Stmt $stmt, Ex isAlwaysTerminating: $isAlwaysTerminating, throwPoints: $throwPoints, impurePoints: $impurePoints, + typeCallback: $resolvedParametersAcceptor !== null + ? fn (MutatingScope $s): Type => $this->resolveReturnType($s, $expr, $s->nativeTypesPromoted ? null : $resolvedParametersAcceptor) + : null, ); } @@ -790,6 +799,20 @@ static function (?Type $offsetType, Type $valueType, bool $optional) use (&$arra } public function resolveType(MutatingScope $scope, Expr $expr): Type + { + return $this->resolveReturnType($scope, $expr, null); + } + + /** + * The stored call-expression type is derived from $preResolvedAcceptor - the + * acceptor processArgs() selected from the arg types gathered on the arg-to-arg + * evolving scope (type-driven, generics resolved). When null (on-demand / + * synthetic pricing, or special cases below), it falls back to selecting from + * the args on the asking scope. + * + * @param FuncCall $expr + */ + private function resolveReturnType(MutatingScope $scope, Expr $expr, ?ParametersAcceptor $preResolvedAcceptor): Type { if ($expr->name instanceof Expr) { $calledOnType = $scope->getType($expr->name); @@ -797,7 +820,7 @@ public function resolveType(MutatingScope $scope, Expr $expr): Type return new ErrorType(); } - $parametersAcceptor = ParametersAcceptorSelector::selectFromArgs( + $parametersAcceptor = $preResolvedAcceptor ?? ParametersAcceptorSelector::selectFromArgs( $scope, $expr->getArgs(), $calledOnType->getCallableParametersAcceptors($scope), @@ -856,7 +879,7 @@ public function resolveType(MutatingScope $scope, Expr $expr): Type } } - $parametersAcceptor = ParametersAcceptorSelector::selectFromArgs( + $parametersAcceptor = $preResolvedAcceptor ?? ParametersAcceptorSelector::selectFromArgs( $scope, $expr->getArgs(), $functionReflection->getVariants(), diff --git a/src/Analyser/ExprHandler/Helper/MethodCallReturnTypeHelper.php b/src/Analyser/ExprHandler/Helper/MethodCallReturnTypeHelper.php index da565b78d8d..b34386598ce 100644 --- a/src/Analyser/ExprHandler/Helper/MethodCallReturnTypeHelper.php +++ b/src/Analyser/ExprHandler/Helper/MethodCallReturnTypeHelper.php @@ -8,6 +8,7 @@ use PHPStan\Analyser\MutatingScope; use PHPStan\DependencyInjection\AutowiredService; use PHPStan\DependencyInjection\Type\DynamicReturnTypeExtensionRegistryProvider; +use PHPStan\Reflection\ParametersAcceptor; use PHPStan\Reflection\ParametersAcceptorSelector; use PHPStan\Type\ObjectType; use PHPStan\Type\Type; @@ -29,6 +30,7 @@ public function methodCallReturnType( Type $typeWithMethod, string $methodName, MethodCall|Expr\StaticCall $methodCall, + ?ParametersAcceptor $preResolvedAcceptor = null, ): ?Type { $typeWithMethod = $scope->filterTypeWithMethod($typeWithMethod, $methodName); @@ -37,7 +39,7 @@ public function methodCallReturnType( } $methodReflection = $typeWithMethod->getMethod($methodName, $scope); - $parametersAcceptor = ParametersAcceptorSelector::selectFromArgs( + $parametersAcceptor = $preResolvedAcceptor ?? ParametersAcceptorSelector::selectFromArgs( $scope, $methodCall->getArgs(), $methodReflection->getVariants(), diff --git a/src/Analyser/ExprHandler/MethodCallHandler.php b/src/Analyser/ExprHandler/MethodCallHandler.php index 171f3f825bf..b8c53046d5a 100644 --- a/src/Analyser/ExprHandler/MethodCallHandler.php +++ b/src/Analyser/ExprHandler/MethodCallHandler.php @@ -31,6 +31,7 @@ use PHPStan\Node\InvalidateExprNode; use PHPStan\Reflection\Callables\SimpleImpurePoint; use PHPStan\Reflection\ExtendedParametersAcceptor; +use PHPStan\Reflection\ParametersAcceptor; use PHPStan\Reflection\ParametersAcceptorSelector; use PHPStan\Reflection\ReflectionProvider; use PHPStan\Type\ErrorType; @@ -97,17 +98,21 @@ public function processExpr(NodeScopeResolver $nodeScopeResolver, Stmt $stmt, Ex $scope = $scope->restoreOriginalScopeAfterClosureBind($originalScope); } $parametersAcceptor = null; + $variants = []; + $namedArgumentsVariants = null; $methodReflection = null; $calledOnType = $scope->getType($expr->var); if ($expr->name instanceof Identifier) { $methodName = $expr->name->name; $methodReflection = $scope->getMethodReflection($calledOnType, $methodName); if ($methodReflection !== null) { + $variants = $methodReflection->getVariants(); + $namedArgumentsVariants = $methodReflection->getNamedArgumentsVariants(); $parametersAcceptor = ParametersAcceptorSelector::selectFromArgs( $scope, $expr->getArgs(), - $methodReflection->getVariants(), - $methodReflection->getNamedArgumentsVariants(), + $variants, + $namedArgumentsVariants, ); } @@ -143,13 +148,15 @@ public function processExpr(NodeScopeResolver $nodeScopeResolver, Stmt $stmt, Ex $stmt, $methodReflection, $methodReflection !== null ? $scope->getNakedMethod($calledOnType, $methodReflection->getName()) : null, - $parametersAcceptor, + $variants, + $namedArgumentsVariants, $normalizedExpr, $scope, $storage, $nodeCallback, $context, ); + $resolvedParametersAcceptor = $argsResult->getResolvedParametersAcceptor(); $scope = $argsResult->getScope(); $nodeScopeResolver->processDroppedArgs($stmt, $expr, $normalizedExpr, $scope, $storage, $context); @@ -195,6 +202,10 @@ public function processExpr(NodeScopeResolver $nodeScopeResolver, Stmt $stmt, Ex $impurePoints = array_merge($impurePoints, $argsResult->getImpurePoints()); $isAlwaysTerminating = $isAlwaysTerminating || $argsResult->isAlwaysTerminating(); + $typeCallback = $resolvedParametersAcceptor !== null + ? fn (MutatingScope $s): Type => $this->resolveReturnType($s, $expr, $s->nativeTypesPromoted ? null : $resolvedParametersAcceptor) + : null; + $result = $this->expressionResultFactory->create( $scope, beforeScope: $beforeScope, @@ -204,6 +215,7 @@ public function processExpr(NodeScopeResolver $nodeScopeResolver, Stmt $stmt, Ex throwPoints: $throwPoints, impurePoints: $impurePoints, containsNullsafe: $varResult->containsNullsafe(), + typeCallback: $typeCallback, ); $calledOnType = $originalScope->getType($expr->var); @@ -232,6 +244,7 @@ public function processExpr(NodeScopeResolver $nodeScopeResolver, Stmt $stmt, Ex isAlwaysTerminating: $result->isAlwaysTerminating(), throwPoints: $result->getThrowPoints(), impurePoints: $result->getImpurePoints(), + typeCallback: $typeCallback, ); } } @@ -240,6 +253,19 @@ public function processExpr(NodeScopeResolver $nodeScopeResolver, Stmt $stmt, Ex } public function resolveType(MutatingScope $scope, Expr $expr): Type + { + return $this->resolveReturnType($scope, $expr, null); + } + + /** + * The stored call-expression type is derived from $preResolvedAcceptor - the + * acceptor processArgs() selected from the arg types gathered on the arg-to-arg + * evolving scope. Null falls back to re-selecting from the args on the asking + * scope (on-demand / synthetic pricing). + * + * @param MethodCall $expr + */ + private function resolveReturnType(MutatingScope $scope, Expr $expr, ?ParametersAcceptor $preResolvedAcceptor): Type { if ($expr->name instanceof Identifier) { if ($scope->nativeTypesPromoted) { @@ -261,6 +287,7 @@ public function resolveType(MutatingScope $scope, Expr $expr): Type $scope->getType($expr->var), $expr->name->name, $expr, + $preResolvedAcceptor, ); if ($returnType === null) { $returnType = new ErrorType(); diff --git a/src/Analyser/ExprHandler/NewHandler.php b/src/Analyser/ExprHandler/NewHandler.php index f96552e523a..968cab88657 100644 --- a/src/Analyser/ExprHandler/NewHandler.php +++ b/src/Analyser/ExprHandler/NewHandler.php @@ -35,6 +35,7 @@ use PHPStan\Parser\NewAssignedToPropertyVisitor; use PHPStan\Reflection\ClassReflection; use PHPStan\Reflection\Dummy\DummyConstructorReflection; +use PHPStan\Reflection\ExtendedMethodReflection; use PHPStan\Reflection\ExtendedParametersAcceptor; use PHPStan\Reflection\MethodReflection; use PHPStan\Reflection\ParametersAcceptor; @@ -201,7 +202,10 @@ public function processExpr(NodeScopeResolver $nodeScopeResolver, Stmt $stmt, Ex } } - $argsResult = $nodeScopeResolver->processArgs($stmt, $constructorReflection, null, $parametersAcceptor, $normalizedExpr, $scope, $storage, $nodeCallback, $context); + $variants = $constructorReflection !== null ? $constructorReflection->getVariants() : []; + $namedArgumentsVariants = $constructorReflection !== null ? $constructorReflection->getNamedArgumentsVariants() : null; + $argsResult = $nodeScopeResolver->processArgs($stmt, $constructorReflection, null, $variants, $namedArgumentsVariants, $normalizedExpr, $scope, $storage, $nodeCallback, $context); + $resolvedParametersAcceptor = $argsResult->getResolvedParametersAcceptor(); $scope = $argsResult->getScope(); $nodeScopeResolver->processDroppedArgs($stmt, $expr, $normalizedExpr, $scope, $storage, $context); $hasYield = $hasYield || $argsResult->hasYield(); @@ -227,11 +231,14 @@ public function processExpr(NodeScopeResolver $nodeScopeResolver, Stmt $stmt, Ex isAlwaysTerminating: $isAlwaysTerminating, throwPoints: $throwPoints, impurePoints: $impurePoints, + typeCallback: $resolvedParametersAcceptor !== null && $expr->class instanceof Name + ? fn (MutatingScope $s): Type => $this->resolveReturnType($s, $expr, $s->nativeTypesPromoted ? null : $resolvedParametersAcceptor) + : null, ); } /** - * @return array{?MethodReflection, ?ClassReflection, ?ParametersAcceptor, ImpurePoint[]} + * @return array{?ExtendedMethodReflection, ?ClassReflection, ?ParametersAcceptor, ImpurePoint[]} */ private function processConstructorReflection(string $className, New_ $expr, MutatingScope $scope, bool $isDynamic): array { @@ -322,9 +329,23 @@ private function getConstructorThrowPoint(MethodReflection $constructorReflectio } public function resolveType(MutatingScope $scope, Expr $expr): Type + { + return $this->resolveReturnType($scope, $expr, null); + } + + /** + * The stored new-expression type is derived from $preResolvedAcceptor - the + * constructor acceptor processArgs() selected from the arg types gathered on + * the arg-to-arg evolving scope (resolves the class's @template parameters + * from constructor args). Null falls back to re-selecting from the args on the + * asking scope (on-demand / synthetic pricing). + * + * @param New_ $expr + */ + private function resolveReturnType(MutatingScope $scope, Expr $expr, ?ParametersAcceptor $preResolvedAcceptor): Type { if ($expr->class instanceof Name) { - return $this->exactInstantiation($scope, $expr, $expr->class); + return $this->exactInstantiation($scope, $expr, $expr->class, $preResolvedAcceptor); } if ($expr->class instanceof Node\Stmt\Class_) { $anonymousClassReflection = $this->reflectionProvider->getAnonymousClassReflection($expr->class, $scope); @@ -336,7 +357,7 @@ public function resolveType(MutatingScope $scope, Expr $expr): Type return $exprType->getObjectTypeOrClassStringObjectType(); } - private function exactInstantiation(MutatingScope $scope, New_ $node, Name $className): Type + private function exactInstantiation(MutatingScope $scope, New_ $node, Name $className, ?ParametersAcceptor $preResolvedAcceptor): Type { $resolvedClassName = $scope->resolveName($className); $isStatic = false; @@ -382,7 +403,7 @@ private function exactInstantiation(MutatingScope $scope, New_ $node, Name $clas $node->getArgs(), ); - $parametersAcceptor = ParametersAcceptorSelector::selectFromArgs( + $parametersAcceptor = $preResolvedAcceptor ?? ParametersAcceptorSelector::selectFromArgs( $scope, $methodCall->getArgs(), $constructorMethod->getVariants(), diff --git a/src/Analyser/ExprHandler/StaticCallHandler.php b/src/Analyser/ExprHandler/StaticCallHandler.php index 7bc5282cdd4..23b244f01c0 100644 --- a/src/Analyser/ExprHandler/StaticCallHandler.php +++ b/src/Analyser/ExprHandler/StaticCallHandler.php @@ -99,6 +99,8 @@ public function processExpr(NodeScopeResolver $nodeScopeResolver, Stmt $stmt, Ex } $parametersAcceptor = null; + $variants = []; + $namedArgumentsVariants = null; $methodReflection = null; $closureBindScope = null; if ($expr->name instanceof Identifier) { @@ -107,11 +109,13 @@ public function processExpr(NodeScopeResolver $nodeScopeResolver, Stmt $stmt, Ex $methodName = $expr->name->name; if ($classType->hasMethod($methodName)->yes()) { $methodReflection = $classType->getMethod($methodName, $scope); + $variants = $methodReflection->getVariants(); + $namedArgumentsVariants = $methodReflection->getNamedArgumentsVariants(); $parametersAcceptor = ParametersAcceptorSelector::selectFromArgs( $scope, $expr->getArgs(), - $methodReflection->getVariants(), - $methodReflection->getNamedArgumentsVariants(), + $variants, + $namedArgumentsVariants, ); $declaringClass = $methodReflection->getDeclaringClass(); @@ -164,11 +168,13 @@ public function processExpr(NodeScopeResolver $nodeScopeResolver, Stmt $stmt, Ex $methodName = $expr->name->name; $methodReflection = $scope->getMethodReflection($classType, $methodName); if ($methodReflection !== null) { + $variants = $methodReflection->getVariants(); + $namedArgumentsVariants = $methodReflection->getNamedArgumentsVariants(); $parametersAcceptor = ParametersAcceptorSelector::selectFromArgs( $scope, $expr->getArgs(), - $methodReflection->getVariants(), - $methodReflection->getNamedArgumentsVariants(), + $variants, + $namedArgumentsVariants, ); } } @@ -217,7 +223,7 @@ public function processExpr(NodeScopeResolver $nodeScopeResolver, Stmt $stmt, Ex $returnType = $parametersAcceptor->getReturnType(); $isAlwaysTerminating = $isAlwaysTerminating || ($returnType instanceof NeverType && $returnType->isExplicit()); } - $argsResult = $nodeScopeResolver->processArgs($stmt, $methodReflection, null, $parametersAcceptor, $normalizedExpr, $scope, $storage, $nodeCallback, $context, $closureBindScope); + $argsResult = $nodeScopeResolver->processArgs($stmt, $methodReflection, null, $variants, $namedArgumentsVariants, $normalizedExpr, $scope, $storage, $nodeCallback, $context, $closureBindScope); $scope = $argsResult->getScope(); $nodeScopeResolver->processDroppedArgs($stmt, $expr, $normalizedExpr, $scope, $storage, $context); $scopeFunction = $scope->getFunction(); diff --git a/src/Analyser/MutatingScope.php b/src/Analyser/MutatingScope.php index 29c93ee2ed0..05f8326d6ef 100644 --- a/src/Analyser/MutatingScope.php +++ b/src/Analyser/MutatingScope.php @@ -1013,6 +1013,20 @@ private function resolveType(string $exprString, Expr $node): Type } if ($exprHandler instanceof TypeResolvingExprHandler) { + // A call handler that processed this node wires a typeCallback onto + // its stored ExpressionResult carrying the acceptor resolved from + // the arg types gathered on the arg-to-arg evolving scope. Prefer it + // over resolveType(), whose own re-selection would lose generics + // inferred from sibling args. resolveType() still answers synthetic / + // not-yet-processed nodes. + $storage = $this->expressionResultStorageStack->getCurrent(); + if ($storage !== null) { + $result = $storage->findExpressionResult($node); + if ($result !== null && $result->hasTypeCallback()) { + return $result->getTypeForScope($this->toMutatingScope()); + } + } + return $exprHandler->resolveType($this, $node); } diff --git a/src/Analyser/NodeScopeResolver.php b/src/Analyser/NodeScopeResolver.php index 71ba8a973b0..afb976120d4 100644 --- a/src/Analyser/NodeScopeResolver.php +++ b/src/Analyser/NodeScopeResolver.php @@ -3618,7 +3618,7 @@ private function processAttributeGroups( ); $expr = new New_($attr->name, $attr->args); $expr = ArgumentsNormalizer::reorderNewArguments($parametersAcceptor, $expr) ?? $expr; - $this->processArgs($stmt, $constructorReflection, null, $parametersAcceptor, $expr, $scope, $storage, $nodeCallback, ExpressionContext::createDeep()); + $this->processArgs($stmt, $constructorReflection, null, $constructorReflection->getVariants(), $constructorReflection->getNamedArgumentsVariants(), $expr, $scope, $storage, $nodeCallback, ExpressionContext::createDeep()); $this->callNodeCallback($nodeCallback, $attr, $scope, $storage); continue; } @@ -3806,26 +3806,61 @@ private function resolveClosureThisType( /** * @param MethodReflection|FunctionReflection|null $calleeReflection + * @param ParametersAcceptor[] $parametersAcceptors + * @param ParametersAcceptor[]|null $namedArgumentsVariants * @param callable(Node $node, Scope $scope): void $nodeCallback */ public function processArgs( Node\Stmt $stmt, $calleeReflection, ?ExtendedMethodReflection $nakedMethodReflection, - ?ParametersAcceptor $parametersAcceptor, + array $parametersAcceptors, + ?array $namedArgumentsVariants, CallLike $callLike, MutatingScope $scope, ExpressionResultStorage $storage, callable $nodeCallback, ExpressionContext $context, ?MutatingScope $closureBindScope = null, - ): ExpressionResult + ): ArgsResult { $args = $callLike->getArgs(); - $parameters = null; - if ($parametersAcceptor !== null) { - $parameters = $parametersAcceptor->getParameters(); + // 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; + + // Metadata acceptor: drives the per-arg by-ref/variadic matching. Selected + // ONCE from all arg types gathered on the initial scope (mirrors the + // original ParametersAcceptorSelector::selectFromArgs()), so multi-variant + // selection - which depends on the total argument count - is stable across + // the per-arg loop rather than flapping as the prefix grows. + $metadataAcceptor = null; + if ($parametersAcceptors !== []) { + $fastPath = count($parametersAcceptors) === 1 + && !ParametersAcceptorSelector::hasAcceptorTemplateOrLateResolvableType($parametersAcceptors[0]) + && !ParametersAcceptorSelector::argsHaveIntrinsicArgOverride($args); + if ($fastPath) { + $metadataAcceptor = $parametersAcceptors[0]; + } else { + $metadataTypes = []; + $metadataUnpack = false; + $metadataHasName = false; + foreach ($args as $i => $arg) { + $originalArg = $arg->getAttribute(ArgumentsNormalizer::ORIGINAL_ARG_ATTRIBUTE) ?? $arg; + if ($arg->value instanceof Expr\Closure || $arg->value instanceof Expr\ArrowFunction) { + $argType = $this->gatherClosureArgType($parametersAcceptors, $i, $arg->value, $scope); + } else { + $argType = $this->readStoredOrPriceOnDemand($arg->value, $scope); + } + $this->addGatheredArgType($metadataTypes, $metadataUnpack, $metadataHasName, $originalArg, $i, $argType); + } + $metadataAcceptor = $this->selectArgsMetadataAcceptor($args, $metadataTypes, $parametersAcceptors, $namedArgumentsVariants, $metadataHasName, $metadataUnpack, $scope); + } } $hasYield = false; @@ -3838,33 +3873,50 @@ public function processArgs( $deferredByRefClosureResults = []; $processingOrder = array_keys($args); - $hasReorderedArgs = false; - foreach ($args as $arg) { - if ($arg->hasAttribute(ArgumentsNormalizer::ORIGINAL_ARG_ATTRIBUTE)) { - $hasReorderedArgs = true; - break; + 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; } - } - if ($hasReorderedArgs) { - usort($processingOrder, static function (int $a, int $b) use ($args): int { - $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(); - }); - } + $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(); + }); 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; + $this->addGatheredArgType($gatheredTypes, $gatheredUnpack, $gatheredHasName, $originalArgForGather, $i, $this->gatherClosureArgType($parametersAcceptors, $i, $arg->value, $scope)); + } + + $parameters = $metadataAcceptor?->getParameters(); + $assignByReference = false; $parameter = null; $parameterType = null; @@ -3890,7 +3942,7 @@ public function processArgs( $parameterNativeType = $matchedParameter->getNativeType(); } $parameter = $matchedParameter; - } elseif (count($parameters) > 0 && $parametersAcceptor->isVariadic()) { + } elseif (count($parameters) > 0 && $metadataAcceptor->isVariadic()) { $lastParameter = array_last($parameters); $assignByReference = $lastParameter->passedByReference()->createsNewVariable(); $parameterType = $lastParameter->getType(); @@ -4059,6 +4111,8 @@ public function processArgs( } } } + + $this->addGatheredArgType($gatheredTypes, $gatheredUnpack, $gatheredHasName, $originalArg, $i, $exprResult->getTypeForScope($scope)); } if ($assignByReference && $lookForUnset) { @@ -4084,14 +4138,27 @@ public function processArgs( $scope = $deferredClosureResult->applyByRefUseScope($scope); } - if ($parameters !== null) { + // 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(). + $resolvedAcceptor = null; + if ($parametersAcceptors !== []) { + $resolvedAcceptor = $this->selectArgsMetadataAcceptor($args, $gatheredTypes, $parametersAcceptors, $namedArgumentsVariants, $gatheredHasName, $gatheredUnpack, $scope); + } + + // The by-ref OUT writeback reads the metadata acceptor: it is selected from + // the full argument count (stable variant) and its parameters are already + // generic-resolved, so OUT types are correct without re-flapping the variant. + $writebackParameters = $metadataAcceptor?->getParameters(); + if ($writebackParameters !== null) { foreach ($args as $i => $arg) { $assignByReference = false; $currentParameter = null; - if (isset($parameters[$i])) { - $currentParameter = $parameters[$i]; - } elseif (count($parameters) > 0 && $parametersAcceptor->isVariadic()) { - $currentParameter = array_last($parameters); + if (isset($writebackParameters[$i])) { + $currentParameter = $writebackParameters[$i]; + } elseif (count($writebackParameters) > 0 && $metadataAcceptor->isVariadic()) { + $currentParameter = array_last($writebackParameters); } if ($currentParameter !== null) { @@ -4142,11 +4209,12 @@ public function processArgs( if (!$argType->isObject()->no()) { $nakedReturnType = null; if ($nakedMethodReflection !== null) { - $nakedParametersAcceptor = ParametersAcceptorSelector::selectFromArgs( - $scope, - $args, + $nakedParametersAcceptor = $this->selectArgsAcceptor( + $gatheredTypes, $nakedMethodReflection->getVariants(), $nakedMethodReflection->getNamedArgumentsVariants(), + $gatheredHasName, + $gatheredUnpack, ); $nakedReturnType = $nakedParametersAcceptor->getReturnType(); } @@ -4167,7 +4235,125 @@ public function processArgs( } // not storing this, it's scope after processing all args - return $this->expressionResultFactory->create($scope, $scope, $callLike, $hasYield, $isAlwaysTerminating, $throwPoints, $impurePoints); + return new ArgsResult( + $this->expressionResultFactory->create($scope, $scope, $callLike, $hasYield, $isAlwaysTerminating, $throwPoints, $impurePoints), + $resolvedAcceptor, + ); + } + + /** + * 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; + } + } + + /** + * 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->readStoredOrPriceOnDemand($e, $scope), + fn (Expr $e): Type => $this->readStoredOrPriceOnDemandNative($e, $scope), + static fn (Type $t): Type => $scope->getIterableValueType($t), + static fn (Type $t): Type => $scope->getIterableKeyType($t), + ); + + return $this->selectArgsAcceptor($gatheredTypes, $overridden, $namedArgumentsVariants, $hasName, $unpack); } /** diff --git a/src/Reflection/ParametersAcceptorSelector.php b/src/Reflection/ParametersAcceptorSelector.php index 8220386c5c0..dbdfd42e528 100644 --- a/src/Reflection/ParametersAcceptorSelector.php +++ b/src/Reflection/ParametersAcceptorSelector.php @@ -580,6 +580,57 @@ public static function applyIntrinsicArgOverrides( return $parametersAcceptors; } + /** + * Whether applyIntrinsicArgOverrides() could rewrite the acceptor's parameter + * types for these args (array_map/filter/walk/find, curl_setopt, implode, + * Closure::bind). When false the single-acceptor metadata is override-free and + * processArgs() can skip re-selecting it per argument. Mirrors the attribute + * dispatch in applyIntrinsicArgOverrides(). + * + * @internal + * @param Node\Arg[] $args + */ + public static function argsHaveIntrinsicArgOverride(array $args): bool + { + if (count($args) === 0) { + return false; + } + + if ($args[0]->value->getAttribute(ArrayMapArgVisitor::ATTRIBUTE_NAME) !== null) { + return true; + } + + if ((bool) $args[0]->getAttribute(CurlSetOptArgVisitor::ATTRIBUTE_NAME)) { + return true; + } + + if (isset($args[1]) && (bool) $args[1]->getAttribute(CurlSetOptArrayArgVisitor::ATTRIBUTE_NAME)) { + return true; + } + + if ((bool) $args[0]->getAttribute(ArrayFilterArgVisitor::ATTRIBUTE_NAME)) { + return true; + } + + if ((bool) $args[0]->getAttribute(ImplodeArgVisitor::ATTRIBUTE_NAME)) { + return true; + } + + if ((bool) $args[0]->getAttribute(ArrayWalkArgVisitor::ATTRIBUTE_NAME)) { + return true; + } + + if ((bool) $args[0]->getAttribute(ArrayFindArgVisitor::ATTRIBUTE_NAME)) { + return true; + } + + if ($args[0]->getAttribute(ClosureBindToVarVisitor::ATTRIBUTE_NAME) !== null) { + return true; + } + + return $args[0]->getAttribute(ClosureBindArgVisitor::ATTRIBUTE_NAME) !== null; + } + /** * @internal */ From d064584d8d4fff9afa908f14dee2bb32ac928f40 Mon Sep 17 00:00:00 2001 From: Ondrej Mirtes Date: Fri, 19 Jun 2026 09:25:41 +0200 Subject: [PATCH 106/113] FuncCallHandler is no longer TypeResolvingExprHandler --- src/Analyser/ExprHandler/FuncCallHandler.php | 316 ++++++++++++++----- 1 file changed, 243 insertions(+), 73 deletions(-) diff --git a/src/Analyser/ExprHandler/FuncCallHandler.php b/src/Analyser/ExprHandler/FuncCallHandler.php index edda233fa02..db21d9db501 100644 --- a/src/Analyser/ExprHandler/FuncCallHandler.php +++ b/src/Analyser/ExprHandler/FuncCallHandler.php @@ -18,6 +18,8 @@ use PHPStan\Analyser\ExpressionResult; use PHPStan\Analyser\ExpressionResultFactory; use PHPStan\Analyser\ExpressionResultStorage; +use PHPStan\Analyser\ExprHandler; +use PHPStan\Analyser\ExprHandler\Helper\DefaultNarrowingHelper; use PHPStan\Analyser\ExprHandler\Helper\VoidToNullTypeTransformer; use PHPStan\Analyser\ImpurePoint; use PHPStan\Analyser\InternalThrowPoint; @@ -26,7 +28,6 @@ use PHPStan\Analyser\NoopNodeCallback; use PHPStan\Analyser\Scope; use PHPStan\Analyser\SpecifiedTypes; -use PHPStan\Analyser\TypeResolvingExprHandler; use PHPStan\Analyser\TypeSpecifier; use PHPStan\Analyser\TypeSpecifierContext; use PHPStan\DependencyInjection\AutowiredParameter; @@ -82,10 +83,10 @@ use function str_starts_with; /** - * @implements TypeResolvingExprHandler + * @implements ExprHandler */ #[AutowiredService] -final class FuncCallHandler implements TypeResolvingExprHandler +final class FuncCallHandler implements ExprHandler { public function __construct( @@ -97,6 +98,8 @@ public function __construct( #[AutowiredParameter] private bool $rememberPossiblyImpureFunctionValues, private ExpressionResultFactory $expressionResultFactory, + private TypeSpecifier $typeSpecifier, + private DefaultNarrowingHelper $defaultNarrowingHelper, ) { } @@ -113,6 +116,7 @@ public function processExpr(NodeScopeResolver $nodeScopeResolver, Stmt $stmt, Ex $variants = []; $namedArgumentsVariants = null; $functionReflection = null; + $nameResult = null; $throwPoints = []; $impurePoints = []; $isAlwaysTerminating = false; @@ -120,12 +124,10 @@ public function processExpr(NodeScopeResolver $nodeScopeResolver, Stmt $stmt, Ex $nameType = $scope->getType($expr->name); if (!$nameType->isCallable()->no()) { $variants = $nameType->getCallableParametersAcceptors($scope); - $parametersAcceptor = ParametersAcceptorSelector::selectFromArgs( - $scope, - $expr->getArgs(), - $variants, - null, - ); + // A structural acceptor (names/positions/variadic) drives the per-arg + // metadata and the throw/impure points - generics are resolved + // type-driven by processArgs() into $resolvedParametersAcceptor. + $parametersAcceptor = $this->combineVariantsForNormalization($expr->getArgs(), $variants, null); } $nameResult = $nodeScopeResolver->processExprNode($stmt, $expr->name, $scope, $storage, $nodeCallback, $context->enterDeep()); @@ -153,12 +155,10 @@ public function processExpr(NodeScopeResolver $nodeScopeResolver, Stmt $stmt, Ex $functionReflection = $this->reflectionProvider->getFunction($expr->name, $scope); $variants = $functionReflection->getVariants(); $namedArgumentsVariants = $functionReflection->getNamedArgumentsVariants(); - $parametersAcceptor = ParametersAcceptorSelector::selectFromArgs( - $scope, - $expr->getArgs(), - $variants, - $namedArgumentsVariants, - ); + // A structural acceptor (names/positions/variadic) drives argument + // normalization, the impure point and the throw points - generics are + // resolved type-driven by processArgs() into $resolvedParametersAcceptor. + $parametersAcceptor = $this->combineVariantsForNormalization($expr->getArgs(), $variants, $namedArgumentsVariants); $impurePoint = SimpleImpurePoint::createFromVariant($functionReflection, $parametersAcceptor, $scope, $expr->getArgs()); if ($impurePoint !== null) { $impurePoints[] = new ImpurePoint($scope, $expr, $impurePoint->getIdentifier(), $impurePoint->getDescription(), $impurePoint->isCertain()); @@ -306,6 +306,50 @@ public function processExpr(NodeScopeResolver $nodeScopeResolver, Stmt $stmt, Ex )->getScope(); } + // The return type is derived from $resolvedParametersAcceptor - the acceptor + // processArgs() selected from the arg types gathered on the arg-to-arg + // evolving scope (type-driven, generics resolved). When null + // (native-types-promoted, on-demand / synthetic pricing, or special cases + // inside resolveReturnType), the acceptor is re-derived from the + // already-processed argument results on the asking scope. + $typeCallback = fn (MutatingScope $s): Type => $this->resolveReturnType( + $nodeScopeResolver, + $s, + $expr, + $nameResult, + $s->nativeTypesPromoted ? null : $resolvedParametersAcceptor, + ); + $specifyTypesCallback = fn (MutatingScope $s, TypeSpecifierContext $specifyContext): SpecifiedTypes => $this->specifyTypes( + $nodeScopeResolver, + $s, + $expr, + $normalizedExpr, + $nameResult, + $resolvedParametersAcceptor, + $specifyContext, + ); + + // Store a preliminary result carrying the type/specify callbacks before the + // throw-point return type is computed: getFunctionThrowPoint() resolves the + // return type through dynamic return type extensions, one of which + // (TypeSpecifyingFunctionsDynamicReturnTypeExtension via + // ImpossibleCheckTypeHelper) narrows this very call. Without a stored result + // that narrowing would re-process this FuncCall on demand and recurse back + // into getFunctionThrowPoint(). The callbacks are scope-independent, so the + // preliminary result answers those asks correctly; the final result below + // overwrites it with the resolved scope and throw/impure points. + $nodeScopeResolver->storeExpressionResult($storage, $expr, $this->expressionResultFactory->create( + $scope, + beforeScope: $beforeScope, + expr: $expr, + hasYield: $hasYield, + isAlwaysTerminating: $isAlwaysTerminating, + throwPoints: [], + impurePoints: [], + typeCallback: $typeCallback, + specifyTypesCallback: $specifyTypesCallback, + )); + if ($normalizedExpr->name instanceof Expr) { $nameType = $scope->getType($normalizedExpr->name); if ( @@ -328,7 +372,25 @@ public function processExpr(NodeScopeResolver $nodeScopeResolver, Stmt $stmt, Ex } if ($functionReflection !== null) { - $functionThrowPoint = $this->getFunctionThrowPoint($functionReflection, $parametersAcceptor, $normalizedExpr, $scope, $context); + // The call's return type, computed from the already-processed argument + // results (resolveReturnType reads them via readStoredOrPriceOnDemand, + // never re-running processArgs) - asking Scope::getType() for the + // FuncCall here would re-enter this handler on demand, as its result is + // not stored yet. + $returnType = $this->resolveReturnType($nodeScopeResolver, $scope, $expr, $nameResult, $resolvedParametersAcceptor); + // The early structural check above (line ~180) only sees the unresolved + // acceptor return type; a conditional-return never (e.g. + // `($x is Foo ? never : string)`) only resolves to never once the actual + // argument types are folded in by the type-driven resolved acceptor. Read + // it from that acceptor's return type, not resolveReturnType(), which + // folds in call_user_func()/dynamic-extension special cases that must not + // make the call itself always-terminating (e.g. + // `call_user_func(fn() => exit())`). + if ($resolvedParametersAcceptor !== null) { + $resolvedReturnType = $resolvedParametersAcceptor->getReturnType(); + $isAlwaysTerminating = $isAlwaysTerminating || ($resolvedReturnType instanceof NeverType && $resolvedReturnType->isExplicit()); + } + $functionThrowPoint = $this->getFunctionThrowPoint($functionReflection, $parametersAcceptor, $returnType, $normalizedExpr, $scope, $context); if ($functionThrowPoint !== null) { $throwPoints[] = $functionThrowPoint; } @@ -588,15 +650,15 @@ public function processExpr(NodeScopeResolver $nodeScopeResolver, Stmt $stmt, Ex isAlwaysTerminating: $isAlwaysTerminating, throwPoints: $throwPoints, impurePoints: $impurePoints, - typeCallback: $resolvedParametersAcceptor !== null - ? fn (MutatingScope $s): Type => $this->resolveReturnType($s, $expr, $s->nativeTypesPromoted ? null : $resolvedParametersAcceptor) - : null, + typeCallback: $typeCallback, + specifyTypesCallback: $specifyTypesCallback, ); } private function getFunctionThrowPoint( FunctionReflection $functionReflection, ?ParametersAcceptor $parametersAcceptor, + Type $returnType, FuncCall $normalizedFuncCall, MutatingScope $scope, ExpressionContext $context, @@ -617,7 +679,6 @@ private function getFunctionThrowPoint( $throwType = $functionReflection->getThrowType(); if ($throwType === null) { - $returnType = $scope->getType($normalizedFuncCall); if ($returnType instanceof NeverType && $returnType->isExplicit()) { $throwType = new ObjectType(Throwable::class); } @@ -645,8 +706,7 @@ private function getFunctionThrowPoint( || $requiredParameters > 0 || count($normalizedFuncCall->getArgs()) > 0 ) { - $functionReturnedType = $scope->getType($normalizedFuncCall); - if (!$context->isInThrow() || !(new ObjectType(Throwable::class))->isSuperTypeOf($functionReturnedType)->yes()) { + if (!$context->isInThrow() || !(new ObjectType(Throwable::class))->isSuperTypeOf($returnType)->yes()) { return InternalThrowPoint::createImplicit($scope, $normalizedFuncCall); } } @@ -798,34 +858,74 @@ static function (?Type $offsetType, Type $valueType, bool $optional) use (&$arra return $arrayType; } - public function resolveType(MutatingScope $scope, Expr $expr): Type + /** + * A structural acceptor for argument normalization, the impure point and the + * throw points: it depends only on argument names/positions/variadic, so it is + * generic-agnostic (the type-driven, generic-resolved acceptor is produced by + * processArgs() instead). Mirrors selectArgsAcceptor()'s variant-set choice - + * named-argument calls select among the named-arguments variants, which carry + * the parameter defaults reorderFuncArguments() needs to fill skipped optionals. + * + * @param Arg[] $args + * @param ParametersAcceptor[] $variants + * @param ParametersAcceptor[]|null $namedArgumentsVariants + */ + private function combineVariantsForNormalization(array $args, array $variants, ?array $namedArgumentsVariants): ParametersAcceptor { - return $this->resolveReturnType($scope, $expr, null); + $hasName = false; + foreach ($args as $arg) { + if ($arg->name !== null) { + $hasName = true; + break; + } + } + + $selectedVariants = ($hasName && $namedArgumentsVariants !== null) ? $namedArgumentsVariants : $variants; + + return count($selectedVariants) === 1 + ? $selectedVariants[0] + : ParametersAcceptorSelector::combineAcceptors($selectedVariants); } /** - * The stored call-expression type is derived from $preResolvedAcceptor - the - * acceptor processArgs() selected from the arg types gathered on the arg-to-arg - * evolving scope (type-driven, generics resolved). When null (on-demand / - * synthetic pricing, or special cases below), it falls back to selecting from - * the args on the asking scope. + * The call-expression type is derived from $preResolvedAcceptor - the acceptor + * processArgs() selected from the arg types gathered on the arg-to-arg evolving + * scope (type-driven, generics resolved). When null (native-types-promoted, or + * a callable callee whose name was processed elsewhere), it falls back to a + * structural acceptor combined from the variants - generic resolution from the + * actual arg types lives in $preResolvedAcceptor, recomputed by on-demand / + * synthetic pricing that re-runs processArgs(). * * @param FuncCall $expr */ - private function resolveReturnType(MutatingScope $scope, Expr $expr, ?ParametersAcceptor $preResolvedAcceptor): Type + private function resolveReturnType(NodeScopeResolver $nodeScopeResolver, MutatingScope $scope, Expr $expr, ?ExpressionResult $nameResult, ?ParametersAcceptor $preResolvedAcceptor): Type { + // the operands/arguments were processed during processExpr; read their + // already computed results instead of re-walking via Scope::getType(). + // Synthetic nodes the resolver builds (e.g. Clone_, call_user_func's inner + // FuncCall) are priced on demand by the same helper. + $getType = static function (Expr $e) use ($expr, $nameResult, $scope, $nodeScopeResolver): Type { + if ($nameResult !== null && $e === $expr->name) { + return $nameResult->getTypeForScope($scope); + } + + return $nodeScopeResolver->readStoredOrPriceOnDemand($e, $scope); + }; + if ($expr->name instanceof Expr) { - $calledOnType = $scope->getType($expr->name); + $calledOnType = $getType($expr->name); if ($calledOnType->isCallable()->no()) { return new ErrorType(); } - $parametersAcceptor = $preResolvedAcceptor ?? ParametersAcceptorSelector::selectFromArgs( - $scope, - $expr->getArgs(), - $calledOnType->getCallableParametersAcceptors($scope), - null, - ); + if ($preResolvedAcceptor !== null) { + $parametersAcceptor = $preResolvedAcceptor; + } else { + $variants = $calledOnType->getCallableParametersAcceptors($scope); + $parametersAcceptor = count($variants) === 1 + ? $variants[0] + : ParametersAcceptorSelector::combineAcceptors($variants); + } $functionName = null; if ($expr->name instanceof String_) { @@ -866,7 +966,7 @@ private function resolveReturnType(MutatingScope $scope, Expr $expr, ?Parameters if ($result !== null) { [, $innerFuncCall] = $result; - return $scope->getType($innerFuncCall); + return $getType($innerFuncCall); } } @@ -875,22 +975,24 @@ private function resolveReturnType(MutatingScope $scope, Expr $expr, ?Parameters if ($result !== null) { [, $innerFuncCall] = $result; - return $scope->getType($innerFuncCall); + return $getType($innerFuncCall); } } - $parametersAcceptor = $preResolvedAcceptor ?? ParametersAcceptorSelector::selectFromArgs( - $scope, - $expr->getArgs(), - $functionReflection->getVariants(), - $functionReflection->getNamedArgumentsVariants(), - ); + if ($preResolvedAcceptor !== null) { + $parametersAcceptor = $preResolvedAcceptor; + } else { + $variants = $functionReflection->getVariants(); + $parametersAcceptor = count($variants) === 1 + ? $variants[0] + : ParametersAcceptorSelector::combineAcceptors($variants); + } $normalizedNode = ArgumentsNormalizer::reorderFuncArguments($parametersAcceptor, $expr); if ($normalizedNode !== null) { if ($functionReflection->getName() === 'clone' && count($normalizedNode->getArgs()) > 0) { - $cloneType = $scope->getType(new Expr\Clone_($normalizedNode->getArgs()[0]->value)); + $cloneType = $getType(new Expr\Clone_($normalizedNode->getArgs()[0]->value)); if (count($normalizedNode->getArgs()) === 2) { - $propertiesType = $scope->getType($normalizedNode->getArgs()[1]->value); + $propertiesType = $getType($normalizedNode->getArgs()[1]->value); if ($propertiesType->isConstantArray()->yes()) { $constantArrays = $propertiesType->getConstantArrays(); if (count($constantArrays) === 1) { @@ -920,22 +1022,26 @@ private function resolveReturnType(MutatingScope $scope, Expr $expr, ?Parameters return VoidToNullTypeTransformer::transform($parametersAcceptor->getReturnType(), $expr); } - public function specifyTypes(TypeSpecifier $typeSpecifier, Scope $scope, Expr $expr, TypeSpecifierContext $context): SpecifiedTypes + /** + * Ported inside-out from the old TypeResolvingExprHandler::specifyTypes(): the + * FunctionTypeSpecifyingExtensions, conditional-return-type and @phpstan-assert + * narrowing are invoked on the already-processed argument results. The acceptor + * is $resolvedParametersAcceptor (type-driven, generics resolved by processArgs) + * rather than re-selected from the args on the asking scope. The subject's own + * default narrowing comes from DefaultNarrowingHelper instead of + * TypeSpecifier::handleDefaultTruthyOrFalseyContext(), which would re-enter this + * expression through TypeSpecifier::create(). + * + * @param FuncCall $expr + */ + private function specifyTypes(NodeScopeResolver $nodeScopeResolver, MutatingScope $scope, Expr $expr, FuncCall $normalizedExpr, ?ExpressionResult $nameResult, ?ParametersAcceptor $resolvedParametersAcceptor, TypeSpecifierContext $context): SpecifiedTypes { if ($expr->name instanceof Name) { if ($this->reflectionProvider->hasFunction($expr->name, $scope)) { - // lazy create parametersAcceptor, as creation can be expensive - $parametersAcceptor = null; - $functionReflection = $this->reflectionProvider->getFunction($expr->name, $scope); - $normalizedExpr = $expr; $args = $expr->getArgs(); - if (count($args) > 0) { - $parametersAcceptor = ParametersAcceptorSelector::selectFromArgs($scope, $args, $functionReflection->getVariants(), $functionReflection->getNamedArgumentsVariants()); - $normalizedExpr = ArgumentsNormalizer::reorderFuncArguments($parametersAcceptor, $expr) ?? $expr; - } - foreach ($typeSpecifier->getFunctionTypeSpecifyingExtensions() as $extension) { + foreach ($this->typeSpecifier->getFunctionTypeSpecifyingExtensions() as $extension) { if (!$extension->isFunctionSupported($functionReflection, $normalizedExpr, $context)) { continue; } @@ -943,24 +1049,22 @@ public function specifyTypes(TypeSpecifier $typeSpecifier, Scope $scope, Expr $e return $extension->specifyTypes($functionReflection, $normalizedExpr, $scope, $context); } - if (count($args) > 0) { - $specifiedTypes = $typeSpecifier->specifyTypesFromConditionalReturnType($context, $expr, $parametersAcceptor, $scope); + if (count($args) > 0 && $resolvedParametersAcceptor !== null) { + $specifiedTypes = $this->typeSpecifier->specifyTypesFromConditionalReturnType($context, $expr, $resolvedParametersAcceptor, $scope); if ($specifiedTypes !== null) { return $specifiedTypes; } } $assertions = $functionReflection->getAsserts(); - if ($assertions->getAll() !== []) { - $parametersAcceptor ??= ParametersAcceptorSelector::selectFromArgs($scope, $args, $functionReflection->getVariants(), $functionReflection->getNamedArgumentsVariants()); - + if ($assertions->getAll() !== [] && $resolvedParametersAcceptor !== null) { $asserts = $assertions->mapTypes(static fn (Type $type) => TemplateTypeHelper::resolveTemplateTypes( $type, - $parametersAcceptor->getResolvedTemplateTypeMap(), - $parametersAcceptor instanceof ExtendedParametersAcceptor ? $parametersAcceptor->getCallSiteVarianceMap() : TemplateTypeVarianceMap::createEmpty(), + $resolvedParametersAcceptor->getResolvedTemplateTypeMap(), + $resolvedParametersAcceptor instanceof ExtendedParametersAcceptor ? $resolvedParametersAcceptor->getCallSiteVarianceMap() : TemplateTypeVarianceMap::createEmpty(), TemplateTypeVariance::createInvariant(), )); - $specifiedTypes = $typeSpecifier->specifyTypesFromAsserts($context, $expr, $asserts, $parametersAcceptor, $scope); + $specifiedTypes = $this->typeSpecifier->specifyTypesFromAsserts($context, $expr, $asserts, $resolvedParametersAcceptor, $scope); if ($specifiedTypes !== null) { return $specifiedTypes ->unionWith($typeSpecifier->handleDefaultTruthyOrFalseyContext($context, $expr, $scope)) @@ -969,30 +1073,38 @@ public function specifyTypes(TypeSpecifier $typeSpecifier, Scope $scope, Expr $e } } - return $typeSpecifier->handleDefaultTruthyOrFalseyContext($context, $expr, $scope); + return $this->defaultFuncCallNarrowing($nodeScopeResolver, $scope, $expr, $nameResult, $context); } - $specifiedTypes = $this->specifyTypesFromCallableCall($typeSpecifier, $context, $expr, $scope); + $specifiedTypes = $this->specifyTypesFromCallableCall($nodeScopeResolver, $context, $expr, $nameResult, $resolvedParametersAcceptor, $scope); if ($specifiedTypes !== null) { return $specifiedTypes; } - return $typeSpecifier->handleDefaultTruthyOrFalseyContext($context, $expr, $scope); + return $this->defaultFuncCallNarrowing($nodeScopeResolver, $scope, $expr, $nameResult, $context); } - private function specifyTypesFromCallableCall(TypeSpecifier $typeSpecifier, TypeSpecifierContext $context, FuncCall $call, Scope $scope): ?SpecifiedTypes + private function specifyTypesFromCallableCall(NodeScopeResolver $nodeScopeResolver, TypeSpecifierContext $context, FuncCall $call, ?ExpressionResult $nameResult, ?ParametersAcceptor $resolvedParametersAcceptor, MutatingScope $scope): ?SpecifiedTypes { if (!$call->name instanceof Expr) { return null; } - $calleeType = $scope->getType($call->name); + $calleeType = $nameResult !== null + ? $nameResult->getTypeForScope($scope) + : $nodeScopeResolver->readStoredOrPriceOnDemand($call->name, $scope); $assertions = null; $parametersAcceptor = null; if ($calleeType->isCallable()->yes()) { - $variants = $calleeType->getCallableParametersAcceptors($scope); - $parametersAcceptor = ParametersAcceptorSelector::selectFromArgs($scope, $call->getArgs(), $variants); + if ($resolvedParametersAcceptor !== null) { + $parametersAcceptor = $resolvedParametersAcceptor; + } else { + $variants = $calleeType->getCallableParametersAcceptors($scope); + $parametersAcceptor = count($variants) === 1 + ? $variants[0] + : ParametersAcceptorSelector::combineAcceptors($variants); + } if ($parametersAcceptor instanceof CallableParametersAcceptor) { $assertions = $parametersAcceptor->getAsserts(); } @@ -1009,7 +1121,65 @@ private function specifyTypesFromCallableCall(TypeSpecifier $typeSpecifier, Type TemplateTypeVariance::createInvariant(), )); - return $typeSpecifier->specifyTypesFromAsserts($context, $call, $asserts, $parametersAcceptor, $scope); + return $this->typeSpecifier->specifyTypesFromAsserts($context, $call, $asserts, $parametersAcceptor, $scope); + } + + /** + * The default truthy/falsey narrowing of the call expression itself, gated by + * the same purity check TypeSpecifier::create() applies: a function with side + * effects (or an unknown / impure callee whose result is not remembered) is not + * narrowable - calling it twice may yield different values - so it contributes + * no entry. Mirrors create()'s FuncCall handling inside-out, without re-entering + * this expression through create(). + * + */ + private function defaultFuncCallNarrowing(NodeScopeResolver $nodeScopeResolver, MutatingScope $scope, FuncCall $expr, ?ExpressionResult $nameResult, TypeSpecifierContext $context): SpecifiedTypes + { + if (!$this->isFuncCallNarrowable($nodeScopeResolver, $scope, $expr, $nameResult)) { + return (new SpecifiedTypes([], []))->setRootExpr($expr); + } + + return $this->defaultNarrowingHelper->specifyDefaultTypes($expr, $context); + } + + private function isFuncCallNarrowable(NodeScopeResolver $nodeScopeResolver, MutatingScope $scope, FuncCall $expr, ?ExpressionResult $nameResult): bool + { + if ($expr->name instanceof Name) { + if (!$this->reflectionProvider->hasFunction($expr->name, $scope)) { + // backwards compatibility with previous behaviour + return false; + } + + $hasSideEffects = $this->reflectionProvider->getFunction($expr->name, $scope)->hasSideEffects(); + if ($hasSideEffects->yes()) { + return false; + } + + return $this->rememberPossiblyImpureFunctionValues || $hasSideEffects->no(); + } + + $nameType = $nameResult !== null + ? $nameResult->getTypeForScope($scope) + : $nodeScopeResolver->readStoredOrPriceOnDemand($expr->name, $scope); + if (!$nameType->isCallable()->yes()) { + return true; + } + + $isPure = null; + foreach ($nameType->getCallableParametersAcceptors($scope) as $variant) { + $variantIsPure = $variant->isPure(); + $isPure = $isPure === null ? $variantIsPure : $isPure->and($variantIsPure); + } + + if ($isPure === null) { + return true; + } + + if ($isPure->no()) { + return false; + } + + return $this->rememberPossiblyImpureFunctionValues || $isPure->yes(); } private function getDynamicFunctionReturnType(MutatingScope $scope, FuncCall $normalizedNode, FunctionReflection $functionReflection): ?Type From 7f2b9fcb156219f1b4e057acf4f358a80a65fbc3 Mon Sep 17 00:00:00 2001 From: Ondrej Mirtes Date: Fri, 19 Jun 2026 09:45:58 +0200 Subject: [PATCH 107/113] MethodCallHandler is no longer TypeResolvingExprHandler --- .../ExprHandler/MethodCallHandler.php | 288 ++++++++++++++---- 1 file changed, 224 insertions(+), 64 deletions(-) diff --git a/src/Analyser/ExprHandler/MethodCallHandler.php b/src/Analyser/ExprHandler/MethodCallHandler.php index b8c53046d5a..bf89e22030c 100644 --- a/src/Analyser/ExprHandler/MethodCallHandler.php +++ b/src/Analyser/ExprHandler/MethodCallHandler.php @@ -2,6 +2,7 @@ namespace PHPStan\Analyser\ExprHandler; +use PhpParser\Node\Arg; use PhpParser\Node\Expr; use PhpParser\Node\Expr\BinaryOp\Identical; use PhpParser\Node\Expr\MethodCall; @@ -13,16 +14,15 @@ use PHPStan\Analyser\ExpressionResult; use PHPStan\Analyser\ExpressionResultFactory; use PHPStan\Analyser\ExpressionResultStorage; +use PHPStan\Analyser\ExprHandler; +use PHPStan\Analyser\ExprHandler\Helper\DefaultNarrowingHelper; use PHPStan\Analyser\ExprHandler\Helper\MethodCallReturnTypeHelper; use PHPStan\Analyser\ExprHandler\Helper\MethodThrowPointHelper; -use PHPStan\Analyser\ExprHandler\Helper\NullsafeShortCircuitingHelper; use PHPStan\Analyser\ImpurePoint; use PHPStan\Analyser\InternalThrowPoint; use PHPStan\Analyser\MutatingScope; use PHPStan\Analyser\NodeScopeResolver; -use PHPStan\Analyser\Scope; use PHPStan\Analyser\SpecifiedTypes; -use PHPStan\Analyser\TypeResolvingExprHandler; use PHPStan\Analyser\TypeSpecifier; use PHPStan\Analyser\TypeSpecifierContext; use PHPStan\DependencyInjection\AutowiredParameter; @@ -50,10 +50,10 @@ use function strtolower; /** - * @implements TypeResolvingExprHandler + * @implements ExprHandler */ #[AutowiredService] -final class MethodCallHandler implements TypeResolvingExprHandler +final class MethodCallHandler implements ExprHandler { public function __construct( @@ -63,6 +63,8 @@ public function __construct( #[AutowiredParameter] private bool $rememberPossiblyImpureFunctionValues, private ExpressionResultFactory $expressionResultFactory, + private TypeSpecifier $typeSpecifier, + private DefaultNarrowingHelper $defaultNarrowingHelper, ) { } @@ -101,25 +103,25 @@ public function processExpr(NodeScopeResolver $nodeScopeResolver, Stmt $stmt, Ex $variants = []; $namedArgumentsVariants = null; $methodReflection = null; - $calledOnType = $scope->getType($expr->var); + $nameResult = null; + // the var was processed above as the receiver; read its already-computed + // result instead of re-walking via Scope::getType(). + $calledOnType = $varResult->getTypeForScope($scope); if ($expr->name instanceof Identifier) { $methodName = $expr->name->name; $methodReflection = $scope->getMethodReflection($calledOnType, $methodName); if ($methodReflection !== null) { $variants = $methodReflection->getVariants(); $namedArgumentsVariants = $methodReflection->getNamedArgumentsVariants(); - $parametersAcceptor = ParametersAcceptorSelector::selectFromArgs( - $scope, - $expr->getArgs(), - $variants, - $namedArgumentsVariants, - ); - + // A structural acceptor (names/positions/variadic) drives argument + // normalization, the impure point and the throw point - generics are + // resolved type-driven by processArgs() into $resolvedParametersAcceptor. + $parametersAcceptor = $this->combineVariantsForNormalization($expr->getArgs(), $variants, $namedArgumentsVariants); } } else { - $methodNameResult = $nodeScopeResolver->processExprNode($stmt, $expr->name, $scope, $storage, $nodeCallback, $context->enterDeep()); - $throwPoints = array_merge($throwPoints, $methodNameResult->getThrowPoints()); - $scope = $methodNameResult->getScope(); + $nameResult = $nodeScopeResolver->processExprNode($stmt, $expr->name, $scope, $storage, $nodeCallback, $context->enterDeep()); + $throwPoints = array_merge($throwPoints, $nameResult->getThrowPoints()); + $scope = $nameResult->getScope(); } if ($methodReflection !== null) { @@ -160,8 +162,67 @@ public function processExpr(NodeScopeResolver $nodeScopeResolver, Stmt $stmt, Ex $scope = $argsResult->getScope(); $nodeScopeResolver->processDroppedArgs($stmt, $expr, $normalizedExpr, $scope, $storage, $context); + // The return type is derived from $resolvedParametersAcceptor - the acceptor + // processArgs() selected from the arg types gathered on the arg-to-arg + // evolving scope (type-driven, generics resolved). When null + // (native-types-promoted, or on-demand / synthetic pricing) the acceptor is + // re-derived from the already-processed argument results on the asking scope. + $typeCallback = fn (MutatingScope $s): Type => $this->resolveReturnType( + $nodeScopeResolver, + $s, + $expr, + $varResult, + $nameResult, + $s->nativeTypesPromoted ? null : $resolvedParametersAcceptor, + ); + $specifyTypesCallback = fn (MutatingScope $s, TypeSpecifierContext $specifyContext): SpecifiedTypes => $this->specifyTypes( + $nodeScopeResolver, + $s, + $expr, + $normalizedExpr, + $varResult, + $resolvedParametersAcceptor, + $specifyContext, + ); + + // Store a preliminary result carrying the type/specify callbacks before the + // throw point is computed: the method throw point resolves the return type + // (resolveReturnType below) through dynamic return type extensions, which can + // narrow this very call on demand. Without a stored result that narrowing + // would re-process this MethodCall on demand and recurse. The callbacks are + // scope-independent, so the preliminary result answers those asks correctly; + // the final result below overwrites it with the resolved scope and + // throw/impure points. + $nodeScopeResolver->storeExpressionResult($storage, $expr, $this->expressionResultFactory->create( + $scope, + beforeScope: $beforeScope, + expr: $expr, + hasYield: $hasYield, + isAlwaysTerminating: $isAlwaysTerminating, + throwPoints: [], + impurePoints: [], + containsNullsafe: $varResult->containsNullsafe(), + typeCallback: $typeCallback, + specifyTypesCallback: $specifyTypesCallback, + )); + if ($methodReflection !== null) { - $methodThrowPoint = $this->methodThrowPointHelper->getThrowPoint($methodReflection, $parametersAcceptor, $normalizedExpr, $scope, $context, $scope->getType($normalizedExpr)); + // The early structural check above only sees the unresolved acceptor + // return type; a conditional-return never (e.g. `($x is Foo ? never : + // string)`) only resolves to never once the actual argument types are + // folded in by the type-driven resolved acceptor. + if ($resolvedParametersAcceptor !== null) { + $resolvedReturnType = $resolvedParametersAcceptor->getReturnType(); + $isAlwaysTerminating = $isAlwaysTerminating || ($resolvedReturnType instanceof NeverType && $resolvedReturnType->isExplicit()); + } + + // The call's return type, computed from the already-processed argument + // results (resolveReturnType reads them via the receiver/name results and + // readStoredOrPriceOnDemand, never re-running processArgs) - asking + // Scope::getType() for the MethodCall here would re-enter this handler on + // demand, as its final result is not stored yet. + $methodCallReturnType = $this->resolveReturnType($nodeScopeResolver, $scope, $expr, $varResult, $nameResult, $resolvedParametersAcceptor); + $methodThrowPoint = $this->methodThrowPointHelper->getThrowPoint($methodReflection, $parametersAcceptor, $normalizedExpr, $scope, $context, $methodCallReturnType); if ($methodThrowPoint !== null) { $throwPoints[] = $methodThrowPoint; } @@ -170,21 +231,27 @@ public function processExpr(NodeScopeResolver $nodeScopeResolver, Stmt $stmt, Ex $nodeScopeResolver->callNodeCallback($nodeCallback, new InvalidateExprNode($normalizedExpr->var), $scope, $storage); $scope = $scope->invalidateExpression($normalizedExpr->var, true, $methodReflection->getDeclaringClass()); } elseif ($this->rememberPossiblyImpureFunctionValues && $methodReflection->hasSideEffects()->maybe() && !$methodReflection->getDeclaringClass()->isBuiltin()) { + // the remembered call value and the @phpstan-self-out type are + // generic-sensitive: resolve them from the type-driven acceptor + // processArgs() selected (generics resolved against the actual arg + // types), falling back to the structural acceptor for dynamic callees. + $acceptorForGenerics = $resolvedParametersAcceptor ?? $parametersAcceptor; $scope = $scope->assignExpression( new PossiblyImpureCallExpr($normalizedExpr, $normalizedExpr->var, sprintf('%s::%s()', $methodReflection->getDeclaringClass()->getDisplayName(), $methodReflection->getName())), - $parametersAcceptor->getReturnType(), + $acceptorForGenerics->getReturnType(), new MixedType(), ); } if (!$methodReflection->isStatic()) { $selfOutType = $methodReflection->getSelfOutType(); if ($selfOutType !== null) { + $acceptorForGenerics = $resolvedParametersAcceptor ?? $parametersAcceptor; $scope = $scope->assignExpression( $normalizedExpr->var, TemplateTypeHelper::resolveTemplateTypes( $selfOutType, - $parametersAcceptor->getResolvedTemplateTypeMap(), - $parametersAcceptor instanceof ExtendedParametersAcceptor ? $parametersAcceptor->getCallSiteVarianceMap() : TemplateTypeVarianceMap::createEmpty(), + $acceptorForGenerics->getResolvedTemplateTypeMap(), + $acceptorForGenerics instanceof ExtendedParametersAcceptor ? $acceptorForGenerics->getCallSiteVarianceMap() : TemplateTypeVarianceMap::createEmpty(), TemplateTypeVariance::createCovariant(), ), $scope->getNativeType($normalizedExpr->var), @@ -202,10 +269,6 @@ public function processExpr(NodeScopeResolver $nodeScopeResolver, Stmt $stmt, Ex $impurePoints = array_merge($impurePoints, $argsResult->getImpurePoints()); $isAlwaysTerminating = $isAlwaysTerminating || $argsResult->isAlwaysTerminating(); - $typeCallback = $resolvedParametersAcceptor !== null - ? fn (MutatingScope $s): Type => $this->resolveReturnType($s, $expr, $s->nativeTypesPromoted ? null : $resolvedParametersAcceptor) - : null; - $result = $this->expressionResultFactory->create( $scope, beforeScope: $beforeScope, @@ -216,9 +279,12 @@ public function processExpr(NodeScopeResolver $nodeScopeResolver, Stmt $stmt, Ex impurePoints: $impurePoints, containsNullsafe: $varResult->containsNullsafe(), typeCallback: $typeCallback, + specifyTypesCallback: $specifyTypesCallback, ); - $calledOnType = $originalScope->getType($expr->var); + // the var was processed above as the receiver; read its already-computed + // result on the original scope instead of re-walking via Scope::getType(). + $calledOnType = $varResult->getTypeForScope($originalScope); if (!$expr->name instanceof Identifier) { return $result; } @@ -244,7 +310,9 @@ public function processExpr(NodeScopeResolver $nodeScopeResolver, Stmt $stmt, Ex isAlwaysTerminating: $result->isAlwaysTerminating(), throwPoints: $result->getThrowPoints(), impurePoints: $result->getImpurePoints(), + containsNullsafe: $varResult->containsNullsafe(), typeCallback: $typeCallback, + specifyTypesCallback: $specifyTypesCallback, ); } } @@ -252,25 +320,32 @@ public function processExpr(NodeScopeResolver $nodeScopeResolver, Stmt $stmt, Ex return $result; } - public function resolveType(MutatingScope $scope, Expr $expr): Type - { - return $this->resolveReturnType($scope, $expr, null); - } - /** - * The stored call-expression type is derived from $preResolvedAcceptor - the - * acceptor processArgs() selected from the arg types gathered on the arg-to-arg - * evolving scope. Null falls back to re-selecting from the args on the asking - * scope (on-demand / synthetic pricing). + * The call-expression type is derived from $preResolvedAcceptor - the acceptor + * processArgs() selected from the arg types gathered on the arg-to-arg evolving + * scope (type-driven, generics resolved). When null (native-types-promoted, or + * on-demand / synthetic pricing) it falls back to re-selecting from the args via + * MethodCallReturnTypeHelper on the asking scope. + * + * The receiver/name were processed during processExpr; their already computed + * results are read instead of re-walking via Scope::getType(). The dynamic-name + * branch builds a synthetic MethodCall priced on demand by the resolver. * * @param MethodCall $expr */ - private function resolveReturnType(MutatingScope $scope, Expr $expr, ?ParametersAcceptor $preResolvedAcceptor): Type + private function resolveReturnType(NodeScopeResolver $nodeScopeResolver, MutatingScope $scope, Expr $expr, ExpressionResult $varResult, ?ExpressionResult $nameResult, ?ParametersAcceptor $preResolvedAcceptor): Type { + // a call on a nullsafe chain whose receiver is currently nullable + // short-circuits to null - the receiver result carries whether the chain + // contains a ?-> (a plain nullable receiver does not propagate). + $shortCircuit = static fn (Type $type): Type => $varResult->containsNullsafe() && TypeCombinator::containsNull($varResult->getTypeForScope($scope)) + ? TypeCombinator::addNull($type) + : $type; + if ($expr->name instanceof Identifier) { if ($scope->nativeTypesPromoted) { $methodReflection = $scope->getMethodReflection( - $scope->getNativeType($expr->var), + $varResult->getNativeTypeForScope($scope), $expr->name->name, ); if ($methodReflection === null) { @@ -279,12 +354,12 @@ private function resolveReturnType(MutatingScope $scope, Expr $expr, ?Parameters $returnType = ParametersAcceptorSelector::combineAcceptors($methodReflection->getVariants())->getNativeReturnType(); } - return NullsafeShortCircuitingHelper::getType($scope, $expr->var, $returnType); + return $shortCircuit($returnType); } $returnType = $this->methodCallReturnTypeHelper->methodCallReturnType( $scope, - $scope->getType($expr->var), + $varResult->getTypeForScope($scope), $expr->name->name, $expr, $preResolvedAcceptor, @@ -292,39 +367,57 @@ private function resolveReturnType(MutatingScope $scope, Expr $expr, ?Parameters if ($returnType === null) { $returnType = new ErrorType(); } - return NullsafeShortCircuitingHelper::getType($scope, $expr->var, $returnType); + return $shortCircuit($returnType); } - $nameType = $scope->getType($expr->name); + $nameType = $nameResult !== null ? $nameResult->getTypeForScope($scope) : $nodeScopeResolver->readStoredOrPriceOnDemand($expr->name, $scope); if (count($nameType->getConstantStrings()) > 0) { return TypeCombinator::union( - ...array_map(static fn ($constantString) => $constantString->getValue() === '' ? new ErrorType() : $scope - ->filterByTruthyValue(new Identical($expr->name, new String_($constantString->getValue()))) - ->getType(new MethodCall($expr->var, new Identifier($constantString->getValue()), $expr->args)), $nameType->getConstantStrings()), + ...array_map(static function ($constantString) use ($expr, $scope, $nodeScopeResolver): Type { + if ($constantString->getValue() === '') { + return new ErrorType(); + } + + // a method call with a concrete name on the name-pinned scope + // is synthetic. + $truthyScope = $scope->filterByTruthyValue(new Identical($expr->name, new String_($constantString->getValue()))); + + return $nodeScopeResolver->priceSyntheticOnDemand( + new MethodCall($expr->var, new Identifier($constantString->getValue()), $expr->args), + $truthyScope, + ); + }, $nameType->getConstantStrings()), ); } return new MixedType(); } - public function specifyTypes(TypeSpecifier $typeSpecifier, Scope $scope, Expr $expr, TypeSpecifierContext $context): SpecifiedTypes + /** + * Ported inside-out from the old TypeResolvingExprHandler::specifyTypes(): the + * MethodTypeSpecifyingExtensions, conditional-return-type and @phpstan-assert + * narrowing are invoked on the already-processed argument results. The acceptor + * is $resolvedParametersAcceptor (type-driven, generics resolved by processArgs) + * rather than re-selected from the args on the asking scope. The subject's own + * default narrowing comes from DefaultNarrowingHelper instead of + * TypeSpecifier::handleDefaultTruthyOrFalseyContext(), which would re-enter this + * expression through TypeSpecifier::create(). + * + * @param MethodCall $expr + * @param MethodCall $normalizedExpr + */ + private function specifyTypes(NodeScopeResolver $nodeScopeResolver, MutatingScope $scope, Expr $expr, Expr $normalizedExpr, ExpressionResult $varResult, ?ParametersAcceptor $resolvedParametersAcceptor, TypeSpecifierContext $context): SpecifiedTypes { if (!$expr->name instanceof Identifier) { - return $typeSpecifier->specifyDefaultTypes($scope, $expr, $context); + return $this->defaultMethodCallNarrowing($scope, $expr, $varResult, $context); } - $methodCalledOnType = $scope->getType($expr->var); + // the var was processed during processExpr; read its already-computed + // result instead of re-walking via Scope::getType(). + $methodCalledOnType = $varResult->getTypeForScope($scope); $methodReflection = $scope->getMethodReflection($methodCalledOnType, $expr->name->name); if ($methodReflection !== null) { - // lazy create parametersAcceptor, as creation can be expensive - $parametersAcceptor = null; - - $normalizedExpr = $expr; $args = $expr->getArgs(); - if (count($args) > 0) { - $parametersAcceptor = ParametersAcceptorSelector::selectFromArgs($scope, $args, $methodReflection->getVariants(), $methodReflection->getNamedArgumentsVariants()); - $normalizedExpr = ArgumentsNormalizer::reorderMethodArguments($parametersAcceptor, $expr) ?? $expr; - } $referencedClasses = $methodCalledOnType->getObjectClassNames(); if ( @@ -332,7 +425,7 @@ public function specifyTypes(TypeSpecifier $typeSpecifier, Scope $scope, Expr $e && $this->reflectionProvider->hasClass($referencedClasses[0]) ) { $methodClassReflection = $this->reflectionProvider->getClass($referencedClasses[0]); - foreach ($typeSpecifier->getMethodTypeSpecifyingExtensionsForClass($methodClassReflection->getName()) as $extension) { + foreach ($this->typeSpecifier->getMethodTypeSpecifyingExtensionsForClass($methodClassReflection->getName()) as $extension) { if (!$extension->isMethodSupported($methodReflection, $normalizedExpr, $context)) { continue; } @@ -341,24 +434,22 @@ public function specifyTypes(TypeSpecifier $typeSpecifier, Scope $scope, Expr $e } } - if (count($args) > 0) { - $specifiedTypes = $typeSpecifier->specifyTypesFromConditionalReturnType($context, $expr, $parametersAcceptor, $scope); + if (count($args) > 0 && $resolvedParametersAcceptor !== null) { + $specifiedTypes = $this->typeSpecifier->specifyTypesFromConditionalReturnType($context, $expr, $resolvedParametersAcceptor, $scope); if ($specifiedTypes !== null) { return $specifiedTypes; } } $assertions = $methodReflection->getAsserts(); - if ($assertions->getAll() !== []) { - $parametersAcceptor ??= ParametersAcceptorSelector::selectFromArgs($scope, $args, $methodReflection->getVariants(), $methodReflection->getNamedArgumentsVariants()); - + if ($assertions->getAll() !== [] && $resolvedParametersAcceptor !== null) { $asserts = $assertions->mapTypes(static fn (Type $type) => TemplateTypeHelper::resolveTemplateTypes( $type, - $parametersAcceptor->getResolvedTemplateTypeMap(), - $parametersAcceptor instanceof ExtendedParametersAcceptor ? $parametersAcceptor->getCallSiteVarianceMap() : TemplateTypeVarianceMap::createEmpty(), + $resolvedParametersAcceptor->getResolvedTemplateTypeMap(), + $resolvedParametersAcceptor instanceof ExtendedParametersAcceptor ? $resolvedParametersAcceptor->getCallSiteVarianceMap() : TemplateTypeVarianceMap::createEmpty(), TemplateTypeVariance::createInvariant(), )); - $specifiedTypes = $typeSpecifier->specifyTypesFromAsserts($context, $expr, $asserts, $parametersAcceptor, $scope); + $specifiedTypes = $this->typeSpecifier->specifyTypesFromAsserts($context, $expr, $asserts, $resolvedParametersAcceptor, $scope); if ($specifiedTypes !== null) { return $specifiedTypes ->unionWith($typeSpecifier->handleDefaultTruthyOrFalseyContext($context, $expr, $scope)) @@ -367,7 +458,76 @@ public function specifyTypes(TypeSpecifier $typeSpecifier, Scope $scope, Expr $e } } - return $typeSpecifier->handleDefaultTruthyOrFalseyContext($context, $expr, $scope); + return $this->defaultMethodCallNarrowing($scope, $expr, $varResult, $context); + } + + /** + * The default truthy/falsey narrowing of the call expression itself, gated by + * the same purity check TypeSpecifier::create() applies: a method with side + * effects (or an unknown method whose result is not remembered) is not + * narrowable - calling it twice may yield different values - so it contributes + * no entry. Mirrors create()'s MethodCall handling inside-out, without + * re-entering this expression through create(). + * + * @param MethodCall $expr + */ + private function defaultMethodCallNarrowing(MutatingScope $scope, Expr $expr, ExpressionResult $varResult, TypeSpecifierContext $context): SpecifiedTypes + { + if (!$this->isMethodCallNarrowable($scope, $expr, $varResult)) { + return (new SpecifiedTypes([], []))->setRootExpr($expr); + } + + return $this->defaultNarrowingHelper->specifyDefaultTypes($expr, $context); + } + + /** @param MethodCall $expr */ + private function isMethodCallNarrowable(MutatingScope $scope, Expr $expr, ExpressionResult $varResult): bool + { + if (!$expr->name instanceof Identifier) { + return true; + } + + $calledOnType = $varResult->getTypeForScope($scope); + $methodReflection = $scope->getMethodReflection($calledOnType, $expr->name->toString()); + if ($methodReflection === null) { + return false; + } + + $hasSideEffects = $methodReflection->hasSideEffects(); + if ($hasSideEffects->yes()) { + return false; + } + + return $this->rememberPossiblyImpureFunctionValues || $hasSideEffects->no(); + } + + /** + * A structural acceptor for argument normalization, the impure point and the + * throw point: it depends only on argument names/positions/variadic, so it is + * generic-agnostic (the type-driven, generic-resolved acceptor is produced by + * processArgs() instead). Named-argument calls select among the named-arguments + * variants, which carry the parameter defaults reorderMethodArguments() needs to + * fill skipped optionals. + * + * @param Arg[] $args + * @param ParametersAcceptor[] $variants + * @param ParametersAcceptor[]|null $namedArgumentsVariants + */ + private function combineVariantsForNormalization(array $args, array $variants, ?array $namedArgumentsVariants): ParametersAcceptor + { + $hasName = false; + foreach ($args as $arg) { + if ($arg->name !== null) { + $hasName = true; + break; + } + } + + $selectedVariants = ($hasName && $namedArgumentsVariants !== null) ? $namedArgumentsVariants : $variants; + + return count($selectedVariants) === 1 + ? $selectedVariants[0] + : ParametersAcceptorSelector::combineAcceptors($selectedVariants); } } From 5e03760de4cdea2e2331a3e402fb89f671863401 Mon Sep 17 00:00:00 2001 From: Ondrej Mirtes Date: Fri, 19 Jun 2026 09:54:08 +0200 Subject: [PATCH 108/113] NewHandler is no longer TypeResolvingExprHandler --- src/Analyser/ExprHandler/NewHandler.php | 171 ++++++++++++++++++------ 1 file changed, 130 insertions(+), 41 deletions(-) diff --git a/src/Analyser/ExprHandler/NewHandler.php b/src/Analyser/ExprHandler/NewHandler.php index 968cab88657..3b9f135034a 100644 --- a/src/Analyser/ExprHandler/NewHandler.php +++ b/src/Analyser/ExprHandler/NewHandler.php @@ -3,6 +3,7 @@ namespace PHPStan\Analyser\ExprHandler; use PhpParser\Node; +use PhpParser\Node\Arg; use PhpParser\Node\Expr; use PhpParser\Node\Expr\New_; use PhpParser\Node\Expr\StaticCall; @@ -13,6 +14,8 @@ use PHPStan\Analyser\ExpressionResult; use PHPStan\Analyser\ExpressionResultFactory; use PHPStan\Analyser\ExpressionResultStorage; +use PHPStan\Analyser\ExprHandler; +use PHPStan\Analyser\ExprHandler\Helper\DefaultNarrowingHelper; use PHPStan\Analyser\ImpurePoint; use PHPStan\Analyser\InternalThrowPoint; use PHPStan\Analyser\MutatingScope; @@ -24,7 +27,6 @@ use PHPStan\Analyser\ThrowPoint; use PHPStan\Analyser\Traverser\ConstructorClassTemplateTraverser; use PHPStan\Analyser\Traverser\GenericTypeTemplateTraverser; -use PHPStan\Analyser\TypeResolvingExprHandler; use PHPStan\Analyser\TypeSpecifier; use PHPStan\Analyser\TypeSpecifierContext; use PHPStan\DependencyInjection\AutowiredParameter; @@ -66,10 +68,10 @@ use function sprintf; /** - * @implements TypeResolvingExprHandler + * @implements ExprHandler */ #[AutowiredService] -final class NewHandler implements TypeResolvingExprHandler +final class NewHandler implements ExprHandler { public function __construct( @@ -80,6 +82,8 @@ public function __construct( #[AutowiredParameter(ref: '%exceptions.implicitThrows%')] private bool $implicitThrows, private ExpressionResultFactory $expressionResultFactory, + private TypeSpecifier $typeSpecifier, + private DefaultNarrowingHelper $defaultNarrowingHelper, ) { } @@ -101,6 +105,7 @@ public function processExpr(NodeScopeResolver $nodeScopeResolver, Stmt $stmt, Ex $impurePoints = []; $isAlwaysTerminating = false; $normalizedExpr = $expr; + $className = null; if ($expr->class instanceof Name) { $className = $scope->resolveName($expr->class); @@ -115,12 +120,10 @@ public function processExpr(NodeScopeResolver $nodeScopeResolver, Stmt $stmt, Ex $classReflection = $this->reflectionProvider->getAnonymousClassReflection($expr->class, $scope); // populates $expr->class->name if ($classReflection->hasConstructor()) { $constructorReflection = $classReflection->getConstructor(); - $parametersAcceptor = ParametersAcceptorSelector::selectFromArgs( - $scope, - $expr->getArgs(), - $constructorReflection->getVariants(), - $constructorReflection->getNamedArgumentsVariants(), - ); + // A structural acceptor (names/positions/variadic) drives argument + // normalization and the throw point - generics are resolved + // type-driven by processArgs() into $resolvedParametersAcceptor. + $parametersAcceptor = $this->combineVariantsForNormalization($expr->getArgs(), $constructorReflection->getVariants(), $constructorReflection->getNamedArgumentsVariants()); if ($constructorReflection->getDeclaringClass()->getName() === $classReflection->getName()) { $constructorResult = null; @@ -164,9 +167,24 @@ public function processExpr(NodeScopeResolver $nodeScopeResolver, Stmt $stmt, Ex } else { $nodeScopeResolver->processStmtNode($expr->class, $scope, $storage, $nodeCallback, StatementContext::createTopLevel()); } + + if ($parametersAcceptor !== null) { + $normalizedExpr = ArgumentsNormalizer::reorderNewArguments($parametersAcceptor, $expr) ?? $expr; + } } else { $isDynamic = true; - $objectClasses = $scope->getType($expr)->getObjectClassNames(); + + $classResult = $nodeScopeResolver->processExprNode($stmt, $expr->class, $scope, $storage, $nodeCallback, $context->enterDeep()); + $scope = $classResult->getScope(); + $hasYield = $classResult->hasYield(); + $throwPoints = $classResult->getThrowPoints(); + $impurePoints = $classResult->getImpurePoints(); + $isAlwaysTerminating = $classResult->isAlwaysTerminating(); + + // The instantiated object type derives from the class expression - read + // its already-processed result rather than asking Scope::getType() for + // the not-yet-stored New_ node, which would re-enter this handler. + $objectClasses = $classResult->getTypeForScope($scope)->getObjectTypeOrClassStringObjectType()->getObjectClassNames(); if (count($objectClasses) === 1) { $objectExprResult = $nodeScopeResolver->processExprNode($stmt, new New_(new Name($objectClasses[0])), $scope, $storage, new NoopNodeCallback(), $context->enterDeep()); $className = $objectClasses[0]; @@ -176,12 +194,6 @@ public function processExpr(NodeScopeResolver $nodeScopeResolver, Stmt $stmt, Ex $additionalThrowPoints = [InternalThrowPoint::createImplicit($scope, $expr)]; } - $classResult = $nodeScopeResolver->processExprNode($stmt, $expr->class, $scope, $storage, $nodeCallback, $context->enterDeep()); - $scope = $classResult->getScope(); - $hasYield = $classResult->hasYield(); - $throwPoints = $classResult->getThrowPoints(); - $impurePoints = $classResult->getImpurePoints(); - $isAlwaysTerminating = $classResult->isAlwaysTerminating(); $throwPoints = array_merge($throwPoints, $additionalThrowPoints); if ($className !== null) { @@ -213,6 +225,45 @@ public function processExpr(NodeScopeResolver $nodeScopeResolver, Stmt $stmt, Ex $impurePoints = array_merge($impurePoints, $argsResult->getImpurePoints()); $isAlwaysTerminating = $isAlwaysTerminating || $argsResult->isAlwaysTerminating(); + // The new-expression type is derived from $resolvedParametersAcceptor - the + // constructor acceptor processArgs() selected from the arg types gathered on + // the arg-to-arg evolving scope (type-driven, resolves the class's @template + // parameters from constructor args). When null (native-types-promoted, or + // on-demand / synthetic pricing), resolveReturnType() re-selects a structural + // acceptor from the args on the asking scope. + $typeCallback = fn (MutatingScope $s): Type => $this->resolveReturnType( + $nodeScopeResolver, + $s, + $expr, + $s->nativeTypesPromoted ? null : $resolvedParametersAcceptor, + ); + $specifyTypesCallback = fn (MutatingScope $s, TypeSpecifierContext $specifyContext): SpecifiedTypes => $this->specifyTypes( + $s, + $expr, + $resolvedParametersAcceptor, + $specifyContext, + ); + + // Store a preliminary result carrying the type/specify callbacks before the + // throw-point return type is computed: getConstructorThrowPoint() and the + // exact-instantiation return type resolution can re-enter on demand (e.g. a + // dynamic static-method return type extension narrowing this very + // instantiation). Without a stored result that narrowing would re-process + // this New_ on demand and recurse. The callbacks are scope-independent, so + // the preliminary result answers those asks correctly; the final result + // below overwrites it with the resolved scope and throw/impure points. + $nodeScopeResolver->storeExpressionResult($storage, $expr, $this->expressionResultFactory->create( + $scope, + beforeScope: $beforeScope, + expr: $expr, + hasYield: $hasYield, + isAlwaysTerminating: $isAlwaysTerminating, + throwPoints: [], + impurePoints: [], + typeCallback: $typeCallback, + specifyTypesCallback: $specifyTypesCallback, + )); + if ($constructorReflection !== null && $parametersAcceptor !== null) { $className ??= $constructorReflection->getDeclaringClass()->getName(); $constructorThrowPoint = $this->getConstructorThrowPoint($constructorReflection, $parametersAcceptor, $expr, new Name\FullyQualified($className), $expr->getArgs(), $scope, $context); @@ -231,9 +282,8 @@ public function processExpr(NodeScopeResolver $nodeScopeResolver, Stmt $stmt, Ex isAlwaysTerminating: $isAlwaysTerminating, throwPoints: $throwPoints, impurePoints: $impurePoints, - typeCallback: $resolvedParametersAcceptor !== null && $expr->class instanceof Name - ? fn (MutatingScope $s): Type => $this->resolveReturnType($s, $expr, $s->nativeTypesPromoted ? null : $resolvedParametersAcceptor) - : null, + typeCallback: $typeCallback, + specifyTypesCallback: $specifyTypesCallback, ); } @@ -251,12 +301,10 @@ private function processConstructorReflection(string $className, New_ $expr, Mut $classReflection = $this->reflectionProvider->getClass($className); if ($classReflection->hasConstructor()) { $constructorReflection = $classReflection->getConstructor(); - $parametersAcceptor = ParametersAcceptorSelector::selectFromArgs( - $scope, - $expr->getArgs(), - $constructorReflection->getVariants(), - $constructorReflection->getNamedArgumentsVariants(), - ); + // A structural acceptor (names/positions/variadic) drives argument + // normalization and the throw point - generics are resolved + // type-driven by processArgs() into $resolvedParametersAcceptor. + $parametersAcceptor = $this->combineVariantsForNormalization($expr->getArgs(), $constructorReflection->getVariants(), $constructorReflection->getNamedArgumentsVariants()); } } @@ -328,21 +376,45 @@ private function getConstructorThrowPoint(MethodReflection $constructorReflectio return null; } - public function resolveType(MutatingScope $scope, Expr $expr): Type + /** + * A structural acceptor for argument normalization and the throw point: it + * depends only on argument names/positions/variadic, so it is generic-agnostic + * (the type-driven, generic-resolved acceptor is produced by processArgs() + * instead). Mirrors the old variant-set choice - named-argument calls select + * among the named-arguments variants, which carry the parameter defaults + * reorderNewArguments() needs to fill skipped optionals. + * + * @param Arg[] $args + * @param ParametersAcceptor[] $variants + * @param ParametersAcceptor[]|null $namedArgumentsVariants + */ + private function combineVariantsForNormalization(array $args, array $variants, ?array $namedArgumentsVariants): ParametersAcceptor { - return $this->resolveReturnType($scope, $expr, null); + $hasName = false; + foreach ($args as $arg) { + if ($arg->name !== null) { + $hasName = true; + break; + } + } + + $selectedVariants = ($hasName && $namedArgumentsVariants !== null) ? $namedArgumentsVariants : $variants; + + return count($selectedVariants) === 1 + ? $selectedVariants[0] + : ParametersAcceptorSelector::combineAcceptors($selectedVariants); } /** * The stored new-expression type is derived from $preResolvedAcceptor - the * constructor acceptor processArgs() selected from the arg types gathered on * the arg-to-arg evolving scope (resolves the class's @template parameters - * from constructor args). Null falls back to re-selecting from the args on the - * asking scope (on-demand / synthetic pricing). + * from constructor args). Null falls back to re-selecting a structural acceptor + * from the args on the asking scope (on-demand / synthetic pricing). * * @param New_ $expr */ - private function resolveReturnType(MutatingScope $scope, Expr $expr, ?ParametersAcceptor $preResolvedAcceptor): Type + private function resolveReturnType(NodeScopeResolver $nodeScopeResolver, MutatingScope $scope, Expr $expr, ?ParametersAcceptor $preResolvedAcceptor): Type { if ($expr->class instanceof Name) { return $this->exactInstantiation($scope, $expr, $expr->class, $preResolvedAcceptor); @@ -353,7 +425,9 @@ private function resolveReturnType(MutatingScope $scope, Expr $expr, ?Parameters return new ObjectType($anonymousClassReflection->getName()); } - $exprType = $scope->getType($expr->class); + // the class expression was processed during processExpr; read its already + // computed result instead of re-walking via Scope::getType(). + $exprType = $nodeScopeResolver->readStoredOrPriceOnDemand($expr->class, $scope); return $exprType->getObjectTypeOrClassStringObjectType(); } @@ -403,8 +477,7 @@ private function exactInstantiation(MutatingScope $scope, New_ $node, Name $clas $node->getArgs(), ); - $parametersAcceptor = $preResolvedAcceptor ?? ParametersAcceptorSelector::selectFromArgs( - $scope, + $parametersAcceptor = $preResolvedAcceptor ?? $this->combineVariantsForNormalization( $methodCall->getArgs(), $constructorMethod->getVariants(), $constructorMethod->getNamedArgumentsVariants(), @@ -434,6 +507,9 @@ private function exactInstantiation(MutatingScope $scope, New_ $node, Name $clas return TypeCombinator::union(...$resolvedTypes); } + // $methodCall is a synthetic StaticCall the handler built - it is not a + // source node, so Scope::getType() prices it on demand (the constructor's + // own never-returning conditional return type). $methodResult = $scope->getType($methodCall); if ($methodResult instanceof NeverType && $methodResult->isExplicit()) { return $methodResult; @@ -639,13 +715,24 @@ classReflection: $classReflection->withTypes($types)->asFinal(), return TypeTraverser::map($newGenericType, new GenericTypeTemplateTraverser($resolvedTemplateTypeMap)); } - public function specifyTypes(TypeSpecifier $typeSpecifier, Scope $scope, Expr $expr, TypeSpecifierContext $context): SpecifiedTypes + /** + * Ported inside-out from the old TypeResolvingExprHandler::specifyTypes(): the + * constructor's @phpstan-assert narrowing is invoked on the already-processed + * argument results. The acceptor is $resolvedParametersAcceptor (type-driven, + * generics resolved by processArgs) rather than re-selected from the args on + * the asking scope. The subject's own default narrowing comes from + * DefaultNarrowingHelper instead of TypeSpecifier::specifyDefaultTypes(), which + * would re-enter this expression through TypeSpecifier::create(). + * + * @param New_ $expr + */ + private function specifyTypes(MutatingScope $scope, Expr $expr, ?ParametersAcceptor $resolvedParametersAcceptor, TypeSpecifierContext $context): SpecifiedTypes { if ( !$expr->class instanceof Name || !$this->reflectionProvider->hasClass($expr->class->toString()) ) { - return $typeSpecifier->specifyDefaultTypes($scope, $expr, $context); + return $this->defaultNarrowingHelper->specifyDefaultTypes($expr, $context); } $classReflection = $this->reflectionProvider->getClass($expr->class->toString()); @@ -654,17 +741,15 @@ public function specifyTypes(TypeSpecifier $typeSpecifier, Scope $scope, Expr $e $methodReflection = $classReflection->getConstructor(); $asserts = $methodReflection->getAsserts(); - if ($asserts->getAll() !== []) { - $parametersAcceptor = ParametersAcceptorSelector::selectFromArgs($scope, $expr->getArgs(), $methodReflection->getVariants(), $methodReflection->getNamedArgumentsVariants()); - + if ($asserts->getAll() !== [] && $resolvedParametersAcceptor !== null) { $asserts = $asserts->mapTypes(static fn (Type $type) => TemplateTypeHelper::resolveTemplateTypes( $type, - $parametersAcceptor->getResolvedTemplateTypeMap(), - $parametersAcceptor instanceof ExtendedParametersAcceptor ? $parametersAcceptor->getCallSiteVarianceMap() : TemplateTypeVarianceMap::createEmpty(), + $resolvedParametersAcceptor->getResolvedTemplateTypeMap(), + $resolvedParametersAcceptor instanceof ExtendedParametersAcceptor ? $resolvedParametersAcceptor->getCallSiteVarianceMap() : TemplateTypeVarianceMap::createEmpty(), TemplateTypeVariance::createInvariant(), )); - $specifiedTypes = $typeSpecifier->specifyTypesFromAsserts($context, $expr, $asserts, $parametersAcceptor, $scope); + $specifiedTypes = $this->typeSpecifier->specifyTypesFromAsserts($context, $expr, $asserts, $resolvedParametersAcceptor, $scope); if ($specifiedTypes !== null) { return $specifiedTypes; @@ -672,6 +757,10 @@ public function specifyTypes(TypeSpecifier $typeSpecifier, Scope $scope, Expr $e } } + // A known class without (applicable) constructor asserts contributes no + // narrowing entry, mirroring the old handler's empty return for this path + // (a `new X()` is always a truthy object, so the default truthy/falsey + // removal that path 1 emits would be a no-op here anyway). return (new SpecifiedTypes([], []))->setRootExpr($expr); } From b1f5af52c57e2771334aaa44b184e59633c1e098 Mon Sep 17 00:00:00 2001 From: Ondrej Mirtes Date: Fri, 19 Jun 2026 10:02:50 +0200 Subject: [PATCH 109/113] StaticCallHandler is no longer TypeResolvingExprHandler The return-type typeCallback reads the processArgs-resolved acceptor directly (naive), so late static binding is not yet re-bound against the asking scope. Three tests are knowingly left failing, to be fixed separately by re-binding static/$this against the asking scope: static-late-binding.php, bug-11687.php, bug-nullsafe-prop-static-access.php. NullsafeShortCircuitingHelper deleted (StaticCall was its last caller). --- .../Helper/NullsafeShortCircuitingHelper.php | 54 --- .../ExprHandler/StaticCallHandler.php | 326 +++++++++++++----- 2 files changed, 243 insertions(+), 137 deletions(-) delete mode 100644 src/Analyser/ExprHandler/Helper/NullsafeShortCircuitingHelper.php diff --git a/src/Analyser/ExprHandler/Helper/NullsafeShortCircuitingHelper.php b/src/Analyser/ExprHandler/Helper/NullsafeShortCircuitingHelper.php deleted file mode 100644 index 72710dd93b5..00000000000 --- a/src/Analyser/ExprHandler/Helper/NullsafeShortCircuitingHelper.php +++ /dev/null @@ -1,54 +0,0 @@ -getType($expr->var); - if (TypeCombinator::containsNull($varType)) { - return TypeCombinator::addNull($type); - } - - return $type; - } - - if ($expr instanceof ArrayDimFetch) { - return self::getType($scope, $expr->var, $type); - } - - if ($expr instanceof PropertyFetch) { - return self::getType($scope, $expr->var, $type); - } - - if ($expr instanceof StaticPropertyFetch && $expr->class instanceof Expr) { - return self::getType($scope, $expr->class, $type); - } - - if ($expr instanceof MethodCall) { - return self::getType($scope, $expr->var, $type); - } - - if ($expr instanceof StaticCall && $expr->class instanceof Expr) { - return self::getType($scope, $expr->class, $type); - } - - return $type; - } - -} diff --git a/src/Analyser/ExprHandler/StaticCallHandler.php b/src/Analyser/ExprHandler/StaticCallHandler.php index 23b244f01c0..5c10c7f5c71 100644 --- a/src/Analyser/ExprHandler/StaticCallHandler.php +++ b/src/Analyser/ExprHandler/StaticCallHandler.php @@ -2,6 +2,7 @@ namespace PHPStan\Analyser\ExprHandler; +use PhpParser\Node\Arg; use PhpParser\Node\Expr; use PhpParser\Node\Expr\BinaryOp\Identical; use PhpParser\Node\Expr\New_; @@ -16,17 +17,16 @@ use PHPStan\Analyser\ExpressionResult; use PHPStan\Analyser\ExpressionResultFactory; use PHPStan\Analyser\ExpressionResultStorage; +use PHPStan\Analyser\ExprHandler; +use PHPStan\Analyser\ExprHandler\Helper\DefaultNarrowingHelper; use PHPStan\Analyser\ExprHandler\Helper\MethodCallReturnTypeHelper; use PHPStan\Analyser\ExprHandler\Helper\MethodThrowPointHelper; -use PHPStan\Analyser\ExprHandler\Helper\NullsafeShortCircuitingHelper; use PHPStan\Analyser\ImpurePoint; use PHPStan\Analyser\InternalThrowPoint; use PHPStan\Analyser\MutatingScope; use PHPStan\Analyser\NodeScopeResolver; use PHPStan\Analyser\NoopNodeCallback; -use PHPStan\Analyser\Scope; use PHPStan\Analyser\SpecifiedTypes; -use PHPStan\Analyser\TypeResolvingExprHandler; use PHPStan\Analyser\TypeSpecifier; use PHPStan\Analyser\TypeSpecifierContext; use PHPStan\DependencyInjection\AutowiredParameter; @@ -35,6 +35,7 @@ use PHPStan\Reflection\Callables\SimpleImpurePoint; use PHPStan\Reflection\ExtendedParametersAcceptor; use PHPStan\Reflection\MethodReflection; +use PHPStan\Reflection\ParametersAcceptor; use PHPStan\Reflection\ParametersAcceptorSelector; use PHPStan\Reflection\ReflectionProvider; use PHPStan\Type\ErrorType; @@ -44,23 +45,20 @@ use PHPStan\Type\MixedType; use PHPStan\Type\NeverType; use PHPStan\Type\ObjectType; -use PHPStan\Type\StaticType; use PHPStan\Type\Type; use PHPStan\Type\TypeCombinator; -use PHPStan\Type\TypeWithClassName; use ReflectionProperty; use function array_map; use function array_merge; use function count; -use function in_array; use function sprintf; use function strtolower; /** - * @implements TypeResolvingExprHandler + * @implements ExprHandler */ #[AutowiredService] -final class StaticCallHandler implements TypeResolvingExprHandler +final class StaticCallHandler implements ExprHandler { public function __construct( @@ -70,6 +68,8 @@ public function __construct( #[AutowiredParameter] private bool $rememberPossiblyImpureFunctionValues, private ExpressionResultFactory $expressionResultFactory, + private TypeSpecifier $typeSpecifier, + private DefaultNarrowingHelper $defaultNarrowingHelper, ) { } @@ -87,6 +87,8 @@ public function processExpr(NodeScopeResolver $nodeScopeResolver, Stmt $stmt, Ex $impurePoints = []; $isAlwaysTerminating = false; $containsNullsafe = false; + $classResult = null; + $nameResult = null; if ($expr->class instanceof Expr) { $classResult = $nodeScopeResolver->processExprNode($stmt, $expr->class, $scope, $storage, $nodeCallback, $context->enterDeep()); $hasYield = $classResult->hasYield(); @@ -111,12 +113,10 @@ public function processExpr(NodeScopeResolver $nodeScopeResolver, Stmt $stmt, Ex $methodReflection = $classType->getMethod($methodName, $scope); $variants = $methodReflection->getVariants(); $namedArgumentsVariants = $methodReflection->getNamedArgumentsVariants(); - $parametersAcceptor = ParametersAcceptorSelector::selectFromArgs( - $scope, - $expr->getArgs(), - $variants, - $namedArgumentsVariants, - ); + // A structural acceptor (names/positions/variadic) drives argument + // normalization, the impure point and the throw point - generics are + // resolved type-driven by processArgs() into $resolvedParametersAcceptor. + $parametersAcceptor = $this->combineVariantsForNormalization($expr->getArgs(), $variants, $namedArgumentsVariants); $declaringClass = $methodReflection->getDeclaringClass(); if ( @@ -164,18 +164,15 @@ public function processExpr(NodeScopeResolver $nodeScopeResolver, Stmt $stmt, Ex $throwPoints[] = InternalThrowPoint::createImplicit($scope, $expr); } } elseif ($expr->class instanceof Expr) { - $classType = $scope->getType($expr->class)->getObjectTypeOrClassStringObjectType(); + // the class expr was processed above as the receiver; read its + // already-computed result instead of re-walking via Scope::getType(). + $classType = $classResult->getTypeForScope($scope)->getObjectTypeOrClassStringObjectType(); $methodName = $expr->name->name; $methodReflection = $scope->getMethodReflection($classType, $methodName); if ($methodReflection !== null) { $variants = $methodReflection->getVariants(); $namedArgumentsVariants = $methodReflection->getNamedArgumentsVariants(); - $parametersAcceptor = ParametersAcceptorSelector::selectFromArgs( - $scope, - $expr->getArgs(), - $variants, - $namedArgumentsVariants, - ); + $parametersAcceptor = $this->combineVariantsForNormalization($expr->getArgs(), $variants, $namedArgumentsVariants); } } } else { @@ -187,7 +184,9 @@ public function processExpr(NodeScopeResolver $nodeScopeResolver, Stmt $stmt, Ex } if ($expr->class instanceof Expr) { - $objectClasses = $scope->getType($expr->class)->getObjectClassNames(); + // the class expr was processed above as the receiver; read its + // already-computed result instead of re-walking via Scope::getType(). + $objectClasses = $classResult->getTypeForScope($scope)->getObjectClassNames(); if (count($objectClasses) !== 1) { $objectClasses = $scope->getType(new New_($expr->class))->getObjectClassNames(); } @@ -224,12 +223,72 @@ public function processExpr(NodeScopeResolver $nodeScopeResolver, Stmt $stmt, Ex $isAlwaysTerminating = $isAlwaysTerminating || ($returnType instanceof NeverType && $returnType->isExplicit()); } $argsResult = $nodeScopeResolver->processArgs($stmt, $methodReflection, null, $variants, $namedArgumentsVariants, $normalizedExpr, $scope, $storage, $nodeCallback, $context, $closureBindScope); + $resolvedParametersAcceptor = $argsResult->getResolvedParametersAcceptor(); $scope = $argsResult->getScope(); $nodeScopeResolver->processDroppedArgs($stmt, $expr, $normalizedExpr, $scope, $storage, $context); $scopeFunction = $scope->getFunction(); + // The early structural check above only sees the unresolved acceptor return + // type; a conditional-return never (e.g. `($x is Foo ? never : string)`) + // only resolves to never once the actual argument types are folded in by the + // type-driven resolved acceptor. + if ($resolvedParametersAcceptor !== null) { + $resolvedReturnType = $resolvedParametersAcceptor->getReturnType(); + $isAlwaysTerminating = $isAlwaysTerminating || ($resolvedReturnType instanceof NeverType && $resolvedReturnType->isExplicit()); + } + + // The return type is derived from $resolvedParametersAcceptor - the acceptor + // processArgs() selected from the arg types gathered on the arg-to-arg + // evolving scope (type-driven, generics resolved). When null + // (native-types-promoted, or on-demand / synthetic pricing) the acceptor is + // re-derived from the already-processed argument results on the asking scope. + $typeCallback = fn (MutatingScope $s): Type => $this->resolveReturnType( + $nodeScopeResolver, + $s, + $expr, + $classResult, + $nameResult, + $s->nativeTypesPromoted ? null : $resolvedParametersAcceptor, + ); + $specifyTypesCallback = fn (MutatingScope $s, TypeSpecifierContext $specifyContext): SpecifiedTypes => $this->specifyTypes( + $nodeScopeResolver, + $s, + $expr, + $normalizedExpr, + $classResult, + $resolvedParametersAcceptor, + $specifyContext, + ); + + // Store a preliminary result carrying the type/specify callbacks before the + // throw point is computed: the method throw point resolves the return type + // (resolveReturnType below) through dynamic static-method return type + // extensions, which can narrow this very call on demand. Without a stored + // result that narrowing would re-process this StaticCall on demand and + // recurse. The callbacks are scope-independent, so the preliminary result + // answers those asks correctly; the final result below overwrites it with the + // resolved scope and throw/impure points. + $nodeScopeResolver->storeExpressionResult($storage, $expr, $this->expressionResultFactory->create( + $scope, + beforeScope: $beforeScope, + expr: $expr, + hasYield: $hasYield, + isAlwaysTerminating: $isAlwaysTerminating, + throwPoints: [], + impurePoints: [], + containsNullsafe: $containsNullsafe, + typeCallback: $typeCallback, + specifyTypesCallback: $specifyTypesCallback, + )); + if ($methodReflection !== null) { - $methodThrowPoint = $this->methodThrowPointHelper->getThrowPoint($methodReflection, $parametersAcceptor, $normalizedExpr, $scope, $context, $scope->getType($normalizedExpr)); + // The call's return type, computed from the already-processed argument + // results (resolveReturnType reads them via the class/name results and + // readStoredOrPriceOnDemand, never re-running processArgs) - asking + // Scope::getType() for the StaticCall here would re-enter this handler on + // demand, as its final result is not stored yet. + $staticCallReturnType = $this->resolveReturnType($nodeScopeResolver, $scope, $expr, $classResult, $nameResult, $resolvedParametersAcceptor); + $methodThrowPoint = $this->methodThrowPointHelper->getThrowPoint($methodReflection, $parametersAcceptor, $normalizedExpr, $scope, $context, $staticCallReturnType); if ($methodThrowPoint !== null) { $throwPoints[] = $methodThrowPoint; } @@ -258,9 +317,13 @@ public function processExpr(NodeScopeResolver $nodeScopeResolver, Stmt $stmt, Ex && $methodReflection->hasSideEffects()->maybe() && !$methodReflection->getDeclaringClass()->isBuiltin() ) { + // the remembered call value is generic-sensitive: resolve it from the + // type-driven acceptor processArgs() selected (generics resolved against + // the actual arg types), falling back to the structural acceptor. + $acceptorForGenerics = $resolvedParametersAcceptor ?? $parametersAcceptor; $scope = $scope->assignExpression( new PossiblyImpureCallExpr($normalizedExpr, new Variable('this'), sprintf('%s::%s()', $methodReflection->getDeclaringClass()->getDisplayName(), $methodReflection->getName())), - $parametersAcceptor->getReturnType(), + $acceptorForGenerics->getReturnType(), new MixedType(), ); } @@ -300,17 +363,44 @@ public function processExpr(NodeScopeResolver $nodeScopeResolver, Stmt $stmt, Ex throwPoints: $throwPoints, impurePoints: $impurePoints, containsNullsafe: $containsNullsafe, + typeCallback: $typeCallback, + specifyTypesCallback: $specifyTypesCallback, ); } - public function resolveType(MutatingScope $scope, Expr $expr): Type + /** + * The call-expression type is derived from $preResolvedAcceptor - the acceptor + * processArgs() selected from the arg types gathered on the arg-to-arg evolving + * scope (type-driven, generics resolved). When null (native-types-promoted, or + * on-demand / synthetic pricing) it falls back to re-selecting from the args via + * MethodCallReturnTypeHelper on the asking scope. + * + * The class/name were processed during processExpr; their already computed + * results are read instead of re-walking via Scope::getType(). The dynamic-name + * branch builds a synthetic StaticCall priced on demand by the resolver. + * + * @param StaticCall $expr + */ + private function resolveReturnType(NodeScopeResolver $nodeScopeResolver, MutatingScope $scope, Expr $expr, ?ExpressionResult $classResult, ?ExpressionResult $nameResult, ?ParametersAcceptor $preResolvedAcceptor): Type { + // a call on a nullsafe chain whose class-receiver is currently nullable + // short-circuits to null - the class result carries whether the chain + // contains a ?-> (a plain nullable receiver does not propagate). + $shortCircuit = static fn (Type $type): Type => $expr->class instanceof Expr + && $classResult !== null + && $classResult->containsNullsafe() + && TypeCombinator::containsNull($classResult->getTypeForScope($scope)) + ? TypeCombinator::addNull($type) + : $type; + if ($expr->name instanceof Identifier) { if ($scope->nativeTypesPromoted) { if ($expr->class instanceof Name) { - $staticMethodCalledOnType = $this->resolveTypeByNameWithLateStaticBinding($scope, $expr->class, $expr->name); + $staticMethodCalledOnType = $scope->resolveTypeByName($expr->class); } else { - $staticMethodCalledOnType = $scope->getNativeType($expr->class); + $staticMethodCalledOnType = $classResult !== null + ? $classResult->getNativeTypeForScope($scope) + : $nodeScopeResolver->readStoredOrPriceOnDemandNative($expr->class, $scope); } $methodReflection = $scope->getMethodReflection( $staticMethodCalledOnType, @@ -322,17 +412,16 @@ public function resolveType(MutatingScope $scope, Expr $expr): Type $callType = ParametersAcceptorSelector::combineAcceptors($methodReflection->getVariants())->getNativeReturnType(); } - if ($expr->class instanceof Expr) { - return NullsafeShortCircuitingHelper::getType($scope, $expr->class, $callType); - } - - return $callType; + return $shortCircuit($callType); } if ($expr->class instanceof Name) { - $staticMethodCalledOnType = $this->resolveTypeByNameWithLateStaticBinding($scope, $expr->class, $expr->name); + $staticMethodCalledOnType = $scope->resolveTypeByName($expr->class); } else { - $staticMethodCalledOnType = TypeCombinator::removeNull($scope->getType($expr->class))->getObjectTypeOrClassStringObjectType(); + $classType = $classResult !== null + ? $classResult->getTypeForScope($scope) + : $nodeScopeResolver->readStoredOrPriceOnDemand($expr->class, $scope); + $staticMethodCalledOnType = TypeCombinator::removeNull($classType)->getObjectTypeOrClassStringObjectType(); } $callType = $this->methodCallReturnTypeHelper->methodCallReturnType( @@ -340,73 +429,70 @@ public function resolveType(MutatingScope $scope, Expr $expr): Type $staticMethodCalledOnType, $expr->name->toString(), $expr, + $preResolvedAcceptor, ); if ($callType === null) { $callType = new ErrorType(); } - if ($expr->class instanceof Expr) { - return NullsafeShortCircuitingHelper::getType($scope, $expr->class, $callType); - } - - return $callType; + return $shortCircuit($callType); } - $nameType = $scope->getType($expr->name); + $nameType = $nameResult !== null ? $nameResult->getTypeForScope($scope) : $nodeScopeResolver->readStoredOrPriceOnDemand($expr->name, $scope); if (count($nameType->getConstantStrings()) > 0) { return TypeCombinator::union( - ...array_map(static fn ($constantString) => $constantString->getValue() === '' ? new ErrorType() : $scope - ->filterByTruthyValue(new Identical($expr->name, new String_($constantString->getValue()))) - ->getType(new Expr\StaticCall($expr->class, new Identifier($constantString->getValue()), $expr->args)), $nameType->getConstantStrings()), - ); - } - - return new MixedType(); - } + ...array_map(static function ($constantString) use ($expr, $scope, $nodeScopeResolver): Type { + if ($constantString->getValue() === '') { + return new ErrorType(); + } - private function resolveTypeByNameWithLateStaticBinding(MutatingScope $scope, Name $class, Identifier $name): TypeWithClassName - { - $classType = $scope->resolveTypeByName($class); + // a static call with a concrete name on the name-pinned scope + // is synthetic. + $truthyScope = $scope->filterByTruthyValue(new Identical($expr->name, new String_($constantString->getValue()))); - if ( - $classType instanceof StaticType - && !in_array($class->toLowerString(), ['self', 'static', 'parent'], true) - ) { - $methodReflectionCandidate = $scope->getMethodReflection( - $classType, - $name->name, + return $nodeScopeResolver->priceSyntheticOnDemand( + new StaticCall($expr->class, new Identifier($constantString->getValue()), $expr->args), + $truthyScope, + ); + }, $nameType->getConstantStrings()), ); - if ($methodReflectionCandidate !== null && $methodReflectionCandidate->isStatic()) { - $classType = $classType->getStaticObjectType(); - } } - return $classType; + return new MixedType(); } - public function specifyTypes(TypeSpecifier $typeSpecifier, Scope $scope, Expr $expr, TypeSpecifierContext $context): SpecifiedTypes + /** + * Ported inside-out from the old TypeResolvingExprHandler::specifyTypes(): the + * StaticMethodTypeSpecifyingExtensions, conditional-return-type and assert + * narrowing are invoked on the already-processed argument + * results. The acceptor is $resolvedParametersAcceptor (type-driven, generics + * resolved by processArgs) rather than re-selected from the args on the asking + * scope. The subject's own default narrowing comes from DefaultNarrowingHelper + * instead of TypeSpecifier::handleDefaultTruthyOrFalseyContext(), which would + * re-enter this expression through TypeSpecifier::create(). + * + * @param StaticCall $expr + * @param StaticCall $normalizedExpr + */ + private function specifyTypes(NodeScopeResolver $nodeScopeResolver, MutatingScope $scope, Expr $expr, Expr $normalizedExpr, ?ExpressionResult $classResult, ?ParametersAcceptor $resolvedParametersAcceptor, TypeSpecifierContext $context): SpecifiedTypes { if (!$expr->name instanceof Identifier) { - return $typeSpecifier->specifyDefaultTypes($scope, $expr, $context); + return $this->defaultNarrowingHelper->specifyDefaultTypes($expr, $context); } if ($expr->class instanceof Name) { $calleeType = $scope->resolveTypeByName($expr->class); } else { - $calleeType = $scope->getType($expr->class); + // the class expr was processed during processExpr; read its + // already-computed result instead of re-walking via Scope::getType(). + $calleeType = $classResult !== null + ? $classResult->getTypeForScope($scope) + : $nodeScopeResolver->readStoredOrPriceOnDemand($expr->class, $scope); } $staticMethodReflection = $scope->getMethodReflection($calleeType, $expr->name->name); if ($staticMethodReflection !== null) { - // lazy create parametersAcceptor, as creation can be expensive - $parametersAcceptor = null; - - $normalizedExpr = $expr; $args = $expr->getArgs(); - if (count($args) > 0) { - $parametersAcceptor = ParametersAcceptorSelector::selectFromArgs($scope, $args, $staticMethodReflection->getVariants(), $staticMethodReflection->getNamedArgumentsVariants()); - $normalizedExpr = ArgumentsNormalizer::reorderStaticCallArguments($parametersAcceptor, $expr) ?? $expr; - } $referencedClasses = $calleeType->getObjectClassNames(); if ( @@ -414,7 +500,7 @@ public function specifyTypes(TypeSpecifier $typeSpecifier, Scope $scope, Expr $e && $this->reflectionProvider->hasClass($referencedClasses[0]) ) { $staticMethodClassReflection = $this->reflectionProvider->getClass($referencedClasses[0]); - foreach ($typeSpecifier->getStaticMethodTypeSpecifyingExtensionsForClass($staticMethodClassReflection->getName()) as $extension) { + foreach ($this->typeSpecifier->getStaticMethodTypeSpecifyingExtensionsForClass($staticMethodClassReflection->getName()) as $extension) { if (!$extension->isStaticMethodSupported($staticMethodReflection, $normalizedExpr, $context)) { continue; } @@ -423,24 +509,22 @@ public function specifyTypes(TypeSpecifier $typeSpecifier, Scope $scope, Expr $e } } - if (count($args) > 0) { - $specifiedTypes = $typeSpecifier->specifyTypesFromConditionalReturnType($context, $expr, $parametersAcceptor, $scope); + if (count($args) > 0 && $resolvedParametersAcceptor !== null) { + $specifiedTypes = $this->typeSpecifier->specifyTypesFromConditionalReturnType($context, $expr, $resolvedParametersAcceptor, $scope); if ($specifiedTypes !== null) { return $specifiedTypes; } } $assertions = $staticMethodReflection->getAsserts(); - if ($assertions->getAll() !== []) { - $parametersAcceptor ??= ParametersAcceptorSelector::selectFromArgs($scope, $args, $staticMethodReflection->getVariants(), $staticMethodReflection->getNamedArgumentsVariants()); - + if ($assertions->getAll() !== [] && $resolvedParametersAcceptor !== null) { $asserts = $assertions->mapTypes(static fn (Type $type) => TemplateTypeHelper::resolveTemplateTypes( $type, - $parametersAcceptor->getResolvedTemplateTypeMap(), - $parametersAcceptor instanceof ExtendedParametersAcceptor ? $parametersAcceptor->getCallSiteVarianceMap() : TemplateTypeVarianceMap::createEmpty(), + $resolvedParametersAcceptor->getResolvedTemplateTypeMap(), + $resolvedParametersAcceptor instanceof ExtendedParametersAcceptor ? $resolvedParametersAcceptor->getCallSiteVarianceMap() : TemplateTypeVarianceMap::createEmpty(), TemplateTypeVariance::createInvariant(), )); - $specifiedTypes = $typeSpecifier->specifyTypesFromAsserts($context, $expr, $asserts, $parametersAcceptor, $scope); + $specifiedTypes = $this->typeSpecifier->specifyTypesFromAsserts($context, $expr, $asserts, $resolvedParametersAcceptor, $scope); if ($specifiedTypes !== null) { return $specifiedTypes ->unionWith($typeSpecifier->handleDefaultTruthyOrFalseyContext($context, $expr, $scope)) @@ -449,7 +533,83 @@ public function specifyTypes(TypeSpecifier $typeSpecifier, Scope $scope, Expr $e } } - return $typeSpecifier->handleDefaultTruthyOrFalseyContext($context, $expr, $scope); + return $this->defaultStaticCallNarrowing($scope, $expr, $classResult, $nodeScopeResolver, $context); + } + + /** + * The default truthy/falsey narrowing of the call expression itself, gated by + * the same purity check TypeSpecifier::create() applies: a static method with + * side effects (or an unknown method whose result is not remembered) is not + * narrowable - calling it twice may yield different values - so it contributes + * no entry. Mirrors create()'s StaticCall handling inside-out, without + * re-entering this expression through create(). + * + * @param StaticCall $expr + */ + private function defaultStaticCallNarrowing(MutatingScope $scope, Expr $expr, ?ExpressionResult $classResult, NodeScopeResolver $nodeScopeResolver, TypeSpecifierContext $context): SpecifiedTypes + { + if (!$this->isStaticCallNarrowable($scope, $expr, $classResult, $nodeScopeResolver)) { + return (new SpecifiedTypes([], []))->setRootExpr($expr); + } + + return $this->defaultNarrowingHelper->specifyDefaultTypes($expr, $context); + } + + /** @param StaticCall $expr */ + private function isStaticCallNarrowable(MutatingScope $scope, Expr $expr, ?ExpressionResult $classResult, NodeScopeResolver $nodeScopeResolver): bool + { + if (!$expr->name instanceof Identifier) { + return true; + } + + if ($expr->class instanceof Name) { + $calleeType = $scope->resolveTypeByName($expr->class); + } else { + $calleeType = $classResult !== null + ? $classResult->getTypeForScope($scope) + : $nodeScopeResolver->readStoredOrPriceOnDemand($expr->class, $scope); + } + + $methodReflection = $scope->getMethodReflection($calleeType, $expr->name->toString()); + if ($methodReflection === null) { + return false; + } + + $hasSideEffects = $methodReflection->hasSideEffects(); + if ($hasSideEffects->yes()) { + return false; + } + + return $this->rememberPossiblyImpureFunctionValues || $hasSideEffects->no(); + } + + /** + * A structural acceptor for argument normalization, the impure point and the + * throw point: it depends only on argument names/positions/variadic, so it is + * generic-agnostic (the type-driven, generic-resolved acceptor is produced by + * processArgs() instead). Named-argument calls select among the named-arguments + * variants, which carry the parameter defaults reorderStaticCallArguments() + * needs to fill skipped optionals. + * + * @param Arg[] $args + * @param ParametersAcceptor[] $variants + * @param ParametersAcceptor[]|null $namedArgumentsVariants + */ + private function combineVariantsForNormalization(array $args, array $variants, ?array $namedArgumentsVariants): ParametersAcceptor + { + $hasName = false; + foreach ($args as $arg) { + if ($arg->name !== null) { + $hasName = true; + break; + } + } + + $selectedVariants = ($hasName && $namedArgumentsVariants !== null) ? $namedArgumentsVariants : $variants; + + return count($selectedVariants) === 1 + ? $selectedVariants[0] + : ParametersAcceptorSelector::combineAcceptors($selectedVariants); } } From 7257b5b95ac48697e7047c95444dc7b4e282fe19 Mon Sep 17 00:00:00 2001 From: Ondrej Mirtes Date: Fri, 19 Jun 2026 10:13:49 +0200 Subject: [PATCH 110/113] Extract combineVariantsForNormalization into a shared ParametersAcceptorSelector helper Replaces the four identical per-handler copies and the attribute-constructor reorderNewArguments site (which still called selectFromArgs) with one structural-acceptor helper. (3 late-static-binding tests remain knowingly red, see prior commit.) --- src/Analyser/ExprHandler/FuncCallHandler.php | 33 ++--------------- .../ExprHandler/MethodCallHandler.php | 32 +---------------- src/Analyser/ExprHandler/NewHandler.php | 36 ++----------------- .../ExprHandler/StaticCallHandler.php | 34 ++---------------- src/Analyser/NodeScopeResolver.php | 3 +- src/Reflection/ParametersAcceptorSelector.php | 28 +++++++++++++++ 6 files changed, 37 insertions(+), 129 deletions(-) diff --git a/src/Analyser/ExprHandler/FuncCallHandler.php b/src/Analyser/ExprHandler/FuncCallHandler.php index db21d9db501..cab3d07523f 100644 --- a/src/Analyser/ExprHandler/FuncCallHandler.php +++ b/src/Analyser/ExprHandler/FuncCallHandler.php @@ -127,7 +127,7 @@ public function processExpr(NodeScopeResolver $nodeScopeResolver, Stmt $stmt, Ex // A structural acceptor (names/positions/variadic) drives the per-arg // metadata and the throw/impure points - generics are resolved // type-driven by processArgs() into $resolvedParametersAcceptor. - $parametersAcceptor = $this->combineVariantsForNormalization($expr->getArgs(), $variants, null); + $parametersAcceptor = ParametersAcceptorSelector::combineVariantsForNormalization($expr->getArgs(), $variants, null); } $nameResult = $nodeScopeResolver->processExprNode($stmt, $expr->name, $scope, $storage, $nodeCallback, $context->enterDeep()); @@ -158,7 +158,7 @@ public function processExpr(NodeScopeResolver $nodeScopeResolver, Stmt $stmt, Ex // A structural acceptor (names/positions/variadic) drives argument // normalization, the impure point and the throw points - generics are // resolved type-driven by processArgs() into $resolvedParametersAcceptor. - $parametersAcceptor = $this->combineVariantsForNormalization($expr->getArgs(), $variants, $namedArgumentsVariants); + $parametersAcceptor = ParametersAcceptorSelector::combineVariantsForNormalization($expr->getArgs(), $variants, $namedArgumentsVariants); $impurePoint = SimpleImpurePoint::createFromVariant($functionReflection, $parametersAcceptor, $scope, $expr->getArgs()); if ($impurePoint !== null) { $impurePoints[] = new ImpurePoint($scope, $expr, $impurePoint->getIdentifier(), $impurePoint->getDescription(), $impurePoint->isCertain()); @@ -858,35 +858,6 @@ static function (?Type $offsetType, Type $valueType, bool $optional) use (&$arra return $arrayType; } - /** - * A structural acceptor for argument normalization, the impure point and the - * throw points: it depends only on argument names/positions/variadic, so it is - * generic-agnostic (the type-driven, generic-resolved acceptor is produced by - * processArgs() instead). Mirrors selectArgsAcceptor()'s variant-set choice - - * named-argument calls select among the named-arguments variants, which carry - * the parameter defaults reorderFuncArguments() needs to fill skipped optionals. - * - * @param Arg[] $args - * @param ParametersAcceptor[] $variants - * @param ParametersAcceptor[]|null $namedArgumentsVariants - */ - private function combineVariantsForNormalization(array $args, array $variants, ?array $namedArgumentsVariants): ParametersAcceptor - { - $hasName = false; - foreach ($args as $arg) { - if ($arg->name !== null) { - $hasName = true; - break; - } - } - - $selectedVariants = ($hasName && $namedArgumentsVariants !== null) ? $namedArgumentsVariants : $variants; - - return count($selectedVariants) === 1 - ? $selectedVariants[0] - : ParametersAcceptorSelector::combineAcceptors($selectedVariants); - } - /** * The call-expression type is derived from $preResolvedAcceptor - the acceptor * processArgs() selected from the arg types gathered on the arg-to-arg evolving diff --git a/src/Analyser/ExprHandler/MethodCallHandler.php b/src/Analyser/ExprHandler/MethodCallHandler.php index bf89e22030c..a1b472f5ac8 100644 --- a/src/Analyser/ExprHandler/MethodCallHandler.php +++ b/src/Analyser/ExprHandler/MethodCallHandler.php @@ -2,7 +2,6 @@ namespace PHPStan\Analyser\ExprHandler; -use PhpParser\Node\Arg; use PhpParser\Node\Expr; use PhpParser\Node\Expr\BinaryOp\Identical; use PhpParser\Node\Expr\MethodCall; @@ -116,7 +115,7 @@ public function processExpr(NodeScopeResolver $nodeScopeResolver, Stmt $stmt, Ex // A structural acceptor (names/positions/variadic) drives argument // normalization, the impure point and the throw point - generics are // resolved type-driven by processArgs() into $resolvedParametersAcceptor. - $parametersAcceptor = $this->combineVariantsForNormalization($expr->getArgs(), $variants, $namedArgumentsVariants); + $parametersAcceptor = ParametersAcceptorSelector::combineVariantsForNormalization($expr->getArgs(), $variants, $namedArgumentsVariants); } } else { $nameResult = $nodeScopeResolver->processExprNode($stmt, $expr->name, $scope, $storage, $nodeCallback, $context->enterDeep()); @@ -501,33 +500,4 @@ private function isMethodCallNarrowable(MutatingScope $scope, Expr $expr, Expres return $this->rememberPossiblyImpureFunctionValues || $hasSideEffects->no(); } - /** - * A structural acceptor for argument normalization, the impure point and the - * throw point: it depends only on argument names/positions/variadic, so it is - * generic-agnostic (the type-driven, generic-resolved acceptor is produced by - * processArgs() instead). Named-argument calls select among the named-arguments - * variants, which carry the parameter defaults reorderMethodArguments() needs to - * fill skipped optionals. - * - * @param Arg[] $args - * @param ParametersAcceptor[] $variants - * @param ParametersAcceptor[]|null $namedArgumentsVariants - */ - private function combineVariantsForNormalization(array $args, array $variants, ?array $namedArgumentsVariants): ParametersAcceptor - { - $hasName = false; - foreach ($args as $arg) { - if ($arg->name !== null) { - $hasName = true; - break; - } - } - - $selectedVariants = ($hasName && $namedArgumentsVariants !== null) ? $namedArgumentsVariants : $variants; - - return count($selectedVariants) === 1 - ? $selectedVariants[0] - : ParametersAcceptorSelector::combineAcceptors($selectedVariants); - } - } diff --git a/src/Analyser/ExprHandler/NewHandler.php b/src/Analyser/ExprHandler/NewHandler.php index 3b9f135034a..4ec578a6650 100644 --- a/src/Analyser/ExprHandler/NewHandler.php +++ b/src/Analyser/ExprHandler/NewHandler.php @@ -3,7 +3,6 @@ namespace PHPStan\Analyser\ExprHandler; use PhpParser\Node; -use PhpParser\Node\Arg; use PhpParser\Node\Expr; use PhpParser\Node\Expr\New_; use PhpParser\Node\Expr\StaticCall; @@ -123,7 +122,7 @@ public function processExpr(NodeScopeResolver $nodeScopeResolver, Stmt $stmt, Ex // A structural acceptor (names/positions/variadic) drives argument // normalization and the throw point - generics are resolved // type-driven by processArgs() into $resolvedParametersAcceptor. - $parametersAcceptor = $this->combineVariantsForNormalization($expr->getArgs(), $constructorReflection->getVariants(), $constructorReflection->getNamedArgumentsVariants()); + $parametersAcceptor = ParametersAcceptorSelector::combineVariantsForNormalization($expr->getArgs(), $constructorReflection->getVariants(), $constructorReflection->getNamedArgumentsVariants()); if ($constructorReflection->getDeclaringClass()->getName() === $classReflection->getName()) { $constructorResult = null; @@ -304,7 +303,7 @@ private function processConstructorReflection(string $className, New_ $expr, Mut // A structural acceptor (names/positions/variadic) drives argument // normalization and the throw point - generics are resolved // type-driven by processArgs() into $resolvedParametersAcceptor. - $parametersAcceptor = $this->combineVariantsForNormalization($expr->getArgs(), $constructorReflection->getVariants(), $constructorReflection->getNamedArgumentsVariants()); + $parametersAcceptor = ParametersAcceptorSelector::combineVariantsForNormalization($expr->getArgs(), $constructorReflection->getVariants(), $constructorReflection->getNamedArgumentsVariants()); } } @@ -376,35 +375,6 @@ private function getConstructorThrowPoint(MethodReflection $constructorReflectio return null; } - /** - * A structural acceptor for argument normalization and the throw point: it - * depends only on argument names/positions/variadic, so it is generic-agnostic - * (the type-driven, generic-resolved acceptor is produced by processArgs() - * instead). Mirrors the old variant-set choice - named-argument calls select - * among the named-arguments variants, which carry the parameter defaults - * reorderNewArguments() needs to fill skipped optionals. - * - * @param Arg[] $args - * @param ParametersAcceptor[] $variants - * @param ParametersAcceptor[]|null $namedArgumentsVariants - */ - private function combineVariantsForNormalization(array $args, array $variants, ?array $namedArgumentsVariants): ParametersAcceptor - { - $hasName = false; - foreach ($args as $arg) { - if ($arg->name !== null) { - $hasName = true; - break; - } - } - - $selectedVariants = ($hasName && $namedArgumentsVariants !== null) ? $namedArgumentsVariants : $variants; - - return count($selectedVariants) === 1 - ? $selectedVariants[0] - : ParametersAcceptorSelector::combineAcceptors($selectedVariants); - } - /** * The stored new-expression type is derived from $preResolvedAcceptor - the * constructor acceptor processArgs() selected from the arg types gathered on @@ -477,7 +447,7 @@ private function exactInstantiation(MutatingScope $scope, New_ $node, Name $clas $node->getArgs(), ); - $parametersAcceptor = $preResolvedAcceptor ?? $this->combineVariantsForNormalization( + $parametersAcceptor = $preResolvedAcceptor ?? ParametersAcceptorSelector::combineVariantsForNormalization( $methodCall->getArgs(), $constructorMethod->getVariants(), $constructorMethod->getNamedArgumentsVariants(), diff --git a/src/Analyser/ExprHandler/StaticCallHandler.php b/src/Analyser/ExprHandler/StaticCallHandler.php index 5c10c7f5c71..ff3345f9e90 100644 --- a/src/Analyser/ExprHandler/StaticCallHandler.php +++ b/src/Analyser/ExprHandler/StaticCallHandler.php @@ -2,7 +2,6 @@ namespace PHPStan\Analyser\ExprHandler; -use PhpParser\Node\Arg; use PhpParser\Node\Expr; use PhpParser\Node\Expr\BinaryOp\Identical; use PhpParser\Node\Expr\New_; @@ -116,7 +115,7 @@ public function processExpr(NodeScopeResolver $nodeScopeResolver, Stmt $stmt, Ex // A structural acceptor (names/positions/variadic) drives argument // normalization, the impure point and the throw point - generics are // resolved type-driven by processArgs() into $resolvedParametersAcceptor. - $parametersAcceptor = $this->combineVariantsForNormalization($expr->getArgs(), $variants, $namedArgumentsVariants); + $parametersAcceptor = ParametersAcceptorSelector::combineVariantsForNormalization($expr->getArgs(), $variants, $namedArgumentsVariants); $declaringClass = $methodReflection->getDeclaringClass(); if ( @@ -172,7 +171,7 @@ public function processExpr(NodeScopeResolver $nodeScopeResolver, Stmt $stmt, Ex if ($methodReflection !== null) { $variants = $methodReflection->getVariants(); $namedArgumentsVariants = $methodReflection->getNamedArgumentsVariants(); - $parametersAcceptor = $this->combineVariantsForNormalization($expr->getArgs(), $variants, $namedArgumentsVariants); + $parametersAcceptor = ParametersAcceptorSelector::combineVariantsForNormalization($expr->getArgs(), $variants, $namedArgumentsVariants); } } } else { @@ -583,33 +582,4 @@ private function isStaticCallNarrowable(MutatingScope $scope, Expr $expr, ?Expre return $this->rememberPossiblyImpureFunctionValues || $hasSideEffects->no(); } - /** - * A structural acceptor for argument normalization, the impure point and the - * throw point: it depends only on argument names/positions/variadic, so it is - * generic-agnostic (the type-driven, generic-resolved acceptor is produced by - * processArgs() instead). Named-argument calls select among the named-arguments - * variants, which carry the parameter defaults reorderStaticCallArguments() - * needs to fill skipped optionals. - * - * @param Arg[] $args - * @param ParametersAcceptor[] $variants - * @param ParametersAcceptor[]|null $namedArgumentsVariants - */ - private function combineVariantsForNormalization(array $args, array $variants, ?array $namedArgumentsVariants): ParametersAcceptor - { - $hasName = false; - foreach ($args as $arg) { - if ($arg->name !== null) { - $hasName = true; - break; - } - } - - $selectedVariants = ($hasName && $namedArgumentsVariants !== null) ? $namedArgumentsVariants : $variants; - - return count($selectedVariants) === 1 - ? $selectedVariants[0] - : ParametersAcceptorSelector::combineAcceptors($selectedVariants); - } - } diff --git a/src/Analyser/NodeScopeResolver.php b/src/Analyser/NodeScopeResolver.php index afb976120d4..f48dd539088 100644 --- a/src/Analyser/NodeScopeResolver.php +++ b/src/Analyser/NodeScopeResolver.php @@ -3610,8 +3610,7 @@ private function processAttributeGroups( $classReflection = $this->reflectionProvider->getClass($className); if ($classReflection->hasConstructor()) { $constructorReflection = $classReflection->getConstructor(); - $parametersAcceptor = ParametersAcceptorSelector::selectFromArgs( - $scope, + $parametersAcceptor = ParametersAcceptorSelector::combineVariantsForNormalization( $attr->args, $constructorReflection->getVariants(), $constructorReflection->getNamedArgumentsVariants(), diff --git a/src/Reflection/ParametersAcceptorSelector.php b/src/Reflection/ParametersAcceptorSelector.php index dbdfd42e528..b0307f8ee88 100644 --- a/src/Reflection/ParametersAcceptorSelector.php +++ b/src/Reflection/ParametersAcceptorSelector.php @@ -776,6 +776,34 @@ public static function selectFromTypes( return GenericParametersAcceptorResolver::resolve($types, self::combineAcceptors($winningAcceptors)); } + /** + * Picks the structural ParametersAcceptor (parameter names/positions/variadic + * only) that drives argument normalization / reordering. Unlike selectFromArgs() + * it never reads argument types from a Scope, so it is safe to call before the + * arguments have been processed - generics are resolved separately, type-driven. + * + * @internal + * @param Node\Arg[] $args + * @param ParametersAcceptor[] $variants + * @param ParametersAcceptor[]|null $namedArgumentsVariants + */ + public static function combineVariantsForNormalization(array $args, array $variants, ?array $namedArgumentsVariants): ParametersAcceptor + { + $hasName = false; + foreach ($args as $arg) { + if ($arg->name !== null) { + $hasName = true; + break; + } + } + + $selectedVariants = $hasName && $namedArgumentsVariants !== null ? $namedArgumentsVariants : $variants; + + return count($selectedVariants) === 1 + ? $selectedVariants[0] + : self::combineAcceptors($selectedVariants); + } + /** * @param ParametersAcceptor[] $acceptors */ From 4c1ffb237a15f8802d349523c5fa67cb22d499b2 Mon Sep 17 00:00:00 2001 From: Ondrej Mirtes Date: Fri, 19 Jun 2026 14:24:58 +0200 Subject: [PATCH 111/113] Add regression test for #13253 Closes https://github.com/phpstan/phpstan/issues/13253 --- tests/PHPStan/Analyser/nsrt/bug-13253.php | 80 +++++++++++++++++++++++ 1 file changed, 80 insertions(+) create mode 100644 tests/PHPStan/Analyser/nsrt/bug-13253.php diff --git a/tests/PHPStan/Analyser/nsrt/bug-13253.php b/tests/PHPStan/Analyser/nsrt/bug-13253.php new file mode 100644 index 00000000000..5bc8ac64c39 --- /dev/null +++ b/tests/PHPStan/Analyser/nsrt/bug-13253.php @@ -0,0 +1,80 @@ += 7.4 + +declare(strict_types = 1); + +namespace Bug13253; + +use Generator; +use ReflectionFunction; +use function PHPStan\Testing\assertType; + +/** + * @template TKey + * @template TValue + */ +class Pfline +{ + + /** @var iterable */ + private iterable $data; + + /** @param iterable $data */ + public function __construct(iterable $data) + { + $this->data = $data; + } + + /** + * @return iterable + */ + public function data(): iterable + { + return $this->data; + } + + /** + * @template TMapKey + * @template TMapValue + * @param null|callable(TValue): Generator $func + * @phpstan-self-out self + * @return self + */ + public function map(?callable $func = null): self + { + return $this; + } + +} + +function (): void { + $reflection = new ReflectionFunction('strlen'); + $params = $reflection->getParameters(); + + // Without chaining (each call advances the type through @phpstan-self-out) + $pipeline = new Pfline($params); + $pipeline->map(function ($param) { + assertType('ReflectionParameter', $param); + yield $param; + }); + $pipeline->map(function ($param) { + assertType('ReflectionParameter', $param); + yield $param->getName(); + }); + $pipeline->map(function ($param) { + assertType('non-empty-string', $param); + yield substr_count('.', $param); + }); + + // With chaining (the type must advance through @return self) + $pipeline = new Pfline($params); + $pipeline->map(function ($param) { + assertType('ReflectionParameter', $param); + yield $param; + })->map(function ($param) { + assertType('ReflectionParameter', $param); + yield $param->getName(); + })->map(function ($param) { + assertType('non-empty-string', $param); + yield substr_count('.', $param); + }); +}; From 0d85dce11b15ed5f6d29f8104eee321ec45e92d8 Mon Sep 17 00:00:00 2001 From: Ondrej Mirtes Date: Fri, 19 Jun 2026 14:30:42 +0200 Subject: [PATCH 112/113] Add regression test for #14396 Closes https://github.com/phpstan/phpstan/issues/14396 --- .../PHPStan/Rules/Exceptions/Bug14396Test.php | 46 +++++++++++++++++++ tests/PHPStan/Rules/Exceptions/bug-14396.neon | 5 ++ .../Rules/Exceptions/data/bug-14396.php | 45 ++++++++++++++++++ 3 files changed, 96 insertions(+) create mode 100644 tests/PHPStan/Rules/Exceptions/Bug14396Test.php create mode 100644 tests/PHPStan/Rules/Exceptions/bug-14396.neon create mode 100644 tests/PHPStan/Rules/Exceptions/data/bug-14396.php diff --git a/tests/PHPStan/Rules/Exceptions/Bug14396Test.php b/tests/PHPStan/Rules/Exceptions/Bug14396Test.php new file mode 100644 index 00000000000..bb0e9c982ed --- /dev/null +++ b/tests/PHPStan/Rules/Exceptions/Bug14396Test.php @@ -0,0 +1,46 @@ + + */ +class Bug14396Test extends RuleTestCase +{ + + protected function getRule(): Rule + { + return new MissingCheckedExceptionInFunctionThrowsRule( + new MissingCheckedExceptionInThrowsCheck(new DefaultExceptionTypeResolver( + self::createReflectionProvider(), + [], + [], + [], + [], + )), + ); + } + + protected function shouldTreatPhpDocTypesAsCertain(): bool + { + return false; + } + + #[RequiresPhp('>= 8.1')] + public function testRule(): void + { + $this->analyse([__DIR__ . '/data/bug-14396.php'], []); + } + + public static function getAdditionalConfigFiles(): array + { + return [ + __DIR__ . '/bug-14396.neon', + ]; + } + +} diff --git a/tests/PHPStan/Rules/Exceptions/bug-14396.neon b/tests/PHPStan/Rules/Exceptions/bug-14396.neon new file mode 100644 index 00000000000..feb290057aa --- /dev/null +++ b/tests/PHPStan/Rules/Exceptions/bug-14396.neon @@ -0,0 +1,5 @@ +parameters: + treatPhpDocTypesAsCertain: false + exceptions: + check: + missingCheckedExceptionInThrows: true diff --git a/tests/PHPStan/Rules/Exceptions/data/bug-14396.php b/tests/PHPStan/Rules/Exceptions/data/bug-14396.php new file mode 100644 index 00000000000..bf8901f8127 --- /dev/null +++ b/tests/PHPStan/Rules/Exceptions/data/bug-14396.php @@ -0,0 +1,45 @@ += 8.1 + +declare(strict_types=1); + +namespace Bug14396; + +enum Status { + case A; + case B; + case C; +} + +class Item { + public function __construct( + public ?Status $status + ) {} +} + +/** +* @param list $list +*/ +function countAFromCollection(array $list): int +{ + $count = 0; + + foreach ($list as $item) { + match ($item->status) { + Status::A => ++$count, + Status::B, + Status::C, + null => null, + }; + } + + return $count; +} + +function countAFromItem(Item $item): ?int { + return match ($item->status) { + Status::A => 1, + Status::B, + Status::C, + null => null, + }; +} From 125cf220928285763d2f445d7064ce6e20d916e6 Mon Sep 17 00:00:00 2001 From: Ondrej Mirtes Date: Sat, 20 Jun 2026 13:55:51 +0200 Subject: [PATCH 113/113] Call handleDefaultTruthyOrFalseyContext on $this->typeSpecifier in migrated handlers MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Rebasing onto 2.2.x auto-merged the @phpstan-assert-if-true default truthy/falsey narrowing (#5880/#5885) into the migrated FuncCall/Method/StaticCall specify callbacks, but using the old-world local $typeSpecifier instead of the property $this->typeSpecifier — crashing with "Call to a member function handleDefaultTruthyOrFalseyContext() on null" whenever the assert-narrowing path was hit (111 ImpossibleCheckType*/CallToFunctionParameters* test errors). --- src/Analyser/ExprHandler/FuncCallHandler.php | 2 +- src/Analyser/ExprHandler/MethodCallHandler.php | 2 +- src/Analyser/ExprHandler/StaticCallHandler.php | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/Analyser/ExprHandler/FuncCallHandler.php b/src/Analyser/ExprHandler/FuncCallHandler.php index cab3d07523f..48a2def446c 100644 --- a/src/Analyser/ExprHandler/FuncCallHandler.php +++ b/src/Analyser/ExprHandler/FuncCallHandler.php @@ -1038,7 +1038,7 @@ private function specifyTypes(NodeScopeResolver $nodeScopeResolver, MutatingScop $specifiedTypes = $this->typeSpecifier->specifyTypesFromAsserts($context, $expr, $asserts, $resolvedParametersAcceptor, $scope); if ($specifiedTypes !== null) { return $specifiedTypes - ->unionWith($typeSpecifier->handleDefaultTruthyOrFalseyContext($context, $expr, $scope)) + ->unionWith($this->typeSpecifier->handleDefaultTruthyOrFalseyContext($context, $expr, $scope)) ->setRootExpr($specifiedTypes->getRootExpr()); } } diff --git a/src/Analyser/ExprHandler/MethodCallHandler.php b/src/Analyser/ExprHandler/MethodCallHandler.php index a1b472f5ac8..5886fa810aa 100644 --- a/src/Analyser/ExprHandler/MethodCallHandler.php +++ b/src/Analyser/ExprHandler/MethodCallHandler.php @@ -451,7 +451,7 @@ private function specifyTypes(NodeScopeResolver $nodeScopeResolver, MutatingScop $specifiedTypes = $this->typeSpecifier->specifyTypesFromAsserts($context, $expr, $asserts, $resolvedParametersAcceptor, $scope); if ($specifiedTypes !== null) { return $specifiedTypes - ->unionWith($typeSpecifier->handleDefaultTruthyOrFalseyContext($context, $expr, $scope)) + ->unionWith($this->typeSpecifier->handleDefaultTruthyOrFalseyContext($context, $expr, $scope)) ->setRootExpr($specifiedTypes->getRootExpr()); } } diff --git a/src/Analyser/ExprHandler/StaticCallHandler.php b/src/Analyser/ExprHandler/StaticCallHandler.php index ff3345f9e90..5476f827fc9 100644 --- a/src/Analyser/ExprHandler/StaticCallHandler.php +++ b/src/Analyser/ExprHandler/StaticCallHandler.php @@ -526,7 +526,7 @@ private function specifyTypes(NodeScopeResolver $nodeScopeResolver, MutatingScop $specifiedTypes = $this->typeSpecifier->specifyTypesFromAsserts($context, $expr, $asserts, $resolvedParametersAcceptor, $scope); if ($specifiedTypes !== null) { return $specifiedTypes - ->unionWith($typeSpecifier->handleDefaultTruthyOrFalseyContext($context, $expr, $scope)) + ->unionWith($this->typeSpecifier->handleDefaultTruthyOrFalseyContext($context, $expr, $scope)) ->setRootExpr($specifiedTypes->getRootExpr()); } }