diff --git a/src/Analyser/NodeScopeResolver.php b/src/Analyser/NodeScopeResolver.php index e5adf0effc..0c25c7271b 100644 --- a/src/Analyser/NodeScopeResolver.php +++ b/src/Analyser/NodeScopeResolver.php @@ -4639,6 +4639,9 @@ private function processClosureNode( throw new ShouldNotHappenException(); } + $returnType = $closureType->getReturnType(); + $isAlwaysTerminating = ($returnType instanceof NeverType && $returnType->isExplicit()); + $nodeCallback(new InClosureNode($closureType, $expr), $closureScope); $executionEnds = []; @@ -4690,7 +4693,7 @@ private function processClosureNode( array_merge($statementResult->getImpurePoints(), $closureImpurePoints), ), $closureScope); - return new ProcessClosureResult($scope, $statementResult->getThrowPoints(), $statementResult->getImpurePoints(), $invalidateExpressions); + return new ProcessClosureResult($scope, $statementResult->getThrowPoints(), $statementResult->getImpurePoints(), $invalidateExpressions, $isAlwaysTerminating); } $count = 0; @@ -4736,7 +4739,7 @@ private function processClosureNode( array_merge($statementResult->getImpurePoints(), $closureImpurePoints), ), $closureScope); - return new ProcessClosureResult($scope->processClosureScope($closureResultScope, null, $byRefUses), $statementResult->getThrowPoints(), $statementResult->getImpurePoints(), $invalidateExpressions); + return new ProcessClosureResult($scope->processClosureScope($closureResultScope, null, $byRefUses), $statementResult->getThrowPoints(), $statementResult->getImpurePoints(), $invalidateExpressions, $isAlwaysTerminating); } /** @@ -5180,6 +5183,7 @@ private function processArgs( if ($callCallbackImmediately) { $throwPoints = array_merge($throwPoints, array_map(static fn (ThrowPoint $throwPoint) => $throwPoint->isExplicit() ? ThrowPoint::createExplicit($scope, $throwPoint->getType(), $arg->value, $throwPoint->canContainAnyThrowable()) : ThrowPoint::createImplicit($scope, $arg->value), $closureResult->getThrowPoints())); $impurePoints = array_merge($impurePoints, $closureResult->getImpurePoints()); + $isAlwaysTerminating = $isAlwaysTerminating || $closureResult->isAlwaysTerminating(); } $uses = []; diff --git a/src/Analyser/ProcessClosureResult.php b/src/Analyser/ProcessClosureResult.php index 0051383278..a133bfa77b 100644 --- a/src/Analyser/ProcessClosureResult.php +++ b/src/Analyser/ProcessClosureResult.php @@ -17,6 +17,7 @@ public function __construct( private array $throwPoints, private array $impurePoints, private array $invalidateExpressions, + private bool $isAlwaysTerminating, ) { } @@ -50,4 +51,9 @@ public function getInvalidateExpressions(): array return $this->invalidateExpressions; } + public function isAlwaysTerminating(): bool + { + return $this->isAlwaysTerminating; + } + } diff --git a/tests/PHPStan/Analyser/ExpressionResultTest.php b/tests/PHPStan/Analyser/ExpressionResultTest.php index a59a5ae73d..56b699f1fc 100644 --- a/tests/PHPStan/Analyser/ExpressionResultTest.php +++ b/tests/PHPStan/Analyser/ExpressionResultTest.php @@ -113,6 +113,22 @@ public static function dataIsAlwaysTerminating(): array 'call_user_func(fn() => exit());', true, ], + [ + '(function() { exit(); })();', + true, + ], + [ + 'function () {};', + false, + ], + [ + 'call_user_func(function() { exit(); });', + true, + ], + [ + 'usort($arr, static function($a, $b):int { return $a <=> $b; });', + false, + ], [ 'var_dump(1+exit());', true,