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..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) + public function __construct(private While_ $originalNode, private array $exitPoints, private bool $hasYield) { 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..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) + public function __construct(private Expr $cond, private array $exitPoints, private bool $hasYield) { 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..480aa85fe4 100644 --- a/tests/PHPStan/Rules/Comparison/WhileLoopAlwaysTrueConditionRuleTest.php +++ b/tests/PHPStan/Rules/Comparison/WhileLoopAlwaysTrueConditionRuleTest.php @@ -54,4 +54,23 @@ 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'], [ + [ + 'While loop condition is always true.', + 33, + ], + [ + 'While loop condition is always true.', + 44, + ], + ]); + } + } 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')); 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); + } + +}