Skip to content

Commit 28b81c7

Browse files
staabmphpstan-bot
authored andcommitted
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()
1 parent 9c915ec commit 28b81c7

File tree

3 files changed

+52
-1
lines changed

3 files changed

+52
-1
lines changed

src/Analyser/ExprHandler/CastStringHandler.php

Lines changed: 25 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,11 +10,16 @@
1010
use PHPStan\Analyser\ExpressionResultStorage;
1111
use PHPStan\Analyser\ExprHandler;
1212
use PHPStan\Analyser\ImpurePoint;
13+
use PHPStan\Analyser\InternalThrowPoint;
1314
use PHPStan\Analyser\MutatingScope;
1415
use PHPStan\Analyser\NodeScopeResolver;
16+
use PHPStan\DependencyInjection\AutowiredParameter;
1517
use PHPStan\DependencyInjection\AutowiredService;
1618
use PHPStan\Reflection\InitializerExprTypeResolver;
19+
use PHPStan\Type\NeverType;
20+
use PHPStan\Type\ObjectType;
1721
use PHPStan\Type\Type;
22+
use Throwable;
1823
use function sprintf;
1924

2025
/**
@@ -26,6 +31,8 @@ final class CastStringHandler implements ExprHandler
2631

2732
public function __construct(
2833
private InitializerExprTypeResolver $initializerExprTypeResolver,
34+
#[AutowiredParameter(ref: '%exceptions.implicitThrows%')]
35+
private bool $implicitThrows,
2936
)
3037
{
3138
}
@@ -39,6 +46,7 @@ public function processExpr(NodeScopeResolver $nodeScopeResolver, Stmt $stmt, Ex
3946
{
4047
$exprResult = $nodeScopeResolver->processExprNode($stmt, $expr->expr, $scope, $storage, $nodeCallback, $context->enterDeep());
4148
$impurePoints = $exprResult->getImpurePoints();
49+
$throwPoints = $exprResult->getThrowPoints();
4250

4351
$exprType = $scope->getType($expr->expr);
4452
$toStringMethod = $scope->getMethodReflection($exprType, '__toString');
@@ -52,6 +60,22 @@ public function processExpr(NodeScopeResolver $nodeScopeResolver, Stmt $stmt, Ex
5260
$toStringMethod->isPure()->no(),
5361
);
5462
}
63+
64+
$throwType = $toStringMethod->getThrowType();
65+
if ($throwType === null) {
66+
$returnType = $toStringMethod->getVariants()[0]->getReturnType();
67+
if ($returnType instanceof NeverType && $returnType->isExplicit()) {
68+
$throwType = new ObjectType(Throwable::class);
69+
}
70+
}
71+
72+
if ($throwType !== null) {
73+
if (!$throwType->isVoid()->yes()) {
74+
$throwPoints[] = InternalThrowPoint::createExplicit($scope, $throwType, $expr, true);
75+
}
76+
} elseif ($this->implicitThrows) {
77+
$throwPoints[] = InternalThrowPoint::createImplicit($scope, $expr);
78+
}
5579
}
5680

5781
$scope = $exprResult->getScope();
@@ -60,7 +84,7 @@ public function processExpr(NodeScopeResolver $nodeScopeResolver, Stmt $stmt, Ex
6084
$scope,
6185
hasYield: $exprResult->hasYield(),
6286
isAlwaysTerminating: $exprResult->isAlwaysTerminating(),
63-
throwPoints: $exprResult->getThrowPoints(),
87+
throwPoints: $throwPoints,
6488
impurePoints: $impurePoints,
6589
truthyScopeCallback: static fn (): MutatingScope => $scope->filterByTruthyValue($expr),
6690
falseyScopeCallback: static fn (): MutatingScope => $scope->filterByFalseyValue($expr),

tests/PHPStan/Rules/Exceptions/CatchWithUnthrownExceptionRuleTest.php

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -734,4 +734,9 @@ public function testBug9146NonStrict(): void
734734
]);
735735
}
736736

737+
public function testBug13806(): void
738+
{
739+
$this->analyse([__DIR__ . '/data/bug-13806.php'], []);
740+
}
741+
737742
}
Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
<?php // lint >= 8.0
2+
3+
namespace Bug13806;
4+
5+
function castToString(\Stringable|string $variable): string {
6+
try {
7+
$value = (string) $variable;
8+
} catch(\Throwable) {
9+
var_dump("Error thrown during string-conversion!");
10+
$value = '';
11+
}
12+
13+
return $value;
14+
}
15+
16+
class MyString {
17+
public function __toString(): never {
18+
throw new \Exception();
19+
}
20+
}
21+
22+
castToString(new MyString());

0 commit comments

Comments
 (0)