Skip to content

Commit 925f29c

Browse files
Fix phpstan/phpstan#6189: Generator suspension points should be treated as infinite loop break points (#5057)
Co-authored-by: Vincent Langlet <vincentlanglet@hotmail.fr>
1 parent bb42c9f commit 925f29c

File tree

9 files changed

+119
-4
lines changed

9 files changed

+119
-4
lines changed

src/Analyser/NodeScopeResolver.php

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1527,7 +1527,7 @@ private function processStmtNode(
15271527
}
15281528

15291529
$isIterableAtLeastOnce = $beforeCondBooleanType->isTrue()->yes();
1530-
$this->callNodeCallback($nodeCallback, new BreaklessWhileLoopNode($stmt, $finalScopeResult->toPublic()->getExitPoints()), $bodyScopeMaybeRan, $storage);
1530+
$this->callNodeCallback($nodeCallback, new BreaklessWhileLoopNode($stmt, $finalScopeResult->toPublic()->getExitPoints(), $finalScopeResult->hasYield()), $bodyScopeMaybeRan, $storage);
15311531

15321532
if ($alwaysIterates) {
15331533
$isAlwaysTerminating = count($finalScopeResult->getExitPointsByType(Break_::class)) === 0;
@@ -1609,7 +1609,7 @@ private function processStmtNode(
16091609
$alwaysIterates = $condBooleanType->isTrue()->yes();
16101610
}
16111611

1612-
$this->callNodeCallback($nodeCallback, new DoWhileLoopConditionNode($stmt->cond, $bodyScopeResult->toPublic()->getExitPoints()), $bodyScope, $storage);
1612+
$this->callNodeCallback($nodeCallback, new DoWhileLoopConditionNode($stmt->cond, $bodyScopeResult->toPublic()->getExitPoints(), $bodyScopeResult->hasYield()), $bodyScope, $storage);
16131613

16141614
if ($alwaysIterates) {
16151615
$alwaysTerminating = count($bodyScopeResult->getExitPointsByType(Break_::class)) === 0;

src/Node/BreaklessWhileLoopNode.php

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@ final class BreaklessWhileLoopNode extends NodeAbstract implements VirtualNode
1616
/**
1717
* @param StatementExitPoint[] $exitPoints
1818
*/
19-
public function __construct(private While_ $originalNode, private array $exitPoints)
19+
public function __construct(private While_ $originalNode, private array $exitPoints, private bool $hasYield)
2020
{
2121
parent::__construct($originalNode->getAttributes());
2222
}
@@ -34,6 +34,11 @@ public function getExitPoints(): array
3434
return $this->exitPoints;
3535
}
3636

37+
public function hasYield(): bool
38+
{
39+
return $this->hasYield;
40+
}
41+
3742
#[Override]
3843
public function getType(): string
3944
{

src/Node/DoWhileLoopConditionNode.php

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@ final class DoWhileLoopConditionNode extends NodeAbstract implements VirtualNode
1313
/**
1414
* @param StatementExitPoint[] $exitPoints
1515
*/
16-
public function __construct(private Expr $cond, private array $exitPoints)
16+
public function __construct(private Expr $cond, private array $exitPoints, private bool $hasYield)
1717
{
1818
parent::__construct($cond->getAttributes());
1919
}
@@ -31,6 +31,11 @@ public function getExitPoints(): array
3131
return $this->exitPoints;
3232
}
3333

34+
public function hasYield(): bool
35+
{
36+
return $this->hasYield;
37+
}
38+
3439
#[Override]
3540
public function getType(): string
3641
{

src/Rules/Comparison/DoWhileLoopConstantConditionRule.php

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,9 @@ public function processNode(Node $node, Scope $scope): array
4242
$exprType = $this->helper->getBooleanType($scope, $node->getCond());
4343
if ($exprType instanceof ConstantBooleanType) {
4444
if ($exprType->getValue()) {
45+
if ($node->hasYield()) {
46+
return [];
47+
}
4548
foreach ($node->getExitPoints() as $exitPoint) {
4649
$statement = $exitPoint->getStatement();
4750
if (!$statement instanceof Continue_) {

src/Rules/Comparison/WhileLoopAlwaysTrueConditionRule.php

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -68,6 +68,10 @@ public function processNode(
6868
$originalNode = $node->getOriginalNode();
6969
$exprType = $this->helper->getBooleanType($scope, $originalNode->cond);
7070
if ($exprType->isTrue()->yes()) {
71+
if ($node->hasYield()) {
72+
return [];
73+
}
74+
7175
$ref = $scope->getFunction() ?? $scope->getAnonymousFunctionReflection();
7276

7377
if ($ref !== null && $ref->getReturnType() instanceof NeverType) {

tests/PHPStan/Rules/Comparison/DoWhileLoopConstantConditionRuleTest.php

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,11 @@ protected function getRule(): Rule
2828
);
2929
}
3030

31+
public function testBug6189(): void
32+
{
33+
$this->analyse([__DIR__ . '/data/bug-6189.php'], []);
34+
}
35+
3136
public function testRule(): void
3237
{
3338
$this->analyse([__DIR__ . '/data/do-while-loop.php'], [

tests/PHPStan/Rules/Comparison/WhileLoopAlwaysTrueConditionRuleTest.php

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -54,4 +54,23 @@ public function testRulePHP81(): void
5454
$this->analyse([__DIR__ . '/data/while-loop-true-php81.php'], []);
5555
}
5656

57+
public function testBug10054(): void
58+
{
59+
$this->analyse([__DIR__ . '/data/bug-10054.php'], []);
60+
}
61+
62+
public function testBug6189(): void
63+
{
64+
$this->analyse([__DIR__ . '/data/bug-6189.php'], [
65+
[
66+
'While loop condition is always true.',
67+
33,
68+
],
69+
[
70+
'While loop condition is always true.',
71+
44,
72+
],
73+
]);
74+
}
75+
5776
}
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
<?php
2+
3+
namespace Bug10054;
4+
5+
function falsePositive(): \Generator {
6+
while (true) {
7+
$item = yield;
8+
9+
yield $item;
10+
}
11+
}
12+
13+
$generator = falsePositive();
14+
15+
var_dump($generator->send('foo'));
Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
1+
<?php declare(strict_types = 1);
2+
3+
namespace Bug6189;
4+
5+
use Generator;
6+
7+
class Foo
8+
{
9+
10+
/**
11+
* @return Generator<int, int, null, void>
12+
*/
13+
public function generatorWithYield(): Generator
14+
{
15+
while (true) {
16+
yield 1;
17+
}
18+
}
19+
20+
/**
21+
* @return Generator<int, int, null, void>
22+
*/
23+
public function generatorWithYieldFrom(): Generator
24+
{
25+
while (true) {
26+
yield from [1, 2, 3];
27+
}
28+
}
29+
30+
/** Still an infinite loop - no yield inside the while body */
31+
public function noYieldInLoop(): void
32+
{
33+
while (true) {
34+
35+
}
36+
}
37+
38+
/**
39+
* @return Generator<int, int, null, void>
40+
*/
41+
public function generatorWithYieldOutsideLoop(): Generator
42+
{
43+
yield 0;
44+
while (true) {
45+
// yield is outside the loop, so this is still an infinite loop
46+
}
47+
}
48+
49+
/**
50+
* @return Generator<int, int, null, void>
51+
*/
52+
public function generatorDoWhileWithYield(): Generator
53+
{
54+
do {
55+
yield 1;
56+
} while (true);
57+
}
58+
59+
}

0 commit comments

Comments
 (0)