diff --git a/src/Analyser/ExprHandler/ArrayDimFetchHandler.php b/src/Analyser/ExprHandler/ArrayDimFetchHandler.php index 9e0bb3021f..dfdc46932d 100644 --- a/src/Analyser/ExprHandler/ArrayDimFetchHandler.php +++ b/src/Analyser/ExprHandler/ArrayDimFetchHandler.php @@ -14,11 +14,11 @@ use PHPStan\Analyser\ExpressionResultFactory; use PHPStan\Analyser\ExpressionResultStorage; use PHPStan\Analyser\ExprHandler; +use PHPStan\Analyser\ExprHandler\Helper\MethodThrowPointHelper; use PHPStan\Analyser\ExprHandler\Helper\NullsafeShortCircuitingHelper; use PHPStan\Analyser\IssetabilityDescriptor; use PHPStan\Analyser\MutatingScope; use PHPStan\Analyser\NodeScopeResolver; -use PHPStan\Analyser\NoopNodeCallback; use PHPStan\Analyser\Scope; use PHPStan\Analyser\SpecifiedTypes; use PHPStan\Analyser\TypeSpecifier; @@ -37,7 +37,10 @@ final class ArrayDimFetchHandler implements ExprHandler { - public function __construct(private ExpressionResultFactory $expressionResultFactory) + public function __construct( + private ExpressionResultFactory $expressionResultFactory, + private MethodThrowPointHelper $methodThrowPointHelper, + ) { } @@ -123,14 +126,12 @@ public function composeResult(NodeScopeResolver $nodeScopeResolver, Stmt $stmt, $varType = $varResult->getType(); if (!$varType->isArray()->yes() && !(new ObjectType(ArrayAccess::class))->isSuperTypeOf($varType)->no()) { - $throwPoints = array_merge($throwPoints, $nodeScopeResolver->processExprNode( - $stmt, - new MethodCall(new TypeExpr($varType), 'offsetGet'), + $throwPoints = array_merge($throwPoints, $this->methodThrowPointHelper->getThrowPointsForCallOnType( $scope, - $storage, - new NoopNodeCallback(), $context, - )->getThrowPoints()); + $varType, + new MethodCall(new TypeExpr($varType), 'offsetGet'), + )); } return $this->expressionResultFactory->create( diff --git a/src/Analyser/ExprHandler/AssignHandler.php b/src/Analyser/ExprHandler/AssignHandler.php index d65abb0056..ba29dbf325 100644 --- a/src/Analyser/ExprHandler/AssignHandler.php +++ b/src/Analyser/ExprHandler/AssignHandler.php @@ -29,6 +29,7 @@ use PHPStan\Analyser\ExpressionResultStorage; use PHPStan\Analyser\ExpressionTypeHolder; use PHPStan\Analyser\ExprHandler; +use PHPStan\Analyser\ExprHandler\Helper\MethodThrowPointHelper; use PHPStan\Analyser\ExprHandler\Helper\NonNullabilityHelper; use PHPStan\Analyser\ImpurePoint; use PHPStan\Analyser\InternalThrowPoint; @@ -105,6 +106,7 @@ public function __construct( private ArrayDimFetchHandler $arrayDimFetchHandler, private PropertyFetchHandler $propertyFetchHandler, private StaticPropertyFetchHandler $staticPropertyFetchHandler, + private MethodThrowPointHelper $methodThrowPointHelper, ) { } @@ -1080,14 +1082,12 @@ public function applyWrite( && !$setVarType->isArray()->yes() && !(new ObjectType(ArrayAccess::class))->isSuperTypeOf($setVarType)->no() ) { - $throwPoints = array_merge($throwPoints, $nodeScopeResolver->processExprNode( - $stmt, - new MethodCall(new TypeExpr($setVarType), 'offsetSet'), + $throwPoints = array_merge($throwPoints, $this->methodThrowPointHelper->getThrowPointsForCallOnType( $scope, - $storage, - new NoopNodeCallback(), $context, - )->getThrowPoints()); + $setVarType, + new MethodCall(new TypeExpr($setVarType), 'offsetSet'), + )); } } elseif ($kind === PreparedAssignTarget::KIND_PROPERTY_FETCH) { if (!$var instanceof PropertyFetch) { @@ -1173,16 +1173,15 @@ public function applyWrite( $assignedExprType = $scope->getType($assignedExpr); $nodeScopeResolver->callNodeCallback($nodeCallback, new PropertyAssignNode($var, $assignedExpr, $isAssignOp), $scopeBeforeAssignEval, $storage); $scope = $scope->assignExpression($var, $assignedExprType, $scope->getNativeType($assignedExpr)); - // simulate dynamic property assign by __set to get throw points + // simulate dynamic property assign by __set to get throw points; + // the receiver's own throw points were already collected by its walk if (!$propertyHolderType->hasMethod('__set')->no()) { - $throwPoints = array_merge($throwPoints, $nodeScopeResolver->processExprNode( - $stmt, - new MethodCall($var->var, '__set'), + $throwPoints = array_merge($throwPoints, $this->methodThrowPointHelper->getThrowPointsForCallOnType( $scope, - $storage, - new NoopNodeCallback(), $context, - )->getThrowPoints()); + $propertyHolderType, + new MethodCall($var->var, '__set'), + )); } } diff --git a/src/Analyser/ExprHandler/Helper/MethodThrowPointHelper.php b/src/Analyser/ExprHandler/Helper/MethodThrowPointHelper.php index 2658beb2ce..5edef89772 100644 --- a/src/Analyser/ExprHandler/Helper/MethodThrowPointHelper.php +++ b/src/Analyser/ExprHandler/Helper/MethodThrowPointHelper.php @@ -4,6 +4,7 @@ use PhpParser\Node\Expr\MethodCall; use PhpParser\Node\Expr\StaticCall; +use PhpParser\Node\Identifier; use PHPStan\Analyser\ExpressionContext; use PHPStan\Analyser\InternalThrowPoint; use PHPStan\Analyser\MutatingScope; @@ -13,10 +14,13 @@ use PHPStan\DependencyInjection\ExtensionsCollection; use PHPStan\Reflection\MethodReflection; use PHPStan\Reflection\ParametersAcceptor; +use PHPStan\Reflection\ParametersAcceptorSelector; +use PHPStan\ShouldNotHappenException; use PHPStan\Type\DynamicMethodThrowTypeExtension; use PHPStan\Type\DynamicStaticMethodThrowTypeExtension; use PHPStan\Type\NeverType; use PHPStan\Type\ObjectType; +use PHPStan\Type\Type; use ReflectionFunction; use ReflectionMethod; use Throwable; @@ -107,4 +111,31 @@ public function getThrowPoint( return null; } + /** + * The throw points of invoking a method on an already-priced receiver + * type - what walking a synthetic MethodCall used to produce, without the + * walk. The method call node is only the throw-point anchor and the + * payload dynamic throw-type extensions receive; nothing processes it. + * + * @return list + */ + public function getThrowPointsForCallOnType(MutatingScope $scope, ExpressionContext $context, Type $calledOnType, MethodCall $methodCall): array + { + if (!$methodCall->name instanceof Identifier) { + throw new ShouldNotHappenException(); + } + + $methodReflection = $scope->getMethodReflection($calledOnType, $methodCall->name->toString()); + if ($methodReflection === null) { + return [InternalThrowPoint::createImplicit($scope, $methodCall)]; + } + + $throwPoint = $this->getThrowPoint($methodReflection, ParametersAcceptorSelector::combineAcceptors($methodReflection->getVariants()), $methodCall, $scope, $context); + if ($throwPoint === null) { + return []; + } + + return [$throwPoint]; + } + } diff --git a/src/Analyser/ExprHandler/IssetHandler.php b/src/Analyser/ExprHandler/IssetHandler.php index 62d9abadf0..7f0d6eb5e3 100644 --- a/src/Analyser/ExprHandler/IssetHandler.php +++ b/src/Analyser/ExprHandler/IssetHandler.php @@ -18,10 +18,10 @@ use PHPStan\Analyser\ExpressionResultFactory; use PHPStan\Analyser\ExpressionResultStorage; use PHPStan\Analyser\ExprHandler; +use PHPStan\Analyser\ExprHandler\Helper\MethodThrowPointHelper; use PHPStan\Analyser\ExprHandler\Helper\NonNullabilityHelper; use PHPStan\Analyser\MutatingScope; use PHPStan\Analyser\NodeScopeResolver; -use PHPStan\Analyser\NoopNodeCallback; use PHPStan\Analyser\Scope; use PHPStan\Analyser\SpecifiedTypes; use PHPStan\Analyser\TypeSpecifier; @@ -62,6 +62,7 @@ final class IssetHandler implements ExprHandler public function __construct( private NonNullabilityHelper $nonNullabilityHelper, private ExpressionResultFactory $expressionResultFactory, + private MethodThrowPointHelper $methodThrowPointHelper, ) { } @@ -375,14 +376,12 @@ public function processExpr(NodeScopeResolver $nodeScopeResolver, Stmt $stmt, Ex continue; } - $throwPoints = array_merge($throwPoints, $nodeScopeResolver->processExprNode( - $stmt, - new MethodCall(new TypeExpr($varType), 'offsetExists'), + $throwPoints = array_merge($throwPoints, $this->methodThrowPointHelper->getThrowPointsForCallOnType( $scope, - $storage, - new NoopNodeCallback(), $context, - )->getThrowPoints()); + $varType, + new MethodCall(new TypeExpr($varType), 'offsetExists'), + )); } foreach (array_reverse($expr->vars) as $var) { $scope = $nodeScopeResolver->lookForUnsetAllowedUndefinedExpressions($scope, $var); diff --git a/src/Analyser/NodeScopeResolver.php b/src/Analyser/NodeScopeResolver.php index d0ad511457..0ebe176db1 100644 --- a/src/Analyser/NodeScopeResolver.php +++ b/src/Analyser/NodeScopeResolver.php @@ -52,6 +52,7 @@ use PhpParser\NodeVisitorAbstract; use PHPStan\Analyser\ExprHandler\AssignHandler; use PHPStan\Analyser\ExprHandler\Helper\ImplicitToStringCallHelper; +use PHPStan\Analyser\ExprHandler\Helper\MethodThrowPointHelper; use PHPStan\BetterReflection\Reflection\Adapter\ReflectionClass; use PHPStan\BetterReflection\Reflection\ReflectionEnum; use PHPStan\BetterReflection\Reflector\Reflector; @@ -2397,14 +2398,12 @@ public function processStmtNode( if ($var instanceof ArrayDimFetch && $var->dim !== null) { $varType = $scope->getType($var->var); if (!$varType->isArray()->yes() && !(new ObjectType(ArrayAccess::class))->isSuperTypeOf($varType)->no()) { - $throwPoints = array_merge($throwPoints, $this->processExprNode( - $stmt, - new MethodCall(new TypeExpr($varType), 'offsetUnset'), + $throwPoints = array_merge($throwPoints, $this->container->getByType(MethodThrowPointHelper::class)->getThrowPointsForCallOnType( $scope, - $storage, - new NoopNodeCallback(), ExpressionContext::createDeep(), - )->getThrowPoints()); + $varType, + new MethodCall(new TypeExpr($varType), 'offsetUnset'), + )); } $clonedVar = $this->deepNodeCloner->cloneNode($var->var);