Skip to content

Commit 3dd5a01

Browse files
authored
Support for endless loops
1 parent bf85e9d commit 3dd5a01

11 files changed

+226
-18
lines changed

src/Analyser/NodeScopeResolver.php

+12-1
Original file line numberDiff line numberDiff line change
@@ -1384,7 +1384,10 @@ private function processStmtNode(
13841384
$loopScope = $this->processExprNode($stmt, $loopExpr, $loopScope, $nodeCallback, ExpressionContext::createTopLevel())->getScope();
13851385
}
13861386
$finalScope = $finalScope->generalizeWith($loopScope);
1387+
1388+
$alwaysIterates = TrinaryLogic::createFromBoolean($context->isTopLevel());
13871389
if ($lastCondExpr !== null) {
1390+
$alwaysIterates = $alwaysIterates->and($finalScope->getType($lastCondExpr)->toBoolean()->isTrue());
13881391
$finalScope = $finalScope->filterByFalseyValue($lastCondExpr);
13891392
}
13901393

@@ -1411,10 +1414,18 @@ private function processStmtNode(
14111414
}
14121415
}
14131416

1417+
if ($alwaysIterates->yes()) {
1418+
$isAlwaysTerminating = count($finalScopeResult->getExitPointsByType(Break_::class)) === 0;
1419+
} elseif ($isIterableAtLeastOnce->yes()) {
1420+
$isAlwaysTerminating = $finalScopeResult->isAlwaysTerminating();
1421+
} else {
1422+
$isAlwaysTerminating = false;
1423+
}
1424+
14141425
return new StatementResult(
14151426
$finalScope,
14161427
$finalScopeResult->hasYield() || $hasYield,
1417-
false/* $finalScopeResult->isAlwaysTerminating() && $isAlwaysIterable*/,
1428+
$isAlwaysTerminating,
14181429
$finalScopeResult->getExitPointsForOuterLoop(),
14191430
array_merge($throwPoints, $finalScopeResult->getThrowPoints()),
14201431
array_merge($impurePoints, $finalScopeResult->getImpurePoints()),

tests/PHPStan/Analyser/StatementResultTest.php

+125-1
Original file line numberDiff line numberDiff line change
@@ -173,6 +173,130 @@ public function dataIsAlwaysTerminating(): array
173173
'while (true) { break; }',
174174
false,
175175
],
176+
[
177+
'while (true) { exit; }',
178+
true,
179+
],
180+
[
181+
'while (true) { while (true) { } }',
182+
true,
183+
],
184+
[
185+
'while (true) { while (true) { return; } }',
186+
true,
187+
],
188+
[
189+
'while (true) { while (true) { break; } }',
190+
true,
191+
],
192+
[
193+
'while (true) { while (true) { exit; } }',
194+
true,
195+
],
196+
[
197+
'while (true) { while (true) { break 2; } }',
198+
false,
199+
],
200+
[
201+
'while (true) { while ($x) { } }',
202+
true,
203+
],
204+
[
205+
'while (true) { while ($x) { return; } }',
206+
true,
207+
],
208+
[
209+
'while (true) { while ($x) { break; } }',
210+
true,
211+
],
212+
[
213+
'while (true) { while ($x) { exit; } }',
214+
true,
215+
],
216+
[
217+
'while (true) { while ($x) { break 2; } }',
218+
false,
219+
],
220+
[
221+
'for (;;) { }',
222+
true,
223+
],
224+
[
225+
'for (;;) { return; }',
226+
true,
227+
],
228+
[
229+
'for (;;) { break; }',
230+
false,
231+
],
232+
[
233+
'for (;;) { exit; }',
234+
true,
235+
],
236+
[
237+
'for (;;) { for (;;) { } }',
238+
true,
239+
],
240+
[
241+
'for (;;) { for (;;) { return; } }',
242+
true,
243+
],
244+
[
245+
'for (;;) { for (;;) { break; } }',
246+
true,
247+
],
248+
[
249+
'for (;;) { for (;;) { exit; } }',
250+
true,
251+
],
252+
[
253+
'for (;;) { for (;;) { break 2; } }',
254+
false,
255+
],
256+
[
257+
'for (;;) { for ($i = 0; $i< 5; $i++) { } }',
258+
true,
259+
],
260+
[
261+
'for (;;) { for ($i = 0; $i< 5; $i++) { return; } }',
262+
true,
263+
],
264+
[
265+
'for (;;) { for ($i = 0; $i< 5; $i++) { break; } }',
266+
true,
267+
],
268+
[
269+
'for (;;) { for ($i = 0; $i< 5; $i++) { exit; } }',
270+
true,
271+
],
272+
[
273+
'for (;;) { for ($i = 0; $i< 5; $i++) { break 2; } }',
274+
false,
275+
],
276+
[
277+
'for ($i = 0; $i < 5;) { }',
278+
true,
279+
],
280+
[
281+
'for ($i = 0; $i < 5; $i--) { }',
282+
true,
283+
],
284+
[
285+
'for (; 0, 1;) { }',
286+
true,
287+
],
288+
[
289+
'for (; 1, 0;) { }',
290+
false,
291+
],
292+
[
293+
'for (; "", "a";) { }',
294+
true,
295+
],
296+
[
297+
'for (; "a", "";) { }',
298+
false,
299+
],
176300
[
177301
'do { } while (doFoo());',
178302
false,
@@ -231,7 +355,7 @@ public function dataIsAlwaysTerminating(): array
231355
],
232356
[
233357
'for ($i = 0; $i < 10; $i++) { return; }',
234-
false, // will be true with range types
358+
true,
235359
],
236360
[
237361
'for ($i = 0; $i < 0; $i++) { return; }',

tests/PHPStan/Rules/Comparison/StrictComparisonOfDifferentTypesRuleTest.php

+5-5
Original file line numberDiff line numberDiff line change
@@ -109,12 +109,12 @@ public function testStrictComparison(): void
109109
140,
110110
],
111111
[
112-
'Strict comparison using !== between StrictComparison\Foo|null and 1 will always evaluate to true.',
113-
154,
112+
'Strict comparison using === between non-empty-array and null will always evaluate to false.',
113+
150,
114114
],
115115
[
116-
'Strict comparison using === between non-empty-array and null will always evaluate to false.',
117-
164,
116+
'Strict comparison using !== between StrictComparison\Foo|null and 1 will always evaluate to true.',
117+
161,
118118
],
119119
[
120120
'Strict comparison using !== between StrictComparison\Node|null and false will always evaluate to true.',
@@ -352,7 +352,7 @@ public function testStrictComparisonWithoutAlwaysTrue(): void
352352
],
353353
[
354354
'Strict comparison using === between non-empty-array and null will always evaluate to false.',
355-
164,
355+
150,
356356
],
357357
[
358358
'Strict comparison using === between 1 and 2 will always evaluate to false.',

tests/PHPStan/Rules/Comparison/data/strict-comparison.php

+7-7
Original file line numberDiff line numberDiff line change
@@ -146,6 +146,13 @@ public function whileWithTypeChange()
146146

147147
public function forWithTypeChange()
148148
{
149+
for (; $val = $this->returnArray();) {
150+
if ($val === null) {
151+
152+
}
153+
$val = null;
154+
}
155+
149156
$foo = null;
150157
for (;;) {
151158
if ($foo !== null) {
@@ -159,13 +166,6 @@ public function forWithTypeChange()
159166
$foo = new self();
160167
}
161168
}
162-
163-
for (; $val = $this->returnArray();) {
164-
if ($val === null) {
165-
166-
}
167-
$val = null;
168-
}
169169
}
170170

171171
private function returnArray(): array

tests/PHPStan/Rules/Missing/MissingReturnRuleTest.php

+18
Original file line numberDiff line numberDiff line change
@@ -325,4 +325,22 @@ public function testBug9309(): void
325325
$this->analyse([__DIR__ . '/data/bug-9309.php'], []);
326326
}
327327

328+
public function testBug6807(): void
329+
{
330+
$this->checkExplicitMixedMissingReturn = true;
331+
$this->analyse([__DIR__ . '/data/bug-6807.php'], []);
332+
}
333+
334+
public function testBug8463(): void
335+
{
336+
$this->checkExplicitMixedMissingReturn = true;
337+
$this->analyse([__DIR__ . '/data/bug-8463.php'], []);
338+
}
339+
340+
public function testBug9374(): void
341+
{
342+
$this->checkExplicitMixedMissingReturn = true;
343+
$this->analyse([__DIR__ . '/data/bug-9374.php'], []);
344+
}
345+
328346
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
<?php declare(strict_types = 1);
2+
3+
namespace Bug6807;
4+
5+
/** @return int */
6+
function test()
7+
{
8+
for ($attempts = 0; ; $attempts++)
9+
{
10+
if ($attempts > 5)
11+
throw new Exception();
12+
13+
if (rand() == 1)
14+
return 5;
15+
}
16+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
<?php
2+
3+
namespace Bug8463;
4+
5+
function f1() : int
6+
{
7+
while(true)
8+
{
9+
if(rand() === rand())
10+
{
11+
return 1;
12+
}
13+
}
14+
}
15+
16+
17+
function f2() : int
18+
{
19+
for(;;)
20+
{
21+
if(rand() === rand())
22+
{
23+
return 1;
24+
}
25+
}
26+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
<?php declare(strict_types = 1);
2+
3+
namespace Bug9374;
4+
5+
function bar(): string {
6+
for ($i = 0; ; ++$i)
7+
return "";
8+
}

tests/PHPStan/Rules/Variables/data/defined-variables-anonymous-function-use.php

+1-1
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@ function () use (&$errorHandler) {
1414
$onlyInIf = 1;
1515
}
1616

17-
for ($forI = 0; $forI < 10, $anotherVariableFromForCond = 1; $forI++, $forJ = $forI) {
17+
for ($forI = 0; $anotherVariableFromForCond = 1, $forI < 10; $forI++, $forJ = $forI) {
1818

1919
}
2020

tests/PHPStan/Rules/Variables/data/defined-variables.php

+3-3
Original file line numberDiff line numberDiff line change
@@ -243,7 +243,7 @@ function () {
243243

244244
}
245245

246-
for ($forI = 0; $forI < 10, $forK = 5; $forI++, $forK++, $forJ = $forI) {
246+
for ($forI = 0; $forK = 5, $forI < 10; $forI++, $forK++, $forJ = $forI) {
247247
echo $forI;
248248
}
249249

@@ -322,7 +322,7 @@ function () {
322322
include($fileB='includeB.php');
323323
echo $fileB;
324324

325-
for ($forLoopVariableInit = 0; $forLoopVariableInit < 5; $forLoopVariableInit = $forLoopVariable, $anotherForLoopVariable = 1) {
325+
for ($forLoopVariableInit = 0; $forLoopVariableInit < 5 && rand(0, 1); $forLoopVariableInit = $forLoopVariable, $anotherForLoopVariable = 1) {
326326
$forLoopVariable = 2;
327327
}
328328
echo $anotherForLoopVariable;
@@ -357,7 +357,7 @@ function () {
357357

358358
}
359359

360-
for (; $forVariableUsedAndThenDefined && $forVariableUsedAndThenDefined = 1;) {
360+
for (; $forVariableUsedAndThenDefined && $forVariableUsedAndThenDefined = 1 && rand(0, 1);) {
361361

362362
}
363363

tests/e2e/baseline.neon

+5
Original file line numberDiff line numberDiff line change
@@ -155,6 +155,11 @@ parameters:
155155
count: 1
156156
path: PHP-Parser/lib/PhpParser/ParserAbstract.php
157157

158+
-
159+
message: "#^Unreachable statement \\- code above always terminates\\.$#"
160+
count: 1
161+
path: PHP-Parser/lib/PhpParser/ParserAbstract.php
162+
158163
-
159164
message: "#^Variable \\$action might not be defined\\.$#"
160165
count: 1

0 commit comments

Comments
 (0)