Skip to content

Commit 31fcad6

Browse files
committed
Support for break X and continue X
1 parent 990ba51 commit 31fcad6

12 files changed

+410
-11
lines changed

src/Analyser/NodeScopeResolver.php

+7-5
Original file line numberDiff line numberDiff line change
@@ -809,7 +809,7 @@ private function processStmtNode(
809809
$finalScope,
810810
$finalScopeResult->hasYield() || $condResult->hasYield(),
811811
$isIterableAtLeastOnce->yes() && $finalScopeResult->isAlwaysTerminating(),
812-
[]
812+
$finalScopeResult->getExitPointsForOuterLoop()
813813
);
814814
} elseif ($stmt instanceof While_) {
815815
$condResult = $this->processExprNode($stmt->cond, $scope, static function (): void {
@@ -875,7 +875,7 @@ private function processStmtNode(
875875
$finalScope,
876876
$finalScopeResult->hasYield() || $condResult->hasYield(),
877877
$isAlwaysTerminating,
878-
[]
878+
$finalScopeResult->getExitPointsForOuterLoop()
879879
);
880880
} elseif ($stmt instanceof Do_) {
881881
$finalScope = null;
@@ -940,7 +940,7 @@ private function processStmtNode(
940940
$finalScope,
941941
$bodyScopeResult->hasYield() || $hasYield,
942942
$alwaysTerminating,
943-
[]
943+
$bodyScopeResult->getExitPointsForOuterLoop()
944944
);
945945
} elseif ($stmt instanceof For_) {
946946
$initScope = $scope;
@@ -1014,7 +1014,7 @@ private function processStmtNode(
10141014
$finalScope,
10151015
$finalScopeResult->hasYield() || $hasYield,
10161016
false/* $finalScopeResult->isAlwaysTerminating() && $isAlwaysIterable*/,
1017-
[]
1017+
$finalScopeResult->getExitPointsForOuterLoop()
10181018
);
10191019
} elseif ($stmt instanceof Switch_) {
10201020
$condResult = $this->processExprNode($stmt->cond, $scope, $nodeCallback, ExpressionContext::createDeep());
@@ -1025,6 +1025,7 @@ private function processStmtNode(
10251025
$hasDefaultCase = false;
10261026
$alwaysTerminating = true;
10271027
$hasYield = $condResult->hasYield();
1028+
$exitPointsForOuterLoop = [];
10281029
foreach ($stmt->cases as $caseNode) {
10291030
if ($caseNode->cond !== null) {
10301031
$condExpr = new BinaryOp\Equal($stmt->cond, $caseNode->cond);
@@ -1047,6 +1048,7 @@ private function processStmtNode(
10471048
foreach ($branchScopeResult->getExitPointsByType(Continue_::class) as $continueExitPoint) {
10481049
$finalScope = $continueExitPoint->getScope()->mergeWith($finalScope);
10491050
}
1051+
$exitPointsForOuterLoop = array_merge($exitPointsForOuterLoop, $branchFinalScopeResult->getExitPointsForOuterLoop());
10501052
if ($branchScopeResult->isAlwaysTerminating()) {
10511053
$alwaysTerminating = $alwaysTerminating && $branchFinalScopeResult->isAlwaysTerminating();
10521054
$prevScope = null;
@@ -1074,7 +1076,7 @@ private function processStmtNode(
10741076
$finalScope = $scope->mergeWith($finalScope);
10751077
}
10761078

1077-
return new StatementResult($finalScope, $hasYield, $alwaysTerminating, []);
1079+
return new StatementResult($finalScope, $hasYield, $alwaysTerminating, $exitPointsForOuterLoop);
10781080
} elseif ($stmt instanceof TryCatch) {
10791081
$branchScopeResult = $this->processStmtNodes($stmt, $stmt->stmts, $scope, $nodeCallback);
10801082
$branchScope = $branchScopeResult->getScope();

src/Analyser/StatementResult.php

+70-6
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22

33
namespace PHPStan\Analyser;
44

5+
use PhpParser\Node\Scalar\LNumber;
56
use PhpParser\Node\Stmt;
67

78
class StatementResult
@@ -58,12 +59,20 @@ public function filterOutLoopExitPoints(): self
5859

5960
foreach ($this->exitPoints as $exitPoint) {
6061
$statement = $exitPoint->getStatement();
61-
if (
62-
$statement instanceof Stmt\Break_
63-
|| $statement instanceof Stmt\Continue_
64-
) {
62+
if (!$statement instanceof Stmt\Break_ && !$statement instanceof Stmt\Continue_) {
63+
continue;
64+
}
65+
66+
$num = $statement->num;
67+
if (!$num instanceof LNumber) {
6568
return new self($this->scope, $this->hasYield, false, $this->exitPoints);
6669
}
70+
71+
if ($num->value !== 1) {
72+
continue;
73+
}
74+
75+
return new self($this->scope, $this->hasYield, false, $this->exitPoints);
6776
}
6877

6978
return $this;
@@ -78,14 +87,31 @@ public function getExitPoints(): array
7887
}
7988

8089
/**
81-
* @param string $stmtClass
90+
* @param class-string<Stmt\Continue_>|class-string<Stmt\Break_> $stmtClass
8291
* @return StatementExitPoint[]
8392
*/
8493
public function getExitPointsByType(string $stmtClass): array
8594
{
8695
$exitPoints = [];
8796
foreach ($this->exitPoints as $exitPoint) {
88-
if (!$exitPoint->getStatement() instanceof $stmtClass) {
97+
$statement = $exitPoint->getStatement();
98+
if (!$statement instanceof $stmtClass) {
99+
continue;
100+
}
101+
102+
$value = $statement->num;
103+
if ($value === null) {
104+
$exitPoints[] = $exitPoint;
105+
continue;
106+
}
107+
108+
if (!$value instanceof LNumber) {
109+
$exitPoints[] = $exitPoint;
110+
continue;
111+
}
112+
113+
$value = $value->value;
114+
if ($value !== 1) {
89115
continue;
90116
}
91117

@@ -95,4 +121,42 @@ public function getExitPointsByType(string $stmtClass): array
95121
return $exitPoints;
96122
}
97123

124+
/**
125+
* @return StatementExitPoint[]
126+
*/
127+
public function getExitPointsForOuterLoop(): array
128+
{
129+
$exitPoints = [];
130+
foreach ($this->exitPoints as $exitPoint) {
131+
$statement = $exitPoint->getStatement();
132+
if (!$statement instanceof Stmt\Continue_ && !$statement instanceof Stmt\Break_) {
133+
continue;
134+
}
135+
if ($statement->num === null) {
136+
continue;
137+
}
138+
if (!$statement->num instanceof LNumber) {
139+
continue;
140+
}
141+
$value = $statement->num->value;
142+
if ($value === 1) {
143+
continue;
144+
}
145+
146+
$newNode = null;
147+
if ($value > 2) {
148+
$newNode = new LNumber($value - 1);
149+
}
150+
if ($statement instanceof Stmt\Continue_) {
151+
$newStatement = new Stmt\Continue_($newNode);
152+
} else {
153+
$newStatement = new Stmt\Break_($newNode);
154+
}
155+
156+
$exitPoints[] = new StatementExitPoint($newStatement, $exitPoint->getScope());
157+
}
158+
159+
return $exitPoints;
160+
}
161+
98162
}

tests/PHPStan/Analyser/NodeScopeResolverTest.php

+30
Original file line numberDiff line numberDiff line change
@@ -10777,6 +10777,31 @@ public function dataBug3777(): array
1077710777
return $this->gatherAssertTypes(__DIR__ . '/../Rules/Properties/data/bug-3777.php');
1077810778
}
1077910779

10780+
public function dataBug2549(): array
10781+
{
10782+
return $this->gatherAssertTypes(__DIR__ . '/data/bug-2549.php');
10783+
}
10784+
10785+
public function dataBug1945(): array
10786+
{
10787+
return $this->gatherAssertTypes(__DIR__ . '/data/bug-1945.php');
10788+
}
10789+
10790+
public function dataBug2003(): array
10791+
{
10792+
return $this->gatherAssertTypes(__DIR__ . '/data/bug-2003.php');
10793+
}
10794+
10795+
public function dataBug651(): array
10796+
{
10797+
return $this->gatherAssertTypes(__DIR__ . '/data/bug-651.php');
10798+
}
10799+
10800+
public function dataBug1283(): array
10801+
{
10802+
return $this->gatherAssertTypes(__DIR__ . '/data/bug-1283.php');
10803+
}
10804+
1078010805
/**
1078110806
* @param string $file
1078210807
* @return array<string, mixed[]>
@@ -10994,6 +11019,11 @@ private function gatherAssertTypes(string $file): array
1099411019
* @dataProvider dataBug4504
1099511020
* @dataProvider dataBug4436
1099611021
* @dataProvider dataBug3777
11022+
* @dataProvider dataBug2549
11023+
* @dataProvider dataBug1945
11024+
* @dataProvider dataBug2003
11025+
* @dataProvider dataBug651
11026+
* @dataProvider dataBug1283
1099711027
* @param string $assertType
1099811028
* @param string $file
1099911029
* @param mixed ...$args
+27
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
<?php
2+
3+
namespace Bug1283;
4+
5+
use PHPStan\TrinaryLogic;
6+
use function PHPStan\Analyser\assertType;
7+
use function PHPStan\Analyser\assertVariableCertainty;
8+
9+
function (array $levels): void {
10+
foreach ($levels as $level) {
11+
switch ($level) {
12+
case 'all':
13+
continue 2;
14+
case 'some':
15+
$allowedElements = array(1, 3);
16+
break;
17+
case 'one':
18+
$allowedElements = array(1);
19+
break;
20+
default:
21+
throw new \UnexpectedValueException(sprintf('Unsupported level `%s`', $level));
22+
}
23+
24+
assertType('array(0 => 1, ?1 => 3)', $allowedElements);
25+
assertVariableCertainty(TrinaryLogic::createYes(), $allowedElements);
26+
}
27+
};
+106
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,106 @@
1+
<?php
2+
3+
namespace Bug1945;
4+
5+
use PHPStan\TrinaryLogic;
6+
use function PHPStan\Analyser\assertType;
7+
use function PHPStan\Analyser\assertVariableCertainty;
8+
9+
function (): void {
10+
foreach (["a", "b", "c"] as $letter) {
11+
switch ($letter) {
12+
case "b":
13+
$foo = 1;
14+
break;
15+
case "c":
16+
$foo = 2;
17+
break;
18+
default:
19+
continue 2;
20+
}
21+
22+
assertType('1|2', $foo);
23+
assertVariableCertainty(TrinaryLogic::createYes(), $foo);
24+
}
25+
};
26+
27+
function (): void {
28+
foreach (["a", "b", "c"] as $letter) {
29+
switch ($letter) {
30+
case "a":
31+
if (rand(0, 10) === 1) {
32+
continue 2;
33+
}
34+
$foo = 1;
35+
break;
36+
case "b":
37+
if (rand(0, 10) === 1) {
38+
continue 2;
39+
}
40+
$foo = 2;
41+
break;
42+
default:
43+
continue 2;
44+
}
45+
46+
assertType('1|2', $foo);
47+
assertVariableCertainty(TrinaryLogic::createYes(), $foo);
48+
}
49+
};
50+
51+
function (array $docs): void {
52+
foreach ($docs as $doc) {
53+
switch (true) {
54+
case 'bar':
55+
continue 2;
56+
break;
57+
default:
58+
$foo = $doc;
59+
break;
60+
}
61+
62+
assertVariableCertainty(TrinaryLogic::createYes(), $foo);
63+
if (!$foo) {
64+
return;
65+
}
66+
}
67+
};
68+
69+
function (array $docs): void {
70+
foreach ($docs as $doc) {
71+
switch (true) {
72+
case 'bar':
73+
continue 2;
74+
default:
75+
$foo = $doc;
76+
break;
77+
}
78+
79+
assertVariableCertainty(TrinaryLogic::createYes(), $foo);
80+
if (!$foo) {
81+
return;
82+
}
83+
}
84+
};
85+
86+
function (array $items): string {
87+
foreach ($items as $item) {
88+
switch ($item) {
89+
case 1:
90+
$string = 'a';
91+
break;
92+
case 2:
93+
$string = 'b';
94+
break;
95+
default:
96+
continue 2;
97+
}
98+
99+
assertType('\'a\'|\'b\'', $string);
100+
assertVariableCertainty(TrinaryLogic::createYes(), $string);
101+
102+
return 'result: ' . $string;
103+
}
104+
105+
return 'ok';
106+
};
+25
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
<?php declare(strict_types=1);
2+
3+
namespace Bug2003;
4+
5+
use PHPStan\TrinaryLogic;
6+
use function PHPStan\Analyser\assertType;
7+
use function PHPStan\Analyser\assertVariableCertainty;
8+
9+
function (array $list): void {
10+
foreach ($list as $part) {
11+
switch (true) {
12+
case isset($list['magic']):
13+
$key = 'to-success';
14+
break;
15+
16+
default:
17+
continue 2;
18+
}
19+
20+
assertType('\'to-success\'', $key);
21+
assertVariableCertainty(TrinaryLogic::createYes(), $key);
22+
23+
echo $key;
24+
}
25+
};

0 commit comments

Comments
 (0)