From 5bc1fec1634cc312154cbf830063e45780978a81 Mon Sep 17 00:00:00 2001 From: ondrejmirtes <104888+ondrejmirtes@users.noreply.github.com> Date: Mon, 9 Mar 2026 16:04:45 +0000 Subject: [PATCH 1/2] Fix false positive for return overwritten by finally when return is in catch - Only emit FinallyExitPointsNode when the finally block always terminates, not just when it has any exit points - A return inside a catch within finally doesn't always execute, so it shouldn't be reported as unconditionally overwriting the try-catch return - New regression test in tests/PHPStan/Rules/Exceptions/data/bug-6670.php Closes https://github.com/phpstan/phpstan/issues/6670 --- src/Analyser/NodeScopeResolver.php | 2 +- .../OverwrittenExitPointByFinallyRuleTest.php | 5 ++++ .../Rules/Exceptions/data/bug-6670.php | 24 +++++++++++++++++++ 3 files changed, 30 insertions(+), 1 deletion(-) create mode 100644 tests/PHPStan/Rules/Exceptions/data/bug-6670.php diff --git a/src/Analyser/NodeScopeResolver.php b/src/Analyser/NodeScopeResolver.php index d6c6832f85..71ea204cfc 100644 --- a/src/Analyser/NodeScopeResolver.php +++ b/src/Analyser/NodeScopeResolver.php @@ -2028,7 +2028,7 @@ public function processStmtNode( $impurePoints = array_merge($impurePoints, $finallyResult->getImpurePoints()); $finallyScope = $finallyResult->getScope(); $finalScope = $finallyResult->isAlwaysTerminating() ? $finalScope : $finalScope->processFinallyScope($finallyScope, $originalFinallyScope); - if (count($finallyResult->getExitPoints()) > 0) { + if (count($finallyResult->getExitPoints()) > 0 && $finallyResult->isAlwaysTerminating()) { $this->callNodeCallback($nodeCallback, new FinallyExitPointsNode( $finallyResult->toPublic()->getExitPoints(), $finallyExitPoints, diff --git a/tests/PHPStan/Rules/Exceptions/OverwrittenExitPointByFinallyRuleTest.php b/tests/PHPStan/Rules/Exceptions/OverwrittenExitPointByFinallyRuleTest.php index 1698ada07c..4f146080bc 100644 --- a/tests/PHPStan/Rules/Exceptions/OverwrittenExitPointByFinallyRuleTest.php +++ b/tests/PHPStan/Rules/Exceptions/OverwrittenExitPointByFinallyRuleTest.php @@ -38,6 +38,11 @@ public function testRule(): void ]); } + public function testBug6670(): void + { + $this->analyse([__DIR__ . '/data/bug-6670.php'], []); + } + public function testBug5627(): void { $this->analyse([__DIR__ . '/data/bug-5627.php'], [ diff --git a/tests/PHPStan/Rules/Exceptions/data/bug-6670.php b/tests/PHPStan/Rules/Exceptions/data/bug-6670.php new file mode 100644 index 0000000000..87e2b324bf --- /dev/null +++ b/tests/PHPStan/Rules/Exceptions/data/bug-6670.php @@ -0,0 +1,24 @@ +clearCache(); + } catch(\Exception $e) { + return 'same string'; + } + } + } + + private function clearCache(): void + { + // do... + } +} From 225e1cd435f09c466d30ca0d308037435977b5a4 Mon Sep 17 00:00:00 2001 From: phpstan-bot Date: Thu, 12 Mar 2026 17:19:20 +0000 Subject: [PATCH 2/2] Add regression tests for issues also fixed by the finally exit point change Closes phpstan/phpstan#12215 Closes phpstan/phpstan#11906 Closes phpstan/phpstan#7665 Co-Authored-By: Claude Opus 4.6 --- .../OverwrittenExitPointByFinallyRuleTest.php | 15 ++++++ .../Rules/Exceptions/data/bug-11906.php | 15 ++++++ .../Rules/Exceptions/data/bug-12215.php | 53 +++++++++++++++++++ .../Rules/Exceptions/data/bug-7665.php | 28 ++++++++++ 4 files changed, 111 insertions(+) create mode 100644 tests/PHPStan/Rules/Exceptions/data/bug-11906.php create mode 100644 tests/PHPStan/Rules/Exceptions/data/bug-12215.php create mode 100644 tests/PHPStan/Rules/Exceptions/data/bug-7665.php diff --git a/tests/PHPStan/Rules/Exceptions/OverwrittenExitPointByFinallyRuleTest.php b/tests/PHPStan/Rules/Exceptions/OverwrittenExitPointByFinallyRuleTest.php index 4f146080bc..82437035fe 100644 --- a/tests/PHPStan/Rules/Exceptions/OverwrittenExitPointByFinallyRuleTest.php +++ b/tests/PHPStan/Rules/Exceptions/OverwrittenExitPointByFinallyRuleTest.php @@ -43,6 +43,21 @@ public function testBug6670(): void $this->analyse([__DIR__ . '/data/bug-6670.php'], []); } + public function testBug12215(): void + { + $this->analyse([__DIR__ . '/data/bug-12215.php'], []); + } + + public function testBug11906(): void + { + $this->analyse([__DIR__ . '/data/bug-11906.php'], []); + } + + public function testBug7665(): void + { + $this->analyse([__DIR__ . '/data/bug-7665.php'], []); + } + public function testBug5627(): void { $this->analyse([__DIR__ . '/data/bug-5627.php'], [ diff --git a/tests/PHPStan/Rules/Exceptions/data/bug-11906.php b/tests/PHPStan/Rules/Exceptions/data/bug-11906.php new file mode 100644 index 0000000000..050f9d6172 --- /dev/null +++ b/tests/PHPStan/Rules/Exceptions/data/bug-11906.php @@ -0,0 +1,15 @@ +expireTimeout = $expireTimeout; + + // acquire lock + $this->acquireTs = microtime(true); + $this->token = random_bytes(64); + } + + protected function release(string $key): bool + { + try { + return $this->releaseWithToken($key, $this->token); + } finally { + $this->token = null; + + $elapsedTime = microtime(true) - $this->acquireTs; + if ($elapsedTime >= $this->expireTimeout) { + throw new \Exception('Execution outside lock exception'); + } + } + } + + protected function release2(string $key): bool + { + try { + return $this->releaseWithToken($key, $this->token); + } finally { + try { + $elapsedTime = microtime(true) - $this->acquireTs; + if ($elapsedTime >= $this->expireTimeout) { + throw new \Exception('Execution outside lock exception'); + } + } finally { + $this->token = null; + } + } + } + + abstract protected function releaseWithToken(string $key, string $token): bool; +} diff --git a/tests/PHPStan/Rules/Exceptions/data/bug-7665.php b/tests/PHPStan/Rules/Exceptions/data/bug-7665.php new file mode 100644 index 0000000000..ed00c3e1cd --- /dev/null +++ b/tests/PHPStan/Rules/Exceptions/data/bug-7665.php @@ -0,0 +1,28 @@ +