diff --git a/src/Type/Php/GmpOperatorTypeSpecifyingExtension.php b/src/Type/Php/GmpOperatorTypeSpecifyingExtension.php new file mode 100644 index 0000000000..ef0a45a956 --- /dev/null +++ b/src/Type/Php/GmpOperatorTypeSpecifyingExtension.php @@ -0,0 +1,62 @@ +>', '<', '<=', '>', '>=', '==', '!=', '<=>'], true) + && ( + $gmpType->isSuperTypeOf($leftSide)->yes() + || $gmpType->isSuperTypeOf($rightSide)->yes() + ); + } + + public function specifyType(string $operatorSigil, Type $leftSide, Type $rightSide): Type + { + $gmpType = new ObjectType('GMP'); + $otherSide = $gmpType->isSuperTypeOf($leftSide)->yes() + ? $rightSide + : $leftSide; + + // Comparison operators return bool or int (for spaceship) + if (in_array($operatorSigil, ['<', '<=', '>', '>=', '==', '!='], true)) { + return new BooleanType(); + } + + if ($operatorSigil === '<=>') { + return IntegerRangeType::fromInterval(-1, 1); + } + + // GMP can operate with: GMP, int, or numeric-string + if ( + $otherSide->isInteger()->yes() + || $otherSide->isNumericString()->yes() + || $gmpType->isSuperTypeOf($otherSide)->yes() + ) { + return $gmpType; + } + + return new ErrorType(); + } + +} diff --git a/src/Type/Php/GmpUnaryOperatorTypeSpecifyingExtension.php b/src/Type/Php/GmpUnaryOperatorTypeSpecifyingExtension.php new file mode 100644 index 0000000000..4aaa3de4d8 --- /dev/null +++ b/src/Type/Php/GmpUnaryOperatorTypeSpecifyingExtension.php @@ -0,0 +1,35 @@ +isSuperTypeOf($operand)->yes(); + } + + public function specifyType(string $operatorSigil, Type $operand): Type + { + return new ObjectType('GMP'); + } + +} diff --git a/tests/PHPStan/Analyser/nsrt/gmp-operators.php b/tests/PHPStan/Analyser/nsrt/gmp-operators.php new file mode 100644 index 0000000000..ae708b560e --- /dev/null +++ b/tests/PHPStan/Analyser/nsrt/gmp-operators.php @@ -0,0 +1,206 @@ +> $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')); +} + +/** + * @param object $obj + */ +function nonGmpObjectsDoNotGetGmpTreatment($obj, int $i): void +{ + // Generic object should NOT be treated as GMP - the extension should not activate + // (object is a supertype of GMP, but GMP is not a supertype of object) + /** @phpstan-ignore binaryOp.invalid */ + assertType('*ERROR*', $obj + $i); + /** @phpstan-ignore binaryOp.invalid */ + assertType('*ERROR*', $i + $obj); +} diff --git a/tests/PHPStan/Analyser/nsrt/pow.php b/tests/PHPStan/Analyser/nsrt/pow.php index 3ca27690db..69a7a3c222 100644 --- a/tests/PHPStan/Analyser/nsrt/pow.php +++ b/tests/PHPStan/Analyser/nsrt/pow.php @@ -20,11 +20,12 @@ function (\GMP $a, \GMP $b): void { }; function (\stdClass $a, \GMP $b): void { - assertType('GMP|stdClass', pow($a, $b)); - assertType('GMP|stdClass', $a ** $b); + // stdClass is not a valid GMP operand, these should error + assertType('*ERROR*', pow($a, $b)); + assertType('*ERROR*', $a ** $b); - assertType('GMP|stdClass', pow($b, $a)); - assertType('GMP|stdClass', $b ** $a); + assertType('*ERROR*', pow($b, $a)); + assertType('*ERROR*', $b ** $a); }; function (): void { diff --git a/tests/PHPStan/Type/Php/GmpOperatorTypeSpecifyingExtensionTest.php b/tests/PHPStan/Type/Php/GmpOperatorTypeSpecifyingExtensionTest.php new file mode 100644 index 0000000000..1ced2596a3 --- /dev/null +++ b/tests/PHPStan/Type/Php/GmpOperatorTypeSpecifyingExtensionTest.php @@ -0,0 +1,136 @@ +extension = new GmpOperatorTypeSpecifyingExtension(); + } + + #[DataProvider('dataSupportedOperations')] + public function testSupportsValidGmpOperations(string $sigil, string $leftType, string $rightType): void + { + $left = $this->createType($leftType); + $right = $this->createType($rightType); + + self::assertTrue($this->extension->isOperatorSupported($sigil, $left, $right)); + } + + public static function dataSupportedOperations(): iterable + { + // GMP + GMP + yield 'GMP + GMP' => ['+', 'GMP', 'GMP']; + yield 'GMP - GMP' => ['-', 'GMP', 'GMP']; + yield 'GMP * GMP' => ['*', 'GMP', 'GMP']; + + // GMP + int (activates, specifyType handles compatibility) + yield 'GMP + int' => ['+', 'GMP', 'int']; + yield 'int + GMP' => ['+', 'int', 'GMP']; + + // GMP + incompatible (activates, specifyType returns ErrorType) + yield 'GMP + stdClass' => ['+', 'GMP', 'stdClass']; + yield 'stdClass + GMP' => ['+', 'stdClass', 'GMP']; + + // Comparison + yield 'GMP < GMP' => ['<', 'GMP', 'GMP']; + yield 'GMP <=> int' => ['<=>', 'GMP', 'int']; + } + + #[DataProvider('dataUnsupportedOperations')] + public function testDoesNotSupportInvalidOperations(string $sigil, string $leftType, string $rightType): void + { + $left = $this->createType($leftType); + $right = $this->createType($rightType); + + self::assertFalse($this->extension->isOperatorSupported($sigil, $left, $right)); + } + + public static function dataUnsupportedOperations(): iterable + { + // Neither side is GMP + yield 'int + int' => ['+', 'int', 'int']; + + // object is a supertype of GMP, but is not GMP itself + // This catches mutations that swap isSuperTypeOf callee/argument + yield 'object + int' => ['+', 'object', 'int']; + yield 'int + object' => ['+', 'int', 'object']; + + // GMP|int union should not be treated as definitely GMP + // This catches mutations that change .yes() to !.no() + yield 'GMP|int + int' => ['+', 'GMP|int', 'int']; + yield 'int + GMP|int' => ['+', 'int', 'GMP|int']; + } + + #[DataProvider('dataSpecifyTypeReturnsError')] + public function testSpecifyTypeReturnsErrorForIncompatibleTypes(string $sigil, string $leftType, string $rightType): void + { + $left = $this->createType($leftType); + $right = $this->createType($rightType); + + self::assertInstanceOf(ErrorType::class, $this->extension->specifyType($sigil, $left, $right)); + } + + public static function dataSpecifyTypeReturnsError(): iterable + { + yield 'GMP + stdClass' => ['+', 'GMP', 'stdClass']; + yield 'stdClass + GMP' => ['+', 'stdClass', 'GMP']; + yield 'GMP + float' => ['+', 'GMP', 'float']; + } + + #[DataProvider('dataSpecifyTypeReturnsGmp')] + public function testSpecifyTypeReturnsGmpForCompatibleTypes(string $sigil, string $leftType, string $rightType): void + { + $left = $this->createType($leftType); + $right = $this->createType($rightType); + + $result = $this->extension->specifyType($sigil, $left, $right); + self::assertInstanceOf(ObjectType::class, $result); + self::assertSame('GMP', $result->getClassName()); + } + + public static function dataSpecifyTypeReturnsGmp(): iterable + { + yield 'GMP + GMP' => ['+', 'GMP', 'GMP']; + yield 'GMP + int' => ['+', 'GMP', 'int']; + yield 'int + GMP' => ['+', 'int', 'GMP']; + } + + private function createType(string $type): Type + { + switch ($type) { + case 'GMP': + return new ObjectType('GMP'); + case 'int': + return new IntegerType(); + case 'float': + return new FloatType(); + case 'object': + return new ObjectType('object'); + case 'stdClass': + return new ObjectType('stdClass'); + case 'GMP|int': + return new UnionType([new ObjectType('GMP'), new IntegerType()]); + default: + throw new InvalidArgumentException(sprintf('Unknown type: %s', $type)); + } + } + +}