Skip to content

Commit e81ccd4

Browse files
committed
Do-while loop - constant condition rule
1 parent 0cde73f commit e81ccd4

File tree

7 files changed

+320
-0
lines changed

7 files changed

+320
-0
lines changed

conf/config.level4.neon

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -64,6 +64,13 @@ services:
6464
tags:
6565
- phpstan.rules.rule
6666

67+
-
68+
class: PHPStan\Rules\Comparison\DoWhileLoopConstantConditionRule
69+
arguments:
70+
treatPhpDocTypesAsCertain: %treatPhpDocTypesAsCertain%
71+
tags:
72+
- phpstan.rules.rule
73+
6774
-
6875
class: PHPStan\Rules\Comparison\ElseIfConstantConditionRule
6976
arguments:

src/Analyser/NodeScopeResolver.php

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -64,6 +64,7 @@
6464
use PHPStan\Node\ClassPropertyNode;
6565
use PHPStan\Node\ClassStatementsGatherer;
6666
use PHPStan\Node\ClosureReturnStatementsNode;
67+
use PHPStan\Node\DoWhileLoopConditionNode;
6768
use PHPStan\Node\ExecutionEndNode;
6869
use PHPStan\Node\FinallyExitPointsNode;
6970
use PHPStan\Node\FunctionReturnStatementsNode;
@@ -973,6 +974,8 @@ private function processStmtNode(
973974
$condBooleanType = $bodyScope->getType($stmt->cond)->toBoolean();
974975
$alwaysIterates = $condBooleanType instanceof ConstantBooleanType && $condBooleanType->getValue();
975976

977+
$nodeCallback(new DoWhileLoopConditionNode($stmt->cond, $bodyScopeResult->getExitPoints()), $bodyScope);
978+
976979
if ($alwaysIterates) {
977980
$alwaysTerminating = count($bodyScopeResult->getExitPointsByType(Break_::class)) === 0;
978981
} else {

src/Analyser/StatementResult.php

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -136,6 +136,7 @@ public function getExitPointsForOuterLoop(): array
136136
foreach ($this->exitPoints as $exitPoint) {
137137
$statement = $exitPoint->getStatement();
138138
if (!$statement instanceof Stmt\Continue_ && !$statement instanceof Stmt\Break_) {
139+
$exitPoints[] = $exitPoint;
139140
continue;
140141
}
141142
if ($statement->num === null) {

src/Node/DoWhileLoopConditionNode.php

Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
<?php declare(strict_types = 1);
2+
3+
namespace PHPStan\Node;
4+
5+
use PhpParser\Node\Expr;
6+
use PhpParser\NodeAbstract;
7+
use PHPStan\Analyser\StatementExitPoint;
8+
9+
class DoWhileLoopConditionNode extends NodeAbstract implements VirtualNode
10+
{
11+
12+
private Expr $cond;
13+
14+
/** @var StatementExitPoint[] */
15+
private array $exitPoints;
16+
17+
/**
18+
* @param StatementExitPoint[] $exitPoints
19+
*/
20+
public function __construct(Expr $cond, array $exitPoints)
21+
{
22+
parent::__construct($cond->getAttributes());
23+
$this->cond = $cond;
24+
$this->exitPoints = $exitPoints;
25+
}
26+
27+
public function getCond(): Expr
28+
{
29+
return $this->cond;
30+
}
31+
32+
/**
33+
* @return StatementExitPoint[]
34+
*/
35+
public function getExitPoints(): array
36+
{
37+
return $this->exitPoints;
38+
}
39+
40+
public function getType(): string
41+
{
42+
return 'PHPStan_Node_ClosureReturnStatementsNode';
43+
}
44+
45+
/**
46+
* @return string[]
47+
*/
48+
public function getSubNodeNames(): array
49+
{
50+
return [];
51+
}
52+
53+
}
Lines changed: 93 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,93 @@
1+
<?php declare(strict_types = 1);
2+
3+
namespace PHPStan\Rules\Comparison;
4+
5+
use PhpParser\Node;
6+
use PhpParser\Node\Scalar\LNumber;
7+
use PhpParser\Node\Stmt\Break_;
8+
use PhpParser\Node\Stmt\Continue_;
9+
use PHPStan\Analyser\Scope;
10+
use PHPStan\Node\DoWhileLoopConditionNode;
11+
use PHPStan\Rules\Rule;
12+
use PHPStan\Rules\RuleErrorBuilder;
13+
use PHPStan\Type\Constant\ConstantBooleanType;
14+
15+
/**
16+
* @implements Rule<DoWhileLoopConditionNode>
17+
*/
18+
class DoWhileLoopConstantConditionRule implements Rule
19+
{
20+
21+
private ConstantConditionRuleHelper $helper;
22+
23+
private bool $treatPhpDocTypesAsCertain;
24+
25+
public function __construct(
26+
ConstantConditionRuleHelper $helper,
27+
bool $treatPhpDocTypesAsCertain
28+
)
29+
{
30+
$this->helper = $helper;
31+
$this->treatPhpDocTypesAsCertain = $treatPhpDocTypesAsCertain;
32+
}
33+
34+
public function getNodeType(): string
35+
{
36+
return DoWhileLoopConditionNode::class;
37+
}
38+
39+
public function processNode(Node $node, Scope $scope): array
40+
{
41+
$exprType = $this->helper->getBooleanType($scope, $node->getCond());
42+
if ($exprType instanceof ConstantBooleanType) {
43+
if ($exprType->getValue()) {
44+
foreach ($node->getExitPoints() as $exitPoint) {
45+
$statement = $exitPoint->getStatement();
46+
if ($statement instanceof Break_) {
47+
return [];
48+
}
49+
if (!$statement instanceof Continue_) {
50+
return [];
51+
}
52+
if ($statement->num === null) {
53+
continue;
54+
}
55+
if (!$statement->num instanceof LNumber) {
56+
continue;
57+
}
58+
$value = $statement->num->value;
59+
if ($value === 1) {
60+
continue;
61+
}
62+
63+
if ($value > 1) {
64+
return [];
65+
}
66+
}
67+
}
68+
69+
$addTip = function (RuleErrorBuilder $ruleErrorBuilder) use ($scope, $node): RuleErrorBuilder {
70+
if (!$this->treatPhpDocTypesAsCertain) {
71+
return $ruleErrorBuilder;
72+
}
73+
74+
$booleanNativeType = $this->helper->getNativeBooleanType($scope, $node->getCond());
75+
if ($booleanNativeType instanceof ConstantBooleanType) {
76+
return $ruleErrorBuilder;
77+
}
78+
79+
return $ruleErrorBuilder->tip('Because the type is coming from a PHPDoc, you can turn off this check by setting <fg=cyan>treatPhpDocTypesAsCertain: false</> in your <fg=cyan>%configurationFile%</>.');
80+
};
81+
82+
return [
83+
$addTip(RuleErrorBuilder::message(sprintf(
84+
'Do-while loop condition is always %s.',
85+
$exprType->getValue() ? 'true' : 'false'
86+
)))->line($node->getCond()->getLine())->build(),
87+
];
88+
}
89+
90+
return [];
91+
}
92+
93+
}
Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,65 @@
1+
<?php declare(strict_types = 1);
2+
3+
namespace PHPStan\Rules\Comparison;
4+
5+
/**
6+
* @extends \PHPStan\Testing\RuleTestCase<DoWhileLoopConstantConditionRule>
7+
*/
8+
class DoWhileLoopConstantConditionRuleTest extends \PHPStan\Testing\RuleTestCase
9+
{
10+
11+
/** @var bool */
12+
private $treatPhpDocTypesAsCertain = true;
13+
14+
protected function getRule(): \PHPStan\Rules\Rule
15+
{
16+
return new DoWhileLoopConstantConditionRule(
17+
new ConstantConditionRuleHelper(
18+
new ImpossibleCheckTypeHelper(
19+
$this->createReflectionProvider(),
20+
$this->getTypeSpecifier(),
21+
[],
22+
$this->treatPhpDocTypesAsCertain
23+
),
24+
$this->treatPhpDocTypesAsCertain
25+
),
26+
$this->treatPhpDocTypesAsCertain
27+
);
28+
}
29+
30+
protected function shouldTreatPhpDocTypesAsCertain(): bool
31+
{
32+
return $this->treatPhpDocTypesAsCertain;
33+
}
34+
35+
public function testRule(): void
36+
{
37+
$this->analyse([__DIR__ . '/data/do-while-loop.php'], [
38+
[
39+
'Do-while loop condition is always true.',
40+
12,
41+
],
42+
[
43+
'Do-while loop condition is always false.',
44+
37,
45+
],
46+
[
47+
'Do-while loop condition is always false.',
48+
46,
49+
],
50+
[
51+
'Do-while loop condition is always false.',
52+
55,
53+
],
54+
[
55+
'Do-while loop condition is always true.',
56+
64,
57+
],
58+
[
59+
'Do-while loop condition is always false.',
60+
73,
61+
],
62+
]);
63+
}
64+
65+
}
Lines changed: 98 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,98 @@
1+
<?php
2+
3+
namespace DoWhileLoopConstantCondition;
4+
5+
class Foo
6+
{
7+
8+
public function doFoo()
9+
{
10+
do {
11+
12+
} while (true); // report
13+
}
14+
15+
public function doFoo2()
16+
{
17+
do {
18+
if (rand(0, 1)) {
19+
return;
20+
}
21+
} while (true); // do not report
22+
}
23+
24+
public function doFoo3()
25+
{
26+
do {
27+
if (rand(0, 1)) {
28+
break;
29+
}
30+
} while (true); // do not report
31+
}
32+
33+
public function doBar()
34+
{
35+
do {
36+
37+
} while (false); // report
38+
}
39+
40+
public function doBar2()
41+
{
42+
do {
43+
if (rand(0, 1)) {
44+
return;
45+
}
46+
} while (false); // report
47+
}
48+
49+
public function doBar3()
50+
{
51+
do {
52+
if (rand(0, 1)) {
53+
break;
54+
}
55+
} while (false); // report
56+
}
57+
58+
public function doFoo4()
59+
{
60+
do {
61+
if (rand(0, 1)) {
62+
continue;
63+
}
64+
} while (true); // report
65+
}
66+
67+
public function doBar4()
68+
{
69+
do {
70+
if (rand(0, 1)) {
71+
continue;
72+
}
73+
} while (false); // report
74+
}
75+
76+
public function doFoo5(array $a)
77+
{
78+
foreach ($a as $v) {
79+
do {
80+
if (rand(0, 1)) {
81+
continue 2;
82+
}
83+
} while (true); // do not report
84+
}
85+
}
86+
87+
public function doFoo6(array $a)
88+
{
89+
foreach ($a as $v) {
90+
do {
91+
if (rand(0, 1)) {
92+
break 2;
93+
}
94+
} while (true); // do not report
95+
}
96+
}
97+
98+
}

0 commit comments

Comments
 (0)