Skip to content

Commit c7c7d7a

Browse files
phpstan-botstaabmclaude
authored
Fix phpstan/phpstan#13806: PHPStan assumes string conversions cannot throw exceptions (#5391)
Co-authored-by: staabm <120441+staabm@users.noreply.github.com> Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
1 parent 4e614b3 commit c7c7d7a

File tree

8 files changed

+230
-93
lines changed

8 files changed

+230
-93
lines changed

src/Analyser/ExprHandler/CastStringHandler.php

Lines changed: 19 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,15 +4,18 @@
44

55
use PhpParser\Node\Expr;
66
use PhpParser\Node\Expr\Cast;
7+
use PhpParser\Node\Identifier;
78
use PhpParser\Node\Stmt;
89
use PHPStan\Analyser\ExpressionContext;
910
use PHPStan\Analyser\ExpressionResult;
1011
use PHPStan\Analyser\ExpressionResultStorage;
1112
use PHPStan\Analyser\ExprHandler;
13+
use PHPStan\Analyser\ExprHandler\Helper\MethodThrowPointHelper;
1214
use PHPStan\Analyser\ImpurePoint;
1315
use PHPStan\Analyser\MutatingScope;
1416
use PHPStan\Analyser\NodeScopeResolver;
1517
use PHPStan\DependencyInjection\AutowiredService;
18+
use PHPStan\Php\PhpVersion;
1619
use PHPStan\Reflection\InitializerExprTypeResolver;
1720
use PHPStan\Type\Type;
1821
use function sprintf;
@@ -26,6 +29,8 @@ final class CastStringHandler implements ExprHandler
2629

2730
public function __construct(
2831
private InitializerExprTypeResolver $initializerExprTypeResolver,
32+
private PhpVersion $phpVersion,
33+
private MethodThrowPointHelper $methodThrowPointHelper,
2934
)
3035
{
3136
}
@@ -39,6 +44,7 @@ public function processExpr(NodeScopeResolver $nodeScopeResolver, Stmt $stmt, Ex
3944
{
4045
$exprResult = $nodeScopeResolver->processExprNode($stmt, $expr->expr, $scope, $storage, $nodeCallback, $context->enterDeep());
4146
$impurePoints = $exprResult->getImpurePoints();
47+
$throwPoints = $exprResult->getThrowPoints();
4248

4349
$exprType = $scope->getType($expr->expr);
4450
$toStringMethod = $scope->getMethodReflection($exprType, '__toString');
@@ -52,6 +58,18 @@ public function processExpr(NodeScopeResolver $nodeScopeResolver, Stmt $stmt, Ex
5258
$toStringMethod->isPure()->no(),
5359
);
5460
}
61+
62+
if ($this->phpVersion->throwsOnStringCast()) {
63+
$throwPoint = $this->methodThrowPointHelper->getThrowPoint(
64+
$toStringMethod,
65+
$toStringMethod->getOnlyVariant(),
66+
new Expr\MethodCall($expr->expr, new Identifier('__toString')),
67+
$scope,
68+
);
69+
if ($throwPoint !== null) {
70+
$throwPoints[] = $throwPoint;
71+
}
72+
}
5573
}
5674

5775
$scope = $exprResult->getScope();
@@ -60,7 +78,7 @@ public function processExpr(NodeScopeResolver $nodeScopeResolver, Stmt $stmt, Ex
6078
$scope,
6179
hasYield: $exprResult->hasYield(),
6280
isAlwaysTerminating: $exprResult->isAlwaysTerminating(),
63-
throwPoints: $exprResult->getThrowPoints(),
81+
throwPoints: $throwPoints,
6482
impurePoints: $impurePoints,
6583
truthyScopeCallback: static fn (): MutatingScope => $scope->filterByTruthyValue($expr),
6684
falseyScopeCallback: static fn (): MutatingScope => $scope->filterByFalseyValue($expr),
Lines changed: 98 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,98 @@
1+
<?php declare(strict_types = 1);
2+
3+
namespace PHPStan\Analyser\ExprHandler\Helper;
4+
5+
use PhpParser\Node\Expr\MethodCall;
6+
use PhpParser\Node\Expr\StaticCall;
7+
use PHPStan\Analyser\InternalThrowPoint;
8+
use PHPStan\Analyser\MutatingScope;
9+
use PHPStan\DependencyInjection\AutowiredParameter;
10+
use PHPStan\DependencyInjection\AutowiredService;
11+
use PHPStan\DependencyInjection\Type\DynamicThrowTypeExtensionProvider;
12+
use PHPStan\Reflection\MethodReflection;
13+
use PHPStan\Reflection\ParametersAcceptor;
14+
use PHPStan\Type\NeverType;
15+
use PHPStan\Type\ObjectType;
16+
use ReflectionFunction;
17+
use ReflectionMethod;
18+
use Throwable;
19+
use function in_array;
20+
21+
#[AutowiredService]
22+
final class MethodThrowPointHelper
23+
{
24+
25+
public function __construct(
26+
private DynamicThrowTypeExtensionProvider $dynamicThrowTypeExtensionProvider,
27+
#[AutowiredParameter(ref: '%exceptions.implicitThrows%')]
28+
private bool $implicitThrows,
29+
)
30+
{
31+
}
32+
33+
public function getThrowPoint(
34+
MethodReflection $methodReflection,
35+
ParametersAcceptor $parametersAcceptor,
36+
MethodCall|StaticCall $normalizedMethodCall,
37+
MutatingScope $scope,
38+
): ?InternalThrowPoint
39+
{
40+
if ($normalizedMethodCall instanceof MethodCall) {
41+
foreach ($this->dynamicThrowTypeExtensionProvider->getDynamicMethodThrowTypeExtensions() as $extension) {
42+
if (!$extension->isMethodSupported($methodReflection)) {
43+
continue;
44+
}
45+
46+
$throwType = $extension->getThrowTypeFromMethodCall($methodReflection, $normalizedMethodCall, $scope);
47+
if ($throwType === null) {
48+
return null;
49+
}
50+
51+
return InternalThrowPoint::createExplicit($scope, $throwType, $normalizedMethodCall, false);
52+
}
53+
} else {
54+
foreach ($this->dynamicThrowTypeExtensionProvider->getDynamicStaticMethodThrowTypeExtensions() as $extension) {
55+
if (!$extension->isStaticMethodSupported($methodReflection)) {
56+
continue;
57+
}
58+
59+
$throwType = $extension->getThrowTypeFromStaticMethodCall($methodReflection, $normalizedMethodCall, $scope);
60+
if ($throwType === null) {
61+
return null;
62+
}
63+
64+
return InternalThrowPoint::createExplicit($scope, $throwType, $normalizedMethodCall, false);
65+
}
66+
}
67+
68+
if (
69+
$normalizedMethodCall instanceof MethodCall
70+
&& in_array($methodReflection->getName(), ['invoke', 'invokeArgs'], true)
71+
&& in_array($methodReflection->getDeclaringClass()->getName(), [ReflectionMethod::class, ReflectionFunction::class], true)
72+
) {
73+
return InternalThrowPoint::createImplicit($scope, $normalizedMethodCall);
74+
}
75+
76+
$throwType = $methodReflection->getThrowType();
77+
if ($throwType === null) {
78+
$returnType = $parametersAcceptor->getReturnType();
79+
if ($returnType instanceof NeverType && $returnType->isExplicit()) {
80+
$throwType = new ObjectType(Throwable::class);
81+
}
82+
}
83+
84+
if ($throwType !== null) {
85+
if (!$throwType->isVoid()->yes()) {
86+
return InternalThrowPoint::createExplicit($scope, $throwType, $normalizedMethodCall, true);
87+
}
88+
} elseif ($this->implicitThrows) {
89+
$methodReturnedType = $scope->getType($normalizedMethodCall);
90+
if (!(new ObjectType(Throwable::class))->isSuperTypeOf($methodReturnedType)->yes()) {
91+
return InternalThrowPoint::createImplicit($scope, $normalizedMethodCall);
92+
}
93+
}
94+
95+
return null;
96+
}
97+
98+
}

src/Analyser/ExprHandler/MethodCallHandler.php

Lines changed: 3 additions & 56 deletions
Original file line numberDiff line numberDiff line change
@@ -14,38 +14,31 @@
1414
use PHPStan\Analyser\ExpressionResultStorage;
1515
use PHPStan\Analyser\ExprHandler;
1616
use PHPStan\Analyser\ExprHandler\Helper\MethodCallReturnTypeHelper;
17+
use PHPStan\Analyser\ExprHandler\Helper\MethodThrowPointHelper;
1718
use PHPStan\Analyser\ExprHandler\Helper\NullsafeShortCircuitingHelper;
1819
use PHPStan\Analyser\ImpurePoint;
1920
use PHPStan\Analyser\InternalThrowPoint;
2021
use PHPStan\Analyser\MutatingScope;
2122
use PHPStan\Analyser\NodeScopeResolver;
2223
use PHPStan\DependencyInjection\AutowiredParameter;
2324
use PHPStan\DependencyInjection\AutowiredService;
24-
use PHPStan\DependencyInjection\Type\DynamicThrowTypeExtensionProvider;
2525
use PHPStan\Node\Expr\PossiblyImpureCallExpr;
2626
use PHPStan\Node\InvalidateExprNode;
2727
use PHPStan\Reflection\Callables\SimpleImpurePoint;
2828
use PHPStan\Reflection\ExtendedParametersAcceptor;
29-
use PHPStan\Reflection\MethodReflection;
30-
use PHPStan\Reflection\ParametersAcceptor;
3129
use PHPStan\Reflection\ParametersAcceptorSelector;
3230
use PHPStan\Type\ErrorType;
3331
use PHPStan\Type\Generic\TemplateTypeHelper;
3432
use PHPStan\Type\Generic\TemplateTypeVariance;
3533
use PHPStan\Type\Generic\TemplateTypeVarianceMap;
3634
use PHPStan\Type\MixedType;
3735
use PHPStan\Type\NeverType;
38-
use PHPStan\Type\ObjectType;
3936
use PHPStan\Type\Type;
4037
use PHPStan\Type\TypeCombinator;
4138
use PHPStan\Type\TypeUtils;
42-
use ReflectionFunction;
43-
use ReflectionMethod;
44-
use Throwable;
4539
use function array_map;
4640
use function array_merge;
4741
use function count;
48-
use function in_array;
4942
use function sprintf;
5043
use function strtolower;
5144

@@ -57,10 +50,8 @@ final class MethodCallHandler implements ExprHandler
5750
{
5851

5952
public function __construct(
60-
private DynamicThrowTypeExtensionProvider $dynamicThrowTypeExtensionProvider,
6153
private MethodCallReturnTypeHelper $methodCallReturnTypeHelper,
62-
#[AutowiredParameter(ref: '%exceptions.implicitThrows%')]
63-
private bool $implicitThrows,
54+
private MethodThrowPointHelper $methodThrowPointHelper,
6455
#[AutowiredParameter]
6556
private bool $rememberPossiblyImpureFunctionValues,
6657
)
@@ -153,7 +144,7 @@ public function processExpr(NodeScopeResolver $nodeScopeResolver, Stmt $stmt, Ex
153144
$scope = $argsResult->getScope();
154145

155146
if ($methodReflection !== null) {
156-
$methodThrowPoint = $this->getMethodThrowPoint($methodReflection, $parametersAcceptor, $normalizedExpr, $scope);
147+
$methodThrowPoint = $this->methodThrowPointHelper->getThrowPoint($methodReflection, $parametersAcceptor, $normalizedExpr, $scope);
157148
if ($methodThrowPoint !== null) {
158149
$throwPoints[] = $methodThrowPoint;
159150
}
@@ -235,50 +226,6 @@ public function processExpr(NodeScopeResolver $nodeScopeResolver, Stmt $stmt, Ex
235226
return $result;
236227
}
237228

238-
private function getMethodThrowPoint(MethodReflection $methodReflection, ParametersAcceptor $parametersAcceptor, MethodCall $normalizedMethodCall, MutatingScope $scope): ?InternalThrowPoint
239-
{
240-
foreach ($this->dynamicThrowTypeExtensionProvider->getDynamicMethodThrowTypeExtensions() as $extension) {
241-
if (!$extension->isMethodSupported($methodReflection)) {
242-
continue;
243-
}
244-
245-
$throwType = $extension->getThrowTypeFromMethodCall($methodReflection, $normalizedMethodCall, $scope);
246-
if ($throwType === null) {
247-
return null;
248-
}
249-
250-
return InternalThrowPoint::createExplicit($scope, $throwType, $normalizedMethodCall, false);
251-
}
252-
253-
if (
254-
in_array($methodReflection->getName(), ['invoke', 'invokeArgs'], true)
255-
&& in_array($methodReflection->getDeclaringClass()->getName(), [ReflectionMethod::class, ReflectionFunction::class], true)
256-
) {
257-
return InternalThrowPoint::createImplicit($scope, $normalizedMethodCall);
258-
}
259-
260-
$throwType = $methodReflection->getThrowType();
261-
if ($throwType === null) {
262-
$returnType = $parametersAcceptor->getReturnType();
263-
if ($returnType instanceof NeverType && $returnType->isExplicit()) {
264-
$throwType = new ObjectType(Throwable::class);
265-
}
266-
}
267-
268-
if ($throwType !== null) {
269-
if (!$throwType->isVoid()->yes()) {
270-
return InternalThrowPoint::createExplicit($scope, $throwType, $normalizedMethodCall, true);
271-
}
272-
} elseif ($this->implicitThrows) {
273-
$methodReturnedType = $scope->getType($normalizedMethodCall);
274-
if (!(new ObjectType(Throwable::class))->isSuperTypeOf($methodReturnedType)->yes()) {
275-
return InternalThrowPoint::createImplicit($scope, $normalizedMethodCall);
276-
}
277-
}
278-
279-
return null;
280-
}
281-
282229
public function resolveType(MutatingScope $scope, Expr $expr): Type
283230
{
284231
if ($expr->name instanceof Identifier) {

src/Analyser/ExprHandler/StaticCallHandler.php

Lines changed: 3 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@
1717
use PHPStan\Analyser\ExpressionResultStorage;
1818
use PHPStan\Analyser\ExprHandler;
1919
use PHPStan\Analyser\ExprHandler\Helper\MethodCallReturnTypeHelper;
20+
use PHPStan\Analyser\ExprHandler\Helper\MethodThrowPointHelper;
2021
use PHPStan\Analyser\ExprHandler\Helper\NullsafeShortCircuitingHelper;
2122
use PHPStan\Analyser\ImpurePoint;
2223
use PHPStan\Analyser\InternalThrowPoint;
@@ -25,7 +26,6 @@
2526
use PHPStan\Analyser\NoopNodeCallback;
2627
use PHPStan\DependencyInjection\AutowiredParameter;
2728
use PHPStan\DependencyInjection\AutowiredService;
28-
use PHPStan\DependencyInjection\Type\DynamicThrowTypeExtensionProvider;
2929
use PHPStan\Node\Expr\PossiblyImpureCallExpr;
3030
use PHPStan\Reflection\Callables\SimpleImpurePoint;
3131
use PHPStan\Reflection\MethodReflection;
@@ -39,7 +39,6 @@
3939
use PHPStan\Type\TypeCombinator;
4040
use PHPStan\Type\TypeWithClassName;
4141
use ReflectionProperty;
42-
use Throwable;
4342
use function array_map;
4443
use function array_merge;
4544
use function count;
@@ -55,10 +54,8 @@ final class StaticCallHandler implements ExprHandler
5554
{
5655

5756
public function __construct(
58-
private DynamicThrowTypeExtensionProvider $dynamicThrowTypeExtensionProvider,
5957
private MethodCallReturnTypeHelper $methodCallReturnTypeHelper,
60-
#[AutowiredParameter(ref: '%exceptions.implicitThrows%')]
61-
private bool $implicitThrows,
58+
private MethodThrowPointHelper $methodThrowPointHelper,
6259
#[AutowiredParameter]
6360
private bool $rememberPossiblyImpureFunctionValues,
6461
)
@@ -198,7 +195,7 @@ public function processExpr(NodeScopeResolver $nodeScopeResolver, Stmt $stmt, Ex
198195
$scopeFunction = $scope->getFunction();
199196

200197
if ($methodReflection !== null) {
201-
$methodThrowPoint = $this->getStaticMethodThrowPoint($methodReflection, $normalizedExpr, $scope);
198+
$methodThrowPoint = $this->methodThrowPointHelper->getThrowPoint($methodReflection, $parametersAcceptor, $normalizedExpr, $scope);
202199
if ($methodThrowPoint !== null) {
203200
$throwPoints[] = $methodThrowPoint;
204201
}
@@ -268,36 +265,6 @@ public function processExpr(NodeScopeResolver $nodeScopeResolver, Stmt $stmt, Ex
268265
);
269266
}
270267

271-
private function getStaticMethodThrowPoint(MethodReflection $methodReflection, StaticCall $normalizedMethodCall, MutatingScope $scope): ?InternalThrowPoint
272-
{
273-
foreach ($this->dynamicThrowTypeExtensionProvider->getDynamicStaticMethodThrowTypeExtensions() as $extension) {
274-
if (!$extension->isStaticMethodSupported($methodReflection)) {
275-
continue;
276-
}
277-
278-
$throwType = $extension->getThrowTypeFromStaticMethodCall($methodReflection, $normalizedMethodCall, $scope);
279-
if ($throwType === null) {
280-
return null;
281-
}
282-
283-
return InternalThrowPoint::createExplicit($scope, $throwType, $normalizedMethodCall, false);
284-
}
285-
286-
if ($methodReflection->getThrowType() !== null) {
287-
$throwType = $methodReflection->getThrowType();
288-
if (!$throwType->isVoid()->yes()) {
289-
return InternalThrowPoint::createExplicit($scope, $throwType, $normalizedMethodCall, true);
290-
}
291-
} elseif ($this->implicitThrows) {
292-
$methodReturnedType = $scope->getType($normalizedMethodCall);
293-
if (!(new ObjectType(Throwable::class))->isSuperTypeOf($methodReturnedType)->yes()) {
294-
return InternalThrowPoint::createImplicit($scope, $normalizedMethodCall);
295-
}
296-
}
297-
298-
return null;
299-
}
300-
301268
public function resolveType(MutatingScope $scope, Expr $expr): Type
302269
{
303270
if ($expr->name instanceof Identifier) {

src/Php/PhpVersion.php

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -506,4 +506,9 @@ public function hasFilterThrowOnFailureConstant(): bool
506506
return $this->versionId >= 80500;
507507
}
508508

509+
public function throwsOnStringCast(): bool
510+
{
511+
return $this->versionId >= 70400;
512+
}
513+
509514
}

tests/PHPStan/Rules/Exceptions/AbilityToDisableImplicitThrowsTest.php

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -69,6 +69,24 @@ public function testPropertyHooks(): void
6969
]);
7070
}
7171

72+
public function testBug13806(): void
73+
{
74+
$this->analyse([__DIR__ . '/data/bug-13806.php'], [
75+
[
76+
'Dead catch - Throwable is never thrown in the try block.',
77+
8,
78+
],
79+
[
80+
'Dead catch - Throwable is never thrown in the try block.',
81+
53,
82+
],
83+
[
84+
'Dead catch - Throwable is never thrown in the try block.',
85+
64,
86+
],
87+
]);
88+
}
89+
7290
public static function getAdditionalConfigFiles(): array
7391
{
7492
return array_merge(

0 commit comments

Comments
 (0)