From 4d8b9771049492f60fa796974208b4e8a7f51dde Mon Sep 17 00:00:00 2001 From: Markus Staab Date: Sat, 7 Sep 2024 08:23:37 +0200 Subject: [PATCH] Implement InvalidDivisionOperationRule --- conf/bleedingEdge.neon | 1 + conf/config.level0.neon | 5 ++ conf/config.neon | 1 + conf/parametersSchema.neon | 1 + .../InvalidDivisionOperationRule.php | 62 +++++++++++++++++++ .../InvalidDivisionOperationRuleTest.php | 45 ++++++++++++++ .../Rules/Operators/data/invalid-division.php | 60 ++++++++++++++++++ 7 files changed, 175 insertions(+) create mode 100644 src/Rules/Operators/InvalidDivisionOperationRule.php create mode 100644 tests/PHPStan/Rules/Operators/InvalidDivisionOperationRuleTest.php create mode 100644 tests/PHPStan/Rules/Operators/data/invalid-division.php diff --git a/conf/bleedingEdge.neon b/conf/bleedingEdge.neon index 8fb6e4da6a..23f9a8b2f0 100644 --- a/conf/bleedingEdge.neon +++ b/conf/bleedingEdge.neon @@ -63,5 +63,6 @@ parameters: explicitThrow: true absentTypeChecks: true requireFileExists: true + divisionByZero: true stubFiles: - ../stubs/bleedingEdge/Rule.stub diff --git a/conf/config.level0.neon b/conf/config.level0.neon index 1382d99ee1..3a4ffb5194 100644 --- a/conf/config.level0.neon +++ b/conf/config.level0.neon @@ -34,6 +34,8 @@ conditionalTags: phpstan.rules.rule: %featureToggles.requireFileExists% PHPStan\Rules\Classes\LocalTypeTraitUseAliasesRule: phpstan.rules.rule: %featureToggles.absentTypeChecks% + PHPStan\Rules\Operators\InvalidDivisionOperationRule: + phpstan.rules.rule: %featureToggles.divisionByZero% rules: - PHPStan\Rules\Api\ApiInstantiationRule @@ -320,3 +322,6 @@ services: class: PHPStan\Rules\Keywords\RequireFileExistsRule arguments: currentWorkingDirectory: %currentWorkingDirectory% + + - + class: PHPStan\Rules\Operators\InvalidDivisionOperationRule diff --git a/conf/config.neon b/conf/config.neon index 6f18952254..40c533482f 100644 --- a/conf/config.neon +++ b/conf/config.neon @@ -99,6 +99,7 @@ parameters: tooWidePropertyType: false explicitThrow: false absentTypeChecks: false + divisionByZero: false fileExtensions: - php checkAdvancedIsset: false diff --git a/conf/parametersSchema.neon b/conf/parametersSchema.neon index 05d6d79f0c..fecf5b4167 100644 --- a/conf/parametersSchema.neon +++ b/conf/parametersSchema.neon @@ -94,6 +94,7 @@ parametersSchema: explicitThrow: bool() absentTypeChecks: bool() requireFileExists: bool() + divisionByZero: bool() ]) fileExtensions: listOf(string()) checkAdvancedIsset: bool() diff --git a/src/Rules/Operators/InvalidDivisionOperationRule.php b/src/Rules/Operators/InvalidDivisionOperationRule.php new file mode 100644 index 0000000000..55a6daacc0 --- /dev/null +++ b/src/Rules/Operators/InvalidDivisionOperationRule.php @@ -0,0 +1,62 @@ + + */ +final class InvalidDivisionOperationRule implements Rule +{ + + public function getNodeType(): string + { + return Node\Expr::class; + } + + public function processNode(Node $node, Scope $scope): array + { + if ($node instanceof Node\Expr\AssignOp\Div || $node instanceof Node\Expr\AssignOp\Mod) { + $identifier = 'assignOp'; + $left = $node->var; + $right = $node->expr; + } elseif ($node instanceof Node\Expr\BinaryOp\Div || $node instanceof Node\Expr\BinaryOp\Mod) { + $identifier = 'binaryOp'; + $left = $node->left; + $right = $node->right; + } else { + return []; + } + + $leftType = $scope->getType($left); + $rightType = $scope->getType($right); + + $zeroType = new UnionType([new ConstantIntegerType(0), new ConstantFloatType(0.0)]); + if (!$rightType->isSuperTypeOf($zeroType)->maybe()) { + // yes() is handled by InvalidBinaryOperationRule + return []; + } + + return [ + RuleErrorBuilder::message(sprintf( + 'Binary operation "%s" between %s and %s might result in an error.', + $node instanceof Node\Expr\AssignOp\Div || $node instanceof Node\Expr\BinaryOp\Div ? '/' : '%', + $leftType->describe(VerbosityLevel::value()), + $rightType->describe(VerbosityLevel::value()), + )) + ->line($left->getStartLine()) + ->identifier(sprintf('%s.invalid', $identifier)) + ->build(), + ]; + } + +} diff --git a/tests/PHPStan/Rules/Operators/InvalidDivisionOperationRuleTest.php b/tests/PHPStan/Rules/Operators/InvalidDivisionOperationRuleTest.php new file mode 100644 index 0000000000..cd4189b762 --- /dev/null +++ b/tests/PHPStan/Rules/Operators/InvalidDivisionOperationRuleTest.php @@ -0,0 +1,45 @@ + + */ +class InvalidDivisionOperationRuleTest extends RuleTestCase +{ + + protected function getRule(): Rule + { + return new InvalidDivisionOperationRule(); + } + + public function testRule(): void + { + $this->analyse([__DIR__ . '/data/invalid-division.php'], [ + [ + 'Binary operation "/" between int and int<0, max> might result in an error.', + 12, + ], + [ + 'Binary operation "/" between int and int<0, max> might result in an error.', + 13, + ], + [ + 'Binary operation "%" between int and int<0, max> might result in an error.', + 21, + ], + [ + 'Binary operation "%" between int and int<0, max> might result in an error.', + 22, + ], + [ + 'Binary operation "/" between mixed and int<0, max> might result in an error.', + 58, + ], + ]); + } + +} diff --git a/tests/PHPStan/Rules/Operators/data/invalid-division.php b/tests/PHPStan/Rules/Operators/data/invalid-division.php new file mode 100644 index 0000000000..9c749ebd1f --- /dev/null +++ b/tests/PHPStan/Rules/Operators/data/invalid-division.php @@ -0,0 +1,60 @@ + $x + */ + public function doDiv(int $y, $x): void + { + echo $y / $x; + $y /= $x; + } + + /** + * @param int<0, max> $x + */ + public function doMod(int $y, $x): void + { + echo $y % $x; + $y %= $x; + } + + public function doMixed(int $y, $mixed): void + { + echo $y % $mixed; + $y %= $mixed; + } + + /** + * @param int $negative + * @param int<1, max> $positive + */ + public function doRanges(int $y, $x): void + { + echo $y / $x; + $y /= $x; + } +} + +function ($i, int $x): void { + $a = range(1, 3/2); + + if ($x !== 0) { + echo $i / $x; + } + + if ($x != 0) { + echo $i / $x; + } + + if ($x > 0) { + echo $i / $x; + } + + if ($x >= 0) { + echo $i / $x; + } +};