From 029c183f4b318621dc3c26823a8a26d56f7e8fcf Mon Sep 17 00:00:00 2001 From: Can Vural Date: Fri, 14 Nov 2025 15:32:47 +0100 Subject: [PATCH 1/2] string cast might throw exception --- src/Analyser/NodeScopeResolver.php | 6 ++++++ .../CatchWithUnthrownExceptionRuleTest.php | 5 +++++ .../Rules/Exceptions/data/bug-13806.php | 20 +++++++++++++++++++ 3 files changed, 31 insertions(+) create mode 100644 tests/PHPStan/Rules/Exceptions/data/bug-13806.php diff --git a/src/Analyser/NodeScopeResolver.php b/src/Analyser/NodeScopeResolver.php index 2a4aa5f449..7afc6bd39c 100644 --- a/src/Analyser/NodeScopeResolver.php +++ b/src/Analyser/NodeScopeResolver.php @@ -3619,6 +3619,12 @@ static function (): void { $exprType = $scope->getType($expr->expr); $toStringMethod = $scope->getMethodReflection($exprType, '__toString'); if ($toStringMethod !== null) { + if ($toStringMethod->getThrowType() !== null) { + $throwPoints[] = InternalThrowPoint::createExplicit($scope, $toStringMethod->getThrowType(), $expr, false); + } else { + $throwPoints[] = InternalThrowPoint::createImplicit($scope, $expr); + } + if (!$toStringMethod->hasSideEffects()->no()) { $impurePoints[] = new ImpurePoint( $scope, diff --git a/tests/PHPStan/Rules/Exceptions/CatchWithUnthrownExceptionRuleTest.php b/tests/PHPStan/Rules/Exceptions/CatchWithUnthrownExceptionRuleTest.php index 272c010d97..7eae69de1c 100644 --- a/tests/PHPStan/Rules/Exceptions/CatchWithUnthrownExceptionRuleTest.php +++ b/tests/PHPStan/Rules/Exceptions/CatchWithUnthrownExceptionRuleTest.php @@ -643,4 +643,9 @@ public function testPropertyHooks(): 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 0000000000..7bca04b411 --- /dev/null +++ b/tests/PHPStan/Rules/Exceptions/data/bug-13806.php @@ -0,0 +1,20 @@ + Date: Fri, 14 Nov 2025 17:24:38 +0100 Subject: [PATCH 2/2] add more tests and add PHP version check --- src/Analyser/NodeScopeResolver.php | 10 +++++---- src/Php/PhpVersion.php | 5 +++++ .../CatchWithUnthrownExceptionRuleTest.php | 7 +++++- .../Rules/Exceptions/data/bug-13806.php | 22 +++++++++++++++++-- 4 files changed, 37 insertions(+), 7 deletions(-) diff --git a/src/Analyser/NodeScopeResolver.php b/src/Analyser/NodeScopeResolver.php index 7afc6bd39c..ea9b94acde 100644 --- a/src/Analyser/NodeScopeResolver.php +++ b/src/Analyser/NodeScopeResolver.php @@ -3619,10 +3619,12 @@ static function (): void { $exprType = $scope->getType($expr->expr); $toStringMethod = $scope->getMethodReflection($exprType, '__toString'); if ($toStringMethod !== null) { - if ($toStringMethod->getThrowType() !== null) { - $throwPoints[] = InternalThrowPoint::createExplicit($scope, $toStringMethod->getThrowType(), $expr, false); - } else { - $throwPoints[] = InternalThrowPoint::createImplicit($scope, $expr); + if ($this->phpVersion->throwsOnStringCast()) { + if ($toStringMethod->getThrowType() !== null) { + $throwPoints[] = InternalThrowPoint::createExplicit($scope, $toStringMethod->getThrowType(), $expr, false); + } else { + $throwPoints[] = InternalThrowPoint::createImplicit($scope, $expr); + } } if (!$toStringMethod->hasSideEffects()->no()) { diff --git a/src/Php/PhpVersion.php b/src/Php/PhpVersion.php index 9657ab9818..1947cfdf05 100644 --- a/src/Php/PhpVersion.php +++ b/src/Php/PhpVersion.php @@ -479,4 +479,9 @@ public function deprecatesIncOnNonNumericString(): bool return $this->versionId >= 80500; } + public function throwsOnStringCast(): bool + { + return $this->versionId >= 70400; + } + } diff --git a/tests/PHPStan/Rules/Exceptions/CatchWithUnthrownExceptionRuleTest.php b/tests/PHPStan/Rules/Exceptions/CatchWithUnthrownExceptionRuleTest.php index 7eae69de1c..f4e94b22a4 100644 --- a/tests/PHPStan/Rules/Exceptions/CatchWithUnthrownExceptionRuleTest.php +++ b/tests/PHPStan/Rules/Exceptions/CatchWithUnthrownExceptionRuleTest.php @@ -645,7 +645,12 @@ public function testPropertyHooks(): void public function testBug13806(): void { - $this->analyse([__DIR__ . '/data/bug-13806.php'], []); + $this->analyse([__DIR__ . '/data/bug-13806.php'], [ + [ + 'Dead catch - InvalidArgumentException is never thrown in the try block.', + 16, + ], + ]); } } diff --git a/tests/PHPStan/Rules/Exceptions/data/bug-13806.php b/tests/PHPStan/Rules/Exceptions/data/bug-13806.php index 7bca04b411..ec9e021791 100644 --- a/tests/PHPStan/Rules/Exceptions/data/bug-13806.php +++ b/tests/PHPStan/Rules/Exceptions/data/bug-13806.php @@ -2,14 +2,24 @@ namespace Bug13806; -function doFoo(MyString $myVariable): void +function doFoo(MyString $myVariable, MyStringVoid $string, $mixed): void { try { (string) $myVariable; - } catch (\InvalidArgumentException) { + } catch (\InvalidArgumentException $e) { // Reported as dead catch, even though the `__toString()` method // in `$myVariable` might throw an exception. } + + try { + (string) $string; + } catch (\InvalidArgumentException $e) { + } + + try { + (string) $mixed; + } catch (\InvalidArgumentException $e) { + } } class MyString { @@ -18,3 +28,11 @@ public function __toString() { throw new \InvalidArgumentException(); } } + +class MyStringVoid { + /** @throws void */ + public function __toString() + { + throw new \InvalidArgumentException(); + } +}