From f1cf75fb77885140eaf8b5d38af5c9bb1fdfb3dc Mon Sep 17 00:00:00 2001 From: phpstan-bot <79867460+phpstan-bot@users.noreply.github.com> Date: Fri, 27 Feb 2026 12:48:18 +0000 Subject: [PATCH 1/3] Suppress always-true loop warnings when loop body contains yield - Generator suspension points (yield/yield from) act as break points, so while(true) { yield $x; } is valid and not a true infinite loop - Added hasYield flag to BreaklessWhileLoopNode and DoWhileLoopConditionNode - WhileLoopAlwaysTrueConditionRule and DoWhileLoopConstantConditionRule now skip the warning when the loop body contains a yield - Loops without yield in a generator still correctly report the warning Closes https://github.com/phpstan/phpstan/issues/6189 --- src/Analyser/NodeScopeResolver.php | 4 +- src/Node/BreaklessWhileLoopNode.php | 7 ++- src/Node/DoWhileLoopConditionNode.php | 7 ++- .../DoWhileLoopConstantConditionRule.php | 3 + .../WhileLoopAlwaysTrueConditionRule.php | 4 ++ .../DoWhileLoopConstantConditionRuleTest.php | 5 ++ .../WhileLoopAlwaysTrueConditionRuleTest.php | 14 +++++ .../Rules/Comparison/data/bug-6189.php | 59 +++++++++++++++++++ 8 files changed, 99 insertions(+), 4 deletions(-) create mode 100644 tests/PHPStan/Rules/Comparison/data/bug-6189.php diff --git a/src/Analyser/NodeScopeResolver.php b/src/Analyser/NodeScopeResolver.php index 86f616953b..5c47745504 100644 --- a/src/Analyser/NodeScopeResolver.php +++ b/src/Analyser/NodeScopeResolver.php @@ -1525,7 +1525,7 @@ private function processStmtNode( } $isIterableAtLeastOnce = $beforeCondBooleanType->isTrue()->yes(); - $this->callNodeCallback($nodeCallback, new BreaklessWhileLoopNode($stmt, $finalScopeResult->toPublic()->getExitPoints()), $bodyScopeMaybeRan, $storage); + $this->callNodeCallback($nodeCallback, new BreaklessWhileLoopNode($stmt, $finalScopeResult->toPublic()->getExitPoints(), $finalScopeResult->hasYield()), $bodyScopeMaybeRan, $storage); if ($alwaysIterates) { $isAlwaysTerminating = count($finalScopeResult->getExitPointsByType(Break_::class)) === 0; @@ -1607,7 +1607,7 @@ private function processStmtNode( $alwaysIterates = $condBooleanType->isTrue()->yes(); } - $this->callNodeCallback($nodeCallback, new DoWhileLoopConditionNode($stmt->cond, $bodyScopeResult->toPublic()->getExitPoints()), $bodyScope, $storage); + $this->callNodeCallback($nodeCallback, new DoWhileLoopConditionNode($stmt->cond, $bodyScopeResult->toPublic()->getExitPoints(), $bodyScopeResult->hasYield()), $bodyScope, $storage); if ($alwaysIterates) { $alwaysTerminating = count($bodyScopeResult->getExitPointsByType(Break_::class)) === 0; diff --git a/src/Node/BreaklessWhileLoopNode.php b/src/Node/BreaklessWhileLoopNode.php index 4bbfe497f7..6cc49905a6 100644 --- a/src/Node/BreaklessWhileLoopNode.php +++ b/src/Node/BreaklessWhileLoopNode.php @@ -16,7 +16,7 @@ final class BreaklessWhileLoopNode extends NodeAbstract implements VirtualNode /** * @param StatementExitPoint[] $exitPoints */ - public function __construct(private While_ $originalNode, private array $exitPoints) + public function __construct(private While_ $originalNode, private array $exitPoints, private bool $hasYield = false) { parent::__construct($originalNode->getAttributes()); } @@ -34,6 +34,11 @@ public function getExitPoints(): array return $this->exitPoints; } + public function hasYield(): bool + { + return $this->hasYield; + } + #[Override] public function getType(): string { diff --git a/src/Node/DoWhileLoopConditionNode.php b/src/Node/DoWhileLoopConditionNode.php index 5e589416eb..5a64050a48 100644 --- a/src/Node/DoWhileLoopConditionNode.php +++ b/src/Node/DoWhileLoopConditionNode.php @@ -13,7 +13,7 @@ final class DoWhileLoopConditionNode extends NodeAbstract implements VirtualNode /** * @param StatementExitPoint[] $exitPoints */ - public function __construct(private Expr $cond, private array $exitPoints) + public function __construct(private Expr $cond, private array $exitPoints, private bool $hasYield = false) { parent::__construct($cond->getAttributes()); } @@ -31,6 +31,11 @@ public function getExitPoints(): array return $this->exitPoints; } + public function hasYield(): bool + { + return $this->hasYield; + } + #[Override] public function getType(): string { diff --git a/src/Rules/Comparison/DoWhileLoopConstantConditionRule.php b/src/Rules/Comparison/DoWhileLoopConstantConditionRule.php index 7087fbd006..fd7435668d 100644 --- a/src/Rules/Comparison/DoWhileLoopConstantConditionRule.php +++ b/src/Rules/Comparison/DoWhileLoopConstantConditionRule.php @@ -42,6 +42,9 @@ public function processNode(Node $node, Scope $scope): array $exprType = $this->helper->getBooleanType($scope, $node->getCond()); if ($exprType instanceof ConstantBooleanType) { if ($exprType->getValue()) { + if ($node->hasYield()) { + return []; + } foreach ($node->getExitPoints() as $exitPoint) { $statement = $exitPoint->getStatement(); if (!$statement instanceof Continue_) { diff --git a/src/Rules/Comparison/WhileLoopAlwaysTrueConditionRule.php b/src/Rules/Comparison/WhileLoopAlwaysTrueConditionRule.php index 3f251f8366..b9af680b1f 100644 --- a/src/Rules/Comparison/WhileLoopAlwaysTrueConditionRule.php +++ b/src/Rules/Comparison/WhileLoopAlwaysTrueConditionRule.php @@ -68,6 +68,10 @@ public function processNode( $originalNode = $node->getOriginalNode(); $exprType = $this->helper->getBooleanType($scope, $originalNode->cond); if ($exprType->isTrue()->yes()) { + if ($node->hasYield()) { + return []; + } + $ref = $scope->getFunction() ?? $scope->getAnonymousFunctionReflection(); if ($ref !== null && $ref->getReturnType() instanceof NeverType) { diff --git a/tests/PHPStan/Rules/Comparison/DoWhileLoopConstantConditionRuleTest.php b/tests/PHPStan/Rules/Comparison/DoWhileLoopConstantConditionRuleTest.php index 519413d54e..cd5f40afca 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 testBug6189(): void + { + $this->analyse([__DIR__ . '/data/bug-6189.php'], []); + } + public function testRule(): void { $this->analyse([__DIR__ . '/data/do-while-loop.php'], [ diff --git a/tests/PHPStan/Rules/Comparison/WhileLoopAlwaysTrueConditionRuleTest.php b/tests/PHPStan/Rules/Comparison/WhileLoopAlwaysTrueConditionRuleTest.php index b0fe019bd4..6eb3c600ec 100644 --- a/tests/PHPStan/Rules/Comparison/WhileLoopAlwaysTrueConditionRuleTest.php +++ b/tests/PHPStan/Rules/Comparison/WhileLoopAlwaysTrueConditionRuleTest.php @@ -54,4 +54,18 @@ public function testRulePHP81(): void $this->analyse([__DIR__ . '/data/while-loop-true-php81.php'], []); } + public function testBug6189(): void + { + $this->analyse([__DIR__ . '/data/bug-6189.php'], [ + [ + 'While loop condition is always true.', + 33, + ], + [ + 'While loop condition is always true.', + 44, + ], + ]); + } + } diff --git a/tests/PHPStan/Rules/Comparison/data/bug-6189.php b/tests/PHPStan/Rules/Comparison/data/bug-6189.php new file mode 100644 index 0000000000..6a1d18de01 --- /dev/null +++ b/tests/PHPStan/Rules/Comparison/data/bug-6189.php @@ -0,0 +1,59 @@ + + */ + public function generatorWithYield(): Generator + { + while (true) { + yield 1; + } + } + + /** + * @return Generator + */ + public function generatorWithYieldFrom(): Generator + { + while (true) { + yield from [1, 2, 3]; + } + } + + /** Still an infinite loop - no yield inside the while body */ + public function noYieldInLoop(): void + { + while (true) { + + } + } + + /** + * @return Generator + */ + public function generatorWithYieldOutsideLoop(): Generator + { + yield 0; + while (true) { + // yield is outside the loop, so this is still an infinite loop + } + } + + /** + * @return Generator + */ + public function generatorDoWhileWithYield(): Generator + { + do { + yield 1; + } while (true); + } + +} From ceff22ae96dde497298158c0608db1229feda0b1 Mon Sep 17 00:00:00 2001 From: Vincent Langlet Date: Sun, 1 Mar 2026 16:40:01 +0100 Subject: [PATCH 2/3] Remove default value --- src/Node/BreaklessWhileLoopNode.php | 2 +- src/Node/DoWhileLoopConditionNode.php | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Node/BreaklessWhileLoopNode.php b/src/Node/BreaklessWhileLoopNode.php index 6cc49905a6..5feca36610 100644 --- a/src/Node/BreaklessWhileLoopNode.php +++ b/src/Node/BreaklessWhileLoopNode.php @@ -16,7 +16,7 @@ final class BreaklessWhileLoopNode extends NodeAbstract implements VirtualNode /** * @param StatementExitPoint[] $exitPoints */ - public function __construct(private While_ $originalNode, private array $exitPoints, private bool $hasYield = false) + public function __construct(private While_ $originalNode, private array $exitPoints, private bool $hasYield) { parent::__construct($originalNode->getAttributes()); } diff --git a/src/Node/DoWhileLoopConditionNode.php b/src/Node/DoWhileLoopConditionNode.php index 5a64050a48..a306bf4692 100644 --- a/src/Node/DoWhileLoopConditionNode.php +++ b/src/Node/DoWhileLoopConditionNode.php @@ -13,7 +13,7 @@ final class DoWhileLoopConditionNode extends NodeAbstract implements VirtualNode /** * @param StatementExitPoint[] $exitPoints */ - public function __construct(private Expr $cond, private array $exitPoints, private bool $hasYield = false) + public function __construct(private Expr $cond, private array $exitPoints, private bool $hasYield) { parent::__construct($cond->getAttributes()); } From 807e892bd8c27ce9e382d47ed5c3962448d4a07d Mon Sep 17 00:00:00 2001 From: Vincent Langlet Date: Sun, 1 Mar 2026 18:42:12 +0100 Subject: [PATCH 3/3] Add non regression test --- .../WhileLoopAlwaysTrueConditionRuleTest.php | 5 +++++ tests/PHPStan/Rules/Comparison/data/bug-10054.php | 15 +++++++++++++++ 2 files changed, 20 insertions(+) create mode 100644 tests/PHPStan/Rules/Comparison/data/bug-10054.php diff --git a/tests/PHPStan/Rules/Comparison/WhileLoopAlwaysTrueConditionRuleTest.php b/tests/PHPStan/Rules/Comparison/WhileLoopAlwaysTrueConditionRuleTest.php index 6eb3c600ec..480aa85fe4 100644 --- a/tests/PHPStan/Rules/Comparison/WhileLoopAlwaysTrueConditionRuleTest.php +++ b/tests/PHPStan/Rules/Comparison/WhileLoopAlwaysTrueConditionRuleTest.php @@ -54,6 +54,11 @@ public function testRulePHP81(): void $this->analyse([__DIR__ . '/data/while-loop-true-php81.php'], []); } + public function testBug10054(): void + { + $this->analyse([__DIR__ . '/data/bug-10054.php'], []); + } + public function testBug6189(): void { $this->analyse([__DIR__ . '/data/bug-6189.php'], [ diff --git a/tests/PHPStan/Rules/Comparison/data/bug-10054.php b/tests/PHPStan/Rules/Comparison/data/bug-10054.php new file mode 100644 index 0000000000..8d1e186c8b --- /dev/null +++ b/tests/PHPStan/Rules/Comparison/data/bug-10054.php @@ -0,0 +1,15 @@ +send('foo'));