diff --git a/README.md b/README.md index 14ade92e..88b7e96f 100644 --- a/README.md +++ b/README.md @@ -7,6 +7,7 @@ [PHPStan](https://phpstan.org/) focuses on finding bugs in your code. But in PHP there's a lot of leeway in how stuff can be written. This repository contains additional rules that revolve around strictly and strongly typed code with no loose casting for those who want additional safety in extremely defensive programming: * Require booleans in `if`, `elseif`, ternary operator, after `!`, and on both sides of `&&` and `||`. +* Require booleans in `while` and `do while` loop conditions. * Require numeric operands or arrays in `+` and numeric operands in `-`/`*`/`/`/`**`/`%`. * Require numeric operand in `$var++`, `$var--`, `++$var`and `--$var`. * These functions contain a `$strict` parameter for better type safety, it must be set to `true`: @@ -64,6 +65,7 @@ parameters: strictRules: disallowedLooseComparison: false booleansInConditions: false + booleansInLoopConditions: false uselessCast: false requireParentConstructorCall: false disallowedBacktick: false diff --git a/rules.neon b/rules.neon index 61b49854..dd20b8d1 100644 --- a/rules.neon +++ b/rules.neon @@ -15,6 +15,7 @@ parameters: allRules: true disallowedLooseComparison: %strictRules.allRules% booleansInConditions: %strictRules.allRules% + booleansInLoopConditions: [%strictRules.allRules%, %featureToggles.bleedingEdge%] uselessCast: %strictRules.allRules% requireParentConstructorCall: %strictRules.allRules% disallowedBacktick: %strictRules.allRules% @@ -37,6 +38,7 @@ parametersSchema: allRules: anyOf(bool(), arrayOf(bool())), disallowedLooseComparison: anyOf(bool(), arrayOf(bool())), booleansInConditions: anyOf(bool(), arrayOf(bool())) + booleansInLoopConditions: anyOf(bool(), arrayOf(bool())) uselessCast: anyOf(bool(), arrayOf(bool())) requireParentConstructorCall: anyOf(bool(), arrayOf(bool())) disallowedBacktick: anyOf(bool(), arrayOf(bool())) @@ -64,12 +66,16 @@ conditionalTags: phpstan.rules.rule: %strictRules.booleansInConditions% PHPStan\Rules\BooleansInConditions\BooleanInBooleanOrRule: phpstan.rules.rule: %strictRules.booleansInConditions% + PHPStan\Rules\BooleansInConditions\BooleanInDoWhileConditionRule: + phpstan.rules.rule: %strictRules.booleansInLoopConditions% PHPStan\Rules\BooleansInConditions\BooleanInElseIfConditionRule: phpstan.rules.rule: %strictRules.booleansInConditions% PHPStan\Rules\BooleansInConditions\BooleanInIfConditionRule: phpstan.rules.rule: %strictRules.booleansInConditions% PHPStan\Rules\BooleansInConditions\BooleanInTernaryOperatorRule: phpstan.rules.rule: %strictRules.booleansInConditions% + PHPStan\Rules\BooleansInConditions\BooleanInWhileConditionRule: + phpstan.rules.rule: %strictRules.booleansInLoopConditions% PHPStan\Rules\Cast\UselessCastRule: phpstan.rules.rule: %strictRules.uselessCast% PHPStan\Rules\Classes\RequireParentConstructCallRule: @@ -163,6 +169,9 @@ services: - class: PHPStan\Rules\BooleansInConditions\BooleanInBooleanOrRule + - + class: PHPStan\Rules\BooleansInConditions\BooleanInDoWhileConditionRule + - class: PHPStan\Rules\BooleansInConditions\BooleanInElseIfConditionRule @@ -172,6 +181,9 @@ services: - class: PHPStan\Rules\BooleansInConditions\BooleanInTernaryOperatorRule + - + class: PHPStan\Rules\BooleansInConditions\BooleanInWhileConditionRule + - class: PHPStan\Rules\Cast\UselessCastRule arguments: diff --git a/src/Rules/BooleansInConditions/BooleanInDoWhileConditionRule.php b/src/Rules/BooleansInConditions/BooleanInDoWhileConditionRule.php new file mode 100644 index 00000000..d0db2962 --- /dev/null +++ b/src/Rules/BooleansInConditions/BooleanInDoWhileConditionRule.php @@ -0,0 +1,46 @@ + + */ +class BooleanInDoWhileConditionRule implements Rule +{ + + private BooleanRuleHelper $helper; + + public function __construct(BooleanRuleHelper $helper) + { + $this->helper = $helper; + } + + public function getNodeType(): string + { + return Node\Stmt\Do_::class; + } + + public function processNode(Node $node, Scope $scope): array + { + if ($this->helper->passesAsBoolean($scope, $node->cond)) { + return []; + } + + $conditionExpressionType = $scope->getType($node->cond); + + return [ + RuleErrorBuilder::message(sprintf( + 'Only booleans are allowed in a do-while condition, %s given.', + $conditionExpressionType->describe(VerbosityLevel::typeOnly()), + ))->identifier('doWhile.condNotBoolean')->build(), + ]; + } + +} diff --git a/src/Rules/BooleansInConditions/BooleanInWhileConditionRule.php b/src/Rules/BooleansInConditions/BooleanInWhileConditionRule.php new file mode 100644 index 00000000..2f1661a6 --- /dev/null +++ b/src/Rules/BooleansInConditions/BooleanInWhileConditionRule.php @@ -0,0 +1,46 @@ + + */ +class BooleanInWhileConditionRule implements Rule +{ + + private BooleanRuleHelper $helper; + + public function __construct(BooleanRuleHelper $helper) + { + $this->helper = $helper; + } + + public function getNodeType(): string + { + return Node\Stmt\While_::class; + } + + public function processNode(Node $node, Scope $scope): array + { + if ($this->helper->passesAsBoolean($scope, $node->cond)) { + return []; + } + + $conditionExpressionType = $scope->getType($node->cond); + + return [ + RuleErrorBuilder::message(sprintf( + 'Only booleans are allowed in a while condition, %s given.', + $conditionExpressionType->describe(VerbosityLevel::typeOnly()), + ))->identifier('while.condNotBoolean')->build(), + ]; + } + +} diff --git a/tests/Rules/BooleansInConditions/BooleanInDoWhileConditionRuleTest.php b/tests/Rules/BooleansInConditions/BooleanInDoWhileConditionRuleTest.php new file mode 100644 index 00000000..a19b0a31 --- /dev/null +++ b/tests/Rules/BooleansInConditions/BooleanInDoWhileConditionRuleTest.php @@ -0,0 +1,34 @@ + + */ +class BooleanInDoWhileConditionRuleTest extends RuleTestCase +{ + + protected function getRule(): Rule + { + return new BooleanInDoWhileConditionRule( + new BooleanRuleHelper( + self::getContainer()->getByType(RuleLevelHelper::class), + ), + ); + } + + public function testRule(): void + { + $this->analyse([__DIR__ . '/data/conditions.php'], [ + [ + 'Only booleans are allowed in a do-while condition, string given.', + 60, + ], + ]); + } + +} diff --git a/tests/Rules/BooleansInConditions/BooleanInWhileConditionRuleTest.php b/tests/Rules/BooleansInConditions/BooleanInWhileConditionRuleTest.php new file mode 100644 index 00000000..82a8a4ed --- /dev/null +++ b/tests/Rules/BooleansInConditions/BooleanInWhileConditionRuleTest.php @@ -0,0 +1,34 @@ + + */ +class BooleanInWhileConditionRuleTest extends RuleTestCase +{ + + protected function getRule(): Rule + { + return new BooleanInWhileConditionRule( + new BooleanRuleHelper( + self::getContainer()->getByType(RuleLevelHelper::class), + ), + ); + } + + public function testRule(): void + { + $this->analyse([__DIR__ . '/data/conditions.php'], [ + [ + 'Only booleans are allowed in a while condition, string given.', + 55, + ], + ]); + } + +} diff --git a/tests/Rules/BooleansInConditions/data/conditions.php b/tests/Rules/BooleansInConditions/data/conditions.php index 968867ff..f69808ac 100644 --- a/tests/Rules/BooleansInConditions/data/conditions.php +++ b/tests/Rules/BooleansInConditions/data/conditions.php @@ -48,3 +48,13 @@ $explicitMixed and $bool; $bool or $explicitMixed; $explicitMixed or $bool; + +$someBool = true; +$someString = 'string'; +while ($someBool) { $someBool = !$someBool; } +while ($someString) { $someString = ''; } + +$someBool = true; +$someString = 'string'; +do { $someBool = !$someBool; } while ($someBool); +do { $someString = ''; } while ($someString);