diff --git a/src/Reflection/InitializerExprTypeResolver.php b/src/Reflection/InitializerExprTypeResolver.php index 0891f487b7..3f73892293 100644 --- a/src/Reflection/InitializerExprTypeResolver.php +++ b/src/Reflection/InitializerExprTypeResolver.php @@ -986,6 +986,12 @@ public function getBitwiseAndType(Expr $left, Expr $right, callable $getTypeCall $leftType = $getTypeCallback($left); $rightType = $getTypeCallback($right); + $specifiedTypes = $this->operatorTypeSpecifyingExtensionRegistryProvider->getRegistry() + ->callOperatorTypeSpecifyingExtensions(new BinaryOp\BitwiseAnd($left, $right), $leftType, $rightType); + if ($specifiedTypes !== null) { + return $specifiedTypes; + } + return $this->getBitwiseAndTypeFromTypes($leftType, $rightType); } @@ -1044,6 +1050,12 @@ public function getBitwiseOrType(Expr $left, Expr $right, callable $getTypeCallb $leftType = $getTypeCallback($left); $rightType = $getTypeCallback($right); + $specifiedTypes = $this->operatorTypeSpecifyingExtensionRegistryProvider->getRegistry() + ->callOperatorTypeSpecifyingExtensions(new BinaryOp\BitwiseOr($left, $right), $leftType, $rightType); + if ($specifiedTypes !== null) { + return $specifiedTypes; + } + return $this->getBitwiseOrTypeFromTypes($leftType, $rightType); } @@ -1092,6 +1104,12 @@ public function getBitwiseXorType(Expr $left, Expr $right, callable $getTypeCall $leftType = $getTypeCallback($left); $rightType = $getTypeCallback($right); + $specifiedTypes = $this->operatorTypeSpecifyingExtensionRegistryProvider->getRegistry() + ->callOperatorTypeSpecifyingExtensions(new BinaryOp\BitwiseXor($left, $right), $leftType, $rightType); + if ($specifiedTypes !== null) { + return $specifiedTypes; + } + return $this->getBitwiseXorTypeFromTypes($leftType, $rightType); } @@ -2034,6 +2052,12 @@ private function resolveConstantArrayTypeComparison(ConstantArrayType $leftType, */ private function resolveCommonMath(Expr\BinaryOp $expr, Type $leftType, Type $rightType): Type { + $specifiedTypes = $this->operatorTypeSpecifyingExtensionRegistryProvider->getRegistry() + ->callOperatorTypeSpecifyingExtensions($expr, $leftType, $rightType); + if ($specifiedTypes !== null) { + return $specifiedTypes; + } + $types = TypeCombinator::union($leftType, $rightType); $leftNumberType = $leftType->toNumber(); $rightNumberType = $rightType->toNumber(); @@ -2073,12 +2097,6 @@ private function resolveCommonMath(Expr\BinaryOp $expr, Type $leftType, Type $ri } } - $specifiedTypes = $this->operatorTypeSpecifyingExtensionRegistryProvider->getRegistry() - ->callOperatorTypeSpecifyingExtensions($expr, $leftType, $rightType); - if ($specifiedTypes !== null) { - return $specifiedTypes; - } - if ( $leftType->isArray()->yes() || $rightType->isArray()->yes() @@ -2581,6 +2599,11 @@ public function getUnaryMinusType(Expr $expr, callable $getTypeCallback): Type { $type = $getTypeCallback($expr); + // GMP supports unary minus and returns GMP + if ($type->isObject()->yes() && (new ObjectType('GMP'))->isSuperTypeOf($type)->yes()) { + return new ObjectType('GMP'); + } + $type = $this->getUnaryMinusTypeFromType($expr, $type); if ($type instanceof IntegerRangeType) { return $getTypeCallback(new Expr\BinaryOp\Mul($expr, new Int_(-1))); @@ -2622,6 +2645,11 @@ public function getBitwiseNotType(Expr $expr, callable $getTypeCallback): Type { $exprType = $getTypeCallback($expr); + // GMP supports bitwise not and returns GMP + if ($exprType->isObject()->yes() && (new ObjectType('GMP'))->isSuperTypeOf($exprType)->yes()) { + return new ObjectType('GMP'); + } + return $this->getBitwiseNotTypeFromType($exprType); } diff --git a/src/Type/Php/GmpOperatorTypeSpecifyingExtension.php b/src/Type/Php/GmpOperatorTypeSpecifyingExtension.php new file mode 100644 index 0000000000..9e78d1c68e --- /dev/null +++ b/src/Type/Php/GmpOperatorTypeSpecifyingExtension.php @@ -0,0 +1,74 @@ +>', '<', '<=', '>', '>=', '==', '!=', '<=>'], true)) { + return false; + } + + $gmpType = new ObjectType('GMP'); + $leftIsGmp = $gmpType->isSuperTypeOf($leftSide)->yes(); + $rightIsGmp = $gmpType->isSuperTypeOf($rightSide)->yes(); + + // At least one side must be GMP + if (!$leftIsGmp && !$rightIsGmp) { + return false; + } + + // The other side must be GMP-compatible (GMP, int, or numeric-string) + // GMP operations with incompatible types (like stdClass) will error at runtime + return $this->isGmpCompatible($leftSide, $gmpType) && $this->isGmpCompatible($rightSide, $gmpType); + } + + private function isGmpCompatible(Type $type, ObjectType $gmpType): bool + { + if ($gmpType->isSuperTypeOf($type)->yes()) { + return true; + } + if ($type->isInteger()->yes()) { + return true; + } + if ($type->isNumericString()->yes()) { + return true; + } + return false; + } + + public function specifyType(string $operatorSigil, Type $leftSide, Type $rightSide): Type + { + $gmpType = new ObjectType('GMP'); + + // Comparison operators return bool or int (for spaceship) + if (in_array($operatorSigil, ['<', '<=', '>', '>=', '==', '!='], true)) { + return new BooleanType(); + } + + if ($operatorSigil === '<=>') { + return IntegerRangeType::fromInterval(-1, 1); + } + + // All arithmetic and bitwise operations on GMP return GMP + // GMP can operate with: GMP, int, or numeric-string + return $gmpType; + } + +} diff --git a/tests/PHPStan/Analyser/nsrt/gmp-operators.php b/tests/PHPStan/Analyser/nsrt/gmp-operators.php new file mode 100644 index 0000000000..5c7d4e5fed --- /dev/null +++ b/tests/PHPStan/Analyser/nsrt/gmp-operators.php @@ -0,0 +1,193 @@ +> $b); + + // GMP on left, int on right + assertType('GMP', $a & $i); + assertType('GMP', $a | $i); + assertType('GMP', $a ^ $i); + assertType('GMP', $a << $i); + assertType('GMP', $a >> $i); + + // int on left, GMP on right + assertType('GMP', $i & $a); + assertType('GMP', $i | $a); + assertType('GMP', $i ^ $a); +} + +function gmpComparisonOperators(\GMP $a, \GMP $b, int $i): void +{ + // GMP compared with GMP + assertType('bool', $a < $b); + assertType('bool', $a <= $b); + assertType('bool', $a > $b); + assertType('bool', $a >= $b); + assertType('bool', $a == $b); + assertType('bool', $a != $b); + assertType('int<-1, 1>', $a <=> $b); + + // GMP on left, int on right + assertType('bool', $a < $i); + assertType('bool', $a <= $i); + assertType('bool', $a > $i); + assertType('bool', $a >= $i); + assertType('bool', $a == $i); + assertType('bool', $a != $i); + assertType('int<-1, 1>', $a <=> $i); + + // int on left, GMP on right + assertType('bool', $i < $a); + assertType('bool', $i <= $a); + assertType('bool', $i > $a); + assertType('bool', $i >= $a); + assertType('bool', $i == $a); + assertType('bool', $i != $a); + assertType('int<-1, 1>', $i <=> $a); +} + +function gmpAssignmentOperators(\GMP $a, int $i): void +{ + $x = $a; + $x += $i; + assertType('GMP', $x); + + $y = $a; + $y -= $i; + assertType('GMP', $y); + + $z = $a; + $z *= $i; + assertType('GMP', $z); +} + +// ============================================================================= +// gmp_* functions (corresponding to operator overloads) +// ============================================================================= + +function gmpArithmeticFunctions(\GMP $a, \GMP $b, int $i): void +{ + // gmp_add corresponds to + + assertType('GMP', gmp_add($a, $b)); + assertType('GMP', gmp_add($a, $i)); + assertType('GMP', gmp_add($i, $a)); + + // gmp_sub corresponds to - + assertType('GMP', gmp_sub($a, $b)); + assertType('GMP', gmp_sub($a, $i)); + assertType('GMP', gmp_sub($i, $a)); + + // gmp_mul corresponds to * + assertType('GMP', gmp_mul($a, $b)); + assertType('GMP', gmp_mul($a, $i)); + assertType('GMP', gmp_mul($i, $a)); + + // gmp_div_q corresponds to / + assertType('GMP', gmp_div_q($a, $b)); + assertType('GMP', gmp_div_q($a, $i)); + + // gmp_div is alias of gmp_div_q + assertType('GMP', gmp_div($a, $b)); + + // gmp_mod corresponds to % + assertType('GMP', gmp_mod($a, $b)); + assertType('GMP', gmp_mod($a, $i)); + + // gmp_pow corresponds to ** + assertType('GMP', gmp_pow($a, 2)); + assertType('GMP', gmp_pow($a, $i)); + + // gmp_neg corresponds to unary - + assertType('GMP', gmp_neg($a)); + + // gmp_abs (no direct operator) + assertType('GMP', gmp_abs($a)); +} + +function gmpBitwiseFunctions(\GMP $a, \GMP $b): void +{ + // gmp_and corresponds to & + assertType('GMP', gmp_and($a, $b)); + + // gmp_or corresponds to | + assertType('GMP', gmp_or($a, $b)); + + // gmp_xor corresponds to ^ + assertType('GMP', gmp_xor($a, $b)); + + // gmp_com corresponds to ~ + assertType('GMP', gmp_com($a)); +} + +function gmpComparisonFunctions(\GMP $a, \GMP $b, int $i): void +{ + // gmp_cmp returns -1, 0, or 1 in practice, but stubs say int + // TODO: Could be improved to int<-1, 1> like the <=> operator + assertType('int', gmp_cmp($a, $b)); + assertType('int', gmp_cmp($a, $i)); +} + +function gmpFromInit(): void +{ + $x = gmp_init('1'); + assertType('GMP', $x); + + // Operator with gmp_init result + $y = $x * 2; + assertType('GMP', $y); + + $z = $x + gmp_init('5'); + assertType('GMP', $z); +} + +function gmpWithNumericString(\GMP $a, string $s): void +{ + // GMP functions accept numeric strings + assertType('GMP', gmp_add($a, '123')); + assertType('GMP', gmp_mul($a, '456')); +}