From dae47bf9404419df34cf497a141c5ba449e46494 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jok=C5=ABbas=20Ramanauskas?= Date: Wed, 17 Sep 2025 17:48:59 +0300 Subject: [PATCH 1/5] ForbidArithmeticOperationOnNonNumberRule: add support for BcMath\Number --- ...rbidArithmeticOperationOnNonNumberRule.php | 24 ++++++++- ...ArithmeticOperationOnNonNumberRuleTest.php | 21 ++++++++ .../bcmath-number-no-numeric.php | 38 +++++++++++++ .../bcmath-number.php | 54 +++++++++++++++++++ 4 files changed, 136 insertions(+), 1 deletion(-) create mode 100644 tests/Rule/data/ForbidArithmeticOperationOnNonNumberRule/bcmath-number-no-numeric.php create mode 100644 tests/Rule/data/ForbidArithmeticOperationOnNonNumberRule/bcmath-number.php diff --git a/src/Rule/ForbidArithmeticOperationOnNonNumberRule.php b/src/Rule/ForbidArithmeticOperationOnNonNumberRule.php index f95f947..493b873 100644 --- a/src/Rule/ForbidArithmeticOperationOnNonNumberRule.php +++ b/src/Rule/ForbidArithmeticOperationOnNonNumberRule.php @@ -18,6 +18,7 @@ use PHPStan\Rules\RuleErrorBuilder; use PHPStan\Type\FloatType; use PHPStan\Type\IntegerType; +use PHPStan\Type\ObjectType; use PHPStan\Type\Type; use PHPStan\Type\UnionType; use PHPStan\Type\VerbosityLevel; @@ -113,6 +114,12 @@ private function processBinary( return []; // array merge syntax } + if (($this->isBcMathNumber($leftType) && $this->isFloat($rightType)) + || ($this->isFloat($leftType) && $this->isBcMathNumber($rightType)) + ) { + return $this->buildBinaryErrors($operator, 'BcMath\\Number and float', $leftType, $rightType); + } + if ( $operator === '%' && (!$leftType->isInteger()->yes() || !$rightType->isInteger()->yes()) @@ -136,7 +143,8 @@ private function isNumeric(Type $type): bool return $int->isSuperTypeOf($type)->yes() || $float->isSuperTypeOf($type)->yes() || $intOrFloat->isSuperTypeOf($type)->yes() - || ($this->allowNumericString && $type->isNumericString()->yes()); + || ($this->allowNumericString && $type->isNumericString()->yes()) + || $this->isBcMathNumber($type); } /** @@ -164,4 +172,18 @@ private function buildBinaryErrors( return [$error]; } + private function isBcMathNumber(Type $type): bool + { + $bcMathNumber = new ObjectType('BcMath\Number'); + + return $type->isSuperTypeOf($bcMathNumber)->yes(); + } + + private function isFloat(Type $type): bool + { + $float = new FloatType(); + + return $type->isSuperTypeOf($float)->yes(); + } + } diff --git a/tests/Rule/ForbidArithmeticOperationOnNonNumberRuleTest.php b/tests/Rule/ForbidArithmeticOperationOnNonNumberRuleTest.php index 458030e..a0b340c 100644 --- a/tests/Rule/ForbidArithmeticOperationOnNonNumberRuleTest.php +++ b/tests/Rule/ForbidArithmeticOperationOnNonNumberRuleTest.php @@ -5,6 +5,7 @@ use LogicException; use PHPStan\Rules\Rule; use ShipMonk\PHPStan\RuleTestCase; +use const PHP_VERSION_ID; /** * @extends RuleTestCase @@ -35,6 +36,26 @@ public function testNoNumericString(): void $this->analyseFile(__DIR__ . '/data/ForbidArithmeticOperationOnNonNumberRule/no-numeric-string.php'); } + public function testBcMathNumber(): void + { + if (PHP_VERSION_ID < 80_400) { + self::markTestSkipped('Requires PHP 8.4'); + } + + $this->allowNumericString = true; + $this->analyseFile(__DIR__ . '/data/ForbidArithmeticOperationOnNonNumberRule/bcmath-number.php'); + } + + public function testBcMathNumberNoNumeric(): void + { + if (PHP_VERSION_ID < 80_400) { + self::markTestSkipped('Requires PHP 8.4'); + } + + $this->allowNumericString = false; + $this->analyseFile(__DIR__ . '/data/ForbidArithmeticOperationOnNonNumberRule/bcmath-number-no-numeric.php'); + } + protected function shouldFailOnPhpErrors(): bool { return false; // https://github.com/phpstan/phpstan-src/pull/3031 diff --git a/tests/Rule/data/ForbidArithmeticOperationOnNonNumberRule/bcmath-number-no-numeric.php b/tests/Rule/data/ForbidArithmeticOperationOnNonNumberRule/bcmath-number-no-numeric.php new file mode 100644 index 0000000..7d198ef --- /dev/null +++ b/tests/Rule/data/ForbidArithmeticOperationOnNonNumberRule/bcmath-number-no-numeric.php @@ -0,0 +1,38 @@ + Date: Mon, 1 Dec 2025 11:12:26 +0100 Subject: [PATCH 2/5] Fix (object + float) classification as BcNumber issue, extend test dataset --- ...rbidArithmeticOperationOnNonNumberRule.php | 34 +-- ...ArithmeticOperationOnNonNumberRuleTest.php | 9 - .../bcmath-number.php | 210 ++++++++++++++---- 3 files changed, 190 insertions(+), 63 deletions(-) diff --git a/src/Rule/ForbidArithmeticOperationOnNonNumberRule.php b/src/Rule/ForbidArithmeticOperationOnNonNumberRule.php index 493b873..acd26be 100644 --- a/src/Rule/ForbidArithmeticOperationOnNonNumberRule.php +++ b/src/Rule/ForbidArithmeticOperationOnNonNumberRule.php @@ -20,6 +20,7 @@ use PHPStan\Type\IntegerType; use PHPStan\Type\ObjectType; use PHPStan\Type\Type; +use PHPStan\Type\TypeTraverser; use PHPStan\Type\UnionType; use PHPStan\Type\VerbosityLevel; use function sprintf; @@ -114,8 +115,8 @@ private function processBinary( return []; // array merge syntax } - if (($this->isBcMathNumber($leftType) && $this->isFloat($rightType)) - || ($this->isFloat($leftType) && $this->isBcMathNumber($rightType)) + if (($this->containsBcMathNumber($leftType) && $this->isFloat($rightType)) + || ($this->isFloat($leftType) && $this->containsBcMathNumber($rightType)) ) { return $this->buildBinaryErrors($operator, 'BcMath\\Number and float', $leftType, $rightType); } @@ -138,13 +139,14 @@ private function isNumeric(Type $type): bool { $int = new IntegerType(); $float = new FloatType(); - $intOrFloat = new UnionType([$int, $float]); + $bcNumber = new ObjectType('BcMath\Number'); + $intOrFloatOrBcNumber = new UnionType([$int, $float, $bcNumber]); return $int->isSuperTypeOf($type)->yes() || $float->isSuperTypeOf($type)->yes() - || $intOrFloat->isSuperTypeOf($type)->yes() - || ($this->allowNumericString && $type->isNumericString()->yes()) - || $this->isBcMathNumber($type); + || $bcNumber->isSuperTypeOf($type)->yes() + || $intOrFloatOrBcNumber->isSuperTypeOf($type)->yes() + || ($this->allowNumericString && $type->isNumericString()->yes()); } /** @@ -172,13 +174,6 @@ private function buildBinaryErrors( return [$error]; } - private function isBcMathNumber(Type $type): bool - { - $bcMathNumber = new ObjectType('BcMath\Number'); - - return $type->isSuperTypeOf($bcMathNumber)->yes(); - } - private function isFloat(Type $type): bool { $float = new FloatType(); @@ -186,4 +181,17 @@ private function isFloat(Type $type): bool return $type->isSuperTypeOf($float)->yes(); } + private function containsBcMathNumber(Type $type): bool + { + $bcMathFound = false; + TypeTraverser::map($type, static function (Type $traversedTyped, callable $traverse) use (&$bcMathFound): Type { + $bcMathNumber = new ObjectType('BcMath\Number'); + if ($bcMathNumber->isSuperTypeOf($traversedTyped)->yes()) { + $bcMathFound = true; + } + return $traverse($traversedTyped); + }); + return $bcMathFound; + } + } diff --git a/tests/Rule/ForbidArithmeticOperationOnNonNumberRuleTest.php b/tests/Rule/ForbidArithmeticOperationOnNonNumberRuleTest.php index a0b340c..81f938e 100644 --- a/tests/Rule/ForbidArithmeticOperationOnNonNumberRuleTest.php +++ b/tests/Rule/ForbidArithmeticOperationOnNonNumberRuleTest.php @@ -5,7 +5,6 @@ use LogicException; use PHPStan\Rules\Rule; use ShipMonk\PHPStan\RuleTestCase; -use const PHP_VERSION_ID; /** * @extends RuleTestCase @@ -38,20 +37,12 @@ public function testNoNumericString(): void public function testBcMathNumber(): void { - if (PHP_VERSION_ID < 80_400) { - self::markTestSkipped('Requires PHP 8.4'); - } - $this->allowNumericString = true; $this->analyseFile(__DIR__ . '/data/ForbidArithmeticOperationOnNonNumberRule/bcmath-number.php'); } public function testBcMathNumberNoNumeric(): void { - if (PHP_VERSION_ID < 80_400) { - self::markTestSkipped('Requires PHP 8.4'); - } - $this->allowNumericString = false; $this->analyseFile(__DIR__ . '/data/ForbidArithmeticOperationOnNonNumberRule/bcmath-number-no-numeric.php'); } diff --git a/tests/Rule/data/ForbidArithmeticOperationOnNonNumberRule/bcmath-number.php b/tests/Rule/data/ForbidArithmeticOperationOnNonNumberRule/bcmath-number.php index 4318b84..f7dec26 100644 --- a/tests/Rule/data/ForbidArithmeticOperationOnNonNumberRule/bcmath-number.php +++ b/tests/Rule/data/ForbidArithmeticOperationOnNonNumberRule/bcmath-number.php @@ -1,6 +1,7 @@ Date: Mon, 1 Dec 2025 12:44:21 +0100 Subject: [PATCH 3/5] Simplify isNumeric --- .../ForbidArithmeticOperationOnNonNumberRule.php | 14 ++++++-------- .../code.php | 2 ++ 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/src/Rule/ForbidArithmeticOperationOnNonNumberRule.php b/src/Rule/ForbidArithmeticOperationOnNonNumberRule.php index acd26be..c31102c 100644 --- a/src/Rule/ForbidArithmeticOperationOnNonNumberRule.php +++ b/src/Rule/ForbidArithmeticOperationOnNonNumberRule.php @@ -137,15 +137,13 @@ private function processBinary( private function isNumeric(Type $type): bool { - $int = new IntegerType(); - $float = new FloatType(); - $bcNumber = new ObjectType('BcMath\Number'); - $intOrFloatOrBcNumber = new UnionType([$int, $float, $bcNumber]); + $numericUnion = new UnionType([ + new IntegerType(), + new FloatType(), + new ObjectType('BcMath\Number'), + ]); - return $int->isSuperTypeOf($type)->yes() - || $float->isSuperTypeOf($type)->yes() - || $bcNumber->isSuperTypeOf($type)->yes() - || $intOrFloatOrBcNumber->isSuperTypeOf($type)->yes() + return $numericUnion->isSuperTypeOf($type)->yes() || ($this->allowNumericString && $type->isNumericString()->yes()); } diff --git a/tests/Rule/data/ForbidArithmeticOperationOnNonNumberRule/code.php b/tests/Rule/data/ForbidArithmeticOperationOnNonNumberRule/code.php index 66643fa..3d4a38f 100644 --- a/tests/Rule/data/ForbidArithmeticOperationOnNonNumberRule/code.php +++ b/tests/Rule/data/ForbidArithmeticOperationOnNonNumberRule/code.php @@ -130,6 +130,8 @@ public function testUnions( -$intFloat; -$intFloatString; // error: Using - over non-number (float|int|string) -$intArray; // error: Using - over non-number (array|int) + + $intString - $intArray; // error: Using - over non-number (int|string - array|int) } /** From 3e02d4c4a25b6a0a94d7a018e32563d2be077cb9 Mon Sep 17 00:00:00 2001 From: Jan Nedbal Date: Mon, 1 Dec 2025 12:47:05 +0100 Subject: [PATCH 4/5] Minor optimization --- src/Rule/ForbidArithmeticOperationOnNonNumberRule.php | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/Rule/ForbidArithmeticOperationOnNonNumberRule.php b/src/Rule/ForbidArithmeticOperationOnNonNumberRule.php index c31102c..af48809 100644 --- a/src/Rule/ForbidArithmeticOperationOnNonNumberRule.php +++ b/src/Rule/ForbidArithmeticOperationOnNonNumberRule.php @@ -182,8 +182,9 @@ private function isFloat(Type $type): bool private function containsBcMathNumber(Type $type): bool { $bcMathFound = false; - TypeTraverser::map($type, static function (Type $traversedTyped, callable $traverse) use (&$bcMathFound): Type { - $bcMathNumber = new ObjectType('BcMath\Number'); + $bcMathNumber = new ObjectType('BcMath\Number'); + + TypeTraverser::map($type, static function (Type $traversedTyped, callable $traverse) use (&$bcMathFound, $bcMathNumber): Type { if ($bcMathNumber->isSuperTypeOf($traversedTyped)->yes()) { $bcMathFound = true; } From d14dd70736986ad8b2c19161bc342adbb89f078b Mon Sep 17 00:00:00 2001 From: Jan Nedbal Date: Mon, 1 Dec 2025 12:51:44 +0100 Subject: [PATCH 5/5] Readme adjustment --- README.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index d911a55..0a035f4 100644 --- a/README.md +++ b/README.md @@ -227,10 +227,11 @@ class EnforceReadonlyPublicPropertyRule { ``` ### forbidArithmeticOperationOnNonNumber -- Disallows using [arithmetic operators](https://www.php.net/manual/en/language.operators.arithmetic.php) with non-numeric types (only float and int is allowed) +- Disallows using [arithmetic operators](https://www.php.net/manual/en/language.operators.arithmetic.php) with non-numeric types (only `float`, `int` and `BcMath\Number` is allowed) - You can allow numeric-string by using `allowNumericString: true` configuration - Modulo operator (`%`) allows only integers as it [emits deprecation otherwise](https://3v4l.org/VpVoq) - Plus operator is allowed for merging arrays +- `float` and `BcMath\Number` cannot be combined as it emits deprecations ```php function add(string $a, string $b) {