Skip to content

Commit df5c98d

Browse files
committed
Check continue/break outside of loop and switch (level 0)
1 parent f9c5714 commit df5c98d

5 files changed

Lines changed: 222 additions & 0 deletions

File tree

build.xml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -137,6 +137,8 @@
137137
<arg path="tests/PHPStan/Rules/Functions/data/arrow-function-nullsafe-by-ref.php"/>
138138
<arg value="--exclude"/>
139139
<arg path="tests/PHPStan/Levels/data/namedArguments.php"/>
140+
<arg value="--exclude"/>
141+
<arg path="tests/PHPStan/Rules/Keywords/data/continue-break.php"/>
140142
<arg path="src" />
141143
<arg path="tests" />
142144
<arg path="compiler/src" />

conf/config.level0.neon

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,7 @@ rules:
4646
- PHPStan\Rules\Functions\ParamAttributesRule
4747
- PHPStan\Rules\Functions\PrintfParametersRule
4848
- PHPStan\Rules\Functions\ReturnNullsafeByRefRule
49+
- PHPStan\Rules\Keywords\ContinueBreakInLoopRule
4950
- PHPStan\Rules\Methods\AbstractMethodInNonAbstractClassRule
5051
- PHPStan\Rules\Methods\ExistingClassesInTypehintsRule
5152
- PHPStan\Rules\Methods\MissingMethodImplementationRule
Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,71 @@
1+
<?php declare(strict_types = 1);
2+
3+
namespace PHPStan\Rules\Keywords;
4+
5+
use PhpParser\Node;
6+
use PhpParser\Node\Stmt;
7+
use PHPStan\Analyser\Scope;
8+
use PHPStan\Rules\Rule;
9+
use PHPStan\Rules\RuleErrorBuilder;
10+
11+
/**
12+
* @implements Rule<Stmt>
13+
*/
14+
class ContinueBreakInLoopRule implements Rule
15+
{
16+
17+
public function getNodeType(): string
18+
{
19+
return Stmt::class;
20+
}
21+
22+
public function processNode(Node $node, Scope $scope): array
23+
{
24+
if (!$node instanceof Stmt\Continue_ && !$node instanceof Stmt\Break_) {
25+
return [];
26+
}
27+
28+
if (!$node->num instanceof Node\Scalar\LNumber) {
29+
$value = 1;
30+
} else {
31+
$value = $node->num->value;
32+
}
33+
34+
$parent = $node->getAttribute('parent');
35+
while ($value > 0) {
36+
if (
37+
$parent === null
38+
|| $parent instanceof Stmt\Function_
39+
|| $parent instanceof Stmt\ClassMethod
40+
|| $parent instanceof Node\Expr\Closure
41+
) {
42+
return [
43+
RuleErrorBuilder::message(sprintf(
44+
'Keyword %s used outside of a loop or a switch statement.',
45+
$node instanceof Stmt\Continue_ ? 'continue' : 'break'
46+
))->nonIgnorable()->build(),
47+
];
48+
}
49+
if (
50+
$parent instanceof Stmt\For_
51+
|| $parent instanceof Stmt\Foreach_
52+
|| $parent instanceof Stmt\Do_
53+
|| $parent instanceof Stmt\While_
54+
) {
55+
$value--;
56+
}
57+
if ($parent instanceof Stmt\Case_) {
58+
$value--;
59+
$parent = $parent->getAttribute('parent');
60+
if (!$parent instanceof Stmt\Switch_) {
61+
throw new \PHPStan\ShouldNotHappenException();
62+
}
63+
}
64+
65+
$parent = $parent->getAttribute('parent');
66+
}
67+
68+
return [];
69+
}
70+
71+
}
Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
<?php declare(strict_types = 1);
2+
3+
namespace PHPStan\Rules\Keywords;
4+
5+
use PHPStan\Testing\RuleTestCase;
6+
7+
/**
8+
* @extends RuleTestCase<ContinueBreakInLoopRule>
9+
*/
10+
class ContinueBreakInLoopRuleTest extends RuleTestCase
11+
{
12+
13+
protected function getRule(): \PHPStan\Rules\Rule
14+
{
15+
return new ContinueBreakInLoopRule();
16+
}
17+
18+
public function testRule(): void
19+
{
20+
if (!self::$useStaticReflectionProvider) {
21+
$this->markTestSkipped('Test requires static reflection.');
22+
}
23+
24+
$this->analyse([__DIR__ . '/data/continue-break.php'], [
25+
[
26+
'Keyword break used outside of a loop or a switch statement.',
27+
67,
28+
],
29+
[
30+
'Keyword break used outside of a loop or a switch statement.',
31+
69,
32+
],
33+
[
34+
'Keyword break used outside of a loop or a switch statement.',
35+
77,
36+
],
37+
[
38+
'Keyword continue used outside of a loop or a switch statement.',
39+
79,
40+
],
41+
[
42+
'Keyword break used outside of a loop or a switch statement.',
43+
87,
44+
],
45+
[
46+
'Keyword break used outside of a loop or a switch statement.',
47+
95,
48+
],
49+
]);
50+
}
51+
52+
}
Lines changed: 96 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,96 @@
1+
<?php
2+
3+
namespace ContinueBreak;
4+
5+
class Foo
6+
{
7+
8+
public function doFoo($foo): void
9+
{
10+
switch ($foo) {
11+
case 1:
12+
break;
13+
default:
14+
break;
15+
}
16+
17+
foreach ([1, 2, 3] as $val) {
18+
if (rand(0, 1)) {
19+
break;
20+
} else {
21+
continue;
22+
}
23+
}
24+
25+
for ($i = 0; $i < 5; $i++) {
26+
if (rand(0, 1)) {
27+
break;
28+
} else {
29+
continue;
30+
}
31+
}
32+
33+
while (true) {
34+
if (rand(0, 1)) {
35+
break;
36+
} else {
37+
continue;
38+
}
39+
}
40+
41+
do {
42+
if (rand(0, 1)) {
43+
break;
44+
} else {
45+
continue;
46+
}
47+
} while (true);
48+
}
49+
50+
public function doLorem($foo)
51+
{
52+
foreach ([1, 2, 3] as $val) {
53+
switch ($foo) {
54+
case 1:
55+
break 2;
56+
default:
57+
break 2;
58+
}
59+
}
60+
}
61+
62+
public function doBar($foo)
63+
{
64+
foreach ([1, 2, 3] as $val) {
65+
switch ($foo) {
66+
case 1:
67+
break 3;
68+
default:
69+
break 3;
70+
}
71+
}
72+
}
73+
74+
public function doBaz()
75+
{
76+
if (rand(0, 1)) {
77+
break;
78+
} else {
79+
continue;
80+
}
81+
}
82+
83+
public function doIpsum($foo)
84+
{
85+
foreach ([1, 2, 3] as $val) {
86+
function (): void {
87+
break;
88+
};
89+
}
90+
}
91+
92+
}
93+
94+
if (rand(0, 1)) {
95+
break;
96+
}

0 commit comments

Comments
 (0)