Skip to content

Commit

Permalink
Do-while loop - constant condition rule
Browse files Browse the repository at this point in the history
  • Loading branch information
ondrejmirtes committed Sep 22, 2021
1 parent 0cde73f commit e81ccd4
Show file tree
Hide file tree
Showing 7 changed files with 320 additions and 0 deletions.
7 changes: 7 additions & 0 deletions conf/config.level4.neon
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,13 @@ services:
tags:
- phpstan.rules.rule

-
class: PHPStan\Rules\Comparison\DoWhileLoopConstantConditionRule
arguments:
treatPhpDocTypesAsCertain: %treatPhpDocTypesAsCertain%
tags:
- phpstan.rules.rule

-
class: PHPStan\Rules\Comparison\ElseIfConstantConditionRule
arguments:
Expand Down
3 changes: 3 additions & 0 deletions src/Analyser/NodeScopeResolver.php
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,7 @@
use PHPStan\Node\ClassPropertyNode;
use PHPStan\Node\ClassStatementsGatherer;
use PHPStan\Node\ClosureReturnStatementsNode;
use PHPStan\Node\DoWhileLoopConditionNode;
use PHPStan\Node\ExecutionEndNode;
use PHPStan\Node\FinallyExitPointsNode;
use PHPStan\Node\FunctionReturnStatementsNode;
Expand Down Expand Up @@ -973,6 +974,8 @@ private function processStmtNode(
$condBooleanType = $bodyScope->getType($stmt->cond)->toBoolean();
$alwaysIterates = $condBooleanType instanceof ConstantBooleanType && $condBooleanType->getValue();

$nodeCallback(new DoWhileLoopConditionNode($stmt->cond, $bodyScopeResult->getExitPoints()), $bodyScope);

if ($alwaysIterates) {
$alwaysTerminating = count($bodyScopeResult->getExitPointsByType(Break_::class)) === 0;
} else {
Expand Down
1 change: 1 addition & 0 deletions src/Analyser/StatementResult.php
Original file line number Diff line number Diff line change
Expand Up @@ -136,6 +136,7 @@ public function getExitPointsForOuterLoop(): array
foreach ($this->exitPoints as $exitPoint) {
$statement = $exitPoint->getStatement();
if (!$statement instanceof Stmt\Continue_ && !$statement instanceof Stmt\Break_) {
$exitPoints[] = $exitPoint;
continue;
}
if ($statement->num === null) {
Expand Down
53 changes: 53 additions & 0 deletions src/Node/DoWhileLoopConditionNode.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
<?php declare(strict_types = 1);

namespace PHPStan\Node;

use PhpParser\Node\Expr;
use PhpParser\NodeAbstract;
use PHPStan\Analyser\StatementExitPoint;

class DoWhileLoopConditionNode extends NodeAbstract implements VirtualNode
{

private Expr $cond;

/** @var StatementExitPoint[] */
private array $exitPoints;

/**
* @param StatementExitPoint[] $exitPoints
*/
public function __construct(Expr $cond, array $exitPoints)
{
parent::__construct($cond->getAttributes());
$this->cond = $cond;
$this->exitPoints = $exitPoints;
}

public function getCond(): Expr
{
return $this->cond;
}

/**
* @return StatementExitPoint[]
*/
public function getExitPoints(): array
{
return $this->exitPoints;
}

public function getType(): string
{
return 'PHPStan_Node_ClosureReturnStatementsNode';
}

/**
* @return string[]
*/
public function getSubNodeNames(): array
{
return [];
}

}
93 changes: 93 additions & 0 deletions src/Rules/Comparison/DoWhileLoopConstantConditionRule.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,93 @@
<?php declare(strict_types = 1);

namespace PHPStan\Rules\Comparison;

use PhpParser\Node;
use PhpParser\Node\Scalar\LNumber;
use PhpParser\Node\Stmt\Break_;
use PhpParser\Node\Stmt\Continue_;
use PHPStan\Analyser\Scope;
use PHPStan\Node\DoWhileLoopConditionNode;
use PHPStan\Rules\Rule;
use PHPStan\Rules\RuleErrorBuilder;
use PHPStan\Type\Constant\ConstantBooleanType;

/**
* @implements Rule<DoWhileLoopConditionNode>
*/
class DoWhileLoopConstantConditionRule implements Rule
{

private ConstantConditionRuleHelper $helper;

private bool $treatPhpDocTypesAsCertain;

public function __construct(
ConstantConditionRuleHelper $helper,
bool $treatPhpDocTypesAsCertain
)
{
$this->helper = $helper;
$this->treatPhpDocTypesAsCertain = $treatPhpDocTypesAsCertain;
}

public function getNodeType(): string
{
return DoWhileLoopConditionNode::class;
}

public function processNode(Node $node, Scope $scope): array
{
$exprType = $this->helper->getBooleanType($scope, $node->getCond());
if ($exprType instanceof ConstantBooleanType) {
if ($exprType->getValue()) {
foreach ($node->getExitPoints() as $exitPoint) {
$statement = $exitPoint->getStatement();
if ($statement instanceof Break_) {
return [];
}
if (!$statement instanceof Continue_) {
return [];
}
if ($statement->num === null) {
continue;
}
if (!$statement->num instanceof LNumber) {
continue;
}
$value = $statement->num->value;
if ($value === 1) {
continue;
}

if ($value > 1) {
return [];
}
}
}

$addTip = function (RuleErrorBuilder $ruleErrorBuilder) use ($scope, $node): RuleErrorBuilder {
if (!$this->treatPhpDocTypesAsCertain) {
return $ruleErrorBuilder;
}

$booleanNativeType = $this->helper->getNativeBooleanType($scope, $node->getCond());
if ($booleanNativeType instanceof ConstantBooleanType) {
return $ruleErrorBuilder;
}

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%</>.');
};

return [
$addTip(RuleErrorBuilder::message(sprintf(
'Do-while loop condition is always %s.',
$exprType->getValue() ? 'true' : 'false'
)))->line($node->getCond()->getLine())->build(),
];
}

return [];
}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
<?php declare(strict_types = 1);

namespace PHPStan\Rules\Comparison;

/**
* @extends \PHPStan\Testing\RuleTestCase<DoWhileLoopConstantConditionRule>
*/
class DoWhileLoopConstantConditionRuleTest extends \PHPStan\Testing\RuleTestCase
{

/** @var bool */
private $treatPhpDocTypesAsCertain = true;

protected function getRule(): \PHPStan\Rules\Rule
{
return new DoWhileLoopConstantConditionRule(
new ConstantConditionRuleHelper(
new ImpossibleCheckTypeHelper(
$this->createReflectionProvider(),
$this->getTypeSpecifier(),
[],
$this->treatPhpDocTypesAsCertain
),
$this->treatPhpDocTypesAsCertain
),
$this->treatPhpDocTypesAsCertain
);
}

protected function shouldTreatPhpDocTypesAsCertain(): bool
{
return $this->treatPhpDocTypesAsCertain;
}

public function testRule(): void
{
$this->analyse([__DIR__ . '/data/do-while-loop.php'], [
[
'Do-while loop condition is always true.',
12,
],
[
'Do-while loop condition is always false.',
37,
],
[
'Do-while loop condition is always false.',
46,
],
[
'Do-while loop condition is always false.',
55,
],
[
'Do-while loop condition is always true.',
64,
],
[
'Do-while loop condition is always false.',
73,
],
]);
}

}
98 changes: 98 additions & 0 deletions tests/PHPStan/Rules/Comparison/data/do-while-loop.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,98 @@
<?php

namespace DoWhileLoopConstantCondition;

class Foo
{

public function doFoo()
{
do {

} while (true); // report
}

public function doFoo2()
{
do {
if (rand(0, 1)) {
return;
}
} while (true); // do not report
}

public function doFoo3()
{
do {
if (rand(0, 1)) {
break;
}
} while (true); // do not report
}

public function doBar()
{
do {

} while (false); // report
}

public function doBar2()
{
do {
if (rand(0, 1)) {
return;
}
} while (false); // report
}

public function doBar3()
{
do {
if (rand(0, 1)) {
break;
}
} while (false); // report
}

public function doFoo4()
{
do {
if (rand(0, 1)) {
continue;
}
} while (true); // report
}

public function doBar4()
{
do {
if (rand(0, 1)) {
continue;
}
} while (false); // report
}

public function doFoo5(array $a)
{
foreach ($a as $v) {
do {
if (rand(0, 1)) {
continue 2;
}
} while (true); // do not report
}
}

public function doFoo6(array $a)
{
foreach ($a as $v) {
do {
if (rand(0, 1)) {
break 2;
}
} while (true); // do not report
}
}

}

0 comments on commit e81ccd4

Please sign in to comment.