diff --git a/src/Analyser/ExprHandler/ArrayDimFetchHandler.php b/src/Analyser/ExprHandler/ArrayDimFetchHandler.php index baea95ddd5..89b256af2b 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; @@ -30,6 +31,12 @@ final class ArrayDimFetchHandler implements ExprHandler { + public function __construct( + private ExpressionResultFactory $expressionResultFactory, + ) + { + } + public function supports(Expr $expr): bool { return $expr instanceof ArrayDimFetch; @@ -75,8 +82,10 @@ 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( + $expr, $scope, + typeCallback: static fn () => new NeverType(), hasYield: $varResult->hasYield(), isAlwaysTerminating: $varResult->isAlwaysTerminating(), throwPoints: $varResult->getThrowPoints(), @@ -92,20 +101,38 @@ public function processExpr(NodeScopeResolver $nodeScopeResolver, Stmt $stmt, Ex $impurePoints = array_merge($dimResult->getImpurePoints(), $varResult->getImpurePoints()); $scope = $varResult->getScope(); - $varType = $scope->getType($expr->var); + $offsetGetResult = $nodeScopeResolver->processExprNode( + $stmt, + new MethodCall($expr->var, new Identifier('offsetGet'), [new Arg($expr->dim)]), + $scope, + new ExpressionResultStorage(), + new NoopNodeCallback(), + $context->enterDeep(), + ); + + $varType = $varResult->getType(); if (!$varType->isArray()->yes() && !(new ObjectType(ArrayAccess::class))->isSuperTypeOf($varType)->no()) { - $throwPoints = array_merge($throwPoints, $nodeScopeResolver->processExprNode( - $stmt, - new MethodCall($expr->var, 'offsetGet'), - $scope, - $storage, - new NoopNodeCallback(), - $context, - )->getThrowPoints()); + $throwPoints = array_merge($throwPoints, $offsetGetResult->getThrowPoints()); } - return new ExpressionResult( + return $this->expressionResultFactory->create( + $expr, $scope, + typeCallback: static function (Expr $uninteresting, MutatingScope $scope) use ($varResult, $dimResult, $offsetGetResult): Type { + $varType = $varResult->getTypeForScope($scope); + if ($varType instanceof NeverType) { + return $varType; + } + + if ( + !$varType->isArray()->yes() + && (new ObjectType(ArrayAccess::class))->isSuperTypeOf($varType)->yes() + ) { + return $offsetGetResult->getTypeForScope($scope); + } + + return $varType->getOffsetValueType($dimResult->getTypeForScope($scope)); + }, 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 a4d150b0e2..33e3933180 100644 --- a/src/Analyser/ExprHandler/ArrayHandler.php +++ b/src/Analyser/ExprHandler/ArrayHandler.php @@ -7,16 +7,19 @@ 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; use PHPStan\Analyser\NodeScopeResolver; +use PHPStan\Analyser\NoopNodeCallback; use PHPStan\DependencyInjection\AutowiredService; use PHPStan\Node\LiteralArrayItem; use PHPStan\Node\LiteralArrayNode; use PHPStan\Reflection\InitializerExprTypeResolver; use PHPStan\Type\Type; use function array_merge; +use function spl_object_id; /** * @implements ExprHandler @@ -26,6 +29,7 @@ final class ArrayHandler implements ExprHandler { public function __construct( + private ExpressionResultFactory $expressionResultFactory, private InitializerExprTypeResolver $initializerExprTypeResolver, ) { @@ -48,11 +52,14 @@ public function processExpr(NodeScopeResolver $nodeScopeResolver, Stmt $stmt, Ex $throwPoints = []; $impurePoints = []; $isAlwaysTerminating = false; + /** @var array */ + $itemResults = []; foreach ($expr->items as $arrayItem) { $itemNodes[] = new LiteralArrayItem($scope, $arrayItem); $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()); @@ -61,6 +68,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()); @@ -69,8 +77,17 @@ public function processExpr(NodeScopeResolver $nodeScopeResolver, Stmt $stmt, Ex } $nodeScopeResolver->callNodeCallback($nodeCallback, new LiteralArrayNode($expr, $itemNodes), $scope, $storage); - return new ExpressionResult( + return $this->expressionResultFactory->create( + $expr, $scope, + typeCallback: fn (Expr $expr, MutatingScope $scope) => $this->initializerExprTypeResolver->getArrayType($expr, static function (Expr $e) use ($itemResults, $scope, $nodeScopeResolver, $stmt): Type { + $id = spl_object_id($e); + if (isset($itemResults[$id])) { + return $itemResults[$id]->getTypeForScope($scope); + } + + return $nodeScopeResolver->processExprNode($stmt, $e, $scope, new ExpressionResultStorage(), new NoopNodeCallback(), ExpressionContext::createDeep())->getTypeForScope($scope); + }), hasYield: $hasYield, isAlwaysTerminating: $isAlwaysTerminating, throwPoints: $throwPoints, diff --git a/src/Analyser/ExprHandler/ArrowFunctionHandler.php b/src/Analyser/ExprHandler/ArrowFunctionHandler.php index 098101f175..fb55895189 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; @@ -23,6 +24,7 @@ final class ArrowFunctionHandler implements ExprHandler { public function __construct( + private ExpressionResultFactory $expressionResultFactory, private ClosureTypeResolver $closureTypeResolver, ) { @@ -37,8 +39,10 @@ public function processExpr(NodeScopeResolver $nodeScopeResolver, Stmt $stmt, Ex { $result = $nodeScopeResolver->processArrowFunctionNode($stmt, $expr, $scope, $storage, $nodeCallback, null); - return new ExpressionResult( + return $this->expressionResultFactory->create( + $expr, $result->getScope(), + typeCallback: static fn (Expr $uninteresting, MutatingScope $scope) => $result->getTypeForScope($scope), hasYield: $result->hasYield(), isAlwaysTerminating: false, throwPoints: [], diff --git a/src/Analyser/ExprHandler/AssignHandler.php b/src/Analyser/ExprHandler/AssignHandler.php index 284ec47619..c8293cb862 100644 --- a/src/Analyser/ExprHandler/AssignHandler.php +++ b/src/Analyser/ExprHandler/AssignHandler.php @@ -21,6 +21,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; @@ -78,6 +79,7 @@ final class AssignHandler implements ExprHandler { public function __construct( + private ExpressionResultFactory $expressionResultFactory, private TypeSpecifier $typeSpecifier, private PhpVersion $phpVersion, ) @@ -105,7 +107,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; @@ -145,7 +147,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($expr->expr, $scope, typeCallback: static fn (Expr $uninteresting, MutatingScope $scope) => $result->getTypeForScope($scope), hasYield: $hasYield, isAlwaysTerminating: $isAlwaysTerminating, throwPoints: $throwPoints, impurePoints: $impurePoints); }, true, ); @@ -159,8 +161,10 @@ static function (MutatingScope $scope) use ($stmt, $expr, $nodeCallback, $contex } } - return new ExpressionResult( + return $this->expressionResultFactory->create( + $expr->expr, $scope, + typeCallback: static fn (Expr $uninteresting, MutatingScope $scope) => $result->getTypeForScope($scope), hasYield: $result->hasYield(), isAlwaysTerminating: $result->isAlwaysTerminating(), throwPoints: $result->getThrowPoints(), @@ -192,10 +196,12 @@ public function processAssignVar( $throwPoints = []; $impurePoints = []; $isAlwaysTerminating = false; + $exprCallbackResult = null; $isAssignOp = $assignedExpr instanceof Expr\AssignOp && !$enterExpressionAssign; if ($var instanceof Variable) { $nodeScopeResolver->storeBeforeScope($storage, $var, $scope); $result = $processExprCallback($scope); + $exprCallbackResult = $result; $hasYield = $result->hasYield(); $throwPoints = $result->getThrowPoints(); $impurePoints = $result->getImpurePoints(); @@ -382,6 +388,7 @@ public function processAssignVar( // 3. eval assigned expr $result = $processExprCallback($scope); + $exprCallbackResult = $result; $hasYield = $hasYield || $result->hasYield(); $throwPoints = array_merge($throwPoints, $result->getThrowPoints()); $impurePoints = array_merge($impurePoints, $result->getImpurePoints()); @@ -508,6 +515,7 @@ public function processAssignVar( $scopeBeforeAssignEval = $scope; $result = $processExprCallback($scope); + $exprCallbackResult = $result; $hasYield = $hasYield || $result->hasYield(); $throwPoints = array_merge($throwPoints, $result->getThrowPoints()); $impurePoints = array_merge($impurePoints, $result->getImpurePoints()); @@ -621,6 +629,7 @@ public function processAssignVar( $scopeBeforeAssignEval = $scope; $result = $processExprCallback($scope); + $exprCallbackResult = $result; $hasYield = $hasYield || $result->hasYield(); $throwPoints = array_merge($throwPoints, $result->getThrowPoints()); $impurePoints = array_merge($impurePoints, $result->getImpurePoints()); @@ -667,6 +676,7 @@ public function processAssignVar( } elseif ($var instanceof List_) { $nodeScopeResolver->storeBeforeScope($storage, $var, $scope); $result = $processExprCallback($scope); + $exprCallbackResult = $result; $hasYield = $result->hasYield(); $throwPoints = array_merge($throwPoints, $result->getThrowPoints()); $impurePoints = array_merge($impurePoints, $result->getImpurePoints()); @@ -706,7 +716,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($arrayItem->value, $scope, typeCallback: static fn () => new MixedType(), hasYield: false, isAlwaysTerminating: false, throwPoints: [], impurePoints: []), $enterExpressionAssign, ); $scope = $result->getScope(); @@ -790,6 +800,7 @@ public function processAssignVar( $isAlwaysTerminating = $varResult->isAlwaysTerminating(); $scope = $varResult->getScope(); $result = $processExprCallback($scope); + $exprCallbackResult = $result; $hasYield = $hasYield || $result->hasYield(); $throwPoints = array_merge($throwPoints, $result->getThrowPoints()); $impurePoints = array_merge($impurePoints, $result->getImpurePoints()); @@ -798,7 +809,7 @@ public function processAssignVar( } // stored where processAssignVar is called - return new ExpressionResult($scope, $hasYield, $isAlwaysTerminating, $throwPoints, $impurePoints); + return $this->expressionResultFactory->create($assignedExpr, $scope, typeCallback: static fn (Expr $uninteresting, MutatingScope $scope) => $exprCallbackResult->getTypeForScope($scope), hasYield: $hasYield, isAlwaysTerminating: $isAlwaysTerminating, throwPoints: $throwPoints, impurePoints: $impurePoints); } private function unwrapAssign(Expr $expr): Expr diff --git a/src/Analyser/ExprHandler/AssignOpHandler.php b/src/Analyser/ExprHandler/AssignOpHandler.php index c06057eeef..6ab448a8fc 100644 --- a/src/Analyser/ExprHandler/AssignOpHandler.php +++ b/src/Analyser/ExprHandler/AssignOpHandler.php @@ -11,11 +11,13 @@ 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; use PHPStan\Analyser\MutatingScope; use PHPStan\Analyser\NodeScopeResolver; +use PHPStan\Analyser\NoopNodeCallback; use PHPStan\DependencyInjection\AutowiredService; use PHPStan\Reflection\InitializerExprTypeResolver; use PHPStan\ShouldNotHappenException; @@ -33,6 +35,7 @@ final class AssignOpHandler implements ExprHandler { public function __construct( + private ExpressionResultFactory $expressionResultFactory, private AssignHandler $assignHandler, private InitializerExprTypeResolver $initializerExprTypeResolver, ) @@ -55,7 +58,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( @@ -66,12 +69,14 @@ static function (MutatingScope $scope) use ($stmt, $expr, $nodeCallback, $contex $exprResult = $nodeScopeResolver->processExprNode($stmt, $expr->expr, $scope, $storage, $nodeCallback, $context->enterDeep()); if ($expr instanceof Expr\AssignOp\Coalesce) { $nodeScopeResolver->storeBeforeScope($storage, $expr, $originalScope); - return new ExpressionResult( + return $this->expressionResultFactory->create( + $expr, $exprResult->getScope()->mergeWith($originalScope), - $exprResult->hasYield(), - $exprResult->isAlwaysTerminating(), - $exprResult->getThrowPoints(), - $exprResult->getImpurePoints(), + typeCallback: static fn (Expr $uninteresting, MutatingScope $scope) => $exprResult->getTypeForScope($scope), + hasYield: $exprResult->hasYield(), + isAlwaysTerminating: $exprResult->isAlwaysTerminating(), + throwPoints: $exprResult->getThrowPoints(), + impurePoints: $exprResult->getImpurePoints(), ); } @@ -86,13 +91,72 @@ static function (MutatingScope $scope) use ($stmt, $expr, $nodeCallback, $contex $throwPoints = $assignResult->getThrowPoints(); if ( ($expr instanceof Expr\AssignOp\Div || $expr instanceof Expr\AssignOp\Mod) && - !$scope->getType($expr->expr)->toNumber()->isSuperTypeOf(new ConstantIntegerType(0))->no() + !$assignResult->getType()->toNumber()->isSuperTypeOf(new ConstantIntegerType(0))->no() ) { $throwPoints[] = InternalThrowPoint::createExplicit($scope, new ObjectType(DivisionByZeroError::class), $expr, false); } - return new ExpressionResult( + return $this->expressionResultFactory->create( + $expr, $scope, + typeCallback: function (Expr $expr, MutatingScope $scope) use ($assignResult, $nodeScopeResolver, $stmt): Type { + if ($expr instanceof Expr\AssignOp\Coalesce) { + // Coalesce assignop type is handled by BinaryOp\Coalesce + return $nodeScopeResolver->processExprNode($stmt, new BinaryOp\Coalesce($expr->var, $expr->expr, $expr->getAttributes()), $scope, new ExpressionResultStorage(), new NoopNodeCallback(), ExpressionContext::createDeep())->getTypeForScope($scope); + } + + $varType = $nodeScopeResolver->processExprNode($stmt, $expr->var, $scope, new ExpressionResultStorage(), new NoopNodeCallback(), ExpressionContext::createDeep())->getTypeForScope($scope); + $exprType = $assignResult->getTypeForScope($scope); + $getType = static function (Expr $e) use ($expr, $varType, $exprType, $scope, $nodeScopeResolver, $stmt): Type { + if ($e === $expr->var) { + return $varType; + } + if ($e === $expr->expr) { + return $exprType; + } + + return $nodeScopeResolver->processExprNode($stmt, $e, $scope, new ExpressionResultStorage(), new NoopNodeCallback(), ExpressionContext::createDeep())->getTypeForScope($scope); + }; + + 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))); + }, hasYield: $assignResult->hasYield(), isAlwaysTerminating: $assignResult->isAlwaysTerminating(), throwPoints: $throwPoints, diff --git a/src/Analyser/ExprHandler/BinaryOpHandler.php b/src/Analyser/ExprHandler/BinaryOpHandler.php index b9d0908f78..f49db02e7e 100644 --- a/src/Analyser/ExprHandler/BinaryOpHandler.php +++ b/src/Analyser/ExprHandler/BinaryOpHandler.php @@ -11,11 +11,13 @@ 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; use PHPStan\Analyser\MutatingScope; use PHPStan\Analyser\NodeScopeResolver; +use PHPStan\Analyser\NoopNodeCallback; use PHPStan\Analyser\RicherScopeGetTypeHelper; use PHPStan\DependencyInjection\AutowiredService; use PHPStan\Php\PhpVersion; @@ -39,6 +41,7 @@ final class BinaryOpHandler implements ExprHandler { public function __construct( + private ExpressionResultFactory $expressionResultFactory, private InitializerExprTypeResolver $initializerExprTypeResolver, private RicherScopeGetTypeHelper $richerScopeGetTypeHelper, private PhpVersion $phpVersion, @@ -64,14 +67,146 @@ public function processExpr(NodeScopeResolver $nodeScopeResolver, Stmt $stmt, Ex $throwPoints = array_merge($leftResult->getThrowPoints(), $rightResult->getThrowPoints()); if ( ($expr instanceof BinaryOp\Div || $expr instanceof BinaryOp\Mod) && - !$leftResult->getScope()->getType($expr->right)->toNumber()->isSuperTypeOf(new ConstantIntegerType(0))->no() + !$rightResult->getType()->toNumber()->isSuperTypeOf(new ConstantIntegerType(0))->no() ) { $throwPoints[] = InternalThrowPoint::createExplicit($leftResult->getScope(), new ObjectType(DivisionByZeroError::class), $expr, false); } $scope = $rightResult->getScope(); - return new ExpressionResult( + return $this->expressionResultFactory->create( + $expr, $scope, + typeCallback: function (Expr $expr, MutatingScope $scope) use ($leftResult, $rightResult, $nodeScopeResolver, $stmt): Type { + $leftType = $leftResult->getTypeForScope($scope); + $rightType = $rightResult->getTypeForScope($scope); + $getType = static function (Expr $e) use ($expr, $leftResult, $rightResult, $scope, $nodeScopeResolver, $stmt): Type { + if ($e === $expr->left) { + return $leftResult->getTypeForScope($scope); + } + if ($e === $expr->right) { + return $rightResult->getTypeForScope($scope); + } + + return $nodeScopeResolver->processExprNode($stmt, $e, $scope, new ExpressionResultStorage(), new NoopNodeCallback(), ExpressionContext::createDeep())->getTypeForScope($scope); + }; + + if ($expr instanceof BinaryOp\Smaller) { + return $leftType->isSmallerThan($rightType, $this->phpVersion)->toBooleanType(); + } + + if ($expr instanceof BinaryOp\SmallerOrEqual) { + return $leftType->isSmallerThanOrEqual($rightType, $this->phpVersion)->toBooleanType(); + } + + if ($expr instanceof BinaryOp\Greater) { + return $rightType->isSmallerThan($leftType, $this->phpVersion)->toBooleanType(); + } + + if ($expr instanceof BinaryOp\GreaterOrEqual) { + return $rightType->isSmallerThanOrEqual($leftType, $this->phpVersion)->toBooleanType(); + } + + 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); + } + + return $this->initializerExprTypeResolver->resolveEqualType($leftType, $rightType)->type; + } + + if ($expr instanceof BinaryOp\NotEqual) { + $equalType = $this->initializerExprTypeResolver->resolveEqualType($leftType, $rightType)->type; + if ($equalType instanceof ConstantBooleanType) { + return new ConstantBooleanType(!$equalType->getValue()); + } + + return new BooleanType(); + } + + if ($expr instanceof BinaryOp\Identical) { + return $this->richerScopeGetTypeHelper->getIdenticalResultWithTypes($scope, $expr, $leftType, $rightType)->type; + } + + if ($expr instanceof BinaryOp\NotIdentical) { + return $this->richerScopeGetTypeHelper->getNotIdenticalResultWithTypes($scope, $expr, $leftType, $rightType)->type; + } + + if ($expr instanceof BinaryOp\LogicalXor) { + $leftBooleanType = $leftType->toBoolean(); + $rightBooleanType = $rightType->toBoolean(); + + if ( + $leftBooleanType instanceof ConstantBooleanType + && $rightBooleanType instanceof ConstantBooleanType + ) { + return new ConstantBooleanType( + $leftBooleanType->getValue() xor $rightBooleanType->getValue(), + ); + } + + return new BooleanType(); + } + + 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\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\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\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\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\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\ShiftRight) { + return $this->initializerExprTypeResolver->getShiftRightType($expr->left, $expr->right, $getType); + } + + throw new ShouldNotHappenException(sprintf('Unhandled %s', get_class($expr))); + }, 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 715a795404..1705bc7c74 100644 --- a/src/Analyser/ExprHandler/BitwiseNotHandler.php +++ b/src/Analyser/ExprHandler/BitwiseNotHandler.php @@ -7,10 +7,12 @@ 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; use PHPStan\Analyser\NodeScopeResolver; +use PHPStan\Analyser\NoopNodeCallback; use PHPStan\DependencyInjection\AutowiredService; use PHPStan\Reflection\InitializerExprTypeResolver; use PHPStan\Type\Type; @@ -23,6 +25,7 @@ final class BitwiseNotHandler implements ExprHandler { public function __construct( + private ExpressionResultFactory $expressionResultFactory, private InitializerExprTypeResolver $initializerExprTypeResolver, ) { @@ -37,8 +40,16 @@ 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( + $expr, $exprResult->getScope(), + typeCallback: fn (Expr $uninteresting, MutatingScope $scope) => $this->initializerExprTypeResolver->getBitwiseNotType($expr->expr, static function (Expr $e) use ($expr, $exprResult, $nodeScopeResolver, $stmt, $scope): Type { + if ($e === $expr->expr) { + return $exprResult->getTypeForScope($scope); + } + + return $nodeScopeResolver->processExprNode($stmt, $e, $scope, new ExpressionResultStorage(), new NoopNodeCallback(), ExpressionContext::createDeep())->getTypeForScope($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 5b672a2db5..dfd1c06729 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\MutatingScope; @@ -33,6 +34,7 @@ final class BooleanAndHandler implements ExprHandler private const BOOLEAN_EXPRESSION_MAX_PROCESS_DEPTH = 4; public function __construct( + private ExpressionResultFactory $expressionResultFactory, private NodeScopeResolver $nodeScopeResolver, ) { @@ -90,7 +92,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->getType(); if ($rightExprType instanceof NeverType && $rightExprType->isExplicit()) { $leftMergedWithRightScope = $leftResult->getFalseyScope(); } else { @@ -99,8 +101,26 @@ 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( + $expr, $leftMergedWithRightScope, + typeCallback: static function (Expr $uninteresting, MutatingScope $scope) use ($leftResult, $rightResult): Type { + $leftBooleanType = $leftResult->getTypeForScope($scope)->toBoolean(); + if ($leftBooleanType->isFalse()->yes()) { + return new ConstantBooleanType(false); + } + + $rightBooleanType = $rightResult->getTypeForScope($scope)->toBoolean(); + if ($rightBooleanType->isFalse()->yes()) { + return new ConstantBooleanType(false); + } + + if ($leftBooleanType->isTrue()->yes() && $rightBooleanType->isTrue()->yes()) { + return new ConstantBooleanType(true); + } + + return new BooleanType(); + }, 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 e718e74e3f..ae15630e4d 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; @@ -23,6 +24,12 @@ final class BooleanNotHandler implements ExprHandler { + public function __construct( + private ExpressionResultFactory $expressionResultFactory, + ) + { + } + public function supports(Expr $expr): bool { return $expr instanceof BooleanNot; @@ -33,8 +40,17 @@ 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( + $expr, $scope, + typeCallback: static function (Expr $uninteresting, MutatingScope $scope) use ($exprResult): Type { + $exprBooleanType = $exprResult->getTypeForScope($scope)->toBoolean(); + if ($exprBooleanType instanceof ConstantBooleanType) { + return new ConstantBooleanType(!$exprBooleanType->getValue()); + } + + return new BooleanType(); + }, hasYield: $exprResult->hasYield(), isAlwaysTerminating: $exprResult->isAlwaysTerminating(), throwPoints: $exprResult->getThrowPoints(), diff --git a/src/Analyser/ExprHandler/BooleanOrHandler.php b/src/Analyser/ExprHandler/BooleanOrHandler.php index 9c828edb94..d82450f589 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\MutatingScope; @@ -31,6 +32,7 @@ final class BooleanOrHandler implements ExprHandler private const BOOLEAN_EXPRESSION_MAX_PROCESS_DEPTH = 4; public function __construct( + private ExpressionResultFactory $expressionResultFactory, private NodeScopeResolver $nodeScopeResolver, ) { @@ -74,7 +76,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->getType(); if ($rightExprType instanceof NeverType && $rightExprType->isExplicit()) { $leftMergedWithRightScope = $leftResult->getTruthyScope(); } else { @@ -83,8 +85,26 @@ 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( + $expr, $leftMergedWithRightScope, + typeCallback: static function (Expr $uninteresting, MutatingScope $scope) use ($leftResult, $rightResult): Type { + $leftBooleanType = $leftResult->getTypeForScope($scope)->toBoolean(); + if ($leftBooleanType->isTrue()->yes()) { + return new ConstantBooleanType(true); + } + + $rightBooleanType = $rightResult->getTypeForScope($scope)->toBoolean(); + if ($rightBooleanType->isTrue()->yes()) { + return new ConstantBooleanType(true); + } + + if ($leftBooleanType->isFalse()->yes() && $rightBooleanType->isFalse()->yes()) { + return new ConstantBooleanType(false); + } + + return new BooleanType(); + }, 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 212af1adc1..e13f129ec2 100644 --- a/src/Analyser/ExprHandler/CastHandler.php +++ b/src/Analyser/ExprHandler/CastHandler.php @@ -7,10 +7,12 @@ 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; use PHPStan\Analyser\NodeScopeResolver; +use PHPStan\Analyser\NoopNodeCallback; use PHPStan\DependencyInjection\AutowiredService; use PHPStan\Reflection\InitializerExprTypeResolver; use PHPStan\Type\NullType; @@ -24,6 +26,7 @@ final class CastHandler implements ExprHandler { public function __construct( + private ExpressionResultFactory $expressionResultFactory, private InitializerExprTypeResolver $initializerExprTypeResolver, ) { @@ -39,8 +42,22 @@ 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( + $expr, $scope, + typeCallback: function (Expr $uninteresting, MutatingScope $scope) use ($expr, $exprResult, $nodeScopeResolver, $stmt): Type { + if ($expr instanceof Cast\Unset_) { + return new NullType(); + } + + return $this->initializerExprTypeResolver->getCastType($expr, static function (Expr $e) use ($expr, $exprResult, $nodeScopeResolver, $stmt, $scope): Type { + if ($e === $expr->expr) { + return $exprResult->getTypeForScope($scope); + } + + return $nodeScopeResolver->processExprNode($stmt, $e, $scope, new ExpressionResultStorage(), new NoopNodeCallback(), ExpressionContext::createDeep())->getTypeForScope($scope); + }); + }, hasYield: $exprResult->hasYield(), isAlwaysTerminating: $exprResult->isAlwaysTerminating(), throwPoints: $exprResult->getThrowPoints(), diff --git a/src/Analyser/ExprHandler/CastStringHandler.php b/src/Analyser/ExprHandler/CastStringHandler.php index fff7f07d48..c36101b795 100644 --- a/src/Analyser/ExprHandler/CastStringHandler.php +++ b/src/Analyser/ExprHandler/CastStringHandler.php @@ -7,11 +7,13 @@ 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; use PHPStan\Analyser\MutatingScope; use PHPStan\Analyser\NodeScopeResolver; +use PHPStan\Analyser\NoopNodeCallback; use PHPStan\DependencyInjection\AutowiredService; use PHPStan\Reflection\InitializerExprTypeResolver; use PHPStan\Type\Type; @@ -25,6 +27,7 @@ final class CastStringHandler implements ExprHandler { public function __construct( + private ExpressionResultFactory $expressionResultFactory, private InitializerExprTypeResolver $initializerExprTypeResolver, ) { @@ -40,7 +43,7 @@ public function processExpr(NodeScopeResolver $nodeScopeResolver, Stmt $stmt, Ex $exprResult = $nodeScopeResolver->processExprNode($stmt, $expr->expr, $scope, $storage, $nodeCallback, $context->enterDeep()); $impurePoints = $exprResult->getImpurePoints(); - $exprType = $scope->getType($expr->expr); + $exprType = $exprResult->getType(); $toStringMethod = $scope->getMethodReflection($exprType, '__toString'); if ($toStringMethod !== null) { if (!$toStringMethod->hasSideEffects()->no()) { @@ -56,8 +59,16 @@ public function processExpr(NodeScopeResolver $nodeScopeResolver, Stmt $stmt, Ex $scope = $exprResult->getScope(); - return new ExpressionResult( + return $this->expressionResultFactory->create( + $expr, $scope, + typeCallback: fn (Expr $uninteresting, MutatingScope $scope) => $this->initializerExprTypeResolver->getCastType($expr, static function (Expr $e) use ($expr, $exprResult, $nodeScopeResolver, $stmt, $scope): Type { + if ($e === $expr->expr) { + return $exprResult->getTypeForScope($scope); + } + + return $nodeScopeResolver->processExprNode($stmt, $e, $scope, new ExpressionResultStorage(), new NoopNodeCallback(), ExpressionContext::createDeep())->getTypeForScope($scope); + }), hasYield: $exprResult->hasYield(), isAlwaysTerminating: $exprResult->isAlwaysTerminating(), throwPoints: $exprResult->getThrowPoints(), diff --git a/src/Analyser/ExprHandler/ClassConstFetchHandler.php b/src/Analyser/ExprHandler/ClassConstFetchHandler.php index 6d9674020e..5886f38e02 100644 --- a/src/Analyser/ExprHandler/ClassConstFetchHandler.php +++ b/src/Analyser/ExprHandler/ClassConstFetchHandler.php @@ -8,10 +8,12 @@ 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; use PHPStan\Analyser\NodeScopeResolver; +use PHPStan\Analyser\NoopNodeCallback; use PHPStan\DependencyInjection\AutowiredService; use PHPStan\Reflection\InitializerExprTypeResolver; use PHPStan\Type\MixedType; @@ -26,6 +28,7 @@ final class ClassConstFetchHandler implements ExprHandler { public function __construct( + private ExpressionResultFactory $expressionResultFactory, private InitializerExprTypeResolver $initializerExprTypeResolver, ) { @@ -57,6 +60,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(); @@ -79,8 +83,27 @@ public function processExpr(NodeScopeResolver $nodeScopeResolver, Stmt $stmt, Ex $isAlwaysTerminating = $isAlwaysTerminating || $nameResult->isAlwaysTerminating(); } - return new ExpressionResult( + return $this->expressionResultFactory->create( + $expr, $scope, + typeCallback: function (Expr $expr, MutatingScope $scope) use ($classResult, $nodeScopeResolver, $stmt): Type { + if (!$expr->name instanceof Identifier) { + return new MixedType(); + } + + return $this->initializerExprTypeResolver->getClassConstFetchTypeByReflection( + $expr->class, + $expr->name->name, + $scope->isInClass() ? $scope->getClassReflection() : null, + static function (Expr $e) use ($expr, $classResult, $scope, $nodeScopeResolver, $stmt): Type { + if ($classResult !== null && $e === $expr->class) { + return $classResult->getTypeForScope($scope); + } + + return $nodeScopeResolver->processExprNode($stmt, $e, $scope, new ExpressionResultStorage(), new NoopNodeCallback(), ExpressionContext::createDeep())->getTypeForScope($scope); + }, + ); + }, hasYield: $hasYield, isAlwaysTerminating: $isAlwaysTerminating, throwPoints: $throwPoints, diff --git a/src/Analyser/ExprHandler/CloneHandler.php b/src/Analyser/ExprHandler/CloneHandler.php index f46bf760e5..aa15ab1512 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; @@ -25,6 +26,12 @@ final class CloneHandler implements ExprHandler { + public function __construct( + private ExpressionResultFactory $expressionResultFactory, + ) + { + } + public function supports(Expr $expr): bool { return $expr instanceof Clone_; @@ -34,8 +41,10 @@ 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( + $expr, $exprResult->getScope(), + typeCallback: static fn (Expr $uninteresting, MutatingScope $scope) => TypeTraverser::map(TypeCombinator::intersect($exprResult->getTypeForScope($scope), new ObjectWithoutClassType()), new CloneTypeTraverser()), hasYield: $exprResult->hasYield(), isAlwaysTerminating: $exprResult->isAlwaysTerminating(), throwPoints: $exprResult->getThrowPoints(), diff --git a/src/Analyser/ExprHandler/ClosureHandler.php b/src/Analyser/ExprHandler/ClosureHandler.php index b706a44dbe..939032d2f1 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; @@ -23,6 +24,7 @@ final class ClosureHandler implements ExprHandler { public function __construct( + private ExpressionResultFactory $expressionResultFactory, private ClosureTypeResolver $closureTypeResolver, ) { @@ -38,8 +40,11 @@ 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( + $expr, $scope, + // TODO: replace with proper typeCallback that doesn't use $scope->getType() + typeCallback: fn (Expr $expr, MutatingScope $scope) => $this->closureTypeResolver->getClosureType($scope, $expr), hasYield: false, isAlwaysTerminating: false, throwPoints: [], diff --git a/src/Analyser/ExprHandler/CoalesceHandler.php b/src/Analyser/ExprHandler/CoalesceHandler.php index 4c9d01d194..1496d857b2 100644 --- a/src/Analyser/ExprHandler/CoalesceHandler.php +++ b/src/Analyser/ExprHandler/CoalesceHandler.php @@ -7,11 +7,13 @@ 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; use PHPStan\Analyser\MutatingScope; use PHPStan\Analyser\NodeScopeResolver; +use PHPStan\Analyser\NoopNodeCallback; use PHPStan\DependencyInjection\AutowiredService; use PHPStan\Type\NeverType; use PHPStan\Type\Type; @@ -26,6 +28,7 @@ final class CoalesceHandler implements ExprHandler { public function __construct( + private ExpressionResultFactory $expressionResultFactory, private NonNullabilityHelper $nonNullabilityHelper, ) { @@ -75,15 +78,44 @@ public function processExpr(NodeScopeResolver $nodeScopeResolver, Stmt $stmt, Ex $rightScope = $scope->filterByFalseyValue($expr); $rightResult = $nodeScopeResolver->processExprNode($stmt, $expr->right, $rightScope, $storage, $nodeCallback, $context->enterDeep()); - $rightExprType = $scope->getType($expr->right); + $rightExprType = $rightResult->getType(); if ($rightExprType instanceof NeverType && $rightExprType->isExplicit()) { $scope = $scope->filterByTruthyValue(new Expr\Isset_([$expr->left])); } else { $scope = $scope->filterByTruthyValue(new Expr\Isset_([$expr->left]))->mergeWith($rightResult->getScope()); } - return new ExpressionResult( + return $this->expressionResultFactory->create( + $expr, $scope, + typeCallback: static function (Expr $uninteresting, MutatingScope $scope) use ($expr, $condResult, $rightResult, $nodeScopeResolver, $stmt): Type { + $typeResolver = static fn (Expr $e): Type => $nodeScopeResolver->processExprNode($stmt, $e, $scope, new ExpressionResultStorage(), new NoopNodeCallback(), ExpressionContext::createDeep())->getTypeForScope($scope); + + $issetResult = $scope->issetCheckWithResolver($expr->left, static function (Type $type): ?bool { + $isNull = $type->isNull(); + if ($isNull->maybe()) { + return null; + } + + return !$isNull->yes(); + }, $typeResolver); + + $leftType = $condResult->getTypeForScope($scope); + $rightType = $rightResult->getTypeForScope($scope); + + if ($issetResult !== null && $issetResult !== false) { + return TypeCombinator::removeNull($leftType); + } + + if ($issetResult === null) { + return TypeCombinator::union( + TypeCombinator::removeNull($leftType), + $rightType, + ); + } + + return $rightType; + }, 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 95a41ed3d3..b1eac69f53 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; @@ -28,6 +29,7 @@ final class ConstFetchHandler implements ExprHandler { public function __construct( + private ExpressionResultFactory $expressionResultFactory, private ConstantResolver $constantResolver, ) { @@ -42,8 +44,48 @@ public function processExpr(NodeScopeResolver $nodeScopeResolver, Stmt $stmt, Ex { $nodeScopeResolver->callNodeCallback($nodeCallback, $expr->name, $scope, $storage); - return new ExpressionResult( + $constName = (string) $expr->name; + $loweredConstName = strtolower($constName); + $names = null; + if ($loweredConstName === 'true') { + $constType = new ConstantBooleanType(true); + } elseif ($loweredConstName === 'false') { + $constType = new ConstantBooleanType(false); + } elseif ($loweredConstName === 'null') { + $constType = new NullType(); + } else { + $namespacedName = null; + if (!$expr->name->isFullyQualified() && $scope->getNamespace() !== null) { + $namespacedName = new FullyQualified([$scope->getNamespace(), $expr->name->toString()]); + } + $globalName = new FullyQualified($expr->name->toString()); + + $constType = $this->constantResolver->resolveConstant($expr->name, $scope) ?? new ErrorType(); + + $names = [$namespacedName, $globalName]; + } + + return $this->expressionResultFactory->create( + $expr, $scope, + typeCallback: function (Expr $expr, MutatingScope $scope) use ($constType, $names) { + if ($names !== null) { + foreach ($names 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(), + ); + } + } + } + + return $constType; + }, hasYield: false, isAlwaysTerminating: false, throwPoints: [], diff --git a/src/Analyser/ExprHandler/EmptyHandler.php b/src/Analyser/ExprHandler/EmptyHandler.php index 2d04b85d3f..e6db0acc07 100644 --- a/src/Analyser/ExprHandler/EmptyHandler.php +++ b/src/Analyser/ExprHandler/EmptyHandler.php @@ -7,11 +7,13 @@ 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; use PHPStan\Analyser\MutatingScope; use PHPStan\Analyser\NodeScopeResolver; +use PHPStan\Analyser\NoopNodeCallback; use PHPStan\DependencyInjection\AutowiredService; use PHPStan\Type\BooleanType; use PHPStan\Type\Constant\ConstantBooleanType; @@ -25,6 +27,7 @@ final class EmptyHandler implements ExprHandler { public function __construct( + private ExpressionResultFactory $expressionResultFactory, private NonNullabilityHelper $nonNullabilityHelper, ) { @@ -69,8 +72,34 @@ 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( + $expr, $scope, + typeCallback: static function (Expr $expr, MutatingScope $scope) use ($nodeScopeResolver, $stmt): Type { + $typeResolver = static fn (Expr $e): Type => $nodeScopeResolver->processExprNode($stmt, $e, $scope, new ExpressionResultStorage(), new NoopNodeCallback(), ExpressionContext::createDeep())->getTypeForScope($scope); + + $result = $scope->issetCheckWithResolver($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(); + }, $typeResolver); + if ($result === null) { + return new BooleanType(); + } + + return new ConstantBooleanType(!$result); + }, hasYield: $exprResult->hasYield(), isAlwaysTerminating: $exprResult->isAlwaysTerminating(), throwPoints: $exprResult->getThrowPoints(), diff --git a/src/Analyser/ExprHandler/ErrorSuppressHandler.php b/src/Analyser/ExprHandler/ErrorSuppressHandler.php index 6472fcdab4..7a55592df8 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; @@ -21,6 +22,12 @@ final class ErrorSuppressHandler implements ExprHandler { + public function __construct( + private ExpressionResultFactory $expressionResultFactory, + ) + { + } + public function supports(Expr $expr): bool { return $expr instanceof ErrorSuppress; @@ -30,8 +37,10 @@ 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( + $expr, $exprResult->getScope(), + typeCallback: static fn (Expr $uninteresting, MutatingScope $scope) => $exprResult->getTypeForScope($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 121fdc12b5..8dac4e1223 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; @@ -25,6 +26,12 @@ final class EvalHandler implements ExprHandler { + public function __construct( + private ExpressionResultFactory $expressionResultFactory, + ) + { + } + public function supports(Expr $expr): bool { return $expr instanceof Eval_; @@ -40,8 +47,10 @@ 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( + $expr, $scope, + typeCallback: static fn () => new MixedType(), 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 010f44b0dd..15ab3e3d4a 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; @@ -24,6 +25,12 @@ final class ExitHandler implements ExprHandler { + public function __construct( + private ExpressionResultFactory $expressionResultFactory, + ) + { + } + public function supports(Expr $expr): bool { return $expr instanceof Exit_; @@ -47,8 +54,10 @@ public function processExpr(NodeScopeResolver $nodeScopeResolver, Stmt $stmt, Ex $scope = $exprResult->getScope(); } - return new ExpressionResult( + return $this->expressionResultFactory->create( + $expr, $scope, + typeCallback: static fn () => new NonAcceptingNeverType(), hasYield: $hasYield, isAlwaysTerminating: true, throwPoints: $throwPoints, diff --git a/src/Analyser/ExprHandler/FirstClassCallableFuncCallHandler.php b/src/Analyser/ExprHandler/FirstClassCallableFuncCallHandler.php index 0396e0883b..65ac62a779 100644 --- a/src/Analyser/ExprHandler/FirstClassCallableFuncCallHandler.php +++ b/src/Analyser/ExprHandler/FirstClassCallableFuncCallHandler.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; @@ -15,7 +16,6 @@ use PHPStan\DependencyInjection\AutowiredService; use PHPStan\Reflection\InitializerExprContext; use PHPStan\Reflection\InitializerExprTypeResolver; -use PHPStan\ShouldNotHappenException; use PHPStan\Type\ObjectType; use PHPStan\Type\Type; @@ -27,6 +27,7 @@ final class FirstClassCallableFuncCallHandler implements ExprHandler { public function __construct( + private ExpressionResultFactory $expressionResultFactory, private InitializerExprTypeResolver $initializerExprTypeResolver, ) { @@ -47,8 +48,36 @@ public function processExpr( ExpressionContext $context, ): ExpressionResult { - // handled in NodeScopeResolver before ExprHandlers are called - throw new ShouldNotHappenException(); + $nameResult = null; + if ($expr->name instanceof Expr) { + $nameResult = $nodeScopeResolver->processExprNode($stmt, $expr->name, $scope, $storage, $nodeCallback, ExpressionContext::createDeep()); + $scope = $nameResult->getScope(); + } + + return $this->expressionResultFactory->create( + $expr, + $scope, + typeCallback: function (Expr $expr, MutatingScope $scope) use ($nameResult): Type { + if ($nameResult !== null) { + $callableType = $nameResult->getTypeForScope($scope); + 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); + }, + hasYield: false, + isAlwaysTerminating: false, + throwPoints: $nameResult !== null ? $nameResult->getThrowPoints() : [], + impurePoints: $nameResult !== null ? $nameResult->getImpurePoints() : [], + ); } public function resolveType(MutatingScope $scope, Expr $expr): Type diff --git a/src/Analyser/ExprHandler/FirstClassCallableMethodCallHandler.php b/src/Analyser/ExprHandler/FirstClassCallableMethodCallHandler.php index 5b6a283c9b..20c6209392 100644 --- a/src/Analyser/ExprHandler/FirstClassCallableMethodCallHandler.php +++ b/src/Analyser/ExprHandler/FirstClassCallableMethodCallHandler.php @@ -9,15 +9,16 @@ 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; use PHPStan\Analyser\NodeScopeResolver; use PHPStan\DependencyInjection\AutowiredService; use PHPStan\Reflection\InitializerExprTypeResolver; -use PHPStan\ShouldNotHappenException; use PHPStan\Type\ObjectType; use PHPStan\Type\Type; +use function array_merge; /** * @implements ExprHandler @@ -27,6 +28,7 @@ final class FirstClassCallableMethodCallHandler implements ExprHandler { public function __construct( + private ExpressionResultFactory $expressionResultFactory, private InitializerExprTypeResolver $initializerExprTypeResolver, ) { @@ -47,8 +49,43 @@ public function processExpr( ExpressionContext $context, ): ExpressionResult { - // handled in NodeScopeResolver before ExprHandlers are called - throw new ShouldNotHappenException(); + $varResult = $nodeScopeResolver->processExprNode($stmt, $expr->var, $scope, $storage, $nodeCallback, ExpressionContext::createDeep()); + $scope = $varResult->getScope(); + $throwPoints = $varResult->getThrowPoints(); + $impurePoints = $varResult->getImpurePoints(); + + if (!$expr->name instanceof Identifier) { + $nameResult = $nodeScopeResolver->processExprNode($stmt, $expr->name, $scope, $storage, $nodeCallback, ExpressionContext::createDeep()); + $scope = $nameResult->getScope(); + $throwPoints = array_merge($throwPoints, $nameResult->getThrowPoints()); + $impurePoints = array_merge($impurePoints, $nameResult->getImpurePoints()); + } + + return $this->expressionResultFactory->create( + $expr, + $scope, + typeCallback: function (Expr $expr, MutatingScope $scope) use ($varResult): Type { + if (!$expr->name instanceof Identifier) { + return new ObjectType(Closure::class); + } + + $varType = $varResult->getTypeForScope($scope); + $method = $scope->getMethodReflection($varType, $expr->name->toString()); + if ($method === null) { + return new ObjectType(Closure::class); + } + + return $this->initializerExprTypeResolver->createFirstClassCallable( + $method, + $method->getVariants(), + $scope->nativeTypesPromoted, + ); + }, + hasYield: false, + isAlwaysTerminating: false, + throwPoints: $throwPoints, + impurePoints: $impurePoints, + ); } public function resolveType(MutatingScope $scope, Expr $expr): Type diff --git a/src/Analyser/ExprHandler/FirstClassCallableNewHandler.php b/src/Analyser/ExprHandler/FirstClassCallableNewHandler.php index 647ed5b7a9..509b97772b 100644 --- a/src/Analyser/ExprHandler/FirstClassCallableNewHandler.php +++ b/src/Analyser/ExprHandler/FirstClassCallableNewHandler.php @@ -8,6 +8,7 @@ use PhpParser\Node\Stmt\Class_; use PHPStan\Analyser\ExpressionContext; use PHPStan\Analyser\ExpressionResult; +use PHPStan\Analyser\ExpressionResultFactory; use PHPStan\Analyser\ExpressionResultStorage; use PHPStan\Analyser\ExprHandler; use PHPStan\Analyser\MutatingScope; @@ -15,7 +16,6 @@ use PHPStan\DependencyInjection\AutowiredService; use PHPStan\Reflection\InitializerExprContext; use PHPStan\Reflection\InitializerExprTypeResolver; -use PHPStan\ShouldNotHappenException; use PHPStan\Type\Type; /** @@ -26,6 +26,7 @@ final class FirstClassCallableNewHandler implements ExprHandler { public function __construct( + private ExpressionResultFactory $expressionResultFactory, private InitializerExprTypeResolver $initializerExprTypeResolver, ) { @@ -46,8 +47,24 @@ public function processExpr( ExpressionContext $context, ): ExpressionResult { - // handled in NodeScopeResolver before ExprHandlers are called - throw new ShouldNotHappenException(); + $throwPoints = []; + $impurePoints = []; + if ($expr->class instanceof Expr) { + $classResult = $nodeScopeResolver->processExprNode($stmt, $expr->class, $scope, $storage, $nodeCallback, ExpressionContext::createDeep()); + $scope = $classResult->getScope(); + $throwPoints = $classResult->getThrowPoints(); + $impurePoints = $classResult->getImpurePoints(); + } + + return $this->expressionResultFactory->create( + $expr, + $scope, + typeCallback: fn (Expr $expr, MutatingScope $scope) => $this->initializerExprTypeResolver->getFirstClassCallableType($expr, InitializerExprContext::fromScope($scope), $scope->nativeTypesPromoted), + hasYield: false, + isAlwaysTerminating: false, + throwPoints: $throwPoints, + impurePoints: $impurePoints, + ); } public function resolveType(MutatingScope $scope, Expr $expr): Type diff --git a/src/Analyser/ExprHandler/FirstClassCallableStaticCallHandler.php b/src/Analyser/ExprHandler/FirstClassCallableStaticCallHandler.php index 0a5261f1f6..756bb05778 100644 --- a/src/Analyser/ExprHandler/FirstClassCallableStaticCallHandler.php +++ b/src/Analyser/ExprHandler/FirstClassCallableStaticCallHandler.php @@ -4,9 +4,11 @@ use PhpParser\Node\Expr; use PhpParser\Node\Expr\StaticCall; +use PhpParser\Node\Identifier; 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; @@ -14,8 +16,8 @@ use PHPStan\DependencyInjection\AutowiredService; use PHPStan\Reflection\InitializerExprContext; use PHPStan\Reflection\InitializerExprTypeResolver; -use PHPStan\ShouldNotHappenException; use PHPStan\Type\Type; +use function array_merge; /** * @implements ExprHandler @@ -25,6 +27,7 @@ final class FirstClassCallableStaticCallHandler implements ExprHandler { public function __construct( + private ExpressionResultFactory $expressionResultFactory, private InitializerExprTypeResolver $initializerExprTypeResolver, ) { @@ -45,8 +48,30 @@ public function processExpr( ExpressionContext $context, ): ExpressionResult { - // handled in NodeScopeResolver before ExprHandlers are called - throw new ShouldNotHappenException(); + $throwPoints = []; + $impurePoints = []; + if ($expr->class instanceof Expr) { + $classResult = $nodeScopeResolver->processExprNode($stmt, $expr->class, $scope, $storage, $nodeCallback, ExpressionContext::createDeep()); + $scope = $classResult->getScope(); + $throwPoints = $classResult->getThrowPoints(); + $impurePoints = $classResult->getImpurePoints(); + } + if (!$expr->name instanceof Identifier) { + $nameResult = $nodeScopeResolver->processExprNode($stmt, $expr->name, $scope, $storage, $nodeCallback, ExpressionContext::createDeep()); + $scope = $nameResult->getScope(); + $throwPoints = array_merge($throwPoints, $nameResult->getThrowPoints()); + $impurePoints = array_merge($impurePoints, $nameResult->getImpurePoints()); + } + + return $this->expressionResultFactory->create( + $expr, + $scope, + typeCallback: fn (Expr $expr, MutatingScope $scope) => $this->initializerExprTypeResolver->getFirstClassCallableType($expr, InitializerExprContext::fromScope($scope), $scope->nativeTypesPromoted), + hasYield: false, + isAlwaysTerminating: false, + throwPoints: $throwPoints, + impurePoints: $impurePoints, + ); } public function resolveType(MutatingScope $scope, Expr $expr): Type diff --git a/src/Analyser/ExprHandler/FuncCallHandler.php b/src/Analyser/ExprHandler/FuncCallHandler.php index 96cf7fa0f1..c06c3e3d7c 100644 --- a/src/Analyser/ExprHandler/FuncCallHandler.php +++ b/src/Analyser/ExprHandler/FuncCallHandler.php @@ -15,6 +15,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; @@ -79,6 +80,7 @@ final class FuncCallHandler implements ExprHandler { public function __construct( + private ExpressionResultFactory $expressionResultFactory, private ReflectionProvider $reflectionProvider, private DynamicThrowTypeExtensionProvider $dynamicThrowTypeExtensionProvider, private DynamicReturnTypeExtensionRegistryProvider $dynamicReturnTypeExtensionRegistryProvider, @@ -86,6 +88,8 @@ public function __construct( private bool $implicitThrows, #[AutowiredParameter] private bool $rememberPossiblyImpureFunctionValues, + #[AutowiredParameter] + private array $earlyTerminatingFunctionCalls, ) { } @@ -102,8 +106,10 @@ public function processExpr(NodeScopeResolver $nodeScopeResolver, Stmt $stmt, Ex $throwPoints = []; $impurePoints = []; $isAlwaysTerminating = false; + $nameResult = null; if ($expr->name instanceof Expr) { - $nameType = $scope->getType($expr->name); + $nameResult = $nodeScopeResolver->processExprNode($stmt, $expr->name, $scope, $storage, $nodeCallback, $context->enterDeep()); + $nameType = $nameResult->getType(); if (!$nameType->isCallable()->no()) { $parametersAcceptor = ParametersAcceptorSelector::selectFromArgs( $scope, @@ -113,7 +119,6 @@ public function processExpr(NodeScopeResolver $nodeScopeResolver, Stmt $stmt, Ex ); } - $nameResult = $nodeScopeResolver->processExprNode($stmt, $expr->name, $scope, $storage, $nodeCallback, $context->enterDeep()); $scope = $nameResult->getScope(); $throwPoints = $nameResult->getThrowPoints(); $impurePoints = $nameResult->getImpurePoints(); @@ -162,6 +167,9 @@ public function processExpr(NodeScopeResolver $nodeScopeResolver, Stmt $stmt, Ex $returnType = $parametersAcceptor->getReturnType(); $isAlwaysTerminating = $isAlwaysTerminating || $returnType instanceof NeverType && $returnType->isExplicit(); } + if (!$isAlwaysTerminating && $expr->name instanceof Name && in_array((string) $expr->name, $this->earlyTerminatingFunctionCalls, true)) { + $isAlwaysTerminating = true; + } if ( $normalizedExpr->name instanceof Name @@ -470,8 +478,108 @@ public function processExpr(NodeScopeResolver $nodeScopeResolver, Stmt $stmt, Ex $scope = $scope->afterOpenSslCall($functionReflection->getName()); } - return new ExpressionResult( + return $this->expressionResultFactory->create( + $expr, $scope, + typeCallback: function (Expr $expr, MutatingScope $scope) use ($nameResult, $nodeScopeResolver, $stmt): Type { + if ($expr->name instanceof Expr) { + $calledOnType = $nameResult->getTypeForScope($scope); + if ($calledOnType->isCallable()->no()) { + return new ErrorType(); + } + + $parametersAcceptor = ParametersAcceptorSelector::selectFromArgs( + $scope, + $expr->getArgs(), + $calledOnType->getCallableParametersAcceptors($scope), + null, + ); + + $functionName = null; + if ($expr->name instanceof String_) { + /** @var non-empty-string $name */ + $name = $expr->name->value; + $functionName = new Name($name); + } elseif ( + $expr->name instanceof FuncCall + && $expr->name->name instanceof Name + && $expr->name->isFirstClassCallable() + ) { + $functionName = $expr->name->name; + } + + $normalizedNode = ArgumentsNormalizer::reorderFuncArguments($parametersAcceptor, $expr); + if ($normalizedNode !== null && $functionName !== null && $this->reflectionProvider->hasFunction($functionName, $scope)) { + $functionReflection = $this->reflectionProvider->getFunction($functionName, $scope); + $resolvedType = $this->getDynamicFunctionReturnType($scope, $normalizedNode, $functionReflection); + if ($resolvedType !== null) { + return $resolvedType; + } + } + + return $parametersAcceptor->getReturnType(); + } + + if (!$this->reflectionProvider->hasFunction($expr->name, $scope)) { + return new ErrorType(); + } + + $functionReflection = $this->reflectionProvider->getFunction($expr->name, $scope); + if ($scope->nativeTypesPromoted) { + return ParametersAcceptorSelector::combineAcceptors($functionReflection->getVariants())->getNativeReturnType(); + } + + if ($functionReflection->getName() === 'call_user_func') { + $result = ArgumentsNormalizer::reorderCallUserFuncArguments($expr, $scope); + if ($result !== null) { + [, $innerFuncCall] = $result; + + return $nodeScopeResolver->processExprNode($stmt, $innerFuncCall, $scope, new ExpressionResultStorage(), new NoopNodeCallback(), ExpressionContext::createDeep())->getTypeForScope($scope); + } + } + + $parametersAcceptor = ParametersAcceptorSelector::selectFromArgs( + $scope, + $expr->getArgs(), + $functionReflection->getVariants(), + $functionReflection->getNamedArgumentsVariants(), + ); + $normalizedNode = ArgumentsNormalizer::reorderFuncArguments($parametersAcceptor, $expr); + if ($normalizedNode !== null) { + if ($functionReflection->getName() === 'clone' && count($normalizedNode->getArgs()) > 0) { + $cloneResult = $nodeScopeResolver->processExprNode($stmt, new Expr\Clone_($normalizedNode->getArgs()[0]->value), $scope, new ExpressionResultStorage(), new NoopNodeCallback(), ExpressionContext::createDeep()); + $cloneType = $cloneResult->getTypeForScope($scope); + if (count($normalizedNode->getArgs()) === 2) { + $propertiesResult = $nodeScopeResolver->processExprNode($stmt, $normalizedNode->getArgs()[1]->value, $scope, new ExpressionResultStorage(), new NoopNodeCallback(), ExpressionContext::createDeep()); + $propertiesType = $propertiesResult->getTypeForScope($scope); + if ($propertiesType->isConstantArray()->yes()) { + $constantArrays = $propertiesType->getConstantArrays(); + if (count($constantArrays) === 1) { + $accessories = []; + foreach ($constantArrays[0]->getKeyTypes() as $keyType) { + $constantKeyTypes = $keyType->getConstantScalarValues(); + if (count($constantKeyTypes) !== 1) { + return $cloneType; + } + $accessories[] = new HasPropertyType((string) $constantKeyTypes[0]); + } + if (count($accessories) > 0 && count($accessories) <= 16) { + return TypeCombinator::intersect($cloneType, ...$accessories); + } + } + } + } + + return $cloneType; + } + $resolvedType = $this->getDynamicFunctionReturnType($scope, $normalizedNode, $functionReflection); + if ($resolvedType !== null) { + return $resolvedType; + } + } + + return VoidToNullTypeTransformer::transform($parametersAcceptor->getReturnType(), $expr); + }, hasYield: $hasYield, isAlwaysTerminating: $isAlwaysTerminating, throwPoints: $throwPoints, @@ -714,6 +822,10 @@ private function getArraySortDoNotPreserveListFunctionType(Type $type): Type public function resolveType(MutatingScope $scope, Expr $expr): Type { + if ($expr->name instanceof Name && in_array((string) $expr->name, $this->earlyTerminatingFunctionCalls, true)) { + return new NeverType(true); + } + if ($expr->name instanceof Expr) { $calledOnType = $scope->getType($expr->name); if ($calledOnType->isCallable()->no()) { diff --git a/src/Analyser/ExprHandler/IncludeHandler.php b/src/Analyser/ExprHandler/IncludeHandler.php index 4987f4db53..e28a7c2258 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; @@ -26,6 +27,12 @@ final class IncludeHandler implements ExprHandler { + public function __construct( + private ExpressionResultFactory $expressionResultFactory, + ) + { + } + public function supports(Expr $expr): bool { return $expr instanceof Include_; @@ -42,8 +49,10 @@ 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( + $expr, $scope, + typeCallback: static fn () => new MixedType(), 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 59d09b1c9a..abd20c1d2a 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; @@ -33,6 +34,12 @@ final class InstanceofHandler implements ExprHandler { + public function __construct( + private ExpressionResultFactory $expressionResultFactory, + ) + { + } + public function supports(Expr $expr): bool { return $expr instanceof Instanceof_; @@ -46,7 +53,19 @@ public function processExpr(NodeScopeResolver $nodeScopeResolver, Stmt $stmt, Ex $impurePoints = $exprResult->getImpurePoints(); $isAlwaysTerminating = $exprResult->isAlwaysTerminating(); $scope = $exprResult->getScope(); - if (!$expr->class instanceof Name) { + $classTypeFromName = null; + $classResult = null; + if ($expr->class instanceof Name) { + $unresolvedClassName = $expr->class->toString(); + if ( + strtolower($unresolvedClassName) === 'static' + && $scope->isInClass() + ) { + $classTypeFromName = new StaticType($scope->getClassReflection()); + } else { + $classTypeFromName = new ObjectType($scope->resolveName($expr->class)); + } + } else { $classResult = $nodeScopeResolver->processExprNode($stmt, $expr->class, $scope, $storage, $nodeCallback, $context->enterDeep()); $scope = $classResult->getScope(); $hasYield = $hasYield || $classResult->hasYield(); @@ -55,8 +74,44 @@ public function processExpr(NodeScopeResolver $nodeScopeResolver, Stmt $stmt, Ex $isAlwaysTerminating = $isAlwaysTerminating || $classResult->isAlwaysTerminating(); } - return new ExpressionResult( + return $this->expressionResultFactory->create( + $expr, $scope, + typeCallback: static function (Expr $uninteresting, MutatingScope $scope) use ($exprResult, $classResult, $classTypeFromName): Type { + $expressionType = $exprResult->getTypeForScope($scope); + if ( + $scope->isInTrait() + && TypeUtils::findThisType($expressionType) !== null + ) { + return new BooleanType(); + } + if ($expressionType instanceof NeverType) { + return new ConstantBooleanType(false); + } + + $uncertainty = false; + if ($classTypeFromName !== null) { + $classType = $classTypeFromName; + } else { + $classType = $classResult->getTypeForScope($scope); + $traverser = new InstanceOfClassTypeTraverser(); + $classType = TypeTraverser::map($classType, $traverser); + $uncertainty = $traverser->getUncertainty(); + } + + if ($classType->isSuperTypeOf(new MixedType())->yes()) { + return new BooleanType(); + } + + $isSuperType = $classType->isSuperTypeOf($expressionType); + if ($isSuperType->no()) { + return new ConstantBooleanType(false); + } elseif ($isSuperType->yes() && !$uncertainty) { + return new ConstantBooleanType(true); + } + + return new BooleanType(); + }, hasYield: $hasYield, isAlwaysTerminating: $isAlwaysTerminating, throwPoints: $throwPoints, diff --git a/src/Analyser/ExprHandler/InterpolatedStringHandler.php b/src/Analyser/ExprHandler/InterpolatedStringHandler.php index 24e4dcab0e..0bbdb294c0 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\MutatingScope; @@ -26,6 +27,7 @@ final class InterpolatedStringHandler implements ExprHandler { public function __construct( + private ExpressionResultFactory $expressionResultFactory, private InitializerExprTypeResolver $initializerExprTypeResolver, ) { @@ -42,11 +44,14 @@ public function processExpr(NodeScopeResolver $nodeScopeResolver, Stmt $stmt, Ex $throwPoints = []; $impurePoints = []; $isAlwaysTerminating = false; - foreach ($expr->parts as $part) { - if (!$part instanceof Expr) { + $partResults = []; + foreach ($expr->parts as $i => $part) { + if (!($part instanceof Expr)) { continue; } + $partResult = $nodeScopeResolver->processExprNode($stmt, $part, $scope, $storage, $nodeCallback, $context->enterDeep()); + $partResults[$i] = $partResult; $hasYield = $hasYield || $partResult->hasYield(); $throwPoints = array_merge($throwPoints, $partResult->getThrowPoints()); $impurePoints = array_merge($impurePoints, $partResult->getImpurePoints()); @@ -54,8 +59,27 @@ public function processExpr(NodeScopeResolver $nodeScopeResolver, Stmt $stmt, Ex $scope = $partResult->getScope(); } - return new ExpressionResult( + return $this->expressionResultFactory->create( + $expr, $scope, + typeCallback: function (Expr $uninteresting, MutatingScope $scope) use ($expr, $partResults): Type { + $resultType = null; + foreach ($expr->parts as $i => $part) { + if ($part instanceof InterpolatedStringPart) { + $partType = new ConstantStringType($part->value); + } else { + $partType = $partResults[$i]->getTypeForScope($scope)->toString(); + } + if ($resultType === null) { + $resultType = $partType; + continue; + } + + $resultType = $this->initializerExprTypeResolver->resolveConcatType($resultType, $partType); + } + + return $resultType ?? new ConstantStringType(''); + }, hasYield: $hasYield, isAlwaysTerminating: $isAlwaysTerminating, throwPoints: $throwPoints, diff --git a/src/Analyser/ExprHandler/IssetHandler.php b/src/Analyser/ExprHandler/IssetHandler.php index fe93e1fe6a..d8b68be655 100644 --- a/src/Analyser/ExprHandler/IssetHandler.php +++ b/src/Analyser/ExprHandler/IssetHandler.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\NonNullabilityHelper; @@ -32,6 +33,7 @@ final class IssetHandler implements ExprHandler { public function __construct( + private ExpressionResultFactory $expressionResultFactory, private NonNullabilityHelper $nonNullabilityHelper, ) { @@ -94,7 +96,7 @@ public function processExpr(NodeScopeResolver $nodeScopeResolver, Stmt $stmt, Ex continue; } - $varType = $scope->getType($var->var); + $varType = $nodeScopeResolver->processExprNode($stmt, $var->var, $scope, new ExpressionResultStorage(), new NoopNodeCallback(), $context->enterDeep())->getType(); if ($varType->isArray()->yes() || (new ObjectType(ArrayAccess::class))->isSuperTypeOf($varType)->no()) { continue; } @@ -115,8 +117,39 @@ public function processExpr(NodeScopeResolver $nodeScopeResolver, Stmt $stmt, Ex $scope = $this->nonNullabilityHelper->revertNonNullability($scope, $nonNullabilityResult->getSpecifiedExpressions()); } - return new ExpressionResult( + return $this->expressionResultFactory->create( + $expr, $scope, + typeCallback: static function (Expr $expr, MutatingScope $scope) use ($nodeScopeResolver, $stmt): Type { + $typeResolver = static fn (Expr $e): Type => $nodeScopeResolver->processExprNode($stmt, $e, $scope, new ExpressionResultStorage(), new NoopNodeCallback(), ExpressionContext::createDeep())->getTypeForScope($scope); + + $issetResult = true; + foreach ($expr->vars as $var) { + $result = $scope->issetCheckWithResolver($var, static function (Type $type): ?bool { + $isNull = $type->isNull(); + if ($isNull->maybe()) { + return null; + } + + return !$isNull->yes(); + }, $typeResolver); + if ($result !== null) { + if (!$result) { + return new ConstantBooleanType($result); + } + + continue; + } + + $issetResult = $result; + } + + if ($issetResult === null) { + return new BooleanType(); + } + + return new ConstantBooleanType($issetResult); + }, hasYield: $hasYield, isAlwaysTerminating: $isAlwaysTerminating, throwPoints: $throwPoints, diff --git a/src/Analyser/ExprHandler/MatchHandler.php b/src/Analyser/ExprHandler/MatchHandler.php index 626e6fc717..e8f46a337a 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; @@ -51,6 +52,7 @@ final class MatchHandler implements ExprHandler { public function __construct( + private ExpressionResultFactory $expressionResultFactory, #[AutowiredParameter] private bool $treatPhpDocTypesAsCertain, ) @@ -184,9 +186,9 @@ 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 { $deepContext = $context->enterDeep(); - $condType = $scope->getType($expr->cond); - $condNativeType = $scope->getNativeType($expr->cond); $condResult = $nodeScopeResolver->processExprNode($stmt, $expr->cond, $scope, $storage, $nodeCallback, $deepContext); + $condType = $condResult->getType(); + $condNativeType = $condResult->getNativeType(); $scope = $condResult->getScope(); $hasYield = $condResult->hasYield(); $throwPoints = $condResult->getThrowPoints(); @@ -199,6 +201,8 @@ public function processExpr(NodeScopeResolver $nodeScopeResolver, Stmt $stmt, Ex $arms = $expr->arms; $armCondsToSkip = []; $armBodyScopes = []; + /** @var ExpressionResult[] */ + $armBodyResults = []; if ($condType->isEnum()->yes()) { // enum match analysis would work even without this if branch // but would be much slower @@ -334,6 +338,7 @@ public function processExpr(NodeScopeResolver $nodeScopeResolver, Stmt $stmt, Ex $nodeCallback, ExpressionContext::createTopLevel(), ); + $armBodyResults[] = $armResult; $armScope = $armResult->getScope(); $armBodyScopes[] = $armScope; $hasYield = $hasYield || $armResult->hasYield(); @@ -368,6 +373,7 @@ public function processExpr(NodeScopeResolver $nodeScopeResolver, Stmt $stmt, Ex $matchArmBody = new MatchExpressionArmBody($matchScope, $arm->body); $armNodes[$i] = new MatchExpressionArm($matchArmBody, [], $arm->getStartLine()); $armResult = $nodeScopeResolver->processExprNode($stmt, $arm->body, $matchScope, $storage, $nodeCallback, ExpressionContext::createTopLevel()); + $armBodyResults[] = $armResult; $matchScope = $armResult->getScope(); $hasYield = $hasYield || $armResult->hasYield(); $throwPoints = array_merge($throwPoints, $armResult->getThrowPoints()); @@ -422,6 +428,7 @@ public function processExpr(NodeScopeResolver $nodeScopeResolver, Stmt $stmt, Ex $nodeCallback, ExpressionContext::createTopLevel(), ); + $armBodyResults[] = $armResult; $armScope = $armResult->getScope(); $armBodyScopes[] = $armScope; $hasYield = $hasYield || $armResult->hasYield(); @@ -472,8 +479,17 @@ public function processExpr(NodeScopeResolver $nodeScopeResolver, Stmt $stmt, Ex $expr->cond = $expr->cond->getExpr(); } - return new ExpressionResult( + return $this->expressionResultFactory->create( + $expr, $scope, + typeCallback: static function (Expr $uninteresting, MutatingScope $scope) use ($armBodyResults): Type { + $types = []; + foreach ($armBodyResults as $armBodyResult) { + $types[] = $armBodyResult->getTypeForScope($scope); + } + + return TypeCombinator::union(...$types); + }, hasYield: $hasYield, isAlwaysTerminating: $isAlwaysTerminating, throwPoints: $throwPoints, diff --git a/src/Analyser/ExprHandler/MethodCallHandler.php b/src/Analyser/ExprHandler/MethodCallHandler.php index bf40faf546..32dcc02211 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; @@ -25,6 +26,7 @@ use PHPStan\Node\Expr\PossiblyImpureCallExpr; use PHPStan\Node\InvalidateExprNode; use PHPStan\Reflection\Callables\SimpleImpurePoint; +use PHPStan\Reflection\ReflectionProvider; use PHPStan\Reflection\ExtendedParametersAcceptor; use PHPStan\Reflection\MethodReflection; use PHPStan\Reflection\ParametersAcceptor; @@ -57,16 +59,30 @@ final class MethodCallHandler implements ExprHandler { public function __construct( + private ExpressionResultFactory $expressionResultFactory, private DynamicThrowTypeExtensionProvider $dynamicThrowTypeExtensionProvider, private MethodCallReturnTypeHelper $methodCallReturnTypeHelper, #[AutowiredParameter(ref: '%exceptions.implicitThrows%')] private bool $implicitThrows, #[AutowiredParameter] private bool $rememberPossiblyImpureFunctionValues, + #[AutowiredParameter] + private array $earlyTerminatingMethodCalls, + private ReflectionProvider $reflectionProvider, ) { + $earlyTerminatingMethodNames = []; + foreach ($this->earlyTerminatingMethodCalls as $methodNames) { + foreach ($methodNames as $methodName) { + $earlyTerminatingMethodNames[strtolower($methodName)] = true; + } + } + $this->earlyTerminatingMethodNames = $earlyTerminatingMethodNames; } + /** @var array */ + private array $earlyTerminatingMethodNames; + public function supports(Expr $expr): bool { return $expr instanceof MethodCall && !$expr->isFirstClassCallable(); @@ -98,7 +114,7 @@ public function processExpr(NodeScopeResolver $nodeScopeResolver, Stmt $stmt, Ex } $parametersAcceptor = null; $methodReflection = null; - $calledOnType = $scope->getType($expr->var); + $calledOnType = $varResult->getType(); if ($expr->name instanceof Identifier) { $methodName = $expr->name->name; $methodReflection = $scope->getMethodReflection($calledOnType, $methodName); @@ -142,6 +158,25 @@ public function processExpr(NodeScopeResolver $nodeScopeResolver, Stmt $stmt, Ex $returnType = $parametersAcceptor->getReturnType(); $isAlwaysTerminating = $returnType instanceof NeverType && $returnType->isExplicit(); } + if (!$isAlwaysTerminating && $expr->name instanceof Identifier && array_key_exists($expr->name->toLowerString(), $this->earlyTerminatingMethodNames)) { + foreach ($calledOnType->getObjectClassNames() as $referencedClass) { + if (!$this->reflectionProvider->hasClass($referencedClass)) { + continue; + } + + $classReflection = $this->reflectionProvider->getClass($referencedClass); + foreach (array_merge([$referencedClass], $classReflection->getParentClassesNames(), $classReflection->getNativeReflection()->getInterfaceNames()) as $className) { + if (!isset($this->earlyTerminatingMethodCalls[$className])) { + continue; + } + + if (in_array($expr->name->name, $this->earlyTerminatingMethodCalls[$className], true)) { + $isAlwaysTerminating = true; + break 2; + } + } + } + } $argsResult = $nodeScopeResolver->processArgs( $stmt, @@ -191,8 +226,38 @@ 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( + $expr, $scope, + typeCallback: function (Expr $expr, MutatingScope $scope) use ($varResult): Type { + if ($expr->name instanceof Identifier) { + $varType = $varResult->getTypeForScope($scope); + + if ($scope->nativeTypesPromoted) { + $methodReflection = $scope->getMethodReflection( + $varType, + $expr->name->name, + ); + if ($methodReflection === null) { + return new ErrorType(); + } + + return ParametersAcceptorSelector::combineAcceptors($methodReflection->getVariants())->getNativeReturnType(); + } + + $returnType = $this->methodCallReturnTypeHelper->methodCallReturnType( + $scope, + $varType, + $expr->name->name, + $expr, + ); + + return $returnType ?? new ErrorType(); + } + + // TODO: handle dynamic method names + return new MixedType(); + }, hasYield: $hasYield, isAlwaysTerminating: $isAlwaysTerminating, throwPoints: $throwPoints, @@ -219,8 +284,10 @@ 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( + $expr, $scope, + typeCallback: static fn (Expr $uninteresting, MutatingScope $scope) => $result->getTypeForScope($scope), hasYield: $result->hasYield(), isAlwaysTerminating: $result->isAlwaysTerminating(), throwPoints: $result->getThrowPoints(), @@ -283,6 +350,26 @@ private function getMethodThrowPoint(MethodReflection $methodReflection, Paramet public function resolveType(MutatingScope $scope, Expr $expr): Type { + if ($expr->name instanceof Identifier && array_key_exists($expr->name->toLowerString(), $this->earlyTerminatingMethodNames)) { + $calledOnType = $scope->getType($expr->var); + foreach ($calledOnType->getObjectClassNames() as $referencedClass) { + if (!$this->reflectionProvider->hasClass($referencedClass)) { + continue; + } + + $classReflection = $this->reflectionProvider->getClass($referencedClass); + foreach (array_merge([$referencedClass], $classReflection->getParentClassesNames(), $classReflection->getNativeReflection()->getInterfaceNames()) as $className) { + if (!isset($this->earlyTerminatingMethodCalls[$className])) { + continue; + } + + if (in_array($expr->name->name, $this->earlyTerminatingMethodCalls[$className], true)) { + return new NeverType(true); + } + } + } + } + if ($expr->name instanceof Identifier) { if ($scope->nativeTypesPromoted) { $methodReflection = $scope->getMethodReflection( diff --git a/src/Analyser/ExprHandler/NewHandler.php b/src/Analyser/ExprHandler/NewHandler.php index 4179b5259d..58fb43a791 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; @@ -63,6 +64,7 @@ final class NewHandler implements ExprHandler { public function __construct( + private ExpressionResultFactory $expressionResultFactory, private ReflectionProvider $reflectionProvider, private DynamicThrowTypeExtensionProvider $dynamicThrowTypeExtensionProvider, private DynamicReturnTypeExtensionRegistryProvider $dynamicReturnTypeExtensionRegistryProvider, @@ -86,6 +88,7 @@ public function processExpr(NodeScopeResolver $nodeScopeResolver, Stmt $stmt, Ex $impurePoints = []; $isAlwaysTerminating = false; $normalizedExpr = $expr; + $classResult = null; if ($expr->class instanceof Name) { $className = $scope->resolveName($expr->class); @@ -202,8 +205,20 @@ 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( + $expr, $scope, + typeCallback: function (Expr $expr, MutatingScope $scope) use ($classResult): Type { + if ($expr->class instanceof Name) { + return $this->exactInstantiation($scope, $expr, $expr->class); + } + if ($expr->class instanceof Node\Stmt\Class_) { + $anonymousClassReflection = $this->reflectionProvider->getAnonymousClassReflection($expr->class, $scope); + return new ObjectType($anonymousClassReflection->getName()); + } + + return $classResult->getTypeForScope($scope)->getObjectTypeOrClassStringObjectType(); + }, hasYield: $hasYield, isAlwaysTerminating: $isAlwaysTerminating, throwPoints: $throwPoints, diff --git a/src/Analyser/ExprHandler/NullsafeMethodCallHandler.php b/src/Analyser/ExprHandler/NullsafeMethodCallHandler.php index 13c0577f85..49eb622a69 100644 --- a/src/Analyser/ExprHandler/NullsafeMethodCallHandler.php +++ b/src/Analyser/ExprHandler/NullsafeMethodCallHandler.php @@ -11,11 +11,13 @@ 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; use PHPStan\Analyser\MutatingScope; use PHPStan\Analyser\NodeScopeResolver; +use PHPStan\Analyser\NoopNodeCallback; use PHPStan\DependencyInjection\AutowiredService; use PHPStan\Node\Printer\ExprPrinter; use PHPStan\Type\NullType; @@ -31,6 +33,7 @@ final class NullsafeMethodCallHandler implements ExprHandler { public function __construct( + private ExpressionResultFactory $expressionResultFactory, private NonNullabilityHelper $nonNullabilityHelper, ) { @@ -60,6 +63,8 @@ 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 { + $varResult = $nodeScopeResolver->processExprNode($stmt, $expr->var, $scope, new ExpressionResultStorage(), new NoopNodeCallback(), ExpressionContext::createDeep()); + $nonNullabilityResult = $this->nonNullabilityHelper->ensureShallowNonNullability($scope, $scope, $expr->var); $attributes = array_merge($expr->getAttributes(), ['virtualNullsafeMethodCall' => true]); unset($attributes[ExprPrinter::ATTRIBUTE_CACHE_KEY]); @@ -78,8 +83,23 @@ public function processExpr(NodeScopeResolver $nodeScopeResolver, Stmt $stmt, Ex ); $scope = $this->nonNullabilityHelper->revertNonNullability($exprResult->getScope(), $nonNullabilityResult->getSpecifiedExpressions()); - return new ExpressionResult( + return $this->expressionResultFactory->create( + $expr, $scope, + typeCallback: static function (Expr $uninteresting, MutatingScope $scope) use ($varResult, $exprResult): Type { + $varType = $varResult->getTypeForScope($scope); + if ($varType->isNull()->yes()) { + return new NullType(); + } + if (!TypeCombinator::containsNull($varType)) { + return $exprResult->getTypeForScope($scope); + } + + return TypeCombinator::union( + $exprResult->getTypeForScope($scope), + new NullType(), + ); + }, hasYield: $exprResult->hasYield(), isAlwaysTerminating: false, throwPoints: $exprResult->getThrowPoints(), diff --git a/src/Analyser/ExprHandler/NullsafePropertyFetchHandler.php b/src/Analyser/ExprHandler/NullsafePropertyFetchHandler.php index f5732f72c4..3e1cac3e85 100644 --- a/src/Analyser/ExprHandler/NullsafePropertyFetchHandler.php +++ b/src/Analyser/ExprHandler/NullsafePropertyFetchHandler.php @@ -11,11 +11,13 @@ 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; use PHPStan\Analyser\MutatingScope; use PHPStan\Analyser\NodeScopeResolver; +use PHPStan\Analyser\NoopNodeCallback; use PHPStan\DependencyInjection\AutowiredService; use PHPStan\Node\Printer\ExprPrinter; use PHPStan\Type\NullType; @@ -31,6 +33,7 @@ final class NullsafePropertyFetchHandler implements ExprHandler { public function __construct( + private ExpressionResultFactory $expressionResultFactory, private NonNullabilityHelper $nonNullabilityHelper, ) { @@ -60,6 +63,8 @@ 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 { + $varResult = $nodeScopeResolver->processExprNode($stmt, $expr->var, $scope, new ExpressionResultStorage(), new NoopNodeCallback(), ExpressionContext::createDeep()); + $nonNullabilityResult = $this->nonNullabilityHelper->ensureShallowNonNullability($scope, $scope, $expr->var); $attributes = array_merge($expr->getAttributes(), ['virtualNullsafePropertyFetch' => true]); unset($attributes[ExprPrinter::ATTRIBUTE_CACHE_KEY]); @@ -70,8 +75,23 @@ 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( + $expr, $scope, + typeCallback: static function (Expr $uninteresting, MutatingScope $scope) use ($varResult, $exprResult): Type { + $varType = $varResult->getTypeForScope($scope); + if ($varType->isNull()->yes()) { + return new NullType(); + } + if (!TypeCombinator::containsNull($varType)) { + return $exprResult->getTypeForScope($scope); + } + + return TypeCombinator::union( + $exprResult->getTypeForScope($scope), + new NullType(), + ); + }, hasYield: $exprResult->hasYield(), isAlwaysTerminating: false, throwPoints: $exprResult->getThrowPoints(), diff --git a/src/Analyser/ExprHandler/PipeHandler.php b/src/Analyser/ExprHandler/PipeHandler.php index fb5c239a84..8dd21aa993 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; @@ -28,6 +29,12 @@ final class PipeHandler implements ExprHandler { + public function __construct( + private ExpressionResultFactory $expressionResultFactory, + ) + { + } + public function supports(Expr $expr): bool { return $expr instanceof Pipe; @@ -80,8 +87,10 @@ public function processExpr(NodeScopeResolver $nodeScopeResolver, Stmt $stmt, Ex $callResult = $nodeScopeResolver->processExprNode($stmt, $callExpr, $scope, $storage, $nodeCallback, $context); - return new ExpressionResult( + return $this->expressionResultFactory->create( + $expr, $callResult->getScope(), + typeCallback: static fn (Expr $uninteresting, MutatingScope $scope) => $callResult->getTypeForScope($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 3fab642bc4..84b4490fb1 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; @@ -22,6 +23,12 @@ final class PostDecHandler implements ExprHandler { + public function __construct( + private ExpressionResultFactory $expressionResultFactory, + ) + { + } + public function supports(Expr $expr): bool { return $expr instanceof PostDec; @@ -40,8 +47,10 @@ public function processExpr(NodeScopeResolver $nodeScopeResolver, Stmt $stmt, Ex $nodeCallback, )->getScope(); - return new ExpressionResult( + return $this->expressionResultFactory->create( + $expr, $scope, + typeCallback: static fn (Expr $uninteresting, MutatingScope $scope) => $varResult->getTypeForScope($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 beb2d6656f..fddb634f40 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; @@ -22,6 +23,12 @@ final class PostIncHandler implements ExprHandler { + public function __construct( + private ExpressionResultFactory $expressionResultFactory, + ) + { + } + public function supports(Expr $expr): bool { return $expr instanceof PostInc; @@ -40,8 +47,10 @@ public function processExpr(NodeScopeResolver $nodeScopeResolver, Stmt $stmt, Ex $nodeCallback, )->getScope(); - return new ExpressionResult( + return $this->expressionResultFactory->create( + $expr, $scope, + typeCallback: static fn (Expr $uninteresting, MutatingScope $scope) => $varResult->getTypeForScope($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 8a062f2c59..bf8e9e0446 100644 --- a/src/Analyser/ExprHandler/PreDecHandler.php +++ b/src/Analyser/ExprHandler/PreDecHandler.php @@ -9,10 +9,12 @@ 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; use PHPStan\Analyser\NodeScopeResolver; +use PHPStan\Analyser\NoopNodeCallback; use PHPStan\DependencyInjection\AutowiredService; use PHPStan\Type\Accessory\AccessoryLiteralStringType; use PHPStan\Type\BenevolentUnionType; @@ -36,6 +38,12 @@ final class PreDecHandler implements ExprHandler { + public function __construct( + private ExpressionResultFactory $expressionResultFactory, + ) + { + } + public function supports(Expr $expr): bool { return $expr instanceof PreDec; @@ -94,6 +102,8 @@ public function processExpr(NodeScopeResolver $nodeScopeResolver, Stmt $stmt, Ex { $varResult = $nodeScopeResolver->processExprNode($stmt, $expr->var, $scope, $storage, $nodeCallback, $context->enterDeep()); + $minusResult = $nodeScopeResolver->processExprNode($stmt, new Minus($expr->var, new Int_(1)), $varResult->getScope(), new ExpressionResultStorage(), new NoopNodeCallback(), ExpressionContext::createDeep()); + $scope = $nodeScopeResolver->processVirtualAssign( $varResult->getScope(), $storage, @@ -103,8 +113,57 @@ public function processExpr(NodeScopeResolver $nodeScopeResolver, Stmt $stmt, Ex $nodeCallback, )->getScope(); - return new ExpressionResult( + return $this->expressionResultFactory->create( + $expr, $scope, + typeCallback: static function (Expr $uninteresting, MutatingScope $scope) use ($varResult, $minusResult): Type { + $varType = $varResult->getTypeForScope($scope); + $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; + } + + $newTypes[] = $scope->getTypeFromValue($varValue); + } + return TypeCombinator::union(...$newTypes); + } + + if ($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(), + ]); + } + + return new BenevolentUnionType([ + new StringType(), + new IntegerType(), + new FloatType(), + ]); + } + + return $minusResult->getTypeForScope($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 7ab9c1eeac..e47599c59e 100644 --- a/src/Analyser/ExprHandler/PreIncHandler.php +++ b/src/Analyser/ExprHandler/PreIncHandler.php @@ -9,10 +9,12 @@ 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; use PHPStan\Analyser\NodeScopeResolver; +use PHPStan\Analyser\NoopNodeCallback; use PHPStan\DependencyInjection\AutowiredService; use PHPStan\Type\Accessory\AccessoryLiteralStringType; use PHPStan\Type\BenevolentUnionType; @@ -37,6 +39,12 @@ final class PreIncHandler implements ExprHandler { + public function __construct( + private ExpressionResultFactory $expressionResultFactory, + ) + { + } + public function supports(Expr $expr): bool { return $expr instanceof PreInc; @@ -95,6 +103,8 @@ public function processExpr(NodeScopeResolver $nodeScopeResolver, Stmt $stmt, Ex { $varResult = $nodeScopeResolver->processExprNode($stmt, $expr->var, $scope, $storage, $nodeCallback, $context->enterDeep()); + $plusResult = $nodeScopeResolver->processExprNode($stmt, new Plus($expr->var, new Int_(1)), $varResult->getScope(), new ExpressionResultStorage(), new NoopNodeCallback(), ExpressionContext::createDeep()); + $scope = $nodeScopeResolver->processVirtualAssign( $varResult->getScope(), $storage, @@ -104,8 +114,57 @@ public function processExpr(NodeScopeResolver $nodeScopeResolver, Stmt $stmt, Ex $nodeCallback, )->getScope(); - return new ExpressionResult( + return $this->expressionResultFactory->create( + $expr, $scope, + typeCallback: static function (Expr $uninteresting, MutatingScope $scope) use ($varResult, $plusResult): Type { + $varType = $varResult->getTypeForScope($scope); + $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; + } + + $newTypes[] = $scope->getTypeFromValue($varValue); + } + return TypeCombinator::union(...$newTypes); + } + + if ($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(), + ]); + } + + return new BenevolentUnionType([ + new StringType(), + new IntegerType(), + new FloatType(), + ]); + } + + return $plusResult->getTypeForScope($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 71f0e2d8c1..4c801a18a7 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\ImpurePoint; @@ -24,6 +25,12 @@ final class PrintHandler implements ExprHandler { + public function __construct( + private ExpressionResultFactory $expressionResultFactory, + ) + { + } + public function supports(Expr $expr): bool { return $expr instanceof Print_; @@ -39,8 +46,10 @@ 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( + $expr, $scope, + typeCallback: static fn () => new ConstantIntegerType(1), hasYield: $exprResult->hasYield(), isAlwaysTerminating: $exprResult->isAlwaysTerminating(), throwPoints: $exprResult->getThrowPoints(), diff --git a/src/Analyser/ExprHandler/PropertyFetchHandler.php b/src/Analyser/ExprHandler/PropertyFetchHandler.php index 52e2c7cd68..ae98ac4359 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; @@ -34,6 +35,7 @@ final class PropertyFetchHandler implements ExprHandler { public function __construct( + private ExpressionResultFactory $expressionResultFactory, private PhpVersion $phpVersion, private PropertyReflectionFinder $propertyReflectionFinder, ) @@ -77,8 +79,34 @@ public function processExpr(NodeScopeResolver $nodeScopeResolver, Stmt $stmt, Ex } } - return new ExpressionResult( + return $this->expressionResultFactory->create( + $expr, $scope, + typeCallback: function (Expr $expr, MutatingScope $scope) use ($varResult): Type { + if ($expr->name instanceof Identifier) { + $holderType = $varResult->getTypeForScope($scope); + + if ($scope->nativeTypesPromoted) { + $propertyReflection = $this->propertyReflectionFinder->findPropertyReflectionFromNodeWithTypes($expr, $scope, $holderType, null); + if ($propertyReflection === null) { + return new ErrorType(); + } + + if (!$propertyReflection->hasNativeType()) { + return new MixedType(); + } + + return $propertyReflection->getNativeType(); + } + + $returnType = $this->propertyFetchType($scope, $holderType, $expr->name->name, $expr); + + return $returnType ?? new ErrorType(); + } + + // TODO: handle dynamic property names + return new MixedType(); + }, hasYield: $hasYield, isAlwaysTerminating: $isAlwaysTerminating, throwPoints: $throwPoints, diff --git a/src/Analyser/ExprHandler/ScalarHandler.php b/src/Analyser/ExprHandler/ScalarHandler.php index f354a32e34..87752665ea 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; @@ -25,6 +26,7 @@ final class ScalarHandler implements ExprHandler { public function __construct( + private ExpressionResultFactory $expressionResultFactory, private InitializerExprTypeResolver $initializerExprTypeResolver, ) { @@ -37,8 +39,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 { - return new ExpressionResult( + return $this->expressionResultFactory->create( + $expr, $scope, + typeCallback: fn (Expr $expr, MutatingScope $scope) => $this->initializerExprTypeResolver->getType($expr, InitializerExprContext::fromScope($scope)), hasYield: false, isAlwaysTerminating: false, throwPoints: [], diff --git a/src/Analyser/ExprHandler/StaticCallHandler.php b/src/Analyser/ExprHandler/StaticCallHandler.php index 1438af86fa..157ccb87dd 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; @@ -30,6 +31,7 @@ use PHPStan\Reflection\Callables\SimpleImpurePoint; use PHPStan\Reflection\MethodReflection; use PHPStan\Reflection\ParametersAcceptor; +use PHPStan\Reflection\ReflectionProvider; use PHPStan\Reflection\ParametersAcceptorSelector; use PHPStan\Type\ErrorType; use PHPStan\Type\MixedType; @@ -56,16 +58,30 @@ final class StaticCallHandler implements ExprHandler { public function __construct( + private ExpressionResultFactory $expressionResultFactory, private DynamicThrowTypeExtensionProvider $dynamicThrowTypeExtensionProvider, private MethodCallReturnTypeHelper $methodCallReturnTypeHelper, #[AutowiredParameter(ref: '%exceptions.implicitThrows%')] private bool $implicitThrows, #[AutowiredParameter] private bool $rememberPossiblyImpureFunctionValues, + #[AutowiredParameter] + private array $earlyTerminatingMethodCalls, + private ReflectionProvider $reflectionProvider, ) { + $earlyTerminatingMethodNames = []; + foreach ($this->earlyTerminatingMethodCalls as $methodNames) { + foreach ($methodNames as $methodName) { + $earlyTerminatingMethodNames[strtolower($methodName)] = true; + } + } + $this->earlyTerminatingMethodNames = $earlyTerminatingMethodNames; } + /** @var array */ + private array $earlyTerminatingMethodNames; + public function supports(Expr $expr): bool { return $expr instanceof StaticCall && !$expr->isFirstClassCallable(); @@ -77,6 +93,7 @@ public function processExpr(NodeScopeResolver $nodeScopeResolver, Stmt $stmt, Ex $throwPoints = []; $impurePoints = []; $isAlwaysTerminating = false; + $classResult = null; if ($expr->class instanceof Expr) { $classResult = $nodeScopeResolver->processExprNode($stmt, $expr->class, $scope, $storage, $nodeCallback, $context->enterDeep()); $hasYield = $classResult->hasYield(); @@ -199,6 +216,28 @@ public function processExpr(NodeScopeResolver $nodeScopeResolver, Stmt $stmt, Ex $returnType = $parametersAcceptor->getReturnType(); $isAlwaysTerminating = $returnType instanceof NeverType && $returnType->isExplicit(); } + if (!$isAlwaysTerminating && $expr->name instanceof Identifier && array_key_exists($expr->name->toLowerString(), $this->earlyTerminatingMethodNames)) { + $staticCalledOnType = $expr->class instanceof Name ? $scope->resolveTypeByName($expr->class) : ($classResult !== null ? $classResult->getType() : null); + if ($staticCalledOnType !== null) { + foreach ($staticCalledOnType->getObjectClassNames() as $referencedClass) { + if (!$this->reflectionProvider->hasClass($referencedClass)) { + continue; + } + + $classReflection = $this->reflectionProvider->getClass($referencedClass); + foreach (array_merge([$referencedClass], $classReflection->getParentClassesNames(), $classReflection->getNativeReflection()->getInterfaceNames()) as $className) { + if (!isset($this->earlyTerminatingMethodCalls[$className])) { + continue; + } + + if (in_array($expr->name->name, $this->earlyTerminatingMethodCalls[$className], true)) { + $isAlwaysTerminating = true; + break 2; + } + } + } + } + } $argsResult = $nodeScopeResolver->processArgs($stmt, $methodReflection, null, $parametersAcceptor, $normalizedExpr, $scope, $storage, $nodeCallback, $context, $closureBindScope); $scope = $argsResult->getScope(); $scopeFunction = $scope->getFunction(); @@ -257,8 +296,48 @@ 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( + $expr, $scope, + typeCallback: function (Expr $expr, MutatingScope $scope) use ($classResult): Type { + if ($expr->name instanceof Identifier) { + if ($expr->class instanceof Name) { + $staticMethodCalledOnType = $this->resolveTypeByNameWithLateStaticBinding($scope, $expr->class, $expr->name); + } elseif ($classResult !== null) { + if ($scope->nativeTypesPromoted) { + $staticMethodCalledOnType = $classResult->getTypeForScope($scope); + } else { + $staticMethodCalledOnType = TypeCombinator::removeNull($classResult->getTypeForScope($scope))->getObjectTypeOrClassStringObjectType(); + } + } else { + return new ErrorType(); + } + + if ($scope->nativeTypesPromoted) { + $methodReflection = $scope->getMethodReflection( + $staticMethodCalledOnType, + $expr->name->name, + ); + if ($methodReflection === null) { + return new ErrorType(); + } + + return ParametersAcceptorSelector::combineAcceptors($methodReflection->getVariants())->getNativeReturnType(); + } + + $callType = $this->methodCallReturnTypeHelper->methodCallReturnType( + $scope, + $staticMethodCalledOnType, + $expr->name->toString(), + $expr, + ); + + return $callType ?? new ErrorType(); + } + + // TODO: handle dynamic method names + return new MixedType(); + }, hasYield: $hasYield, isAlwaysTerminating: $isAlwaysTerminating, throwPoints: $throwPoints, @@ -303,6 +382,26 @@ private function getStaticMethodThrowPoint(MethodReflection $methodReflection, P public function resolveType(MutatingScope $scope, Expr $expr): Type { + if ($expr->name instanceof Identifier && array_key_exists($expr->name->toLowerString(), $this->earlyTerminatingMethodNames)) { + $staticCalledOnType = $expr->class instanceof Name ? $scope->resolveTypeByName($expr->class) : $scope->getType($expr->class); + foreach ($staticCalledOnType->getObjectClassNames() as $referencedClass) { + if (!$this->reflectionProvider->hasClass($referencedClass)) { + continue; + } + + $classReflection = $this->reflectionProvider->getClass($referencedClass); + foreach (array_merge([$referencedClass], $classReflection->getParentClassesNames(), $classReflection->getNativeReflection()->getInterfaceNames()) as $className) { + if (!isset($this->earlyTerminatingMethodCalls[$className])) { + continue; + } + + if (in_array($expr->name->name, $this->earlyTerminatingMethodCalls[$className], true)) { + return new NeverType(true); + } + } + } + } + if ($expr->name instanceof Identifier) { if ($scope->nativeTypesPromoted) { if ($expr->class instanceof Name) { diff --git a/src/Analyser/ExprHandler/StaticPropertyFetchHandler.php b/src/Analyser/ExprHandler/StaticPropertyFetchHandler.php index a22cfb6e89..e006c821d4 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; @@ -35,6 +36,7 @@ final class StaticPropertyFetchHandler implements ExprHandler { public function __construct( + private ExpressionResultFactory $expressionResultFactory, private PropertyReflectionFinder $propertyReflectionFinder, ) { @@ -59,6 +61,7 @@ public function processExpr(NodeScopeResolver $nodeScopeResolver, Stmt $stmt, Ex ), ]; $isAlwaysTerminating = false; + $classResult = null; if ($expr->class instanceof Expr) { $classResult = $nodeScopeResolver->processExprNode($stmt, $expr->class, $scope, $storage, $nodeCallback, $context->enterDeep()); $hasYield = $classResult->hasYield(); @@ -76,8 +79,37 @@ public function processExpr(NodeScopeResolver $nodeScopeResolver, Stmt $stmt, Ex $scope = $nameResult->getScope(); } - return new ExpressionResult( + return $this->expressionResultFactory->create( + $expr, $scope, + typeCallback: function (Expr $expr, MutatingScope $scope) use ($classResult): Type { + if ($expr->name instanceof VarLikeIdentifier) { + if ($expr->class instanceof Name) { + $holderType = $scope->resolveTypeByName($expr->class); + } else { + $holderType = TypeCombinator::removeNull($classResult->getTypeForScope($scope))->getObjectTypeOrClassStringObjectType(); + } + + if ($scope->nativeTypesPromoted) { + $propertyReflection = $this->propertyReflectionFinder->findPropertyReflectionFromNodeWithTypes($expr, $scope, $holderType, null); + if ($propertyReflection === null) { + return new ErrorType(); + } + if (!$propertyReflection->hasNativeType()) { + return new MixedType(); + } + + return $propertyReflection->getNativeType(); + } + + $fetchType = $this->propertyFetchType($scope, $holderType, $expr->name->toString(), $expr); + + return $fetchType ?? new ErrorType(); + } + + // TODO: handle dynamic property names + return new MixedType(); + }, hasYield: $hasYield, isAlwaysTerminating: $isAlwaysTerminating, throwPoints: $throwPoints, diff --git a/src/Analyser/ExprHandler/TernaryHandler.php b/src/Analyser/ExprHandler/TernaryHandler.php index 81b58b16c8..14c0fa8ba1 100644 --- a/src/Analyser/ExprHandler/TernaryHandler.php +++ b/src/Analyser/ExprHandler/TernaryHandler.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; @@ -26,6 +27,7 @@ final class TernaryHandler implements ExprHandler { public function __construct( + private ExpressionResultFactory $expressionResultFactory, private NodeScopeResolver $nodeScopeResolver, ) { @@ -78,19 +80,17 @@ public function processExpr(NodeScopeResolver $nodeScopeResolver, Stmt $stmt, Ex $impurePoints = $ternaryCondResult->getImpurePoints(); $ifTrueScope = $ternaryCondResult->getTruthyScope(); $ifFalseScope = $ternaryCondResult->getFalseyScope(); - $ifTrueType = null; - if ($expr->if === null) { $elseResult = $nodeScopeResolver->processExprNode($stmt, $expr->else, $ifFalseScope, $storage, $nodeCallback, $context); $throwPoints = array_merge($throwPoints, $elseResult->getThrowPoints()); $impurePoints = array_merge($impurePoints, $elseResult->getImpurePoints()); $ifFalseScope = $elseResult->getScope(); + $ifResult = null; } else { $ifResult = $nodeScopeResolver->processExprNode($stmt, $expr->if, $ifTrueScope, $storage, $nodeCallback, $context); $throwPoints = array_merge($throwPoints, $ifResult->getThrowPoints()); $impurePoints = array_merge($impurePoints, $ifResult->getImpurePoints()); $ifTrueScope = $ifResult->getScope(); - $ifTrueType = $ifTrueScope->getType($expr->if); $elseResult = $nodeScopeResolver->processExprNode($stmt, $expr->else, $ifFalseScope, $storage, $nodeCallback, $context); $throwPoints = array_merge($throwPoints, $elseResult->getThrowPoints()); @@ -98,27 +98,55 @@ public function processExpr(NodeScopeResolver $nodeScopeResolver, Stmt $stmt, Ex $ifFalseScope = $elseResult->getScope(); } - $condType = $scope->getType($expr->cond); - if ($condType->isTrue()->yes()) { + $ifTrueType = $ifResult !== null ? $ifResult->getType() : null; + $ifFalseType = $elseResult->getType(); + + if ($ternaryCondResult->getType()->toBoolean()->isTrue()->yes()) { $finalScope = $ifTrueScope; - } elseif ($condType->isFalse()->yes()) { + } elseif ($ternaryCondResult->getType()->toBoolean()->isFalse()->yes()) { $finalScope = $ifFalseScope; } else { if ($ifTrueType instanceof NeverType && $ifTrueType->isExplicit()) { $finalScope = $ifFalseScope; + } elseif ($ifFalseType instanceof NeverType && $ifFalseType->isExplicit()) { + $finalScope = $ifTrueScope; } else { - $ifFalseType = $ifFalseScope->getType($expr->else); - - if ($ifFalseType instanceof NeverType && $ifFalseType->isExplicit()) { - $finalScope = $ifTrueScope; - } else { - $finalScope = $ifTrueScope->mergeWith($ifFalseScope); - } + $finalScope = $ifTrueScope->mergeWith($ifFalseScope); } } - return new ExpressionResult( + return $this->expressionResultFactory->create( + $expr, $finalScope, + typeCallback: static function (Expr $uninteresting, MutatingScope $scope) use ($expr, $ternaryCondResult, $ifResult, $elseResult): Type { + $booleanCondType = $ternaryCondResult->getTypeForScope($scope)->toBoolean(); + + if ($expr->if === null) { + if ($booleanCondType->isTrue()->yes()) { + return $ternaryCondResult->getTypeForScope($scope); + } + if ($booleanCondType->isFalse()->yes()) { + return $elseResult->getTypeForScope($scope); + } + + return TypeCombinator::union( + TypeCombinator::removeFalsey($ternaryCondResult->getTypeForScope($scope)), + $elseResult->getTypeForScope($scope), + ); + } + + if ($booleanCondType->isTrue()->yes()) { + return $ifResult->getTypeForScope($scope); + } + if ($booleanCondType->isFalse()->yes()) { + return $elseResult->getTypeForScope($scope); + } + + return TypeCombinator::union( + $ifResult->getTypeForScope($scope), + $elseResult->getTypeForScope($scope), + ); + }, hasYield: $ternaryCondResult->hasYield(), isAlwaysTerminating: $ternaryCondResult->isAlwaysTerminating(), throwPoints: $throwPoints, diff --git a/src/Analyser/ExprHandler/ThrowHandler.php b/src/Analyser/ExprHandler/ThrowHandler.php index 7a51007317..d250391cb7 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; @@ -24,6 +25,12 @@ final class ThrowHandler implements ExprHandler { + public function __construct( + private ExpressionResultFactory $expressionResultFactory, + ) + { + } + public function supports(Expr $expr): bool { return $expr instanceof Throw_; @@ -33,11 +40,13 @@ public function processExpr(NodeScopeResolver $nodeScopeResolver, Stmt $stmt, Ex { $exprResult = $nodeScopeResolver->processExprNode($stmt, $expr->expr, $scope, $storage, $nodeCallback, ExpressionContext::createDeep()); - return new ExpressionResult( + return $this->expressionResultFactory->create( + $expr, $scope, + typeCallback: static fn () => new NonAcceptingNeverType(), hasYield: false, isAlwaysTerminating: $exprResult->isAlwaysTerminating(), - throwPoints: array_merge($exprResult->getThrowPoints(), [InternalThrowPoint::createExplicit($scope, $scope->getType($expr->expr), $expr, false)]), + throwPoints: array_merge($exprResult->getThrowPoints(), [InternalThrowPoint::createExplicit($scope, $exprResult->getType(), $expr, false)]), impurePoints: $exprResult->getImpurePoints(), ); } diff --git a/src/Analyser/ExprHandler/UnaryMinusHandler.php b/src/Analyser/ExprHandler/UnaryMinusHandler.php index 077652da19..af7ba0beea 100644 --- a/src/Analyser/ExprHandler/UnaryMinusHandler.php +++ b/src/Analyser/ExprHandler/UnaryMinusHandler.php @@ -7,10 +7,12 @@ 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; use PHPStan\Analyser\NodeScopeResolver; +use PHPStan\Analyser\NoopNodeCallback; use PHPStan\DependencyInjection\AutowiredService; use PHPStan\Reflection\InitializerExprTypeResolver; use PHPStan\Type\Type; @@ -23,6 +25,7 @@ final class UnaryMinusHandler implements ExprHandler { public function __construct( + private ExpressionResultFactory $expressionResultFactory, private InitializerExprTypeResolver $initializerExprTypeResolver, ) { @@ -37,8 +40,16 @@ 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( + $expr, $exprResult->getScope(), + typeCallback: fn (Expr $uninteresting, MutatingScope $scope) => $this->initializerExprTypeResolver->getUnaryMinusType($expr->expr, static function (Expr $e) use ($expr, $exprResult, $nodeScopeResolver, $stmt, $scope): Type { + if ($e === $expr->expr) { + return $exprResult->getTypeForScope($scope); + } + + return $nodeScopeResolver->processExprNode($stmt, $e, $scope, new ExpressionResultStorage(), new NoopNodeCallback(), ExpressionContext::createDeep())->getTypeForScope($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 501fc6e095..0c78e0f0d6 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; @@ -21,6 +22,12 @@ final class UnaryPlusHandler implements ExprHandler { + public function __construct( + private ExpressionResultFactory $expressionResultFactory, + ) + { + } + public function supports(Expr $expr): bool { return $expr instanceof UnaryPlus; @@ -30,8 +37,10 @@ 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( + $expr, $exprResult->getScope(), + typeCallback: static fn (Expr $uninteresting, MutatingScope $scope) => $exprResult->getTypeForScope($scope)->toNumber(), hasYield: $exprResult->hasYield(), isAlwaysTerminating: $exprResult->isAlwaysTerminating(), throwPoints: $exprResult->getThrowPoints(), diff --git a/src/Analyser/ExprHandler/VariableHandler.php b/src/Analyser/ExprHandler/VariableHandler.php index 2ce7a670fc..4980d53455 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; @@ -31,6 +32,12 @@ final class VariableHandler implements ExprHandler { + public function __construct( + private ExpressionResultFactory $expressionResultFactory, + ) + { + } + public function supports(Expr $expr): bool { return $expr instanceof Variable; @@ -74,6 +81,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); @@ -86,14 +94,28 @@ public function processExpr(NodeScopeResolver $nodeScopeResolver, Stmt $stmt, Ex $isAlwaysTerminating = $nameResult->isAlwaysTerminating(); $scope = $nameResult->getScope(); } - return new ExpressionResult( + return $this->expressionResultFactory->create( + $expr, $scope, - $hasYield, - $isAlwaysTerminating, - $throwPoints, - $impurePoints, - static fn (): MutatingScope => $scope->filterByTruthyValue($expr), - static fn (): MutatingScope => $scope->filterByFalseyValue($expr), + typeCallback: static function (Expr $expr, MutatingScope $scope): Type { + if (is_string($expr->name)) { + if ($scope->hasVariableType($expr->name)->no()) { + return new ErrorType(); + } + + return $scope->getVariableType($expr->name); + } + + // TODO: handle dynamic variable names with constant strings + // needs a different approach for filterByTruthyValue + return new MixedType(); + }, + 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/Virtual/AlwaysRememberedExprHandler.php b/src/Analyser/ExprHandler/Virtual/AlwaysRememberedExprHandler.php index 65dcbbc50f..4311782142 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; @@ -21,6 +22,12 @@ final class AlwaysRememberedExprHandler implements ExprHandler { + public function __construct( + private ExpressionResultFactory $expressionResultFactory, + ) + { + } + public function supports(Expr $expr): bool { return $expr instanceof AlwaysRememberedExpr; @@ -40,8 +47,10 @@ public function processExpr( $innerResult = $nodeScopeResolver->processExprNode($stmt, $innerExpr, $scope, $storage, $nodeCallback, $context); $scope = $innerResult->getScope(); - return new ExpressionResult( + return $this->expressionResultFactory->create( + $expr, $scope, + typeCallback: static fn (Expr $uninteresting, MutatingScope $scope) => $scope->nativeTypesPromoted ? $expr->getNativeExprType() : $expr->getExprType(), 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 2b7dc4340d..030dbabc32 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; @@ -21,6 +22,12 @@ final class ExistingArrayDimFetchHandler implements ExprHandler { + public function __construct( + private ExpressionResultFactory $expressionResultFactory, + ) + { + } + public function supports(Expr $expr): bool { return $expr instanceof ExistingArrayDimFetch; @@ -28,11 +35,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 { - // 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 + $varResult = $nodeScopeResolver->processExprNode($stmt, $expr->getVar(), $scope, $storage, $nodeCallback, $context->enterDeep()); + $dimResult = $nodeScopeResolver->processExprNode($stmt, $expr->getDim(), $varResult->getScope(), $storage, $nodeCallback, $context->enterDeep()); - return new ExpressionResult( + return $this->expressionResultFactory->create( + $expr, $scope, + typeCallback: static fn (Expr $uninteresting, MutatingScope $scope) => $varResult->getTypeForScope($scope)->getOffsetValueType($dimResult->getTypeForScope($scope)), hasYield: false, isAlwaysTerminating: false, throwPoints: [], diff --git a/src/Analyser/ExprHandler/Virtual/FunctionCallableNodeHandler.php b/src/Analyser/ExprHandler/Virtual/FunctionCallableNodeHandler.php index d609d8cfb3..f06be0a70d 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; @@ -22,6 +23,12 @@ final class FunctionCallableNodeHandler implements ExprHandler { + public function __construct( + private ExpressionResultFactory $expressionResultFactory, + ) + { + } + public function supports(Expr $expr): bool { return $expr instanceof FunctionCallableNode; @@ -42,8 +49,10 @@ public function processExpr(NodeScopeResolver $nodeScopeResolver, Stmt $stmt, Ex $isAlwaysTerminating = $nameResult->isAlwaysTerminating(); } - return new ExpressionResult( + return $this->expressionResultFactory->create( + $expr, $scope, + typeCallback: static fn () => new MixedType(), hasYield: $hasYield, isAlwaysTerminating: $isAlwaysTerminating, throwPoints: $throwPoints, diff --git a/src/Analyser/ExprHandler/Virtual/GetIterableKeyTypeExprHandler.php b/src/Analyser/ExprHandler/Virtual/GetIterableKeyTypeExprHandler.php index a0f602a0d1..14d1333d7b 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; @@ -21,6 +22,12 @@ final class GetIterableKeyTypeExprHandler implements ExprHandler { + public function __construct( + private ExpressionResultFactory $expressionResultFactory, + ) + { + } + public function supports(Expr $expr): bool { return $expr instanceof GetIterableKeyTypeExpr; @@ -28,11 +35,12 @@ 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 + $innerResult = $nodeScopeResolver->processExprNode($stmt, $expr->getExpr(), $scope, $storage, $nodeCallback, $context->enterDeep()); - return new ExpressionResult( + return $this->expressionResultFactory->create( + $expr, $scope, + typeCallback: static fn (Expr $uninteresting, MutatingScope $scope) => $scope->getIterableKeyType($innerResult->getTypeForScope($scope)), hasYield: false, isAlwaysTerminating: false, throwPoints: [], diff --git a/src/Analyser/ExprHandler/Virtual/GetIterableValueTypeExprHandler.php b/src/Analyser/ExprHandler/Virtual/GetIterableValueTypeExprHandler.php index 9d1e2e6dcf..466b975b95 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; @@ -21,6 +22,12 @@ final class GetIterableValueTypeExprHandler implements ExprHandler { + public function __construct( + private ExpressionResultFactory $expressionResultFactory, + ) + { + } + public function supports(Expr $expr): bool { return $expr instanceof GetIterableValueTypeExpr; @@ -28,11 +35,12 @@ 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 + $innerResult = $nodeScopeResolver->processExprNode($stmt, $expr->getExpr(), $scope, $storage, $nodeCallback, $context->enterDeep()); - return new ExpressionResult( + return $this->expressionResultFactory->create( + $expr, $scope, + typeCallback: static fn (Expr $uninteresting, MutatingScope $scope) => $scope->getIterableValueType($innerResult->getTypeForScope($scope)), hasYield: false, isAlwaysTerminating: false, throwPoints: [], diff --git a/src/Analyser/ExprHandler/Virtual/GetOffsetValueTypeExprHandler.php b/src/Analyser/ExprHandler/Virtual/GetOffsetValueTypeExprHandler.php index dfcc91a501..26266c0d5e 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; @@ -21,6 +22,12 @@ final class GetOffsetValueTypeExprHandler implements ExprHandler { + public function __construct( + private ExpressionResultFactory $expressionResultFactory, + ) + { + } + public function supports(Expr $expr): bool { return $expr instanceof GetOffsetValueTypeExpr; @@ -28,11 +35,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 { - // 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 + $varResult = $nodeScopeResolver->processExprNode($stmt, $expr->getVar(), $scope, $storage, $nodeCallback, $context->enterDeep()); + $dimResult = $nodeScopeResolver->processExprNode($stmt, $expr->getDim(), $varResult->getScope(), $storage, $nodeCallback, $context->enterDeep()); - return new ExpressionResult( + return $this->expressionResultFactory->create( + $expr, $scope, + typeCallback: static fn (Expr $uninteresting, MutatingScope $scope) => $varResult->getTypeForScope($scope)->getOffsetValueType($dimResult->getTypeForScope($scope)), hasYield: false, isAlwaysTerminating: false, throwPoints: [], diff --git a/src/Analyser/ExprHandler/Virtual/InstantiationCallableNodeHandler.php b/src/Analyser/ExprHandler/Virtual/InstantiationCallableNodeHandler.php index 4e5ac24f80..c6e87e5571 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; @@ -22,6 +23,12 @@ final class InstantiationCallableNodeHandler implements ExprHandler { + public function __construct( + private ExpressionResultFactory $expressionResultFactory, + ) + { + } + public function supports(Expr $expr): bool { return $expr instanceof InstantiationCallableNode; @@ -42,8 +49,10 @@ public function processExpr(NodeScopeResolver $nodeScopeResolver, Stmt $stmt, Ex $isAlwaysTerminating = $classResult->isAlwaysTerminating(); } - return new ExpressionResult( + return $this->expressionResultFactory->create( + $expr, $scope, + typeCallback: static fn () => new MixedType(), hasYield: $hasYield, isAlwaysTerminating: $isAlwaysTerminating, throwPoints: $throwPoints, diff --git a/src/Analyser/ExprHandler/Virtual/MethodCallableNodeHandler.php b/src/Analyser/ExprHandler/Virtual/MethodCallableNodeHandler.php index cabf8f72a8..2049693c04 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; @@ -23,6 +24,12 @@ final class MethodCallableNodeHandler implements ExprHandler { + public function __construct( + private ExpressionResultFactory $expressionResultFactory, + ) + { + } + public function supports(Expr $expr): bool { return $expr instanceof MethodCallableNode; @@ -45,8 +52,10 @@ public function processExpr(NodeScopeResolver $nodeScopeResolver, Stmt $stmt, Ex $isAlwaysTerminating = $nameResult->isAlwaysTerminating(); } - return new ExpressionResult( + return $this->expressionResultFactory->create( + $expr, $scope, + typeCallback: static fn () => new MixedType(), hasYield: $hasYield, isAlwaysTerminating: $isAlwaysTerminating, throwPoints: $throwPoints, diff --git a/src/Analyser/ExprHandler/Virtual/NativeTypeExprHandler.php b/src/Analyser/ExprHandler/Virtual/NativeTypeExprHandler.php index 39a0572b65..513d748f42 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; @@ -21,6 +22,12 @@ final class NativeTypeExprHandler implements ExprHandler { + public function __construct( + private ExpressionResultFactory $expressionResultFactory, + ) + { + } + public function supports(Expr $expr): bool { return $expr instanceof NativeTypeExpr; @@ -31,8 +38,10 @@ 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( + $expr, $scope, + typeCallback: static fn (Expr $uninteresting, MutatingScope $scope) => $scope->nativeTypesPromoted ? $expr->getNativeType() : $expr->getPhpDocType(), hasYield: false, isAlwaysTerminating: false, throwPoints: [], diff --git a/src/Analyser/ExprHandler/Virtual/OriginalPropertyTypeExprHandler.php b/src/Analyser/ExprHandler/Virtual/OriginalPropertyTypeExprHandler.php index 909f59a588..c23c6fa266 100644 --- a/src/Analyser/ExprHandler/Virtual/OriginalPropertyTypeExprHandler.php +++ b/src/Analyser/ExprHandler/Virtual/OriginalPropertyTypeExprHandler.php @@ -3,9 +3,11 @@ namespace PHPStan\Analyser\ExprHandler\Virtual; use PhpParser\Node\Expr; +use PhpParser\Node\Name; 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; @@ -24,6 +26,7 @@ final class OriginalPropertyTypeExprHandler implements ExprHandler { public function __construct( + private ExpressionResultFactory $expressionResultFactory, private PropertyReflectionFinder $propertyReflectionFinder, ) { @@ -36,11 +39,34 @@ 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 + $propertyFetch = $expr->getPropertyFetch(); + if ($propertyFetch instanceof Expr\PropertyFetch) { + $holderResult = $nodeScopeResolver->processExprNode($stmt, $propertyFetch->var, $scope, $storage, $nodeCallback, $context->enterDeep()); + } else { + $holderResult = $propertyFetch->class instanceof Expr + ? $nodeScopeResolver->processExprNode($stmt, $propertyFetch->class, $scope, $storage, $nodeCallback, $context->enterDeep()) + : null; + } - return new ExpressionResult( + return $this->expressionResultFactory->create( + $expr, $scope, + typeCallback: function (Expr $expr, MutatingScope $scope) use ($propertyFetch, $holderResult): Type { + if ($holderResult !== null) { + $holderType = $holderResult->getTypeForScope($scope); + } elseif ($propertyFetch instanceof Expr\StaticPropertyFetch && $propertyFetch->class instanceof Name) { + $holderType = $scope->resolveTypeByName($propertyFetch->class); + } else { + return new ErrorType(); + } + + $propertyReflection = $this->propertyReflectionFinder->findPropertyReflectionFromNodeWithTypes($propertyFetch, $scope, $holderType, null); + if ($propertyReflection === null) { + return new ErrorType(); + } + + return $propertyReflection->getReadableType(); + }, hasYield: false, isAlwaysTerminating: false, throwPoints: [], diff --git a/src/Analyser/ExprHandler/Virtual/SetExistingOffsetValueTypeExprHandler.php b/src/Analyser/ExprHandler/Virtual/SetExistingOffsetValueTypeExprHandler.php index cbc691f1d9..13cee9bd4e 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; @@ -23,6 +24,12 @@ final class SetExistingOffsetValueTypeExprHandler implements ExprHandler { + public function __construct( + private ExpressionResultFactory $expressionResultFactory, + ) + { + } + public function supports(Expr $expr): bool { return $expr instanceof SetExistingOffsetValueTypeExpr; @@ -30,11 +37,31 @@ 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 + $varResult = $nodeScopeResolver->processExprNode($stmt, $expr->getVar(), $scope, $storage, $nodeCallback, $context->enterDeep()); + $dimResult = $nodeScopeResolver->processExprNode($stmt, $expr->getDim(), $varResult->getScope(), $storage, $nodeCallback, $context->enterDeep()); + $valueResult = $nodeScopeResolver->processExprNode($stmt, $expr->getValue(), $dimResult->getScope(), $storage, $nodeCallback, $context->enterDeep()); - return new ExpressionResult( + $propertyFetchResult = $expr->getVar() instanceof OriginalPropertyTypeExpr + ? $nodeScopeResolver->processExprNode($stmt, $expr->getVar()->getPropertyFetch(), $scope, $storage, $nodeCallback, $context->enterDeep()) + : null; + + return $this->expressionResultFactory->create( + $expr, $scope, + typeCallback: static function (Expr $uninteresting, MutatingScope $scope) use ($varResult, $dimResult, $valueResult, $propertyFetchResult): Type { + $varType = $varResult->getTypeForScope($scope); + if ($propertyFetchResult !== null) { + $currentPropertyType = $propertyFetchResult->getTypeForScope($scope); + if ($varType instanceof UnionType) { + $varType = $varType->filterTypes(static fn (Type $innerType) => !$innerType->isSuperTypeOf($currentPropertyType)->no()); + } + } + + return $varType->setExistingOffsetValueType( + $dimResult->getTypeForScope($scope), + $valueResult->getTypeForScope($scope), + ); + }, hasYield: false, isAlwaysTerminating: false, throwPoints: [], diff --git a/src/Analyser/ExprHandler/Virtual/SetOffsetValueTypeExprHandler.php b/src/Analyser/ExprHandler/Virtual/SetOffsetValueTypeExprHandler.php index 4bf97ae13a..6319ab5058 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; @@ -23,6 +24,12 @@ final class SetOffsetValueTypeExprHandler implements ExprHandler { + public function __construct( + private ExpressionResultFactory $expressionResultFactory, + ) + { + } + public function supports(Expr $expr): bool { return $expr instanceof SetOffsetValueTypeExpr; @@ -30,11 +37,31 @@ 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 + $varResult = $nodeScopeResolver->processExprNode($stmt, $expr->getVar(), $scope, $storage, $nodeCallback, $context->enterDeep()); + $dimResult = $expr->getDim() !== null ? $nodeScopeResolver->processExprNode($stmt, $expr->getDim(), $varResult->getScope(), $storage, $nodeCallback, $context->enterDeep()) : null; + $valueResult = $nodeScopeResolver->processExprNode($stmt, $expr->getValue(), ($dimResult ?? $varResult)->getScope(), $storage, $nodeCallback, $context->enterDeep()); - return new ExpressionResult( + $propertyFetchResult = $expr->getVar() instanceof OriginalPropertyTypeExpr + ? $nodeScopeResolver->processExprNode($stmt, $expr->getVar()->getPropertyFetch(), $scope, $storage, $nodeCallback, $context->enterDeep()) + : null; + + return $this->expressionResultFactory->create( + $expr, $scope, + typeCallback: static function (Expr $uninteresting, MutatingScope $scope) use ($varResult, $dimResult, $valueResult, $propertyFetchResult): Type { + $varType = $varResult->getTypeForScope($scope); + if ($propertyFetchResult !== null) { + $currentPropertyType = $propertyFetchResult->getTypeForScope($scope); + if ($varType instanceof UnionType) { + $varType = $varType->filterTypes(static fn (Type $innerType) => !$innerType->isSuperTypeOf($currentPropertyType)->no()); + } + } + + return $varType->setOffsetValueType( + $dimResult !== null ? $dimResult->getTypeForScope($scope) : null, + $valueResult->getTypeForScope($scope), + ); + }, hasYield: false, isAlwaysTerminating: false, throwPoints: [], diff --git a/src/Analyser/ExprHandler/Virtual/StaticMethodCallableNodeHandler.php b/src/Analyser/ExprHandler/Virtual/StaticMethodCallableNodeHandler.php index f70872144b..053f79c03d 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; @@ -23,6 +24,12 @@ final class StaticMethodCallableNodeHandler implements ExprHandler { + public function __construct( + private ExpressionResultFactory $expressionResultFactory, + ) + { + } + public function supports(Expr $expr): bool { return $expr instanceof StaticMethodCallableNode; @@ -51,8 +58,10 @@ public function processExpr(NodeScopeResolver $nodeScopeResolver, Stmt $stmt, Ex $isAlwaysTerminating = $isAlwaysTerminating || $nameResult->isAlwaysTerminating(); } - return new ExpressionResult( + return $this->expressionResultFactory->create( + $expr, $scope, + typeCallback: static fn () => new MixedType(), hasYield: $hasYield, isAlwaysTerminating: $isAlwaysTerminating, throwPoints: $throwPoints, diff --git a/src/Analyser/ExprHandler/Virtual/TypeExprHandler.php b/src/Analyser/ExprHandler/Virtual/TypeExprHandler.php index 316753bb38..1fffc8a08c 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; @@ -21,6 +22,12 @@ final class TypeExprHandler implements ExprHandler { + public function __construct( + private ExpressionResultFactory $expressionResultFactory, + ) + { + } + public function supports(Expr $expr): bool { return $expr instanceof TypeExpr; @@ -31,8 +38,10 @@ 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( + $expr, $scope, + typeCallback: static fn () => $expr->getExprType(), hasYield: false, isAlwaysTerminating: false, throwPoints: [], diff --git a/src/Analyser/ExprHandler/Virtual/UnsetOffsetExprHandler.php b/src/Analyser/ExprHandler/Virtual/UnsetOffsetExprHandler.php index 1406ae5633..e3ab407450 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; @@ -21,6 +22,12 @@ final class UnsetOffsetExprHandler implements ExprHandler { + public function __construct( + private ExpressionResultFactory $expressionResultFactory, + ) + { + } + public function supports(Expr $expr): bool { return $expr instanceof UnsetOffsetExpr; @@ -28,11 +35,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 { - // 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 + $varResult = $nodeScopeResolver->processExprNode($stmt, $expr->getVar(), $scope, $storage, $nodeCallback, $context->enterDeep()); + $dimResult = $nodeScopeResolver->processExprNode($stmt, $expr->getDim(), $varResult->getScope(), $storage, $nodeCallback, $context->enterDeep()); - return new ExpressionResult( + return $this->expressionResultFactory->create( + $expr, $scope, + typeCallback: static fn (Expr $uninteresting, MutatingScope $scope) => $varResult->getTypeForScope($scope)->unsetOffset($dimResult->getTypeForScope($scope)), hasYield: false, isAlwaysTerminating: false, throwPoints: [], diff --git a/src/Analyser/ExprHandler/YieldFromHandler.php b/src/Analyser/ExprHandler/YieldFromHandler.php index 3669cc6ea5..16e3caf013 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; @@ -27,6 +28,12 @@ final class YieldFromHandler implements ExprHandler { + public function __construct( + private ExpressionResultFactory $expressionResultFactory, + ) + { + } + public function supports(Expr $expr): bool { return $expr instanceof YieldFrom; @@ -48,8 +55,17 @@ 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( + $expr, $scope, + typeCallback: static function (Expr $uninteresting, MutatingScope $scope) use ($exprResult): Type { + $generatorReturnType = $exprResult->getTypeForScope($scope)->getTemplateType(Generator::class, 'TReturn'); + if ($generatorReturnType instanceof ErrorType) { + return new MixedType(); + } + + return $generatorReturnType; + }, 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 33dd84e06f..6f66412142 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; @@ -27,6 +28,12 @@ final class YieldHandler implements ExprHandler { + public function __construct( + private ExpressionResultFactory $expressionResultFactory, + ) + { + } + public function supports(Expr $expr): bool { return $expr instanceof Yield_; @@ -78,8 +85,23 @@ public function processExpr(NodeScopeResolver $nodeScopeResolver, Stmt $stmt, Ex $isAlwaysTerminating = $isAlwaysTerminating || $valueResult->isAlwaysTerminating(); } - return new ExpressionResult( + return $this->expressionResultFactory->create( + $expr, $scope, + typeCallback: static function (Expr $uninteresting, MutatingScope $scope): 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; + }, hasYield: true, isAlwaysTerminating: $isAlwaysTerminating, throwPoints: $throwPoints, diff --git a/src/Analyser/ExpressionResult.php b/src/Analyser/ExpressionResult.php index 746c518953..7710858f5b 100644 --- a/src/Analyser/ExpressionResult.php +++ b/src/Analyser/ExpressionResult.php @@ -2,9 +2,24 @@ namespace PHPStan\Analyser; +use PhpParser\Node\Expr; +use PhpParser\Node\Expr\FuncCall; +use PhpParser\Node\Expr\Match_; +use PhpParser\Node\Expr\MethodCall; +use PhpParser\Node\Expr\Variable; +use PHPStan\DependencyInjection\GenerateFactory; +use PHPStan\DependencyInjection\Type\ExpressionTypeResolverExtensionRegistryProvider; +use PHPStan\Type\Type; +use PHPStan\Type\TypeCombinator; +use PHPStan\Type\TypeUtils; + +#[GenerateFactory(interface: ExpressionResultFactory::class)] final class ExpressionResult { + /** @var callable(Expr, MutatingScope): Type */ + private $typeCallback; + /** @var (callable(): MutatingScope)|null */ private $truthyScopeCallback; @@ -15,26 +30,53 @@ final class ExpressionResult private ?MutatingScope $falseyScope = null; + private ?Type $cachedType = null; + + private ?Type $cachedNativeType = null; + + private ?Type $cachedKeepVoidType = null; + /** * @param InternalThrowPoint[] $throwPoints * @param ImpurePoint[] $impurePoints + * @param callable(MutatingScope): Type $typeCallback * @param (callable(): MutatingScope)|null $truthyScopeCallback * @param (callable(): MutatingScope)|null $falseyScopeCallback */ public function __construct( + private ExpressionTypeResolverExtensionRegistryProvider $expressionTypeResolverExtensionRegistryProvider, + private Expr $expr, private MutatingScope $scope, private bool $hasYield, private bool $isAlwaysTerminating, private array $throwPoints, private array $impurePoints, + callable $typeCallback, ?callable $truthyScopeCallback = null, ?callable $falseyScopeCallback = null, ) { + $this->typeCallback = $typeCallback; $this->truthyScopeCallback = $truthyScopeCallback; $this->falseyScopeCallback = $falseyScopeCallback; } + private function withExpr(Expr $expr): self + { + return new self( + $this->expressionTypeResolverExtensionRegistryProvider, + $expr, + $this->scope, + $this->hasYield, + $this->isAlwaysTerminating, + $this->throwPoints, + $this->impurePoints, + $this->typeCallback, + $this->truthyScopeCallback, + $this->falseyScopeCallback, + ); + } + public function getScope(): MutatingScope { return $this->scope; @@ -96,4 +138,97 @@ public function isAlwaysTerminating(): bool return $this->isAlwaysTerminating; } + /** + * `ExpressionResult::getType()` is a replacement for `MutatingScope::getType(Expr)` + * for use inside `ExprHandler::processExpr()` implementations. + */ + public function getType(): Type + { + if ($this->cachedType !== null) { + return $this->cachedType; + } + + return $this->cachedType = TypeUtils::resolveLateResolvableTypes($this->getTypeByScope($this->scope)); + } + + /** + * `ExpressionResult::getTypeForScope(Scope)` is used + * instead of `$scope->getType(Expr)` inside typeCallback ExpressionResultFactory argument. + */ + public function getTypeForScope(MutatingScope $scope): Type + { + if ($scope->nativeTypesPromoted) { + return $this->getNativeType(); + } + + return $this->getType(); + } + + /** + * `ExpressionResult::getNativeType()` is a replacement for `MutatingScope::getNativeType(Expr)` + * for use inside `ExprHandler::processExpr()` implementations. + */ + public function getNativeType(): Type + { + if ($this->cachedNativeType !== null) { + return $this->cachedNativeType; + } + + return $this->cachedNativeType = TypeUtils::resolveLateResolvableTypes($this->getTypeByScope($this->scope->doNotTreatPhpDocTypesAsCertain())); + } + + public function getKeepVoidType(): Type + { + if ($this->cachedKeepVoidType !== null) { + return $this->cachedKeepVoidType; + } + + if ( + !$this->expr instanceof Match_ + && ( + ( + !$this->expr instanceof FuncCall + && !$this->expr instanceof MethodCall + && !$this->expr instanceof Expr\NullsafeMethodCall + && !$this->expr instanceof Expr\StaticCall + ) || $this->expr->isFirstClassCallable() + ) + ) { + return $this->getType(); + } + + $originalType = $this->getType(); + if (!TypeCombinator::containsNull($originalType)) { + return $this->cachedKeepVoidType = $originalType; + } + + $clonedExpr = clone $this->expr; + $clonedExpr->setAttribute(MutatingScope::KEEP_VOID_ATTRIBUTE_NAME, true); + + return $this->cachedKeepVoidType = $this->withExpr($clonedExpr)->getType(); + } + + private function getTypeByScope(MutatingScope $scope): Type + { + foreach ($this->expressionTypeResolverExtensionRegistryProvider->getRegistry()->getExtensions() as $extension) { + $type = $extension->getType($this->expr, $scope); + if ($type !== null) { + return $type; + } + } + + if ( + !$this->expr instanceof Variable + && !$this->expr instanceof Expr\Closure + && !$this->expr instanceof Expr\ArrowFunction + && $scope->hasExpressionType($this->expr)->yes() + ) { + $exprString = $scope->getNodeKey($this->expr); + return $scope->expressionTypes[$exprString]->getType(); + } + + $typeCallback = $this->typeCallback; + return $typeCallback($this->expr, $scope); + } + } diff --git a/src/Analyser/ExpressionResultFactory.php b/src/Analyser/ExpressionResultFactory.php new file mode 100644 index 0000000000..06a951639a --- /dev/null +++ b/src/Analyser/ExpressionResultFactory.php @@ -0,0 +1,30 @@ +issetCheckWithResolver($expr, $typeCallback, fn (Expr $expr): Type => $this->getType($expr), $result); + } + + /** + * @param callable(Type): ?bool $typeCallback + * @param callable(Expr): Type $typeResolver + */ + public function issetCheckWithResolver(Expr $expr, callable $typeCallback, callable $typeResolver, ?bool $result = null): ?bool + { if ($expr instanceof Node\Expr\Variable && is_string($expr->name)) { $hasVariable = $this->hasVariableType($expr->name); if ($hasVariable->maybe()) { @@ -1014,41 +1023,46 @@ public function issetCheck(Expr $expr, callable $typeCallback, ?bool $result = n return $result; } elseif ($expr instanceof Node\Expr\ArrayDimFetch && $expr->dim !== null) { - $type = $this->getType($expr->var); + $type = $typeResolver($expr->var); if (!$type->isOffsetAccessible()->yes()) { - return $result ?? $this->issetCheckUndefined($expr->var); + return $result ?? $this->issetCheckUndefinedWithResolver($expr->var, $typeResolver); } - $dimType = $this->getType($expr->dim); + $dimType = $typeResolver($expr->dim); $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->issetCheck($expr->var, $typeCallback, $result); + return $this->issetCheckWithResolver($expr->var, $typeCallback, $typeResolver, $result); } } - // Has offset, it is nullable return null; } elseif ($expr instanceof Node\Expr\PropertyFetch || $expr instanceof Node\Expr\StaticPropertyFetch) { - $propertyReflection = $this->propertyReflectionFinder->findPropertyReflectionFromNode($expr, $this); + if ($expr instanceof Node\Expr\PropertyFetch) { + $holderType = $typeResolver($expr->var); + } elseif ($expr->class instanceof Name) { + $holderType = $this->resolveTypeByName($expr->class); + } else { + $holderType = $typeResolver($expr->class); + } + + $propertyReflection = $this->propertyReflectionFinder->findPropertyReflectionFromNodeWithTypes($expr, $this, $holderType, null); if ($propertyReflection === null) { if ($expr instanceof Node\Expr\PropertyFetch) { - return $this->issetCheckUndefined($expr->var); + return $this->issetCheckUndefinedWithResolver($expr->var, $typeResolver); } if ($expr->class instanceof Expr) { - return $this->issetCheckUndefined($expr->class); + return $this->issetCheckUndefinedWithResolver($expr->class, $typeResolver); } return null; @@ -1056,11 +1070,11 @@ public function issetCheck(Expr $expr, callable $typeCallback, ?bool $result = n if (!$propertyReflection->isNative()) { if ($expr instanceof Node\Expr\PropertyFetch) { - return $this->issetCheckUndefined($expr->var); + return $this->issetCheckUndefinedWithResolver($expr->var, $typeResolver); } if ($expr->class instanceof Expr) { - return $this->issetCheckUndefined($expr->class); + return $this->issetCheckUndefinedWithResolver($expr->class, $typeResolver); } return null; @@ -1069,11 +1083,11 @@ public function issetCheck(Expr $expr, callable $typeCallback, ?bool $result = n if ($propertyReflection->hasNativeType() && !$propertyReflection->isVirtual()->yes()) { if (!$this->hasExpressionType($expr)->yes()) { if ($expr instanceof Node\Expr\PropertyFetch) { - return $this->issetCheckUndefined($expr->var); + return $this->issetCheckUndefinedWithResolver($expr->var, $typeResolver); } if ($expr->class instanceof Expr) { - return $this->issetCheckUndefined($expr->class); + return $this->issetCheckUndefinedWithResolver($expr->class, $typeResolver); } return null; @@ -1087,11 +1101,11 @@ public function issetCheck(Expr $expr, callable $typeCallback, ?bool $result = n $result = $typeCallback($propertyReflection->getWritableType()); if ($result !== null) { if ($expr instanceof Node\Expr\PropertyFetch) { - return $this->issetCheck($expr->var, $typeCallback, $result); + return $this->issetCheckWithResolver($expr->var, $typeCallback, $typeResolver, $result); } if ($expr->class instanceof Expr) { - return $this->issetCheck($expr->class, $typeCallback, $result); + return $this->issetCheckWithResolver($expr->class, $typeCallback, $typeResolver, $result); } } @@ -1102,10 +1116,13 @@ public function issetCheck(Expr $expr, callable $typeCallback, ?bool $result = n return $result; } - return $typeCallback($this->getType($expr)); + return $typeCallback($typeResolver($expr)); } - private function issetCheckUndefined(Expr $expr): ?bool + /** + * @param callable(Expr): Type $typeResolver + */ + private function issetCheckUndefinedWithResolver(Expr $expr, callable $typeResolver): ?bool { if ($expr instanceof Node\Expr\Variable && is_string($expr->name)) { $hasVariable = $this->hasVariableType($expr->name); @@ -1117,32 +1134,37 @@ private function issetCheckUndefined(Expr $expr): ?bool } if ($expr instanceof Node\Expr\ArrayDimFetch && $expr->dim !== null) { - $type = $this->getType($expr->var); + $type = $typeResolver($expr->var); if (!$type->isOffsetAccessible()->yes()) { - return $this->issetCheckUndefined($expr->var); + return $this->issetCheckUndefinedWithResolver($expr->var, $typeResolver); } - $dimType = $this->getType($expr->dim); + $dimType = $typeResolver($expr->dim); $hasOffsetValue = $type->hasOffsetValueType($dimType); if (!$hasOffsetValue->no()) { - return $this->issetCheckUndefined($expr->var); + return $this->issetCheckUndefinedWithResolver($expr->var, $typeResolver); } return false; } if ($expr instanceof Expr\PropertyFetch) { - return $this->issetCheckUndefined($expr->var); + return $this->issetCheckUndefinedWithResolver($expr->var, $typeResolver); } if ($expr instanceof Expr\StaticPropertyFetch && $expr->class instanceof Expr) { - return $this->issetCheckUndefined($expr->class); + return $this->issetCheckUndefinedWithResolver($expr->class, $typeResolver); } return null; } + private function issetCheckUndefined(Expr $expr): ?bool + { + return $this->issetCheckUndefinedWithResolver($expr, fn (Expr $expr): Type => $this->getType($expr)); + } + /** @api */ public function getNativeType(Expr $expr): Type { @@ -1176,7 +1198,7 @@ public function getKeepVoidType(Expr $node): Type return $this->getType($clonedNode); } - public function doNotTreatPhpDocTypesAsCertain(): Scope + public function doNotTreatPhpDocTypesAsCertain(): self { return $this->promoteNativeTypes(); } diff --git a/src/Analyser/NodeScopeResolver.php b/src/Analyser/NodeScopeResolver.php index 8a112cda54..b69c776e3f 100644 --- a/src/Analyser/NodeScopeResolver.php +++ b/src/Analyser/NodeScopeResolver.php @@ -222,6 +222,7 @@ public function __construct( private readonly ParameterClosureTypeExtensionProvider $parameterClosureTypeExtensionProvider, private readonly ScopeFactory $scopeFactory, private readonly DeepNodeCloner $deepNodeCloner, + private readonly ExpressionResultFactory $expressionResultFactory, #[AutowiredParameter] private readonly bool $polluteScopeWithLoopInitialAssignments, #[AutowiredParameter] @@ -911,7 +912,6 @@ public function processStmtNode( if ($stmt->expr instanceof Expr\Throw_) { $scope = $stmtScope; } - $earlyTerminationExpr = $this->findEarlyTerminatingExpr($stmt->expr, $scope); $hasAssign = false; $currentScope = $scope; $result = $this->processExprNode($stmt, $stmt->expr, $scope, $storage, static function (Node $node, Scope $scope) use ($nodeCallback, $currentScope, &$hasAssign): void { @@ -924,6 +924,7 @@ public function processStmtNode( } $nodeCallback($node, $scope); }, ExpressionContext::createTopLevel()); + $earlyTerminationExpr = $this->findEarlyTerminatingExpr($stmt->expr, $result); $throwPoints = array_filter($result->getThrowPoints(), static fn ($throwPoint) => $throwPoint->isExplicit()); if ( count($result->getImpurePoints()) === 0 @@ -1094,9 +1095,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(); @@ -1130,8 +1131,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->getType() : $condResult->getNativeType())->toBoolean(); $throwPoints = array_merge($throwPoints, $condResult->getThrowPoints()); $impurePoints = array_merge($impurePoints, $condResult->getImpurePoints()); $condScope = $condResult->getScope(); @@ -1297,7 +1298,7 @@ public function processStmtNode( $finalScope = $breakExitPoint->getScope()->mergeWith($finalScope); } - $exprType = $scope->getType($stmt->expr); + $exprType = $condResult->getType(); $hasExpr = $scope->hasExpressionType($stmt->expr); if ( count($breakExitPoints) === 0 @@ -1335,7 +1336,7 @@ public function processStmtNode( return new ArrayType($type->getKeyType(), $arrayDimFetchLoopType); }); - $newExprNativeType = TypeTraverser::map($scope->getNativeType($stmt->expr), static function (Type $type, callable $traverse) use ($arrayDimFetchLoopNativeType): Type { + $newExprNativeType = TypeTraverser::map($condResult->getNativeType(), static function (Type $type, callable $traverse) use ($arrayDimFetchLoopNativeType): Type { if ($type instanceof UnionType || $type instanceof IntersectionType) { return $traverse($type); } @@ -1386,7 +1387,7 @@ public function processStmtNode( $throwPoints = array_merge($throwPoints, $finalScopeResult->getThrowPoints()); $impurePoints = array_merge($impurePoints, $finalScopeResult->getImpurePoints()); } - if (!(new ObjectType(Traversable::class))->isSuperTypeOf($scope->getType($stmt->expr))->no()) { + if (!(new ObjectType(Traversable::class))->isSuperTypeOf($condResult->getType())->no()) { $throwPoints[] = InternalThrowPoint::createImplicit($scope, $stmt->expr); } if ($context->isTopLevel() && $stmt->byRef) { @@ -1405,7 +1406,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->getType() : $condResult->getNativeType())->toBoolean(); $condScope = $condResult->getFalseyScope(); if (!$context->isTopLevel() && $beforeCondBooleanType->isFalse()->yes()) { if (!$this->polluteScopeWithLoopInitialAssignments) { @@ -2050,7 +2051,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->processExprNode($stmt, $var->var, $scope, new ExpressionResultStorage(), new NoopNodeCallback(), ExpressionContext::createDeep())->getType(); if (!$varType->isArray()->yes() && !(new ObjectType(ArrayAccess::class))->isSuperTypeOf($varType)->no()) { $throwPoints = array_merge($throwPoints, $this->processExprNode( $stmt, @@ -2196,8 +2197,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->getType(), + $constResult->getNativeType(), ); } } elseif ($stmt instanceof Node\Stmt\EnumCase) { @@ -2416,50 +2417,9 @@ private function lookForExpressionCallback(MutatingScope $scope, Expr $expr, Clo return $scope; } - private function findEarlyTerminatingExpr(Expr $expr, Scope $scope): ?Expr + private function findEarlyTerminatingExpr(Expr $expr, ExpressionResult $result): ?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); - } else { - if ($expr->class instanceof Name) { - $methodCalledOnType = $scope->resolveTypeByName($expr->class); - } else { - $methodCalledOnType = $scope->getType($expr->class); - } - } - - foreach ($methodCalledOnType->getObjectClassNames() as $referencedClass) { - if (!$this->reflectionProvider->hasClass($referencedClass)) { - continue; - } - - $classReflection = $this->reflectionProvider->getClass($referencedClass); - foreach (array_merge([$referencedClass], $classReflection->getParentClassesNames(), $classReflection->getNativeReflection()->getInterfaceNames()) as $className) { - if (!isset($this->earlyTerminatingMethodCalls[$className])) { - continue; - } - - if (in_array((string) $expr->name, $this->earlyTerminatingMethodCalls[$className], true)) { - return $expr; - } - } - } - } - } - - if ($expr instanceof FuncCall && $expr->name instanceof Name) { - if (in_array((string) $expr->name, $this->earlyTerminatingFunctionCalls, true)) { - return $expr; - } - } - - if ($expr instanceof Expr\Exit_ || $expr instanceof Expr\Throw_) { - return $expr; - } - - $exprType = $scope->getType($expr); + $exprType = $result->getType(); if ($exprType instanceof NeverType && $exprType->isExplicit()) { return $expr; } @@ -2509,11 +2469,21 @@ public function processExprNode( if ($expr instanceof List_) { // only in assign and foreach, processed elsewhere - return new ExpressionResult($scope, hasYield: false, isAlwaysTerminating: false, throwPoints: [], impurePoints: []); + return $this->expressionResultFactory->create( + $expr, + $scope, + typeCallback: static fn () => new MixedType(), + hasYield: false, + isAlwaysTerminating: false, + throwPoints: [], + impurePoints: [], + ); } - return new ExpressionResult( + return $this->expressionResultFactory->create( + $expr, $scope, + typeCallback: static fn () => new MixedType(), hasYield: false, isAlwaysTerminating: false, throwPoints: [], @@ -2678,7 +2648,8 @@ public function processClosureNode( $inAssignRightSideVariableName === $use->var->name && $inAssignRightSideExpr !== null ) { - $inAssignRightSideType = $scope->getType($inAssignRightSideExpr); + $inAssignRightSideResult = $this->processExprNode(new Node\Stmt\Expression($inAssignRightSideExpr), $inAssignRightSideExpr, $scope, new ExpressionResultStorage(), new NoopNodeCallback(), ExpressionContext::createDeep()); + $inAssignRightSideType = $inAssignRightSideResult->getType(); if ($inAssignRightSideType instanceof ClosureType) { $variableType = $inAssignRightSideType; } else { @@ -2689,7 +2660,7 @@ public function processClosureNode( $variableType = TypeCombinator::union($scope->getVariableType($inAssignRightSideVariableName), $inAssignRightSideType); } } - $inAssignRightSideNativeType = $scope->getNativeType($inAssignRightSideExpr); + $inAssignRightSideNativeType = $inAssignRightSideResult->getNativeType(); if ($inAssignRightSideNativeType instanceof ClosureType) { $variableNativeType = $inAssignRightSideNativeType; } else { @@ -2886,7 +2857,15 @@ 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( + $expr, + $scope, + typeCallback: static fn (Expr $unassigned, MutatingScope $scope) => $scope->getType($expr), // todo + hasYield: false, + isAlwaysTerminating: $exprResult->isAlwaysTerminating(), + throwPoints: $exprResult->getThrowPoints(), + impurePoints: $exprResult->getImpurePoints(), + ); } /** @@ -3528,7 +3507,15 @@ 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( + $callLike, + $scope, + typeCallback: static fn () => new MixedType(), + hasYield: $hasYield, + isAlwaysTerminating: $isAlwaysTerminating, + throwPoints: $throwPoints, + impurePoints: $impurePoints, + ); } /** @@ -3665,7 +3652,15 @@ 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( + $assignedExpr, + $scope, + typeCallback: static fn () => new MixedType(), + hasYield: false, + isAlwaysTerminating: false, + throwPoints: [], + impurePoints: [], + ), false, ); } diff --git a/src/Analyser/RicherScopeGetTypeHelper.php b/src/Analyser/RicherScopeGetTypeHelper.php index 132c187580..32ed489654 100644 --- a/src/Analyser/RicherScopeGetTypeHelper.php +++ b/src/Analyser/RicherScopeGetTypeHelper.php @@ -10,6 +10,7 @@ use PHPStan\Rules\Properties\PropertyReflectionFinder; use PHPStan\Type\BooleanType; use PHPStan\Type\Constant\ConstantBooleanType; +use PHPStan\Type\Type; use PHPStan\Type\TypeResult; use function is_string; @@ -28,6 +29,14 @@ public function __construct( * @return TypeResult */ public function getIdenticalResult(Scope $scope, Identical $expr): TypeResult + { + return $this->getIdenticalResultWithTypes($scope, $expr, $scope->getType($expr->left), $scope->getType($expr->right)); + } + + /** + * @return TypeResult + */ + public function getIdenticalResultWithTypes(Scope $scope, Identical $expr, Type $leftType, Type $rightType): TypeResult { if ( $expr->left instanceof Variable @@ -39,9 +48,6 @@ public function getIdenticalResult(Scope $scope, Identical $expr): TypeResult return new TypeResult(new ConstantBooleanType(true), []); } - $leftType = $scope->getType($expr->left); - $rightType = $scope->getType($expr->right); - if ( ( $expr->left instanceof Node\Expr\PropertyFetch @@ -80,7 +86,15 @@ public function getIdenticalResult(Scope $scope, Identical $expr): TypeResult */ public function getNotIdenticalResult(Scope $scope, Node\Expr\BinaryOp\NotIdentical $expr): TypeResult { - $identicalResult = $this->getIdenticalResult($scope, new Identical($expr->left, $expr->right)); + return $this->getNotIdenticalResultWithTypes($scope, $expr, $scope->getType($expr->left), $scope->getType($expr->right)); + } + + /** + * @return TypeResult + */ + public function getNotIdenticalResultWithTypes(Scope $scope, Node\Expr\BinaryOp\NotIdentical $expr, Type $leftType, Type $rightType): TypeResult + { + $identicalResult = $this->getIdenticalResultWithTypes($scope, new Identical($expr->left, $expr->right), $leftType, $rightType); $identicalType = $identicalResult->type; if ($identicalType instanceof ConstantBooleanType) { return new TypeResult(new ConstantBooleanType(!$identicalType->getValue()), $identicalResult->reasons); diff --git a/src/Rules/Properties/PropertyReflectionFinder.php b/src/Rules/Properties/PropertyReflectionFinder.php index b25682687b..498dd82305 100644 --- a/src/Rules/Properties/PropertyReflectionFinder.php +++ b/src/Rules/Properties/PropertyReflectionFinder.php @@ -90,14 +90,39 @@ public function findPropertyReflectionFromNode($propertyFetch, Scope $scope): ?F { if ($propertyFetch instanceof Node\Expr\PropertyFetch) { $propertyHolderType = $scope->getType($propertyFetch->var); + $nameType = $propertyFetch->name instanceof Expr ? $scope->getType($propertyFetch->name) : null; + + return $this->findPropertyReflectionFromNodeWithTypes($propertyFetch, $scope, $propertyHolderType, $nameType); + } + + if ($propertyFetch->class instanceof Node\Name) { + $propertyHolderType = $scope->resolveTypeByName($propertyFetch->class); + } else { + $propertyHolderType = $scope->getType($propertyFetch->class); + } + + $nameType = $propertyFetch->name instanceof Expr ? $scope->getType($propertyFetch->name) : null; + + return $this->findPropertyReflectionFromNodeWithTypes($propertyFetch, $scope, $propertyHolderType, $nameType); + } + + /** + * @param Node\Expr\PropertyFetch|Node\Expr\StaticPropertyFetch $propertyFetch + */ + public function findPropertyReflectionFromNodeWithTypes($propertyFetch, Scope $scope, Type $holderType, ?Type $nameType): ?FoundPropertyReflection + { + if ($propertyFetch instanceof Node\Expr\PropertyFetch) { if ($propertyFetch->name instanceof Node\Identifier) { - return $this->findInstancePropertyReflection($propertyHolderType, $propertyFetch->name->name, $scope); + return $this->findInstancePropertyReflection($holderType, $propertyFetch->name->name, $scope); + } + + if ($nameType === null) { + return null; } - $nameType = $scope->getType($propertyFetch->name); $nameTypeConstantStrings = $nameType->getConstantStrings(); if (count($nameTypeConstantStrings) === 1) { - return $this->findInstancePropertyReflection($propertyHolderType, $nameTypeConstantStrings[0]->getValue(), $scope); + return $this->findInstancePropertyReflection($holderType, $nameTypeConstantStrings[0]->getValue(), $scope); } return null; @@ -107,13 +132,7 @@ public function findPropertyReflectionFromNode($propertyFetch, Scope $scope): ?F return null; } - if ($propertyFetch->class instanceof Node\Name) { - $propertyHolderType = $scope->resolveTypeByName($propertyFetch->class); - } else { - $propertyHolderType = $scope->getType($propertyFetch->class); - } - - return $this->findStaticPropertyReflection($propertyHolderType, $propertyFetch->name->name, $scope); + return $this->findStaticPropertyReflection($holderType, $propertyFetch->name->name, $scope); } private function findInstancePropertyReflection(Type $propertyHolderType, string $propertyName, Scope $scope): ?FoundPropertyReflection diff --git a/src/Testing/RuleTestCase.php b/src/Testing/RuleTestCase.php index 0565407424..1e7b76d600 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\Fiber\FiberNodeScopeResolver; use PHPStan\Analyser\FileAnalyser; use PHPStan\Analyser\IgnoreErrorExtensionProvider; @@ -109,6 +110,7 @@ protected function createNodeScopeResolver(): NodeScopeResolver self::getContainer()->getByType(ParameterClosureTypeExtensionProvider::class), self::createScopeFactory($reflectionProvider, $typeSpecifier), self::getContainer()->getByType(DeepNodeCloner::class), + self::getContainer()->getByType(ExpressionResultFactory::class), $this->shouldPolluteScopeWithLoopInitialAssignments(), $this->shouldPolluteScopeWithAlwaysIterableForeach(), self::getContainer()->getParameter('polluteScopeWithBlock'), diff --git a/src/Testing/TypeInferenceTestCase.php b/src/Testing/TypeInferenceTestCase.php index fb793c45c9..5234372e69 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\Fiber\FiberNodeScopeResolver; use PHPStan\Analyser\MutatingScope; use PHPStan\Analyser\NodeScopeResolver; @@ -84,6 +85,7 @@ protected static function createNodeScopeResolver(): NodeScopeResolver $container->getByType(ParameterClosureTypeExtensionProvider::class), self::createScopeFactory($reflectionProvider, $typeSpecifier), self::getContainer()->getByType(DeepNodeCloner::class), + self::getContainer()->getByType(ExpressionResultFactory::class), $container->getParameter('polluteScopeWithLoopInitialAssignments'), $container->getParameter('polluteScopeWithAlwaysIterableForeach'), $container->getParameter('polluteScopeWithBlock'), diff --git a/tests/PHPStan/Analyser/AnalyserTest.php b/tests/PHPStan/Analyser/AnalyserTest.php index 27f9daddb0..738a61d2f9 100644 --- a/tests/PHPStan/Analyser/AnalyserTest.php +++ b/tests/PHPStan/Analyser/AnalyserTest.php @@ -825,6 +825,7 @@ private function createAnalyser(): Analyser $container->getByType(ParameterClosureTypeExtensionProvider::class), self::createScopeFactory($reflectionProvider, $typeSpecifier), $container->getByType(DeepNodeCloner::class), + $container->getByType(ExpressionResultFactory::class), false, true, true, diff --git a/tests/PHPStan/Analyser/Fiber/FiberNodeScopeResolverRuleTest.php b/tests/PHPStan/Analyser/Fiber/FiberNodeScopeResolverRuleTest.php index e963de6bc9..7a778f020d 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\NodeScopeResolver; use PHPStan\Analyser\Scope; use PHPStan\DependencyInjection\Type\ParameterClosureThisExtensionProvider; @@ -129,6 +130,7 @@ protected function createNodeScopeResolver(): NodeScopeResolver self::getContainer()->getByType(ParameterClosureTypeExtensionProvider::class), self::createScopeFactory($reflectionProvider, $typeSpecifier), self::getContainer()->getByType(DeepNodeCloner::class), + self::getContainer()->getByType(ExpressionResultFactory::class), $this->shouldPolluteScopeWithLoopInitialAssignments(), $this->shouldPolluteScopeWithAlwaysIterableForeach(), self::getContainer()->getParameter('polluteScopeWithBlock'), diff --git a/tests/PHPStan/Analyser/Fiber/FiberNodeScopeResolverTest.php b/tests/PHPStan/Analyser/Fiber/FiberNodeScopeResolverTest.php index 5bea8cce79..fe24a8849e 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\NodeScopeResolver; use PHPStan\DependencyInjection\Type\ParameterClosureThisExtensionProvider; use PHPStan\DependencyInjection\Type\ParameterClosureTypeExtensionProvider; @@ -62,6 +63,7 @@ protected static function createNodeScopeResolver(): NodeScopeResolver $container->getByType(ParameterClosureTypeExtensionProvider::class), self::createScopeFactory($reflectionProvider, $typeSpecifier), $container->getByType(DeepNodeCloner::class), + self::getContainer()->getByType(ExpressionResultFactory::class), $container->getParameter('polluteScopeWithLoopInitialAssignments'), $container->getParameter('polluteScopeWithAlwaysIterableForeach'), $container->getParameter('polluteScopeWithBlock'),