From 89802089639ea9c55855129b9ab2cae629add0a1 Mon Sep 17 00:00:00 2001 From: staabm <120441+staabm@users.noreply.github.com> Date: Fri, 3 Apr 2026 09:04:34 +0000 Subject: [PATCH 1/9] Fix phpstan/phpstan#13806: String cast should create throw points for __toString() - Added throw point creation in CastStringHandler when __toString() method exists - Respects explicit throw types from __toString() and implicit throws configuration - New regression test in tests/PHPStan/Rules/Exceptions/data/bug-13806.php - The root cause was that CastStringHandler tracked impurity but not throw points for __toString() --- .../ExprHandler/CastStringHandler.php | 26 ++++++++++++++++++- .../CatchWithUnthrownExceptionRuleTest.php | 5 ++++ .../Rules/Exceptions/data/bug-13806.php | 22 ++++++++++++++++ 3 files changed, 52 insertions(+), 1 deletion(-) create mode 100644 tests/PHPStan/Rules/Exceptions/data/bug-13806.php diff --git a/src/Analyser/ExprHandler/CastStringHandler.php b/src/Analyser/ExprHandler/CastStringHandler.php index fff7f07d48d..40be28f1873 100644 --- a/src/Analyser/ExprHandler/CastStringHandler.php +++ b/src/Analyser/ExprHandler/CastStringHandler.php @@ -10,11 +10,16 @@ use PHPStan\Analyser\ExpressionResultStorage; use PHPStan\Analyser\ExprHandler; use PHPStan\Analyser\ImpurePoint; +use PHPStan\Analyser\InternalThrowPoint; use PHPStan\Analyser\MutatingScope; use PHPStan\Analyser\NodeScopeResolver; +use PHPStan\DependencyInjection\AutowiredParameter; use PHPStan\DependencyInjection\AutowiredService; use PHPStan\Reflection\InitializerExprTypeResolver; +use PHPStan\Type\NeverType; +use PHPStan\Type\ObjectType; use PHPStan\Type\Type; +use Throwable; use function sprintf; /** @@ -26,6 +31,8 @@ final class CastStringHandler implements ExprHandler public function __construct( private InitializerExprTypeResolver $initializerExprTypeResolver, + #[AutowiredParameter(ref: '%exceptions.implicitThrows%')] + private bool $implicitThrows, ) { } @@ -39,6 +46,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 +60,22 @@ public function processExpr(NodeScopeResolver $nodeScopeResolver, Stmt $stmt, Ex $toStringMethod->isPure()->no(), ); } + + $throwType = $toStringMethod->getThrowType(); + if ($throwType === null) { + $returnType = $toStringMethod->getVariants()[0]->getReturnType(); + if ($returnType instanceof NeverType && $returnType->isExplicit()) { + $throwType = new ObjectType(Throwable::class); + } + } + + if ($throwType !== null) { + if (!$throwType->isVoid()->yes()) { + $throwPoints[] = InternalThrowPoint::createExplicit($scope, $throwType, $expr, true); + } + } elseif ($this->implicitThrows) { + $throwPoints[] = InternalThrowPoint::createImplicit($scope, $expr); + } } $scope = $exprResult->getScope(); @@ -60,7 +84,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/tests/PHPStan/Rules/Exceptions/CatchWithUnthrownExceptionRuleTest.php b/tests/PHPStan/Rules/Exceptions/CatchWithUnthrownExceptionRuleTest.php index 71aa016b571..566f178eba1 100644 --- a/tests/PHPStan/Rules/Exceptions/CatchWithUnthrownExceptionRuleTest.php +++ b/tests/PHPStan/Rules/Exceptions/CatchWithUnthrownExceptionRuleTest.php @@ -734,4 +734,9 @@ public function testBug9146NonStrict(): void ]); } + public function testBug13806(): void + { + $this->analyse([__DIR__ . '/data/bug-13806.php'], []); + } + } 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..bd33d725a4d --- /dev/null +++ b/tests/PHPStan/Rules/Exceptions/data/bug-13806.php @@ -0,0 +1,22 @@ += 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 { + public function __toString(): never { + throw new \Exception(); + } +} + +castToString(new MyString()); From c1167430d458ceab36ce728756b5dfe271e8111d Mon Sep 17 00:00:00 2001 From: phpstan-bot Date: Fri, 3 Apr 2026 14:02:10 +0000 Subject: [PATCH 2/9] Make string cast throw points dependent on PHP version __toString() can only throw exceptions since PHP 7.4. Add PhpVersion::throwsOnStringCast() and gate the throw point creation in CastStringHandler on it. Co-Authored-By: Claude Opus 4.6 --- .../ExprHandler/CastStringHandler.php | 26 +++++++++++-------- src/Php/PhpVersion.php | 5 ++++ 2 files changed, 20 insertions(+), 11 deletions(-) diff --git a/src/Analyser/ExprHandler/CastStringHandler.php b/src/Analyser/ExprHandler/CastStringHandler.php index 40be28f1873..23f5060723c 100644 --- a/src/Analyser/ExprHandler/CastStringHandler.php +++ b/src/Analyser/ExprHandler/CastStringHandler.php @@ -15,6 +15,7 @@ use PHPStan\Analyser\NodeScopeResolver; use PHPStan\DependencyInjection\AutowiredParameter; use PHPStan\DependencyInjection\AutowiredService; +use PHPStan\Php\PhpVersion; use PHPStan\Reflection\InitializerExprTypeResolver; use PHPStan\Type\NeverType; use PHPStan\Type\ObjectType; @@ -31,6 +32,7 @@ final class CastStringHandler implements ExprHandler public function __construct( private InitializerExprTypeResolver $initializerExprTypeResolver, + private PhpVersion $phpVersion, #[AutowiredParameter(ref: '%exceptions.implicitThrows%')] private bool $implicitThrows, ) @@ -61,20 +63,22 @@ public function processExpr(NodeScopeResolver $nodeScopeResolver, Stmt $stmt, Ex ); } - $throwType = $toStringMethod->getThrowType(); - if ($throwType === null) { - $returnType = $toStringMethod->getVariants()[0]->getReturnType(); - if ($returnType instanceof NeverType && $returnType->isExplicit()) { - $throwType = new ObjectType(Throwable::class); + if ($this->phpVersion->throwsOnStringCast()) { + $throwType = $toStringMethod->getThrowType(); + if ($throwType === null) { + $returnType = $toStringMethod->getVariants()[0]->getReturnType(); + if ($returnType instanceof NeverType && $returnType->isExplicit()) { + $throwType = new ObjectType(Throwable::class); + } } - } - if ($throwType !== null) { - if (!$throwType->isVoid()->yes()) { - $throwPoints[] = InternalThrowPoint::createExplicit($scope, $throwType, $expr, true); + if ($throwType !== null) { + if (!$throwType->isVoid()->yes()) { + $throwPoints[] = InternalThrowPoint::createExplicit($scope, $throwType, $expr, true); + } + } elseif ($this->implicitThrows) { + $throwPoints[] = InternalThrowPoint::createImplicit($scope, $expr); } - } elseif ($this->implicitThrows) { - $throwPoints[] = InternalThrowPoint::createImplicit($scope, $expr); } } 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; + } + } From 5f45b022e3ef1d39ff4c6dc680a3b5ae3d93322e Mon Sep 17 00:00:00 2001 From: phpstan-bot Date: Fri, 3 Apr 2026 14:14:27 +0000 Subject: [PATCH 3/9] Add test for non-object string cast reporting dead catch Ensures that casting a non-object type (int) to string correctly reports the catch block as dead code, since no __toString() method is involved and no exception can be thrown. Co-Authored-By: Claude Opus 4.6 --- .../Exceptions/CatchWithUnthrownExceptionRuleTest.php | 7 ++++++- tests/PHPStan/Rules/Exceptions/data/bug-13806.php | 11 +++++++++++ 2 files changed, 17 insertions(+), 1 deletion(-) diff --git a/tests/PHPStan/Rules/Exceptions/CatchWithUnthrownExceptionRuleTest.php b/tests/PHPStan/Rules/Exceptions/CatchWithUnthrownExceptionRuleTest.php index 566f178eba1..60ca25066b2 100644 --- a/tests/PHPStan/Rules/Exceptions/CatchWithUnthrownExceptionRuleTest.php +++ b/tests/PHPStan/Rules/Exceptions/CatchWithUnthrownExceptionRuleTest.php @@ -736,7 +736,12 @@ public function testBug9146NonStrict(): void public function testBug13806(): void { - $this->analyse([__DIR__ . '/data/bug-13806.php'], []); + $this->analyse([__DIR__ . '/data/bug-13806.php'], [ + [ + 'Dead catch - Throwable is never thrown in the try block.', + 27, + ], + ]); } } diff --git a/tests/PHPStan/Rules/Exceptions/data/bug-13806.php b/tests/PHPStan/Rules/Exceptions/data/bug-13806.php index bd33d725a4d..612d41fb201 100644 --- a/tests/PHPStan/Rules/Exceptions/data/bug-13806.php +++ b/tests/PHPStan/Rules/Exceptions/data/bug-13806.php @@ -20,3 +20,14 @@ public function __toString(): never { } castToString(new MyString()); + +function castIntToString(int $variable): string { + try { + $value = (string) $variable; + } catch(\Throwable) { + var_dump("Error thrown during string-conversion!"); + $value = ''; + } + + return $value; +} From 8afde9540594827342d2ffbe907336e0e0573e68 Mon Sep 17 00:00:00 2001 From: phpstan-bot Date: Fri, 3 Apr 2026 14:34:21 +0000 Subject: [PATCH 4/9] Remove native return type from __toString() test to avoid lint failure The native `never` return type on `__toString()` causes a lint error since PHP requires `__toString()` to declare `string` as its return type. Use a `@return never` PHPDoc instead. Co-Authored-By: Claude Opus 4.6 --- .../Rules/Exceptions/CatchWithUnthrownExceptionRuleTest.php | 2 +- tests/PHPStan/Rules/Exceptions/data/bug-13806.php | 3 ++- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/tests/PHPStan/Rules/Exceptions/CatchWithUnthrownExceptionRuleTest.php b/tests/PHPStan/Rules/Exceptions/CatchWithUnthrownExceptionRuleTest.php index 60ca25066b2..cafe5544ed8 100644 --- a/tests/PHPStan/Rules/Exceptions/CatchWithUnthrownExceptionRuleTest.php +++ b/tests/PHPStan/Rules/Exceptions/CatchWithUnthrownExceptionRuleTest.php @@ -739,7 +739,7 @@ public function testBug13806(): void $this->analyse([__DIR__ . '/data/bug-13806.php'], [ [ 'Dead catch - Throwable is never thrown in the try block.', - 27, + 28, ], ]); } diff --git a/tests/PHPStan/Rules/Exceptions/data/bug-13806.php b/tests/PHPStan/Rules/Exceptions/data/bug-13806.php index 612d41fb201..d9022049973 100644 --- a/tests/PHPStan/Rules/Exceptions/data/bug-13806.php +++ b/tests/PHPStan/Rules/Exceptions/data/bug-13806.php @@ -14,7 +14,8 @@ function castToString(\Stringable|string $variable): string { } class MyString { - public function __toString(): never { + /** @return never */ + public function __toString() { throw new \Exception(); } } From 0d082bf010d7d0a14bcd3d8b50f8f5bca72b8537 Mon Sep 17 00:00:00 2001 From: phpstan-bot Date: Fri, 3 Apr 2026 14:50:49 +0000 Subject: [PATCH 5/9] Add tests for @throws Exception and @throws void on string cast Co-Authored-By: Claude Opus 4.6 --- .../CatchWithUnthrownExceptionRuleTest.php | 6 +++- .../Rules/Exceptions/data/bug-13806.php | 36 +++++++++++++++++++ 2 files changed, 41 insertions(+), 1 deletion(-) diff --git a/tests/PHPStan/Rules/Exceptions/CatchWithUnthrownExceptionRuleTest.php b/tests/PHPStan/Rules/Exceptions/CatchWithUnthrownExceptionRuleTest.php index cafe5544ed8..03e5554b19c 100644 --- a/tests/PHPStan/Rules/Exceptions/CatchWithUnthrownExceptionRuleTest.php +++ b/tests/PHPStan/Rules/Exceptions/CatchWithUnthrownExceptionRuleTest.php @@ -739,7 +739,11 @@ public function testBug13806(): void $this->analyse([__DIR__ . '/data/bug-13806.php'], [ [ 'Dead catch - Throwable is never thrown in the try block.', - 28, + 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 index d9022049973..c2900fb2f27 100644 --- a/tests/PHPStan/Rules/Exceptions/data/bug-13806.php +++ b/tests/PHPStan/Rules/Exceptions/data/bug-13806.php @@ -22,6 +22,42 @@ public function __toString() { 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; From f9188ced94df6d4b009ca36bd130682013401a8f Mon Sep 17 00:00:00 2001 From: phpstan-bot Date: Fri, 3 Apr 2026 15:11:09 +0000 Subject: [PATCH 6/9] Use getOnlyVariant() instead of getVariants()[0] for __toString() __toString() always has exactly one variant, so getOnlyVariant() is the correct API to use here. Co-Authored-By: Claude Opus 4.6 --- src/Analyser/ExprHandler/CastStringHandler.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Analyser/ExprHandler/CastStringHandler.php b/src/Analyser/ExprHandler/CastStringHandler.php index 23f5060723c..c7117f66ae4 100644 --- a/src/Analyser/ExprHandler/CastStringHandler.php +++ b/src/Analyser/ExprHandler/CastStringHandler.php @@ -66,7 +66,7 @@ public function processExpr(NodeScopeResolver $nodeScopeResolver, Stmt $stmt, Ex if ($this->phpVersion->throwsOnStringCast()) { $throwType = $toStringMethod->getThrowType(); if ($throwType === null) { - $returnType = $toStringMethod->getVariants()[0]->getReturnType(); + $returnType = $toStringMethod->getOnlyVariant()->getReturnType(); if ($returnType instanceof NeverType && $returnType->isExplicit()) { $throwType = new ObjectType(Throwable::class); } From a2a0f3b074d7f8e08731860a9c179115785efe6e Mon Sep 17 00:00:00 2001 From: phpstan-bot Date: Fri, 3 Apr 2026 15:14:17 +0000 Subject: [PATCH 7/9] Use getOnlyVariant() and add test for implicitThrows=false Address review feedback: - Use getOnlyVariant() instead of getVariants()[0] for __toString() - Add test in AbilityToDisableImplicitThrowsTest to verify string cast throw points behave correctly when implicitThrows is disabled Co-Authored-By: Claude Opus 4.6 --- .../AbilityToDisableImplicitThrowsTest.php | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) 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( From d898b9ab568d93fa80180cb8e7b635b4f5ff3a7c Mon Sep 17 00:00:00 2001 From: phpstan-bot Date: Fri, 3 Apr 2026 16:00:58 +0000 Subject: [PATCH 8/9] Extract MethodThrowPointHelper to share throw point logic Refactor duplicate throw point resolution from CastStringHandler, MethodCallHandler, and StaticCallHandler into a shared helper class. This also fixes StaticCallHandler missing the never-return-type fallback to Throwable that MethodCallHandler already had. Co-Authored-By: Claude Opus 4.6 --- .../ExprHandler/CastStringHandler.php | 32 +++--- .../Helper/MethodThrowPointHelper.php | 97 +++++++++++++++++++ .../ExprHandler/MethodCallHandler.php | 59 +---------- .../ExprHandler/StaticCallHandler.php | 39 +------- 4 files changed, 114 insertions(+), 113 deletions(-) create mode 100644 src/Analyser/ExprHandler/Helper/MethodThrowPointHelper.php diff --git a/src/Analyser/ExprHandler/CastStringHandler.php b/src/Analyser/ExprHandler/CastStringHandler.php index c7117f66ae4..b04319c54fe 100644 --- a/src/Analyser/ExprHandler/CastStringHandler.php +++ b/src/Analyser/ExprHandler/CastStringHandler.php @@ -4,23 +4,20 @@ 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\InternalThrowPoint; use PHPStan\Analyser\MutatingScope; use PHPStan\Analyser\NodeScopeResolver; -use PHPStan\DependencyInjection\AutowiredParameter; use PHPStan\DependencyInjection\AutowiredService; use PHPStan\Php\PhpVersion; use PHPStan\Reflection\InitializerExprTypeResolver; -use PHPStan\Type\NeverType; -use PHPStan\Type\ObjectType; use PHPStan\Type\Type; -use Throwable; use function sprintf; /** @@ -33,8 +30,7 @@ final class CastStringHandler implements ExprHandler public function __construct( private InitializerExprTypeResolver $initializerExprTypeResolver, private PhpVersion $phpVersion, - #[AutowiredParameter(ref: '%exceptions.implicitThrows%')] - private bool $implicitThrows, + private MethodThrowPointHelper $methodThrowPointHelper, ) { } @@ -64,20 +60,14 @@ public function processExpr(NodeScopeResolver $nodeScopeResolver, Stmt $stmt, Ex } if ($this->phpVersion->throwsOnStringCast()) { - $throwType = $toStringMethod->getThrowType(); - if ($throwType === null) { - $returnType = $toStringMethod->getOnlyVariant()->getReturnType(); - if ($returnType instanceof NeverType && $returnType->isExplicit()) { - $throwType = new ObjectType(Throwable::class); - } - } - - if ($throwType !== null) { - if (!$throwType->isVoid()->yes()) { - $throwPoints[] = InternalThrowPoint::createExplicit($scope, $throwType, $expr, true); - } - } elseif ($this->implicitThrows) { - $throwPoints[] = InternalThrowPoint::createImplicit($scope, $expr); + $throwPoint = $this->methodThrowPointHelper->getThrowPoint( + $toStringMethod, + $toStringMethod->getOnlyVariant(), + new Expr\MethodCall($expr->expr, new Identifier('__toString')), + $scope + ); + if ($throwPoint !== null) { + $throwPoints[] = $throwPoint; } } } diff --git a/src/Analyser/ExprHandler/Helper/MethodThrowPointHelper.php b/src/Analyser/ExprHandler/Helper/MethodThrowPointHelper.php new file mode 100644 index 00000000000..a0949f37ed0 --- /dev/null +++ b/src/Analyser/ExprHandler/Helper/MethodThrowPointHelper.php @@ -0,0 +1,97 @@ +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) { From eacb40394fa566f81e661db5086158af106c08bf Mon Sep 17 00:00:00 2001 From: phpstan-bot Date: Fri, 3 Apr 2026 16:46:38 +0000 Subject: [PATCH 9/9] Fix coding standard violations in CastStringHandler and MethodThrowPointHelper Co-Authored-By: Claude Opus 4.6 --- src/Analyser/ExprHandler/CastStringHandler.php | 2 +- src/Analyser/ExprHandler/Helper/MethodThrowPointHelper.php | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/src/Analyser/ExprHandler/CastStringHandler.php b/src/Analyser/ExprHandler/CastStringHandler.php index b04319c54fe..336e14dd921 100644 --- a/src/Analyser/ExprHandler/CastStringHandler.php +++ b/src/Analyser/ExprHandler/CastStringHandler.php @@ -64,7 +64,7 @@ public function processExpr(NodeScopeResolver $nodeScopeResolver, Stmt $stmt, Ex $toStringMethod, $toStringMethod->getOnlyVariant(), new Expr\MethodCall($expr->expr, new Identifier('__toString')), - $scope + $scope, ); if ($throwPoint !== null) { $throwPoints[] = $throwPoint; diff --git a/src/Analyser/ExprHandler/Helper/MethodThrowPointHelper.php b/src/Analyser/ExprHandler/Helper/MethodThrowPointHelper.php index a0949f37ed0..05b1314b5b1 100644 --- a/src/Analyser/ExprHandler/Helper/MethodThrowPointHelper.php +++ b/src/Analyser/ExprHandler/Helper/MethodThrowPointHelper.php @@ -16,6 +16,7 @@ use ReflectionFunction; use ReflectionMethod; use Throwable; +use function in_array; #[AutowiredService] final class MethodThrowPointHelper