Skip to content

Commit 01f336d

Browse files
VincentLangletphpstan-bot
authored andcommitted
Fix phpstan/phpstan#5952: Track __toString() throws in echo statements
- Added __toString() throw point tracking to echo statement handling in NodeScopeResolver - Reuses existing CastStringHandler pattern: checks PhpVersion::throwsOnStringCast() and MethodThrowPointHelper - New regression test in tests/PHPStan/Rules/Exceptions/data/bug-5952.php - The root cause was that echo $obj did not consider implicit __toString() calls when collecting throw points
1 parent 7b8a821 commit 01f336d

File tree

3 files changed

+91
-0
lines changed

3 files changed

+91
-0
lines changed

src/Analyser/NodeScopeResolver.php

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -110,6 +110,7 @@
110110
use PHPStan\Parser\ImmediatelyInvokedClosureVisitor;
111111
use PHPStan\Parser\LineAttributesVisitor;
112112
use PHPStan\Parser\Parser;
113+
use PHPStan\Php\PhpVersion;
113114
use PHPStan\PhpDoc\PhpDocInheritanceResolver;
114115
use PHPStan\PhpDoc\ResolvedPhpDocBlock;
115116
use PHPStan\PhpDoc\Tag\VarTag;
@@ -862,9 +863,26 @@ public function processStmtNode(
862863
$hasYield = false;
863864
$throwPoints = [];
864865
$isAlwaysTerminating = false;
866+
$phpVersion = $this->container->getByType(PhpVersion::class);
867+
$methodThrowPointHelper = $this->container->getByType(ExprHandler\Helper\MethodThrowPointHelper::class);
865868
foreach ($stmt->exprs as $echoExpr) {
866869
$result = $this->processExprNode($stmt, $echoExpr, $scope, $storage, $nodeCallback, ExpressionContext::createDeep());
867870
$throwPoints = array_merge($throwPoints, $result->getThrowPoints());
871+
if ($phpVersion->throwsOnStringCast()) {
872+
$exprType = $scope->getType($echoExpr);
873+
$toStringMethod = $scope->getMethodReflection($exprType, '__toString');
874+
if ($toStringMethod !== null) {
875+
$throwPoint = $methodThrowPointHelper->getThrowPoint(
876+
$toStringMethod,
877+
$toStringMethod->getOnlyVariant(),
878+
new Expr\MethodCall($echoExpr, new Identifier('__toString')),
879+
$scope,
880+
);
881+
if ($throwPoint !== null) {
882+
$throwPoints[] = $throwPoint;
883+
}
884+
}
885+
}
868886
$scope = $result->getScope();
869887
$hasYield = $hasYield || $result->hasYield();
870888
$isAlwaysTerminating = $isAlwaysTerminating || $result->isAlwaysTerminating();

tests/PHPStan/Rules/Exceptions/CatchWithUnthrownExceptionRuleTest.php

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -748,4 +748,18 @@ public function testBug13806(): void
748748
]);
749749
}
750750

751+
public function testBug5952(): void
752+
{
753+
$this->analyse([__DIR__ . '/data/bug-5952.php'], [
754+
[
755+
'Dead catch - Exception is never thrown in the try block.',
756+
51,
757+
],
758+
[
759+
'Dead catch - Exception is never thrown in the try block.',
760+
57,
761+
],
762+
]);
763+
}
764+
751765
}
Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
1+
<?php // lint >= 7.4
2+
3+
namespace Bug5952;
4+
5+
class Foo
6+
{
7+
public function __toString(): string
8+
{
9+
throw new \Exception();
10+
}
11+
}
12+
13+
$foo = new Foo();
14+
15+
try {
16+
echo $foo;
17+
} catch (\Exception $e) {
18+
echo "Should be printed";
19+
}
20+
21+
class Bar
22+
{
23+
/** @throws \Exception */
24+
public function __toString(): string
25+
{
26+
throw new \Exception();
27+
}
28+
}
29+
30+
$bar = new Bar();
31+
32+
try {
33+
echo $bar;
34+
} catch (\Exception $e) {
35+
echo "Should be printed";
36+
}
37+
38+
class Baz
39+
{
40+
/** @throws void */
41+
public function __toString(): string
42+
{
43+
return 'hello';
44+
}
45+
}
46+
47+
$baz = new Baz();
48+
49+
try {
50+
echo $baz;
51+
} catch (\Exception $e) {
52+
echo "Should not be printed";
53+
}
54+
55+
try {
56+
echo 123;
57+
} catch (\Exception $e) {
58+
echo "Should not be printed";
59+
}

0 commit comments

Comments
 (0)