From e81ccd40a4872bde4f6578919b93b4d821021557 Mon Sep 17 00:00:00 2001 From: Ondrej Mirtes Date: Wed, 22 Sep 2021 15:35:07 +0200 Subject: [PATCH] Do-while loop - constant condition rule --- conf/config.level4.neon | 7 ++ src/Analyser/NodeScopeResolver.php | 3 + src/Analyser/StatementResult.php | 1 + src/Node/DoWhileLoopConditionNode.php | 53 ++++++++++ .../DoWhileLoopConstantConditionRule.php | 93 ++++++++++++++++++ .../DoWhileLoopConstantConditionRuleTest.php | 65 ++++++++++++ .../Rules/Comparison/data/do-while-loop.php | 98 +++++++++++++++++++ 7 files changed, 320 insertions(+) create mode 100644 src/Node/DoWhileLoopConditionNode.php create mode 100644 src/Rules/Comparison/DoWhileLoopConstantConditionRule.php create mode 100644 tests/PHPStan/Rules/Comparison/DoWhileLoopConstantConditionRuleTest.php create mode 100644 tests/PHPStan/Rules/Comparison/data/do-while-loop.php diff --git a/conf/config.level4.neon b/conf/config.level4.neon index 43fe1bddf6..d14c723e71 100644 --- a/conf/config.level4.neon +++ b/conf/config.level4.neon @@ -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: diff --git a/src/Analyser/NodeScopeResolver.php b/src/Analyser/NodeScopeResolver.php index 6f96a4cf18..8471ca54b4 100644 --- a/src/Analyser/NodeScopeResolver.php +++ b/src/Analyser/NodeScopeResolver.php @@ -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; @@ -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 { diff --git a/src/Analyser/StatementResult.php b/src/Analyser/StatementResult.php index ed4a85465f..9636198738 100644 --- a/src/Analyser/StatementResult.php +++ b/src/Analyser/StatementResult.php @@ -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) { diff --git a/src/Node/DoWhileLoopConditionNode.php b/src/Node/DoWhileLoopConditionNode.php new file mode 100644 index 0000000000..6fdc87cfb0 --- /dev/null +++ b/src/Node/DoWhileLoopConditionNode.php @@ -0,0 +1,53 @@ +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 []; + } + +} diff --git a/src/Rules/Comparison/DoWhileLoopConstantConditionRule.php b/src/Rules/Comparison/DoWhileLoopConstantConditionRule.php new file mode 100644 index 0000000000..2b24587113 --- /dev/null +++ b/src/Rules/Comparison/DoWhileLoopConstantConditionRule.php @@ -0,0 +1,93 @@ + + */ +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 treatPhpDocTypesAsCertain: false in your %configurationFile%.'); + }; + + return [ + $addTip(RuleErrorBuilder::message(sprintf( + 'Do-while loop condition is always %s.', + $exprType->getValue() ? 'true' : 'false' + )))->line($node->getCond()->getLine())->build(), + ]; + } + + return []; + } + +} diff --git a/tests/PHPStan/Rules/Comparison/DoWhileLoopConstantConditionRuleTest.php b/tests/PHPStan/Rules/Comparison/DoWhileLoopConstantConditionRuleTest.php new file mode 100644 index 0000000000..c064156ea5 --- /dev/null +++ b/tests/PHPStan/Rules/Comparison/DoWhileLoopConstantConditionRuleTest.php @@ -0,0 +1,65 @@ + + */ +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, + ], + ]); + } + +} diff --git a/tests/PHPStan/Rules/Comparison/data/do-while-loop.php b/tests/PHPStan/Rules/Comparison/data/do-while-loop.php new file mode 100644 index 0000000000..02eb303d34 --- /dev/null +++ b/tests/PHPStan/Rules/Comparison/data/do-while-loop.php @@ -0,0 +1,98 @@ +