diff --git a/src/Analyser/ExprHandler/MethodCallHandler.php b/src/Analyser/ExprHandler/MethodCallHandler.php index 35c5455da5..826d757a20 100644 --- a/src/Analyser/ExprHandler/MethodCallHandler.php +++ b/src/Analyser/ExprHandler/MethodCallHandler.php @@ -136,7 +136,7 @@ public function processExpr(NodeScopeResolver $nodeScopeResolver, Stmt $stmt, Ex if ($parametersAcceptor !== null) { $normalizedExpr = ArgumentsNormalizer::reorderMethodArguments($parametersAcceptor, $expr) ?? $expr; $returnType = $parametersAcceptor->getReturnType(); - $isAlwaysTerminating = $returnType instanceof NeverType && $returnType->isExplicit(); + $isAlwaysTerminating = $isAlwaysTerminating || ($returnType instanceof NeverType && $returnType->isExplicit()); } $argsResult = $nodeScopeResolver->processArgs( diff --git a/src/Analyser/ExprHandler/StaticCallHandler.php b/src/Analyser/ExprHandler/StaticCallHandler.php index 16eceaefbc..2c06152b2c 100644 --- a/src/Analyser/ExprHandler/StaticCallHandler.php +++ b/src/Analyser/ExprHandler/StaticCallHandler.php @@ -192,7 +192,7 @@ public function processExpr(NodeScopeResolver $nodeScopeResolver, Stmt $stmt, Ex if ($parametersAcceptor !== null) { $normalizedExpr = ArgumentsNormalizer::reorderStaticCallArguments($parametersAcceptor, $expr) ?? $expr; $returnType = $parametersAcceptor->getReturnType(); - $isAlwaysTerminating = $returnType instanceof NeverType && $returnType->isExplicit(); + $isAlwaysTerminating = $isAlwaysTerminating || ($returnType instanceof NeverType && $returnType->isExplicit()); } $argsResult = $nodeScopeResolver->processArgs($stmt, $methodReflection, null, $parametersAcceptor, $normalizedExpr, $scope, $storage, $nodeCallback, $context, $closureBindScope); $scope = $argsResult->getScope(); diff --git a/tests/PHPStan/Rules/DeadCode/UnreachableStatementRuleTest.php b/tests/PHPStan/Rules/DeadCode/UnreachableStatementRuleTest.php index e79a185935..964a35da05 100644 --- a/tests/PHPStan/Rules/DeadCode/UnreachableStatementRuleTest.php +++ b/tests/PHPStan/Rules/DeadCode/UnreachableStatementRuleTest.php @@ -373,4 +373,28 @@ public function testBug13331(): void $this->analyse([__DIR__ . '/data/bug-13331.php'], []); } + #[RequiresPhp('>= 8.2')] + public function testBug14328(): void + { + $this->treatPhpDocTypesAsCertain = true; + $this->analyse([__DIR__ . '/data/bug-14328.php'], [ + [ + 'Unreachable statement - code above always terminates.', + 20, + ], + [ + 'Unreachable statement - code above always terminates.', + 26, + ], + [ + 'Unreachable statement - code above always terminates.', + 32, + ], + [ + 'Unreachable statement - code above always terminates.', + 38, + ], + ]); + } + } diff --git a/tests/PHPStan/Rules/DeadCode/data/bug-14328.php b/tests/PHPStan/Rules/DeadCode/data/bug-14328.php new file mode 100644 index 0000000000..c0e707561e --- /dev/null +++ b/tests/PHPStan/Rules/DeadCode/data/bug-14328.php @@ -0,0 +1,39 @@ += 8.2 + +declare(strict_types = 1); + +namespace Bug14328; + +class Foo { + public function returnThis(mixed $value): self { + return $this; + } + + public static function returnSelf(mixed $value): self { + return new self(); + } +} + +function testMethodCallChainedWithMethodCall(): void { + $callback = fn (): never => throw new \Exception(); + $x = (new Foo())->returnThis($callback())->returnThis('x'); + $y = 'this will never run'; +} + +function testMethodCallChainedWithStaticCall(): void { + $callback = fn (): never => throw new \Exception(); + $x = (new Foo())->returnThis($callback())::returnSelf('x'); + $y = 'this will never run'; +} + +function testStaticCallChainedWithMethodCall(): void { + $callback = fn (): never => throw new \Exception(); + $a = Foo::returnSelf($callback())->returnThis('x'); + $b = 'this will never run either'; +} + +function testStaticCallChainedWithStaticCall(): void { + $callback = fn (): never => throw new \Exception(); + $a = Foo::returnSelf($callback())::returnSelf('x'); + $b = 'this will never run either'; +}