Skip to content

Commit 32e6017

Browse files
phpstan-botclaude
authored andcommitted
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 <noreply@anthropic.com>
1 parent a2a0f3b commit 32e6017

File tree

4 files changed

+114
-112
lines changed

4 files changed

+114
-112
lines changed

src/Analyser/ExprHandler/CastStringHandler.php

Lines changed: 11 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -4,23 +4,20 @@
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;
13-
use PHPStan\Analyser\InternalThrowPoint;
1415
use PHPStan\Analyser\MutatingScope;
1516
use PHPStan\Analyser\NodeScopeResolver;
16-
use PHPStan\DependencyInjection\AutowiredParameter;
1717
use PHPStan\DependencyInjection\AutowiredService;
1818
use PHPStan\Php\PhpVersion;
1919
use PHPStan\Reflection\InitializerExprTypeResolver;
20-
use PHPStan\Type\NeverType;
21-
use PHPStan\Type\ObjectType;
2220
use PHPStan\Type\Type;
23-
use Throwable;
2421
use function sprintf;
2522

2623
/**
@@ -33,8 +30,7 @@ final class CastStringHandler implements ExprHandler
3330
public function __construct(
3431
private InitializerExprTypeResolver $initializerExprTypeResolver,
3532
private PhpVersion $phpVersion,
36-
#[AutowiredParameter(ref: '%exceptions.implicitThrows%')]
37-
private bool $implicitThrows,
33+
private MethodThrowPointHelper $methodThrowPointHelper,
3834
)
3935
{
4036
}
@@ -64,20 +60,14 @@ public function processExpr(NodeScopeResolver $nodeScopeResolver, Stmt $stmt, Ex
6460
}
6561

6662
if ($this->phpVersion->throwsOnStringCast()) {
67-
$throwType = $toStringMethod->getThrowType();
68-
if ($throwType === null) {
69-
$returnType = $toStringMethod->getOnlyVariant()->getReturnType();
70-
if ($returnType instanceof NeverType && $returnType->isExplicit()) {
71-
$throwType = new ObjectType(Throwable::class);
72-
}
73-
}
74-
75-
if ($throwType !== null) {
76-
if (!$throwType->isVoid()->yes()) {
77-
$throwPoints[] = InternalThrowPoint::createExplicit($scope, $throwType, $expr, true);
78-
}
79-
} elseif ($this->implicitThrows) {
80-
$throwPoints[] = InternalThrowPoint::createImplicit($scope, $expr);
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;
8171
}
8272
}
8373
}
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\ArgumentsNormalizer;
8+
use PHPStan\Analyser\InternalThrowPoint;
9+
use PHPStan\Analyser\MutatingScope;
10+
use PHPStan\DependencyInjection\AutowiredParameter;
11+
use PHPStan\DependencyInjection\AutowiredService;
12+
use PHPStan\DependencyInjection\Type\DynamicThrowTypeExtensionProvider;
13+
use PHPStan\Reflection\MethodReflection;
14+
use PHPStan\Reflection\ParametersAcceptor;
15+
use PHPStan\Type\NeverType;
16+
use PHPStan\Type\ObjectType;
17+
use ReflectionFunction;
18+
use ReflectionMethod;
19+
use Throwable;
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: 2 additions & 55 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
)
@@ -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) {

0 commit comments

Comments
 (0)