From 48a38a377f3fab5d9ab60ec5a785f1c67d0c8a41 Mon Sep 17 00:00:00 2001 From: Mikko Pesari Date: Mon, 16 Feb 2026 19:41:12 +0200 Subject: [PATCH] Fix BcMath\Number unary plus/minus/bitwise-not --- .../InitializerExprTypeResolver.php | 20 +++++++++++++++++++ src/Type/ObjectType.php | 4 ++++ tests/PHPStan/Analyser/nsrt/bcmath-number.php | 13 +++++++++--- .../InvalidUnaryOperationRuleTest.php | 11 ++++++++++ .../Operators/data/unary-bcmath-number.php | 17 ++++++++++++++++ 5 files changed, 62 insertions(+), 3 deletions(-) create mode 100644 tests/PHPStan/Rules/Operators/data/unary-bcmath-number.php diff --git a/src/Reflection/InitializerExprTypeResolver.php b/src/Reflection/InitializerExprTypeResolver.php index 2794147dcc..feec1bf2ac 100644 --- a/src/Reflection/InitializerExprTypeResolver.php +++ b/src/Reflection/InitializerExprTypeResolver.php @@ -993,6 +993,10 @@ public function getBitwiseAndTypeFromTypes(Type $leftType, Type $rightType): Typ return $this->getNeverType($leftType, $rightType); } + if ((new ObjectType('BcMath\Number'))->isSuperTypeOf($leftType)->yes()) { + return new ErrorType(); + } + $result = $this->getFiniteOrConstantScalarTypes($leftType, $rightType, static fn ($a, $b) => $a & $b); if ($result instanceof Type) { return $result; @@ -1051,6 +1055,10 @@ public function getBitwiseOrTypeFromTypes(Type $leftType, Type $rightType): Type return $this->getNeverType($leftType, $rightType); } + if ((new ObjectType('BcMath\Number'))->isSuperTypeOf($leftType)->yes()) { + return new ErrorType(); + } + $result = $this->getFiniteOrConstantScalarTypes($leftType, $rightType, static fn ($a, $b) => $a | $b); if ($result instanceof Type) { return $result; @@ -1159,6 +1167,10 @@ public function getBitwiseXorTypeFromTypes(Type $leftType, Type $rightType): Typ return $this->getNeverType($leftType, $rightType); } + if ((new ObjectType('BcMath\Number'))->isSuperTypeOf($leftType)->yes()) { + return new ErrorType(); + } + $result = $this->getFiniteOrConstantScalarTypes($leftType, $rightType, static fn ($a, $b) => $a ^ $b); if ($result instanceof Type) { return $result; @@ -1765,6 +1777,10 @@ public function getShiftLeftTypeFromTypes(Expr $left, Expr $right, Type $leftTyp return $this->getNeverType($leftType, $rightType); } + if ((new ObjectType('BcMath\Number'))->isSuperTypeOf($leftType)->yes()) { + return new ErrorType(); + } + $leftTypes = $leftType->getConstantScalarTypes(); $rightTypes = $rightType->getConstantScalarTypes(); $leftTypesCount = count($leftTypes); @@ -1829,6 +1845,10 @@ public function getShiftRightTypeFromTypes(Expr $left, Expr $right, Type $leftTy return $this->getNeverType($leftType, $rightType); } + if ((new ObjectType('BcMath\Number'))->isSuperTypeOf($leftType)->yes()) { + return new ErrorType(); + } + $leftTypes = $leftType->getConstantScalarTypes(); $rightTypes = $rightType->getConstantScalarTypes(); $leftTypesCount = count($leftTypes); diff --git a/src/Type/ObjectType.php b/src/Type/ObjectType.php index 8e295b9fc4..634770df9b 100644 --- a/src/Type/ObjectType.php +++ b/src/Type/ObjectType.php @@ -762,6 +762,10 @@ public function toNumber(): Type ]); } + if ($this->isInstanceOf('BcMath\Number')->yes()) { + return new ObjectType('BcMath\Number'); + } + return new ErrorType(); } diff --git a/tests/PHPStan/Analyser/nsrt/bcmath-number.php b/tests/PHPStan/Analyser/nsrt/bcmath-number.php index d78887e4ef..9002f9ce6c 100644 --- a/tests/PHPStan/Analyser/nsrt/bcmath-number.php +++ b/tests/PHPStan/Analyser/nsrt/bcmath-number.php @@ -380,9 +380,9 @@ public function bcVsNever(Number $a): void { for ($b = 1; $b < count([]); $b++) { assertType('*NEVER*', $a + $b); - assertType('*ERROR*', $a - $b); // Inconsistency: getPlusType handles never types right at the beginning, getMinusType doesn't. - assertType('*ERROR*', $a * $b); - assertType('*ERROR*', $a / $b); + assertType('*NEVER*', $a - $b); + assertType('*NEVER*', $a * $b); + assertType('*NEVER*', $a / $b); assertType('*NEVER*', $a % $b); assertType('non-empty-string&numeric-string', $a . $b); assertType('*ERROR*', $a ** $b); @@ -413,4 +413,11 @@ public function bcIncDec(Number $a): void assertType('BcMath\Number', --$a); assertType('BcMath\Number', $a--); } + + public function bcUnaryOp(Number $a): void + { + assertType('BcMath\Number', +$a); + assertType('BcMath\Number', -$a); + assertType('*ERROR*', ~$a); + } } diff --git a/tests/PHPStan/Rules/Operators/InvalidUnaryOperationRuleTest.php b/tests/PHPStan/Rules/Operators/InvalidUnaryOperationRuleTest.php index 7300106db7..5fc873675a 100644 --- a/tests/PHPStan/Rules/Operators/InvalidUnaryOperationRuleTest.php +++ b/tests/PHPStan/Rules/Operators/InvalidUnaryOperationRuleTest.php @@ -169,4 +169,15 @@ public function testUnion(): void ]); } + #[RequiresPhp('>= 8.4')] + public function testBcMathNumber(): void + { + $this->analyse([__DIR__ . '/data/unary-bcmath-number.php'], [ + [ + 'Unary operation "~" on BcMath\Number results in an error.', + 16, + ], + ]); + } + } diff --git a/tests/PHPStan/Rules/Operators/data/unary-bcmath-number.php b/tests/PHPStan/Rules/Operators/data/unary-bcmath-number.php new file mode 100644 index 0000000000..de1dbbcab6 --- /dev/null +++ b/tests/PHPStan/Rules/Operators/data/unary-bcmath-number.php @@ -0,0 +1,17 @@ +