From e9b415fdf5cc046daf8235f4d15a0effe4fdd0c2 Mon Sep 17 00:00:00 2001 From: Markus Staab Date: Tue, 22 Jul 2025 16:40:42 +0200 Subject: [PATCH 1/5] Fix missing detection of dead code in closures --- src/Analyser/NodeScopeResolver.php | 5 +++-- src/Analyser/ProcessClosureResult.php | 6 ++++++ tests/PHPStan/Analyser/ExpressionResultTest.php | 4 ++++ 3 files changed, 13 insertions(+), 2 deletions(-) diff --git a/src/Analyser/NodeScopeResolver.php b/src/Analyser/NodeScopeResolver.php index e5adf0effc..68da4bf69b 100644 --- a/src/Analyser/NodeScopeResolver.php +++ b/src/Analyser/NodeScopeResolver.php @@ -4690,7 +4690,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, $statementResult->isAlwaysTerminating()); } $count = 0; @@ -4736,7 +4736,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, $statementResult->isAlwaysTerminating()); } /** @@ -5180,6 +5180,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..bd8583054e 100644 --- a/tests/PHPStan/Analyser/ExpressionResultTest.php +++ b/tests/PHPStan/Analyser/ExpressionResultTest.php @@ -113,6 +113,10 @@ public static function dataIsAlwaysTerminating(): array 'call_user_func(fn() => exit());', true, ], + [ + 'call_user_func(function() { exit(); });', + true, + ], [ 'var_dump(1+exit());', true, From b6beb935a95760d0e30204adae150dd9b118883e Mon Sep 17 00:00:00 2001 From: Markus Staab Date: Tue, 22 Jul 2025 16:43:30 +0200 Subject: [PATCH 2/5] Update ExpressionResultTest.php --- tests/PHPStan/Analyser/ExpressionResultTest.php | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/tests/PHPStan/Analyser/ExpressionResultTest.php b/tests/PHPStan/Analyser/ExpressionResultTest.php index bd8583054e..a7221f38b3 100644 --- a/tests/PHPStan/Analyser/ExpressionResultTest.php +++ b/tests/PHPStan/Analyser/ExpressionResultTest.php @@ -117,6 +117,10 @@ public static function dataIsAlwaysTerminating(): array 'call_user_func(function() { exit(); });', true, ], + [ + 'usort($arr, static function($a, $b):int { return $a <=> $b; });', + false, + ], [ 'var_dump(1+exit());', true, From 9df38e91ac1bc229715255d673ee32051c361b88 Mon Sep 17 00:00:00 2001 From: Markus Staab Date: Tue, 22 Jul 2025 16:59:38 +0200 Subject: [PATCH 3/5] Update ExpressionResultTest.php --- tests/PHPStan/Analyser/ExpressionResultTest.php | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/tests/PHPStan/Analyser/ExpressionResultTest.php b/tests/PHPStan/Analyser/ExpressionResultTest.php index a7221f38b3..ecd5fccffd 100644 --- a/tests/PHPStan/Analyser/ExpressionResultTest.php +++ b/tests/PHPStan/Analyser/ExpressionResultTest.php @@ -113,6 +113,10 @@ public static function dataIsAlwaysTerminating(): array 'call_user_func(fn() => exit());', true, ], + [ + '(function() { exit(); })();', + true, + ], [ 'call_user_func(function() { exit(); });', true, From d6b744943958a1c52fa0beaf768c6635763b0fcc Mon Sep 17 00:00:00 2001 From: Markus Staab Date: Fri, 1 Aug 2025 13:55:45 +0200 Subject: [PATCH 4/5] Update NodeScopeResolver.php --- src/Analyser/NodeScopeResolver.php | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/src/Analyser/NodeScopeResolver.php b/src/Analyser/NodeScopeResolver.php index 68da4bf69b..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, $statementResult->isAlwaysTerminating()); + 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, $statementResult->isAlwaysTerminating()); + return new ProcessClosureResult($scope->processClosureScope($closureResultScope, null, $byRefUses), $statementResult->getThrowPoints(), $statementResult->getImpurePoints(), $invalidateExpressions, $isAlwaysTerminating); } /** From f2608e1787ffd34e84fb97124fc668a43dcec3da Mon Sep 17 00:00:00 2001 From: Markus Staab Date: Fri, 1 Aug 2025 14:05:21 +0200 Subject: [PATCH 5/5] Update ExpressionResultTest.php --- tests/PHPStan/Analyser/ExpressionResultTest.php | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/tests/PHPStan/Analyser/ExpressionResultTest.php b/tests/PHPStan/Analyser/ExpressionResultTest.php index ecd5fccffd..56b699f1fc 100644 --- a/tests/PHPStan/Analyser/ExpressionResultTest.php +++ b/tests/PHPStan/Analyser/ExpressionResultTest.php @@ -117,6 +117,10 @@ public static function dataIsAlwaysTerminating(): array '(function() { exit(); })();', true, ], + [ + 'function () {};', + false, + ], [ 'call_user_func(function() { exit(); });', true,