From e3832a78b8f3b557e451eeb37c5b0b603b75e63a Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Fri, 27 Feb 2026 16:02:19 +0000 Subject: [PATCH] Fix do-while(true) false positive when loop body has explicit throw points - DoWhileLoopConstantConditionRule now considers explicit throw points as valid loop exit mechanisms, suppressing the "always true" warning - Added throw points to DoWhileLoopConditionNode alongside exit points - Updated NodeScopeResolver to pass throw points when creating the node - New regression test in tests/PHPStan/Rules/Comparison/data/bug-5865.php Closes https://github.com/phpstan/phpstan/issues/5865 --- src/Analyser/NodeScopeResolver.php | 3 ++- src/Node/DoWhileLoopConditionNode.php | 12 ++++++++- .../DoWhileLoopConstantConditionRule.php | 5 ++++ .../DoWhileLoopConstantConditionRuleTest.php | 5 ++++ .../Rules/Comparison/data/bug-5865.php | 25 +++++++++++++++++++ 5 files changed, 48 insertions(+), 2 deletions(-) create mode 100644 tests/PHPStan/Rules/Comparison/data/bug-5865.php diff --git a/src/Analyser/NodeScopeResolver.php b/src/Analyser/NodeScopeResolver.php index 86f616953b..b3abca6c66 100644 --- a/src/Analyser/NodeScopeResolver.php +++ b/src/Analyser/NodeScopeResolver.php @@ -1607,7 +1607,8 @@ private function processStmtNode( $alwaysIterates = $condBooleanType->isTrue()->yes(); } - $this->callNodeCallback($nodeCallback, new DoWhileLoopConditionNode($stmt->cond, $bodyScopeResult->toPublic()->getExitPoints()), $bodyScope, $storage); + $publicResult = $bodyScopeResult->toPublic(); + $this->callNodeCallback($nodeCallback, new DoWhileLoopConditionNode($stmt->cond, $publicResult->getExitPoints(), $publicResult->getThrowPoints()), $bodyScope, $storage); if ($alwaysIterates) { $alwaysTerminating = count($bodyScopeResult->getExitPointsByType(Break_::class)) === 0; diff --git a/src/Node/DoWhileLoopConditionNode.php b/src/Node/DoWhileLoopConditionNode.php index 5e589416eb..daace47ef2 100644 --- a/src/Node/DoWhileLoopConditionNode.php +++ b/src/Node/DoWhileLoopConditionNode.php @@ -6,14 +6,16 @@ use PhpParser\Node\Expr; use PhpParser\NodeAbstract; use PHPStan\Analyser\StatementExitPoint; +use PHPStan\Analyser\ThrowPoint; final class DoWhileLoopConditionNode extends NodeAbstract implements VirtualNode { /** * @param StatementExitPoint[] $exitPoints + * @param ThrowPoint[] $throwPoints */ - public function __construct(private Expr $cond, private array $exitPoints) + public function __construct(private Expr $cond, private array $exitPoints, private array $throwPoints) { parent::__construct($cond->getAttributes()); } @@ -31,6 +33,14 @@ public function getExitPoints(): array return $this->exitPoints; } + /** + * @return ThrowPoint[] + */ + public function getThrowPoints(): array + { + return $this->throwPoints; + } + #[Override] public function getType(): string { diff --git a/src/Rules/Comparison/DoWhileLoopConstantConditionRule.php b/src/Rules/Comparison/DoWhileLoopConstantConditionRule.php index 7087fbd006..d66cffa5e1 100644 --- a/src/Rules/Comparison/DoWhileLoopConstantConditionRule.php +++ b/src/Rules/Comparison/DoWhileLoopConstantConditionRule.php @@ -54,6 +54,11 @@ public function processNode(Node $node, Scope $scope): array return []; } } + foreach ($node->getThrowPoints() as $throwPoint) { + if ($throwPoint->isExplicit()) { + return []; + } + } } else { foreach ($node->getExitPoints() as $exitPoint) { $statement = $exitPoint->getStatement(); diff --git a/tests/PHPStan/Rules/Comparison/DoWhileLoopConstantConditionRuleTest.php b/tests/PHPStan/Rules/Comparison/DoWhileLoopConstantConditionRuleTest.php index 519413d54e..3d05ccb85a 100644 --- a/tests/PHPStan/Rules/Comparison/DoWhileLoopConstantConditionRuleTest.php +++ b/tests/PHPStan/Rules/Comparison/DoWhileLoopConstantConditionRuleTest.php @@ -28,6 +28,11 @@ protected function getRule(): Rule ); } + public function testBug5865(): void + { + $this->analyse([__DIR__ . '/data/bug-5865.php'], []); + } + public function testRule(): void { $this->analyse([__DIR__ . '/data/do-while-loop.php'], [ diff --git a/tests/PHPStan/Rules/Comparison/data/bug-5865.php b/tests/PHPStan/Rules/Comparison/data/bug-5865.php new file mode 100644 index 0000000000..0f562bd0d8 --- /dev/null +++ b/tests/PHPStan/Rules/Comparison/data/bug-5865.php @@ -0,0 +1,25 @@ +foo(); + } while (true); + } catch (\RuntimeException $e) { + // ok + } + } + + /** + * @throws \RuntimeException + */ + public function foo(): void + { + throw new \RuntimeException(); + } +}