Skip to content

Commit 2044bb0

Browse files
ondrejmirtesclaude
andcommitted
Resolve early-terminating calls to an explicit never type in the call handlers
A MethodCall/StaticCall/FuncCall configured via earlyTerminatingMethodCalls/earlyTerminatingFunctionCalls now resolves to an explicit NeverType in its handler, through a shared EarlyTerminatingCallHelper. findEarlyTerminatingExpr()'s duplicate list-matching goes away - its existing explicit-never check covers the configured calls the same way it already covers exit/die/throw and signature-never calls - and the lists move off NodeScopeResolver's constructor onto the helper as DI parameters. The test-case overrides are replaced by a nodeScopeResolverEarlyTerminating.neon parameter file. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_019wqGgaD7iqL44t1KgpJS7b
1 parent 3152f6d commit 2044bb0

13 files changed

Lines changed: 118 additions & 105 deletions

src/Analyser/ExprHandler/FuncCallHandler.php

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@
1919
use PHPStan\Analyser\ExpressionResultFactory;
2020
use PHPStan\Analyser\ExpressionResultStorage;
2121
use PHPStan\Analyser\ExprHandler;
22+
use PHPStan\Analyser\ExprHandler\Helper\EarlyTerminatingCallHelper;
2223
use PHPStan\Analyser\ExprHandler\Helper\OutputBufferHelper;
2324
use PHPStan\Analyser\ExprHandler\Helper\VoidToNullTypeTransformer;
2425
use PHPStan\Analyser\ImpurePoint;
@@ -95,6 +96,7 @@ final class FuncCallHandler implements ExprHandler
9596
* @param ExtensionsCollection<DynamicFunctionThrowTypeExtension> $dynamicFunctionThrowTypeExtensions
9697
*/
9798
public function __construct(
99+
private EarlyTerminatingCallHelper $earlyTerminatingCallHelper,
98100
private ReflectionProvider $reflectionProvider,
99101
#[AutowiredExtensions(of: DynamicFunctionThrowTypeExtension::class)]
100102
private ExtensionsCollection $dynamicFunctionThrowTypeExtensions,
@@ -811,6 +813,13 @@ static function (?Type $offsetType, Type $valueType, bool $optional) use (&$arra
811813

812814
public function resolveType(MutatingScope $scope, Expr $expr): Type
813815
{
816+
if (
817+
$expr->name instanceof Name
818+
&& $this->earlyTerminatingCallHelper->isEarlyTerminatingFunctionCall($expr->name->toString())
819+
) {
820+
return new NeverType(true);
821+
}
822+
814823
if ($expr->name instanceof Expr) {
815824
$calledOnType = $scope->getType($expr->name);
816825
if ($calledOnType->isCallable()->no()) {
Lines changed: 80 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,80 @@
1+
<?php declare(strict_types = 1);
2+
3+
namespace PHPStan\Analyser\ExprHandler\Helper;
4+
5+
use PHPStan\DependencyInjection\AutowiredParameter;
6+
use PHPStan\DependencyInjection\AutowiredService;
7+
use PHPStan\Reflection\ReflectionProvider;
8+
use PHPStan\Type\Type;
9+
use function array_key_exists;
10+
use function array_merge;
11+
use function in_array;
12+
use function strtolower;
13+
14+
/**
15+
* Decides whether a method/function call is configured as early-terminating
16+
* (`parameters.earlyTerminatingMethodCalls` / `earlyTerminatingFunctionCalls`).
17+
* The call handlers use this to give such a call an explicit NeverType, from
18+
* which NodeScopeResolver derives the statement's exit point - so the engine no
19+
* longer reaches Scope::getType() to find early-terminating expressions.
20+
*/
21+
#[AutowiredService]
22+
final class EarlyTerminatingCallHelper
23+
{
24+
25+
/** @var array<string, true> */
26+
private array $earlyTerminatingMethodNames;
27+
28+
/**
29+
* @param string[][] $earlyTerminatingMethodCalls className(string) => methods(string[])
30+
* @param array<int, string> $earlyTerminatingFunctionCalls
31+
*/
32+
public function __construct(
33+
private ReflectionProvider $reflectionProvider,
34+
#[AutowiredParameter]
35+
private array $earlyTerminatingMethodCalls,
36+
#[AutowiredParameter]
37+
private array $earlyTerminatingFunctionCalls,
38+
)
39+
{
40+
$earlyTerminatingMethodNames = [];
41+
foreach ($this->earlyTerminatingMethodCalls as $methodNames) {
42+
foreach ($methodNames as $methodName) {
43+
$earlyTerminatingMethodNames[strtolower($methodName)] = true;
44+
}
45+
}
46+
$this->earlyTerminatingMethodNames = $earlyTerminatingMethodNames;
47+
}
48+
49+
public function isEarlyTerminatingMethodCall(string $methodName, Type $calledOnType): bool
50+
{
51+
if (!array_key_exists(strtolower($methodName), $this->earlyTerminatingMethodNames)) {
52+
return false;
53+
}
54+
55+
foreach ($calledOnType->getObjectClassNames() as $referencedClass) {
56+
if (!$this->reflectionProvider->hasClass($referencedClass)) {
57+
continue;
58+
}
59+
60+
$classReflection = $this->reflectionProvider->getClass($referencedClass);
61+
foreach (array_merge([$referencedClass], $classReflection->getParentClassesNames(), $classReflection->getNativeReflection()->getInterfaceNames()) as $className) {
62+
if (!isset($this->earlyTerminatingMethodCalls[$className])) {
63+
continue;
64+
}
65+
66+
if (in_array($methodName, $this->earlyTerminatingMethodCalls[$className], true)) {
67+
return true;
68+
}
69+
}
70+
}
71+
72+
return false;
73+
}
74+
75+
public function isEarlyTerminatingFunctionCall(string $functionName): bool
76+
{
77+
return in_array($functionName, $this->earlyTerminatingFunctionCalls, true);
78+
}
79+
80+
}

src/Analyser/ExprHandler/MethodCallHandler.php

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@
1414
use PHPStan\Analyser\ExpressionResultFactory;
1515
use PHPStan\Analyser\ExpressionResultStorage;
1616
use PHPStan\Analyser\ExprHandler;
17+
use PHPStan\Analyser\ExprHandler\Helper\EarlyTerminatingCallHelper;
1718
use PHPStan\Analyser\ExprHandler\Helper\MethodCallReturnTypeHelper;
1819
use PHPStan\Analyser\ExprHandler\Helper\MethodThrowPointHelper;
1920
use PHPStan\Analyser\ExprHandler\Helper\NullsafeShortCircuitingHelper;
@@ -56,6 +57,7 @@ final class MethodCallHandler implements ExprHandler
5657
{
5758

5859
public function __construct(
60+
private EarlyTerminatingCallHelper $earlyTerminatingCallHelper,
5961
private MethodCallReturnTypeHelper $methodCallReturnTypeHelper,
6062
private MethodThrowPointHelper $methodThrowPointHelper,
6163
private ReflectionProvider $reflectionProvider,
@@ -246,6 +248,13 @@ public function processExpr(NodeScopeResolver $nodeScopeResolver, Stmt $stmt, Ex
246248

247249
public function resolveType(MutatingScope $scope, Expr $expr): Type
248250
{
251+
if (
252+
$expr->name instanceof Identifier
253+
&& $this->earlyTerminatingCallHelper->isEarlyTerminatingMethodCall($expr->name->name, $scope->getType($expr->var))
254+
) {
255+
return new NeverType(true);
256+
}
257+
249258
if ($expr->name instanceof Identifier) {
250259
if ($scope->nativeTypesPromoted) {
251260
$methodReflection = $scope->getMethodReflection(

src/Analyser/ExprHandler/StaticCallHandler.php

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@
1717
use PHPStan\Analyser\ExpressionResultFactory;
1818
use PHPStan\Analyser\ExpressionResultStorage;
1919
use PHPStan\Analyser\ExprHandler;
20+
use PHPStan\Analyser\ExprHandler\Helper\EarlyTerminatingCallHelper;
2021
use PHPStan\Analyser\ExprHandler\Helper\MethodCallReturnTypeHelper;
2122
use PHPStan\Analyser\ExprHandler\Helper\MethodThrowPointHelper;
2223
use PHPStan\Analyser\ExprHandler\Helper\NullsafeShortCircuitingHelper;
@@ -64,6 +65,7 @@ final class StaticCallHandler implements ExprHandler
6465
{
6566

6667
public function __construct(
68+
private EarlyTerminatingCallHelper $earlyTerminatingCallHelper,
6769
private MethodCallReturnTypeHelper $methodCallReturnTypeHelper,
6870
private MethodThrowPointHelper $methodThrowPointHelper,
6971
private ReflectionProvider $reflectionProvider,
@@ -302,6 +304,15 @@ public function processExpr(NodeScopeResolver $nodeScopeResolver, Stmt $stmt, Ex
302304

303305
public function resolveType(MutatingScope $scope, Expr $expr): Type
304306
{
307+
if ($expr->name instanceof Identifier) {
308+
$earlyTerminatingClassType = $expr->class instanceof Name
309+
? $scope->resolveTypeByName($expr->class)
310+
: $scope->getType($expr->class);
311+
if ($this->earlyTerminatingCallHelper->isEarlyTerminatingMethodCall($expr->name->name, $earlyTerminatingClassType)) {
312+
return new NeverType(true);
313+
}
314+
}
315+
305316
if ($expr->name instanceof Identifier) {
306317
if ($scope->nativeTypesPromoted) {
307318
if ($expr->class instanceof Name) {

src/Analyser/NodeScopeResolver.php

Lines changed: 0 additions & 53 deletions
Original file line numberDiff line numberDiff line change
@@ -205,18 +205,13 @@ class NodeScopeResolver
205205
/** @var array<string, true> filePath(string) => bool(true) */
206206
private array $analysedFiles = [];
207207

208-
/** @var array<string, true> */
209-
private array $earlyTerminatingMethodNames;
210-
211208
/** @var array<string, true> */
212209
private array $calledMethodStack = [];
213210

214211
/** @var array<string, MutatingScope|null> */
215212
private array $calledMethodResults = [];
216213

217214
/**
218-
* @param string[][] $earlyTerminatingMethodCalls className(string) => methods(string[])
219-
* @param array<int, string> $earlyTerminatingFunctionCalls
220215
* @param ExtensionsCollection<FunctionParameterOutTypeExtension> $functionParameterOutTypeExtensions
221216
* @param ExtensionsCollection<MethodParameterOutTypeExtension> $methodParameterOutTypeExtensions
222217
* @param ExtensionsCollection<StaticMethodParameterOutTypeExtension> $staticMethodParameterOutTypeExtensions
@@ -268,10 +263,6 @@ public function __construct(
268263
private readonly bool $polluteScopeWithAlwaysIterableForeach,
269264
#[AutowiredParameter]
270265
private readonly bool $polluteScopeWithBlock,
271-
#[AutowiredParameter]
272-
private readonly array $earlyTerminatingMethodCalls,
273-
#[AutowiredParameter]
274-
private readonly array $earlyTerminatingFunctionCalls,
275266
#[AutowiredParameter(ref: '%exceptions.implicitThrows%')]
276267
private readonly bool $implicitThrows,
277268
#[AutowiredParameter]
@@ -280,13 +271,6 @@ public function __construct(
280271
protected readonly ExpressionResultFactory $expressionResultFactory,
281272
)
282273
{
283-
$earlyTerminatingMethodNames = [];
284-
foreach ($this->earlyTerminatingMethodCalls as $methodNames) {
285-
foreach ($methodNames as $methodName) {
286-
$earlyTerminatingMethodNames[strtolower($methodName)] = true;
287-
}
288-
}
289-
$this->earlyTerminatingMethodNames = $earlyTerminatingMethodNames;
290274
}
291275

292276
/**
@@ -2758,43 +2742,6 @@ private function lookForExpressionCallback(MutatingScope $scope, Expr $expr, Clo
27582742

27592743
private function findEarlyTerminatingExpr(Expr $expr, Scope $scope): ?Expr
27602744
{
2761-
if (($expr instanceof MethodCall || $expr instanceof Expr\StaticCall) && $expr->name instanceof Node\Identifier) {
2762-
if (array_key_exists($expr->name->toLowerString(), $this->earlyTerminatingMethodNames)) {
2763-
if ($expr instanceof MethodCall) {
2764-
$methodCalledOnType = $scope->getType($expr->var);
2765-
} else {
2766-
if ($expr->class instanceof Name) {
2767-
$methodCalledOnType = $scope->resolveTypeByName($expr->class);
2768-
} else {
2769-
$methodCalledOnType = $scope->getType($expr->class);
2770-
}
2771-
}
2772-
2773-
foreach ($methodCalledOnType->getObjectClassNames() as $referencedClass) {
2774-
if (!$this->reflectionProvider->hasClass($referencedClass)) {
2775-
continue;
2776-
}
2777-
2778-
$classReflection = $this->reflectionProvider->getClass($referencedClass);
2779-
foreach (array_merge([$referencedClass], $classReflection->getParentClassesNames(), $classReflection->getNativeReflection()->getInterfaceNames()) as $className) {
2780-
if (!isset($this->earlyTerminatingMethodCalls[$className])) {
2781-
continue;
2782-
}
2783-
2784-
if (in_array((string) $expr->name, $this->earlyTerminatingMethodCalls[$className], true)) {
2785-
return $expr;
2786-
}
2787-
}
2788-
}
2789-
}
2790-
}
2791-
2792-
if ($expr instanceof FuncCall && $expr->name instanceof Name) {
2793-
if (in_array((string) $expr->name, $this->earlyTerminatingFunctionCalls, true)) {
2794-
return $expr;
2795-
}
2796-
}
2797-
27982745
if ($expr instanceof Expr\Exit_ || $expr instanceof Expr\Throw_) {
27992746
return $expr;
28002747
}

src/Testing/RuleTestCase.php

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -126,8 +126,6 @@ protected function createNodeScopeResolver(): NodeScopeResolver
126126
$this->shouldPolluteScopeWithLoopInitialAssignments(),
127127
$this->shouldPolluteScopeWithAlwaysIterableForeach(),
128128
self::getContainer()->getParameter('polluteScopeWithBlock'),
129-
[],
130-
[],
131129
self::getContainer()->getParameter('exceptions')['implicitThrows'],
132130
$this->shouldTreatPhpDocTypesAsCertain(),
133131
self::getContainer()->getByType(ImplicitToStringCallHelper::class),

src/Testing/TypeInferenceTestCase.php

Lines changed: 0 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -101,8 +101,6 @@ protected static function createNodeScopeResolver(): NodeScopeResolver
101101
$container->getParameter('polluteScopeWithLoopInitialAssignments'),
102102
$container->getParameter('polluteScopeWithAlwaysIterableForeach'),
103103
$container->getParameter('polluteScopeWithBlock'),
104-
static::getEarlyTerminatingMethodCalls(),
105-
static::getEarlyTerminatingFunctionCalls(),
106104
$container->getParameter('exceptions')['implicitThrows'],
107105
$container->getParameter('treatPhpDocTypesAsCertain'),
108106
$container->getByType(ImplicitToStringCallHelper::class),
@@ -500,16 +498,4 @@ protected static function getAdditionalAnalysedFiles(): array
500498
return [];
501499
}
502500

503-
/** @return string[][] */
504-
protected static function getEarlyTerminatingMethodCalls(): array
505-
{
506-
return [];
507-
}
508-
509-
/** @return string[] */
510-
protected static function getEarlyTerminatingFunctionCalls(): array
511-
{
512-
return [];
513-
}
514-
515501
}

tests/PHPStan/Analyser/AnalyserTest.php

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -842,8 +842,6 @@ private function createAnalyser(): Analyser
842842
false,
843843
true,
844844
true,
845-
[],
846-
[],
847845
true,
848846
$this->shouldTreatPhpDocTypesAsCertain(),
849847
$container->getByType(ImplicitToStringCallHelper::class),

tests/PHPStan/Analyser/Fiber/FiberNodeScopeResolverRuleTest.php

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -146,8 +146,6 @@ protected function createNodeScopeResolver(): NodeScopeResolver
146146
$this->shouldPolluteScopeWithLoopInitialAssignments(),
147147
$this->shouldPolluteScopeWithAlwaysIterableForeach(),
148148
self::getContainer()->getParameter('polluteScopeWithBlock'),
149-
[],
150-
[],
151149
self::getContainer()->getParameter('exceptions')['implicitThrows'],
152150
$this->shouldTreatPhpDocTypesAsCertain(),
153151
self::getContainer()->getByType(ImplicitToStringCallHelper::class),

tests/PHPStan/Analyser/Fiber/FiberNodeScopeResolverTest.php

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -79,8 +79,6 @@ protected static function createNodeScopeResolver(): NodeScopeResolver
7979
$container->getParameter('polluteScopeWithLoopInitialAssignments'),
8080
$container->getParameter('polluteScopeWithAlwaysIterableForeach'),
8181
$container->getParameter('polluteScopeWithBlock'),
82-
static::getEarlyTerminatingMethodCalls(),
83-
static::getEarlyTerminatingFunctionCalls(),
8482
$container->getParameter('exceptions')['implicitThrows'],
8583
$container->getParameter('treatPhpDocTypesAsCertain'),
8684
$container->getByType(ImplicitToStringCallHelper::class),

0 commit comments

Comments
 (0)