Skip to content

Commit 0521bc6

Browse files
phpstan-botclaude
andcommitted
Fix unreachable statement info lost in chained method calls
- MethodCallHandler and StaticCallHandler overwrote $isAlwaysTerminating instead of OR-ing with the existing value when checking the return type - This caused never-returning arguments in chained calls to not propagate the always-terminating status to the outer call - Added regression test for phpstan/phpstan#14328 Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
1 parent 889361f commit 0521bc6

4 files changed

Lines changed: 30 additions & 2 deletions

File tree

src/Analyser/ExprHandler/MethodCallHandler.php

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -136,7 +136,7 @@ public function processExpr(NodeScopeResolver $nodeScopeResolver, Stmt $stmt, Ex
136136
if ($parametersAcceptor !== null) {
137137
$normalizedExpr = ArgumentsNormalizer::reorderMethodArguments($parametersAcceptor, $expr) ?? $expr;
138138
$returnType = $parametersAcceptor->getReturnType();
139-
$isAlwaysTerminating = $returnType instanceof NeverType && $returnType->isExplicit();
139+
$isAlwaysTerminating = $isAlwaysTerminating || ($returnType instanceof NeverType && $returnType->isExplicit());
140140
}
141141

142142
$argsResult = $nodeScopeResolver->processArgs(

src/Analyser/ExprHandler/StaticCallHandler.php

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -192,7 +192,7 @@ public function processExpr(NodeScopeResolver $nodeScopeResolver, Stmt $stmt, Ex
192192
if ($parametersAcceptor !== null) {
193193
$normalizedExpr = ArgumentsNormalizer::reorderStaticCallArguments($parametersAcceptor, $expr) ?? $expr;
194194
$returnType = $parametersAcceptor->getReturnType();
195-
$isAlwaysTerminating = $returnType instanceof NeverType && $returnType->isExplicit();
195+
$isAlwaysTerminating = $isAlwaysTerminating || ($returnType instanceof NeverType && $returnType->isExplicit());
196196
}
197197
$argsResult = $nodeScopeResolver->processArgs($stmt, $methodReflection, null, $parametersAcceptor, $normalizedExpr, $scope, $storage, $nodeCallback, $context, $closureBindScope);
198198
$scope = $argsResult->getScope();

tests/PHPStan/Rules/DeadCode/UnreachableStatementRuleTest.php

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -373,4 +373,16 @@ public function testBug13331(): void
373373
$this->analyse([__DIR__ . '/data/bug-13331.php'], []);
374374
}
375375

376+
#[RequiresPhp('>= 8.2')]
377+
public function testBug14328(): void
378+
{
379+
$this->treatPhpDocTypesAsCertain = true;
380+
$this->analyse([__DIR__ . '/data/bug-14328.php'], [
381+
[
382+
'Unreachable statement - code above always terminates.',
383+
16,
384+
],
385+
]);
386+
}
387+
376388
}
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
<?php // lint >= 8.2
2+
3+
declare(strict_types = 1);
4+
5+
namespace Bug14328;
6+
7+
$callback = fn (): never => throw new \Exception();
8+
9+
class Foo {
10+
public function returnThis(mixed $value): self {
11+
return $this;
12+
}
13+
}
14+
15+
$x = new Foo()->returnThis($callback())->returnThis('x');
16+
$y = 'this will never run';

0 commit comments

Comments
 (0)