Skip to content

Commit fb3f83e

Browse files
committed
While loop - condition always true
1 parent 217fac3 commit fb3f83e

File tree

6 files changed

+218
-1
lines changed

6 files changed

+218
-1
lines changed

conf/config.level4.neon

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -144,6 +144,13 @@ services:
144144
tags:
145145
- phpstan.rules.rule
146146

147+
-
148+
class: PHPStan\Rules\Comparison\WhileLoopAlwaysTrueConditionRule
149+
arguments:
150+
treatPhpDocTypesAsCertain: %treatPhpDocTypesAsCertain%
151+
tags:
152+
- phpstan.rules.rule
153+
147154
-
148155
class: PHPStan\Rules\TooWideTypehints\TooWideMethodReturnTypehintRule
149156
arguments:

src/Analyser/NodeScopeResolver.php

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -56,6 +56,7 @@
5656
use PHPStan\File\FileReader;
5757
use PHPStan\Node\BooleanAndNode;
5858
use PHPStan\Node\BooleanOrNode;
59+
use PHPStan\Node\BreaklessWhileLoopNode;
5960
use PHPStan\Node\CatchWithUnthrownExceptionNode;
6061
use PHPStan\Node\ClassConstantsNode;
6162
use PHPStan\Node\ClassMethodsNode;
@@ -899,9 +900,13 @@ private function processStmtNode(
899900
$isIterableAtLeastOnce = $beforeCondBooleanType instanceof ConstantBooleanType && $beforeCondBooleanType->getValue();
900901
$alwaysIterates = $condBooleanType instanceof ConstantBooleanType && $condBooleanType->getValue();
901902
$neverIterates = $condBooleanType instanceof ConstantBooleanType && !$condBooleanType->getValue();
903+
$breakCount = count($finalScopeResult->getExitPointsByType(Break_::class));
904+
if ($breakCount === 0) {
905+
$nodeCallback(new BreaklessWhileLoopNode($stmt), $bodyScopeMaybeRan);
906+
}
902907

903908
if ($alwaysIterates) {
904-
$isAlwaysTerminating = count($finalScopeResult->getExitPointsByType(Break_::class)) === 0;
909+
$isAlwaysTerminating = $breakCount === 0;
905910
} elseif ($isIterableAtLeastOnce) {
906911
$isAlwaysTerminating = $finalScopeResult->isAlwaysTerminating();
907912
} else {

src/Node/BreaklessWhileLoopNode.php

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
<?php declare(strict_types = 1);
2+
3+
namespace PHPStan\Node;
4+
5+
use PhpParser\Node\Stmt\While_;
6+
use PhpParser\NodeAbstract;
7+
8+
/** @api */
9+
class BreaklessWhileLoopNode extends NodeAbstract implements VirtualNode
10+
{
11+
12+
private While_ $originalNode;
13+
14+
public function __construct(While_ $originalNode)
15+
{
16+
parent::__construct($originalNode->getAttributes());
17+
$this->originalNode = $originalNode;
18+
}
19+
20+
public function getOriginalNode(): While_
21+
{
22+
return $this->originalNode;
23+
}
24+
25+
public function getType(): string
26+
{
27+
return 'PHPStan_Node_BreaklessWhileLoop';
28+
}
29+
30+
/**
31+
* @return string[]
32+
*/
33+
public function getSubNodeNames(): array
34+
{
35+
return [];
36+
}
37+
38+
}
Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,63 @@
1+
<?php declare(strict_types = 1);
2+
3+
namespace PHPStan\Rules\Comparison;
4+
5+
use PHPStan\Node\BreaklessWhileLoopNode;
6+
use PHPStan\Rules\RuleErrorBuilder;
7+
use PHPStan\Type\Constant\ConstantBooleanType;
8+
9+
/**
10+
* @implements \PHPStan\Rules\Rule<BreaklessWhileLoopNode>
11+
*/
12+
class WhileLoopAlwaysTrueConditionRule implements \PHPStan\Rules\Rule
13+
{
14+
15+
private ConstantConditionRuleHelper $helper;
16+
17+
private bool $treatPhpDocTypesAsCertain;
18+
19+
public function __construct(
20+
ConstantConditionRuleHelper $helper,
21+
bool $treatPhpDocTypesAsCertain
22+
)
23+
{
24+
$this->helper = $helper;
25+
$this->treatPhpDocTypesAsCertain = $treatPhpDocTypesAsCertain;
26+
}
27+
28+
public function getNodeType(): string
29+
{
30+
return BreaklessWhileLoopNode::class;
31+
}
32+
33+
public function processNode(
34+
\PhpParser\Node $node,
35+
\PHPStan\Analyser\Scope $scope
36+
): array
37+
{
38+
$originalNode = $node->getOriginalNode();
39+
$exprType = $this->helper->getBooleanType($scope, $originalNode->cond);
40+
if ($exprType instanceof ConstantBooleanType && $exprType->getValue()) {
41+
$addTip = function (RuleErrorBuilder $ruleErrorBuilder) use ($scope, $originalNode): RuleErrorBuilder {
42+
if (!$this->treatPhpDocTypesAsCertain) {
43+
return $ruleErrorBuilder;
44+
}
45+
46+
$booleanNativeType = $this->helper->getNativeBooleanType($scope, $originalNode->cond);
47+
if ($booleanNativeType instanceof ConstantBooleanType) {
48+
return $ruleErrorBuilder;
49+
}
50+
51+
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%</>.');
52+
};
53+
54+
return [
55+
$addTip(RuleErrorBuilder::message('While loop condition is always true.'))->line($originalNode->cond->getLine())
56+
->build(),
57+
];
58+
}
59+
60+
return [];
61+
}
62+
63+
}
Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
<?php declare(strict_types = 1);
2+
3+
namespace PHPStan\Rules\Comparison;
4+
5+
/**
6+
* @extends \PHPStan\Testing\RuleTestCase<WhileLoopAlwaysTrueConditionRule>
7+
*/
8+
class WhileLoopAlwaysTrueConditionRuleTest 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 WhileLoopAlwaysTrueConditionRule(
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/while-loop-true.php'], [
38+
[
39+
'While loop condition is always true.',
40+
10,
41+
],
42+
[
43+
'While loop condition is always true.',
44+
20,
45+
'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%</>.',
46+
],
47+
]);
48+
}
49+
50+
}
Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
<?php
2+
3+
namespace WhileLoopTrue;
4+
5+
class Foo
6+
{
7+
8+
public function doFoo(): void
9+
{
10+
while (true) {
11+
12+
}
13+
}
14+
15+
/**
16+
* @param 1 $s
17+
*/
18+
public function doBar($s): void
19+
{
20+
while ($s) {
21+
22+
}
23+
}
24+
25+
/**
26+
* @param string $s
27+
*/
28+
public function doBar2($s): void
29+
{
30+
while ($s === null) { // reported by StrictComparisonOfDifferentTypesRule
31+
32+
}
33+
}
34+
35+
public function doBar3(): void
36+
{
37+
while (true) {
38+
if (rand(0, 1)) {
39+
break;
40+
}
41+
}
42+
}
43+
44+
public function doBar4(): void
45+
{
46+
$b = true;
47+
while ($b) {
48+
if (rand(0, 1)) {
49+
$b = false;
50+
}
51+
}
52+
}
53+
54+
}

0 commit comments

Comments
 (0)