diff --git a/src/Analyser/ExprHandler/CastStringHandler.php b/src/Analyser/ExprHandler/CastStringHandler.php index fff7f07d48d..336e14dd921 100644 --- a/src/Analyser/ExprHandler/CastStringHandler.php +++ b/src/Analyser/ExprHandler/CastStringHandler.php @@ -4,15 +4,18 @@ use PhpParser\Node\Expr; use PhpParser\Node\Expr\Cast; +use PhpParser\Node\Identifier; use PhpParser\Node\Stmt; use PHPStan\Analyser\ExpressionContext; use PHPStan\Analyser\ExpressionResult; use PHPStan\Analyser\ExpressionResultStorage; use PHPStan\Analyser\ExprHandler; +use PHPStan\Analyser\ExprHandler\Helper\MethodThrowPointHelper; use PHPStan\Analyser\ImpurePoint; use PHPStan\Analyser\MutatingScope; use PHPStan\Analyser\NodeScopeResolver; use PHPStan\DependencyInjection\AutowiredService; +use PHPStan\Php\PhpVersion; use PHPStan\Reflection\InitializerExprTypeResolver; use PHPStan\Type\Type; use function sprintf; @@ -26,6 +29,8 @@ final class CastStringHandler implements ExprHandler public function __construct( private InitializerExprTypeResolver $initializerExprTypeResolver, + private PhpVersion $phpVersion, + private MethodThrowPointHelper $methodThrowPointHelper, ) { } @@ -39,6 +44,7 @@ public function processExpr(NodeScopeResolver $nodeScopeResolver, Stmt $stmt, Ex { $exprResult = $nodeScopeResolver->processExprNode($stmt, $expr->expr, $scope, $storage, $nodeCallback, $context->enterDeep()); $impurePoints = $exprResult->getImpurePoints(); + $throwPoints = $exprResult->getThrowPoints(); $exprType = $scope->getType($expr->expr); $toStringMethod = $scope->getMethodReflection($exprType, '__toString'); @@ -52,6 +58,18 @@ public function processExpr(NodeScopeResolver $nodeScopeResolver, Stmt $stmt, Ex $toStringMethod->isPure()->no(), ); } + + if ($this->phpVersion->throwsOnStringCast()) { + $throwPoint = $this->methodThrowPointHelper->getThrowPoint( + $toStringMethod, + $toStringMethod->getOnlyVariant(), + new Expr\MethodCall($expr->expr, new Identifier('__toString')), + $scope, + ); + if ($throwPoint !== null) { + $throwPoints[] = $throwPoint; + } + } } $scope = $exprResult->getScope(); @@ -60,7 +78,7 @@ public function processExpr(NodeScopeResolver $nodeScopeResolver, Stmt $stmt, Ex $scope, hasYield: $exprResult->hasYield(), isAlwaysTerminating: $exprResult->isAlwaysTerminating(), - throwPoints: $exprResult->getThrowPoints(), + throwPoints: $throwPoints, impurePoints: $impurePoints, truthyScopeCallback: static fn (): MutatingScope => $scope->filterByTruthyValue($expr), falseyScopeCallback: static fn (): MutatingScope => $scope->filterByFalseyValue($expr), diff --git a/src/Analyser/ExprHandler/Helper/MethodThrowPointHelper.php b/src/Analyser/ExprHandler/Helper/MethodThrowPointHelper.php new file mode 100644 index 00000000000..05b1314b5b1 --- /dev/null +++ b/src/Analyser/ExprHandler/Helper/MethodThrowPointHelper.php @@ -0,0 +1,98 @@ +dynamicThrowTypeExtensionProvider->getDynamicMethodThrowTypeExtensions() as $extension) { + if (!$extension->isMethodSupported($methodReflection)) { + continue; + } + + $throwType = $extension->getThrowTypeFromMethodCall($methodReflection, $normalizedMethodCall, $scope); + if ($throwType === null) { + return null; + } + + return InternalThrowPoint::createExplicit($scope, $throwType, $normalizedMethodCall, false); + } + } else { + foreach ($this->dynamicThrowTypeExtensionProvider->getDynamicStaticMethodThrowTypeExtensions() as $extension) { + if (!$extension->isStaticMethodSupported($methodReflection)) { + continue; + } + + $throwType = $extension->getThrowTypeFromStaticMethodCall($methodReflection, $normalizedMethodCall, $scope); + if ($throwType === null) { + return null; + } + + return InternalThrowPoint::createExplicit($scope, $throwType, $normalizedMethodCall, false); + } + } + + if ( + $normalizedMethodCall instanceof MethodCall + && in_array($methodReflection->getName(), ['invoke', 'invokeArgs'], true) + && in_array($methodReflection->getDeclaringClass()->getName(), [ReflectionMethod::class, ReflectionFunction::class], true) + ) { + return InternalThrowPoint::createImplicit($scope, $normalizedMethodCall); + } + + $throwType = $methodReflection->getThrowType(); + if ($throwType === null) { + $returnType = $parametersAcceptor->getReturnType(); + if ($returnType instanceof NeverType && $returnType->isExplicit()) { + $throwType = new ObjectType(Throwable::class); + } + } + + if ($throwType !== null) { + if (!$throwType->isVoid()->yes()) { + return InternalThrowPoint::createExplicit($scope, $throwType, $normalizedMethodCall, true); + } + } elseif ($this->implicitThrows) { + $methodReturnedType = $scope->getType($normalizedMethodCall); + if (!(new ObjectType(Throwable::class))->isSuperTypeOf($methodReturnedType)->yes()) { + return InternalThrowPoint::createImplicit($scope, $normalizedMethodCall); + } + } + + return null; + } + +} diff --git a/src/Analyser/ExprHandler/MethodCallHandler.php b/src/Analyser/ExprHandler/MethodCallHandler.php index 13124493336..28e0181c331 100644 --- a/src/Analyser/ExprHandler/MethodCallHandler.php +++ b/src/Analyser/ExprHandler/MethodCallHandler.php @@ -14,6 +14,7 @@ use PHPStan\Analyser\ExpressionResultStorage; use PHPStan\Analyser\ExprHandler; use PHPStan\Analyser\ExprHandler\Helper\MethodCallReturnTypeHelper; +use PHPStan\Analyser\ExprHandler\Helper\MethodThrowPointHelper; use PHPStan\Analyser\ExprHandler\Helper\NullsafeShortCircuitingHelper; use PHPStan\Analyser\ImpurePoint; use PHPStan\Analyser\InternalThrowPoint; @@ -21,13 +22,10 @@ use PHPStan\Analyser\NodeScopeResolver; use PHPStan\DependencyInjection\AutowiredParameter; use PHPStan\DependencyInjection\AutowiredService; -use PHPStan\DependencyInjection\Type\DynamicThrowTypeExtensionProvider; use PHPStan\Node\Expr\PossiblyImpureCallExpr; use PHPStan\Node\InvalidateExprNode; use PHPStan\Reflection\Callables\SimpleImpurePoint; use PHPStan\Reflection\ExtendedParametersAcceptor; -use PHPStan\Reflection\MethodReflection; -use PHPStan\Reflection\ParametersAcceptor; use PHPStan\Reflection\ParametersAcceptorSelector; use PHPStan\Type\ErrorType; use PHPStan\Type\Generic\TemplateTypeHelper; @@ -35,17 +33,12 @@ use PHPStan\Type\Generic\TemplateTypeVarianceMap; use PHPStan\Type\MixedType; use PHPStan\Type\NeverType; -use PHPStan\Type\ObjectType; use PHPStan\Type\Type; use PHPStan\Type\TypeCombinator; use PHPStan\Type\TypeUtils; -use ReflectionFunction; -use ReflectionMethod; -use Throwable; use function array_map; use function array_merge; use function count; -use function in_array; use function sprintf; use function strtolower; @@ -57,10 +50,8 @@ final class MethodCallHandler implements ExprHandler { public function __construct( - private DynamicThrowTypeExtensionProvider $dynamicThrowTypeExtensionProvider, private MethodCallReturnTypeHelper $methodCallReturnTypeHelper, - #[AutowiredParameter(ref: '%exceptions.implicitThrows%')] - private bool $implicitThrows, + private MethodThrowPointHelper $methodThrowPointHelper, #[AutowiredParameter] private bool $rememberPossiblyImpureFunctionValues, ) @@ -153,7 +144,7 @@ public function processExpr(NodeScopeResolver $nodeScopeResolver, Stmt $stmt, Ex $scope = $argsResult->getScope(); if ($methodReflection !== null) { - $methodThrowPoint = $this->getMethodThrowPoint($methodReflection, $parametersAcceptor, $normalizedExpr, $scope); + $methodThrowPoint = $this->methodThrowPointHelper->getThrowPoint($methodReflection, $parametersAcceptor, $normalizedExpr, $scope); if ($methodThrowPoint !== null) { $throwPoints[] = $methodThrowPoint; } @@ -235,50 +226,6 @@ public function processExpr(NodeScopeResolver $nodeScopeResolver, Stmt $stmt, Ex return $result; } - private function getMethodThrowPoint(MethodReflection $methodReflection, ParametersAcceptor $parametersAcceptor, MethodCall $normalizedMethodCall, MutatingScope $scope): ?InternalThrowPoint - { - foreach ($this->dynamicThrowTypeExtensionProvider->getDynamicMethodThrowTypeExtensions() as $extension) { - if (!$extension->isMethodSupported($methodReflection)) { - continue; - } - - $throwType = $extension->getThrowTypeFromMethodCall($methodReflection, $normalizedMethodCall, $scope); - if ($throwType === null) { - return null; - } - - return InternalThrowPoint::createExplicit($scope, $throwType, $normalizedMethodCall, false); - } - - if ( - in_array($methodReflection->getName(), ['invoke', 'invokeArgs'], true) - && in_array($methodReflection->getDeclaringClass()->getName(), [ReflectionMethod::class, ReflectionFunction::class], true) - ) { - return InternalThrowPoint::createImplicit($scope, $normalizedMethodCall); - } - - $throwType = $methodReflection->getThrowType(); - if ($throwType === null) { - $returnType = $parametersAcceptor->getReturnType(); - if ($returnType instanceof NeverType && $returnType->isExplicit()) { - $throwType = new ObjectType(Throwable::class); - } - } - - if ($throwType !== null) { - if (!$throwType->isVoid()->yes()) { - return InternalThrowPoint::createExplicit($scope, $throwType, $normalizedMethodCall, true); - } - } elseif ($this->implicitThrows) { - $methodReturnedType = $scope->getType($normalizedMethodCall); - if (!(new ObjectType(Throwable::class))->isSuperTypeOf($methodReturnedType)->yes()) { - return InternalThrowPoint::createImplicit($scope, $normalizedMethodCall); - } - } - - return null; - } - public function resolveType(MutatingScope $scope, Expr $expr): Type { if ($expr->name instanceof Identifier) { diff --git a/src/Analyser/ExprHandler/StaticCallHandler.php b/src/Analyser/ExprHandler/StaticCallHandler.php index c8e2b2cb9f3..ee0a758d491 100644 --- a/src/Analyser/ExprHandler/StaticCallHandler.php +++ b/src/Analyser/ExprHandler/StaticCallHandler.php @@ -17,6 +17,7 @@ use PHPStan\Analyser\ExpressionResultStorage; use PHPStan\Analyser\ExprHandler; use PHPStan\Analyser\ExprHandler\Helper\MethodCallReturnTypeHelper; +use PHPStan\Analyser\ExprHandler\Helper\MethodThrowPointHelper; use PHPStan\Analyser\ExprHandler\Helper\NullsafeShortCircuitingHelper; use PHPStan\Analyser\ImpurePoint; use PHPStan\Analyser\InternalThrowPoint; @@ -25,7 +26,6 @@ use PHPStan\Analyser\NoopNodeCallback; use PHPStan\DependencyInjection\AutowiredParameter; use PHPStan\DependencyInjection\AutowiredService; -use PHPStan\DependencyInjection\Type\DynamicThrowTypeExtensionProvider; use PHPStan\Node\Expr\PossiblyImpureCallExpr; use PHPStan\Reflection\Callables\SimpleImpurePoint; use PHPStan\Reflection\MethodReflection; @@ -39,7 +39,6 @@ use PHPStan\Type\TypeCombinator; use PHPStan\Type\TypeWithClassName; use ReflectionProperty; -use Throwable; use function array_map; use function array_merge; use function count; @@ -55,10 +54,8 @@ final class StaticCallHandler implements ExprHandler { public function __construct( - private DynamicThrowTypeExtensionProvider $dynamicThrowTypeExtensionProvider, private MethodCallReturnTypeHelper $methodCallReturnTypeHelper, - #[AutowiredParameter(ref: '%exceptions.implicitThrows%')] - private bool $implicitThrows, + private MethodThrowPointHelper $methodThrowPointHelper, #[AutowiredParameter] private bool $rememberPossiblyImpureFunctionValues, ) @@ -198,7 +195,7 @@ public function processExpr(NodeScopeResolver $nodeScopeResolver, Stmt $stmt, Ex $scopeFunction = $scope->getFunction(); if ($methodReflection !== null) { - $methodThrowPoint = $this->getStaticMethodThrowPoint($methodReflection, $normalizedExpr, $scope); + $methodThrowPoint = $this->methodThrowPointHelper->getThrowPoint($methodReflection, $parametersAcceptor, $normalizedExpr, $scope); if ($methodThrowPoint !== null) { $throwPoints[] = $methodThrowPoint; } @@ -268,36 +265,6 @@ public function processExpr(NodeScopeResolver $nodeScopeResolver, Stmt $stmt, Ex ); } - private function getStaticMethodThrowPoint(MethodReflection $methodReflection, StaticCall $normalizedMethodCall, MutatingScope $scope): ?InternalThrowPoint - { - foreach ($this->dynamicThrowTypeExtensionProvider->getDynamicStaticMethodThrowTypeExtensions() as $extension) { - if (!$extension->isStaticMethodSupported($methodReflection)) { - continue; - } - - $throwType = $extension->getThrowTypeFromStaticMethodCall($methodReflection, $normalizedMethodCall, $scope); - if ($throwType === null) { - return null; - } - - return InternalThrowPoint::createExplicit($scope, $throwType, $normalizedMethodCall, false); - } - - if ($methodReflection->getThrowType() !== null) { - $throwType = $methodReflection->getThrowType(); - if (!$throwType->isVoid()->yes()) { - return InternalThrowPoint::createExplicit($scope, $throwType, $normalizedMethodCall, true); - } - } elseif ($this->implicitThrows) { - $methodReturnedType = $scope->getType($normalizedMethodCall); - if (!(new ObjectType(Throwable::class))->isSuperTypeOf($methodReturnedType)->yes()) { - return InternalThrowPoint::createImplicit($scope, $normalizedMethodCall); - } - } - - return null; - } - public function resolveType(MutatingScope $scope, Expr $expr): Type { if ($expr->name instanceof Identifier) { diff --git a/src/Php/PhpVersion.php b/src/Php/PhpVersion.php index f24800b9a4d..9b198d567f5 100644 --- a/src/Php/PhpVersion.php +++ b/src/Php/PhpVersion.php @@ -506,4 +506,9 @@ public function hasFilterThrowOnFailureConstant(): bool return $this->versionId >= 80500; } + public function throwsOnStringCast(): bool + { + return $this->versionId >= 70400; + } + } diff --git a/tests/PHPStan/Rules/Exceptions/AbilityToDisableImplicitThrowsTest.php b/tests/PHPStan/Rules/Exceptions/AbilityToDisableImplicitThrowsTest.php index 8dd7f30a059..708941f96e1 100644 --- a/tests/PHPStan/Rules/Exceptions/AbilityToDisableImplicitThrowsTest.php +++ b/tests/PHPStan/Rules/Exceptions/AbilityToDisableImplicitThrowsTest.php @@ -69,6 +69,24 @@ public function testPropertyHooks(): void ]); } + public function testBug13806(): void + { + $this->analyse([__DIR__ . '/data/bug-13806.php'], [ + [ + 'Dead catch - Throwable is never thrown in the try block.', + 8, + ], + [ + 'Dead catch - Throwable is never thrown in the try block.', + 53, + ], + [ + 'Dead catch - Throwable is never thrown in the try block.', + 64, + ], + ]); + } + public static function getAdditionalConfigFiles(): array { return array_merge( diff --git a/tests/PHPStan/Rules/Exceptions/CatchWithUnthrownExceptionRuleTest.php b/tests/PHPStan/Rules/Exceptions/CatchWithUnthrownExceptionRuleTest.php index 71aa016b571..03e5554b19c 100644 --- a/tests/PHPStan/Rules/Exceptions/CatchWithUnthrownExceptionRuleTest.php +++ b/tests/PHPStan/Rules/Exceptions/CatchWithUnthrownExceptionRuleTest.php @@ -734,4 +734,18 @@ public function testBug9146NonStrict(): void ]); } + public function testBug13806(): void + { + $this->analyse([__DIR__ . '/data/bug-13806.php'], [ + [ + 'Dead catch - Throwable is never thrown in the try block.', + 53, + ], + [ + 'Dead catch - Throwable is never thrown in the try block.', + 64, + ], + ]); + } + } diff --git a/tests/PHPStan/Rules/Exceptions/data/bug-13806.php b/tests/PHPStan/Rules/Exceptions/data/bug-13806.php new file mode 100644 index 00000000000..c2900fb2f27 --- /dev/null +++ b/tests/PHPStan/Rules/Exceptions/data/bug-13806.php @@ -0,0 +1,70 @@ += 8.0 + +namespace Bug13806; + +function castToString(\Stringable|string $variable): string { + try { + $value = (string) $variable; + } catch(\Throwable) { + var_dump("Error thrown during string-conversion!"); + $value = ''; + } + + return $value; +} + +class MyString { + /** @return never */ + public function __toString() { + throw new \Exception(); + } +} + +castToString(new MyString()); + +class ThrowsException { + /** @throws \Exception */ + public function __toString(): string { + throw new \Exception(); + } +} + +function castThrowsException(ThrowsException $variable): string { + try { + $value = (string) $variable; + } catch(\Throwable) { + var_dump("Error thrown during string-conversion!"); + $value = ''; + } + + return $value; +} + +class ThrowsVoid { + /** @throws void */ + public function __toString(): string { + return 'hello'; + } +} + +function castThrowsVoid(ThrowsVoid $variable): string { + try { + $value = (string) $variable; + } catch(\Throwable) { + var_dump("Error thrown during string-conversion!"); + $value = ''; + } + + return $value; +} + +function castIntToString(int $variable): string { + try { + $value = (string) $variable; + } catch(\Throwable) { + var_dump("Error thrown during string-conversion!"); + $value = ''; + } + + return $value; +}