diff --git a/src/Reflection/InitializerExprTypeResolver.php b/src/Reflection/InitializerExprTypeResolver.php index f7a8f872e2..5e5336da67 100644 --- a/src/Reflection/InitializerExprTypeResolver.php +++ b/src/Reflection/InitializerExprTypeResolver.php @@ -2611,6 +2611,22 @@ public function getUnaryMinusType(Expr $expr, callable $getTypeCallback): Type { $type = $getTypeCallback($expr); + // GMP supports unary minus and returns GMP + $gmpType = new ObjectType('GMP'); + if ($gmpType->isSuperTypeOf($type)->yes()) { + return $gmpType; + } + + // Handle union types containing GMP + if ($gmpType->isSuperTypeOf($type)->maybe()) { + $nonGmpType = TypeCombinator::remove($type, $gmpType); + $nonGmpResult = $this->getUnaryMinusTypeFromType($expr, $nonGmpType); + if ($nonGmpResult instanceof IntegerRangeType) { + $nonGmpResult = $getTypeCallback(new Expr\BinaryOp\Mul($expr, new Int_(-1))); + } + return TypeCombinator::union($gmpType, $nonGmpResult); + } + $type = $this->getUnaryMinusTypeFromType($expr, $type); if ($type instanceof IntegerRangeType) { return $getTypeCallback(new Expr\BinaryOp\Mul($expr, new Int_(-1))); @@ -2652,6 +2668,19 @@ public function getBitwiseNotType(Expr $expr, callable $getTypeCallback): Type { $exprType = $getTypeCallback($expr); + // GMP supports bitwise not and returns GMP + $gmpType = new ObjectType('GMP'); + if ($gmpType->isSuperTypeOf($exprType)->yes()) { + return $gmpType; + } + + // Handle union types containing GMP + if ($gmpType->isSuperTypeOf($exprType)->maybe()) { + $nonGmpType = TypeCombinator::remove($exprType, $gmpType); + $nonGmpResult = $this->getBitwiseNotTypeFromType($nonGmpType); + return TypeCombinator::union($gmpType, $nonGmpResult); + } + 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..d99a9a500a --- /dev/null +++ b/tests/PHPStan/Analyser/nsrt/gmp-operators.php @@ -0,0 +1,215 @@ +> $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')); +} + +// ============================================================================= +// Union types - unary operators handle GMP in unions correctly +// ============================================================================= + +/** + * @param \GMP|int $maybeGmp + */ +function gmpUnionUnaryMinus($maybeGmp): void +{ + // Unary minus on GMP|int returns GMP (from GMP branch) | int (from int branch) + assertType('GMP|int', -$maybeGmp); +} + +/** + * @param \GMP|int $maybeGmp + */ +function gmpUnionBitwiseNot($maybeGmp): void +{ + // Bitwise NOT on GMP|int returns GMP (from GMP branch) | int (from int branch) + assertType('GMP|int', ~$maybeGmp); +}