From 7da985ceba06874b302d21024f7d7aa62909cbcd Mon Sep 17 00:00:00 2001 From: Eric Stern Date: Sun, 15 Mar 2026 11:32:51 -0700 Subject: [PATCH 1/8] Add test coverage for GMP operator overloads and gmp_* functions This adds comprehensive type inference tests for GMP operations: - Arithmetic operators (+, -, *, /, %, **) with GMP on left and right - Bitwise operators (&, |, ^, ~, <<, >>) with GMP on left and right - Comparison operators (<, <=, >, >=, ==, !=, <=>) with GMP on left and right - Assignment operators (+=, -=, *=) - Corresponding gmp_* functions (gmp_add, gmp_sub, gmp_mul, etc.) These tests currently fail because PHPStan lacks a GmpOperatorTypeSpecifyingExtension to specify that GMP operations return GMP rather than int|float. Related: https://github.com/phpstan/phpstan/issues/12123 Co-Authored-By: Claude Opus 4.5 --- tests/PHPStan/Analyser/nsrt/gmp-operators.php | 192 ++++++++++++++++++ 1 file changed, 192 insertions(+) create mode 100644 tests/PHPStan/Analyser/nsrt/gmp-operators.php diff --git a/tests/PHPStan/Analyser/nsrt/gmp-operators.php b/tests/PHPStan/Analyser/nsrt/gmp-operators.php new file mode 100644 index 0000000000..d5d0ee1ae0 --- /dev/null +++ b/tests/PHPStan/Analyser/nsrt/gmp-operators.php @@ -0,0 +1,192 @@ +> $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 corresponds to <=> + assertType('int<-1, 1>', gmp_cmp($a, $b)); + assertType('int<-1, 1>', 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')); +} From eb85b95fcfe13050a00ceccfb0dde83b5030f024 Mon Sep 17 00:00:00 2001 From: Eric Stern Date: Sun, 15 Mar 2026 12:00:02 -0700 Subject: [PATCH 2/8] Implement GMP operator type specifying extension Add GmpOperatorTypeSpecifyingExtension to properly infer return types for GMP operator overloads. GMP supports arithmetic (+, -, *, /, %, **), bitwise (&, |, ^, ~, <<, >>), and comparison (<, <=, >, >=, ==, !=, <=>) operators. The extension only claims support when both operands are GMP-compatible (GMP, int, or numeric-string). Operations with incompatible types like stdClass are left to the default type inference. Also update InitializerExprTypeResolver to call operator extensions early for object types in resolveCommonMath and bitwise methods, and add explicit GMP handling for unary operators (-$a, ~$a). Fixes phpstan/phpstan#14288 Co-Authored-By: Claude Opus 4.5 --- .../GmpOperatorTypeSpecifyingExtension.php | 74 +++++++++++++++++++ tests/PHPStan/Analyser/nsrt/gmp-operators.php | 7 +- 2 files changed, 78 insertions(+), 3 deletions(-) create mode 100644 src/Type/Php/GmpOperatorTypeSpecifyingExtension.php 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 index d5d0ee1ae0..5c7d4e5fed 100644 --- a/tests/PHPStan/Analyser/nsrt/gmp-operators.php +++ b/tests/PHPStan/Analyser/nsrt/gmp-operators.php @@ -166,9 +166,10 @@ function gmpBitwiseFunctions(\GMP $a, \GMP $b): void function gmpComparisonFunctions(\GMP $a, \GMP $b, int $i): void { - // gmp_cmp corresponds to <=> - assertType('int<-1, 1>', gmp_cmp($a, $b)); - assertType('int<-1, 1>', gmp_cmp($a, $i)); + // 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 From eaebe39fb61ffcfdf67439dbe4a338db3ee6ad69 Mon Sep 17 00:00:00 2001 From: Eric Stern Date: Tue, 24 Mar 2026 09:13:07 -0700 Subject: [PATCH 3/8] Add GMP unary operator type specifying extension Implements UnaryOperatorTypeSpecifyingExtension for GMP to handle unary minus (-), unary plus (+), and bitwise NOT (~) operators. Co-Authored-By: Claude Opus 4.5 --- ...mpUnaryOperatorTypeSpecifyingExtension.php | 35 +++++++++++++++++++ 1 file changed, 35 insertions(+) create mode 100644 src/Type/Php/GmpUnaryOperatorTypeSpecifyingExtension.php 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'); + } + +} From 73b16d4f583579964f2e2357d0d4855518672432 Mon Sep 17 00:00:00 2001 From: Eric Stern Date: Wed, 25 Mar 2026 10:38:14 -0700 Subject: [PATCH 4/8] Refactor GmpOperatorTypeSpecifyingExtension to match BcMath pattern - Simplify isOperatorSupported to only check if one side is GMP - Move compatibility checking to specifyType, returning ErrorType for incompatible operands (like stdClass) - Add unit tests for the extension - Update pow.php test to expect ErrorType for stdClass ** GMP Co-Authored-By: Claude Opus 4.5 --- .../GmpOperatorTypeSpecifyingExtension.php | 48 +++---- tests/PHPStan/Analyser/nsrt/gmp-operators.php | 13 ++ tests/PHPStan/Analyser/nsrt/pow.php | 9 +- ...GmpOperatorTypeSpecifyingExtensionTest.php | 127 ++++++++++++++++++ 4 files changed, 163 insertions(+), 34 deletions(-) create mode 100644 tests/PHPStan/Type/Php/GmpOperatorTypeSpecifyingExtensionTest.php diff --git a/src/Type/Php/GmpOperatorTypeSpecifyingExtension.php b/src/Type/Php/GmpOperatorTypeSpecifyingExtension.php index 9e78d1c68e..ef0a45a956 100644 --- a/src/Type/Php/GmpOperatorTypeSpecifyingExtension.php +++ b/src/Type/Php/GmpOperatorTypeSpecifyingExtension.php @@ -4,6 +4,7 @@ use PHPStan\DependencyInjection\AutowiredService; use PHPStan\Type\BooleanType; +use PHPStan\Type\ErrorType; use PHPStan\Type\IntegerRangeType; use PHPStan\Type\NeverType; use PHPStan\Type\ObjectType; @@ -21,41 +22,21 @@ public function isOperatorSupported(string $operatorSigil, Type $leftSide, Type return false; } - if (!in_array($operatorSigil, ['+', '-', '*', '/', '**', '%', '&', '|', '^', '<<', '>>', '<', '<=', '>', '>=', '==', '!=', '<=>'], 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; + return in_array($operatorSigil, ['+', '-', '*', '/', '**', '%', '&', '|', '^', '<<', '>>', '<', '<=', '>', '>=', '==', '!=', '<=>'], 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)) { @@ -66,9 +47,16 @@ public function specifyType(string $operatorSigil, Type $leftSide, Type $rightSi 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; + if ( + $otherSide->isInteger()->yes() + || $otherSide->isNumericString()->yes() + || $gmpType->isSuperTypeOf($otherSide)->yes() + ) { + return $gmpType; + } + + return new ErrorType(); } } diff --git a/tests/PHPStan/Analyser/nsrt/gmp-operators.php b/tests/PHPStan/Analyser/nsrt/gmp-operators.php index 5c7d4e5fed..ae708b560e 100644 --- a/tests/PHPStan/Analyser/nsrt/gmp-operators.php +++ b/tests/PHPStan/Analyser/nsrt/gmp-operators.php @@ -191,3 +191,16 @@ function gmpWithNumericString(\GMP $a, string $s): void 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..dbeac8a82b --- /dev/null +++ b/tests/PHPStan/Type/Php/GmpOperatorTypeSpecifyingExtensionTest.php @@ -0,0 +1,127 @@ +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 + { + return match ($type) { + 'GMP' => new ObjectType('GMP'), + 'int' => new IntegerType(), + 'float' => new FloatType(), + 'object' => new ObjectType('object'), + 'stdClass' => new ObjectType('stdClass'), + 'GMP|int' => new UnionType([new ObjectType('GMP'), new IntegerType()]), + default => throw new InvalidArgumentException(sprintf('Unknown type: %s', $type)), + }; + } + +} From 83dbf7ef2d0820c89448ba2e6ca927806a7e972f Mon Sep 17 00:00:00 2001 From: Eric Stern Date: Wed, 25 Mar 2026 11:12:02 -0700 Subject: [PATCH 5/8] Fix test compatibility issues - Add #[Override] attribute to setUp() method - Replace match expression with switch for PHP 7.4 compatibility Co-Authored-By: Claude Opus 4.5 --- ...GmpOperatorTypeSpecifyingExtensionTest.php | 27 ++++++++++++------- 1 file changed, 18 insertions(+), 9 deletions(-) diff --git a/tests/PHPStan/Type/Php/GmpOperatorTypeSpecifyingExtensionTest.php b/tests/PHPStan/Type/Php/GmpOperatorTypeSpecifyingExtensionTest.php index dbeac8a82b..1ced2596a3 100644 --- a/tests/PHPStan/Type/Php/GmpOperatorTypeSpecifyingExtensionTest.php +++ b/tests/PHPStan/Type/Php/GmpOperatorTypeSpecifyingExtensionTest.php @@ -3,6 +3,7 @@ namespace PHPStan\Type\Php; use InvalidArgumentException; +use Override; use PHPStan\Testing\PHPStanTestCase; use PHPStan\Type\ErrorType; use PHPStan\Type\FloatType; @@ -18,6 +19,7 @@ class GmpOperatorTypeSpecifyingExtensionTest extends PHPStanTestCase private GmpOperatorTypeSpecifyingExtension $extension; + #[Override] protected function setUp(): void { $this->extension = new GmpOperatorTypeSpecifyingExtension(); @@ -113,15 +115,22 @@ public static function dataSpecifyTypeReturnsGmp(): iterable private function createType(string $type): Type { - return match ($type) { - 'GMP' => new ObjectType('GMP'), - 'int' => new IntegerType(), - 'float' => new FloatType(), - 'object' => new ObjectType('object'), - 'stdClass' => new ObjectType('stdClass'), - 'GMP|int' => new UnionType([new ObjectType('GMP'), new IntegerType()]), - default => throw new InvalidArgumentException(sprintf('Unknown type: %s', $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)); + } } } From f0245d1472c08f64c0e3459857a784a8821df2bb Mon Sep 17 00:00:00 2001 From: Eric Stern Date: Wed, 25 Mar 2026 16:44:02 -0700 Subject: [PATCH 6/8] Add mutation testing coverage for GmpOperatorTypeSpecifyingExtension - Use ObjectWithoutClassType for generic object type (not ObjectType('object')) - Add test cases for object+GMP to catch IsSuperTypeOfCalleeAndArgumentMutator on line 37 - Add test case for GMP|int+int to catch TrinaryLogicMutator on line 37 - Add test cases for GMP+int|stdClass to catch TrinaryLogicMutator on line 52 Co-Authored-By: Claude Opus 4.5 --- ...GmpOperatorTypeSpecifyingExtensionTest.php | 23 ++++++++++++++++++- 1 file changed, 22 insertions(+), 1 deletion(-) diff --git a/tests/PHPStan/Type/Php/GmpOperatorTypeSpecifyingExtensionTest.php b/tests/PHPStan/Type/Php/GmpOperatorTypeSpecifyingExtensionTest.php index 1ced2596a3..ec4290848b 100644 --- a/tests/PHPStan/Type/Php/GmpOperatorTypeSpecifyingExtensionTest.php +++ b/tests/PHPStan/Type/Php/GmpOperatorTypeSpecifyingExtensionTest.php @@ -9,6 +9,7 @@ use PHPStan\Type\FloatType; use PHPStan\Type\IntegerType; use PHPStan\Type\ObjectType; +use PHPStan\Type\ObjectWithoutClassType; use PHPStan\Type\Type; use PHPStan\Type\UnionType; use PHPUnit\Framework\Attributes\DataProvider; @@ -93,6 +94,21 @@ public static function dataSpecifyTypeReturnsError(): iterable yield 'GMP + stdClass' => ['+', 'GMP', 'stdClass']; yield 'stdClass + GMP' => ['+', 'stdClass', 'GMP']; yield 'GMP + float' => ['+', 'GMP', 'float']; + + // object is a supertype of GMP - these catch line 37 IsSuperTypeOfCalleeAndArgumentMutator + // When mutation swaps callee/argument, $otherSide incorrectly becomes GMP instead of object + yield 'object + GMP' => ['+', 'object', 'GMP']; + yield 'GMP + object' => ['+', 'GMP', 'object']; + + // GMP|int is Maybe-GMP - catches line 37 TrinaryLogicMutator + // When mutation changes .yes() to !.no(), $otherSide incorrectly becomes int instead of GMP|int + // Note: int + GMP|int returns GMP (other=int which is valid), only GMP|int + int returns error + yield 'GMP|int + int (specifyType)' => ['+', 'GMP|int', 'int']; + + // int|stdClass has isInteger()=Maybe - catches line 52 TrinaryLogicMutator + // When mutation changes .yes() to !.no(), isInteger() incorrectly returns true + yield 'GMP + int|stdClass' => ['+', 'GMP', 'int|stdClass']; + yield 'int|stdClass + GMP' => ['+', 'int|stdClass', 'GMP']; } #[DataProvider('dataSpecifyTypeReturnsGmp')] @@ -111,6 +127,9 @@ public static function dataSpecifyTypeReturnsGmp(): iterable yield 'GMP + GMP' => ['+', 'GMP', 'GMP']; yield 'GMP + int' => ['+', 'GMP', 'int']; yield 'int + GMP' => ['+', 'int', 'GMP']; + + // When left is int and right is GMP|int, other=int which is valid + yield 'int + GMP|int' => ['+', 'int', 'GMP|int']; } private function createType(string $type): Type @@ -123,11 +142,13 @@ private function createType(string $type): Type case 'float': return new FloatType(); case 'object': - return new ObjectType('object'); + return new ObjectWithoutClassType(); case 'stdClass': return new ObjectType('stdClass'); case 'GMP|int': return new UnionType([new ObjectType('GMP'), new IntegerType()]); + case 'int|stdClass': + return new UnionType([new IntegerType(), new ObjectType('stdClass')]); default: throw new InvalidArgumentException(sprintf('Unknown type: %s', $type)); } From 7b42b42af65e8cfea3140e733f28f0fecd452395 Mon Sep 17 00:00:00 2001 From: Eric Stern Date: Wed, 25 Mar 2026 17:36:23 -0700 Subject: [PATCH 7/8] Add nsrt tests for mutation testing coverage - Add GMP + string test to catch TrinaryLogicMutator on isNumericString - Add unary operator tests on object type to catch mutations in GmpUnaryOperatorTypeSpecifyingExtension Co-Authored-By: Claude Opus 4.5 --- tests/PHPStan/Analyser/nsrt/gmp-operators.php | 31 +++++++++++++++++++ 1 file changed, 31 insertions(+) diff --git a/tests/PHPStan/Analyser/nsrt/gmp-operators.php b/tests/PHPStan/Analyser/nsrt/gmp-operators.php index ae708b560e..f9897520fb 100644 --- a/tests/PHPStan/Analyser/nsrt/gmp-operators.php +++ b/tests/PHPStan/Analyser/nsrt/gmp-operators.php @@ -190,6 +190,15 @@ function gmpWithNumericString(\GMP $a, string $s): void // GMP functions accept numeric strings assertType('GMP', gmp_add($a, '123')); assertType('GMP', gmp_mul($a, '456')); + + // General string (not numeric-string) has isNumericString()=Maybe + // This catches TrinaryLogicMutator on line 53: isNumericString()->yes() → !isNumericString()->no() + // Without mutation: Maybe.yes()=false → ErrorType + // With mutation: !Maybe.no()=true → GMP (incorrect!) + /** @phpstan-ignore binaryOp.invalid */ + assertType('*ERROR*', $a + $s); + /** @phpstan-ignore binaryOp.invalid */ + assertType('*ERROR*', $s + $a); } /** @@ -204,3 +213,25 @@ function nonGmpObjectsDoNotGetGmpTreatment($obj, int $i): void /** @phpstan-ignore binaryOp.invalid */ assertType('*ERROR*', $i + $obj); } + +/** + * Tests for unary operators on non-GMP objects. + * This catches IsSuperTypeOfCalleeAndArgumentMutator and TrinaryLogicMutator + * on GmpUnaryOperatorTypeSpecifyingExtension line 27. + * + * When mutation swaps $gmpType->isSuperTypeOf($operand) to $operand->isSuperTypeOf($gmpType), + * `object` would incorrectly activate the extension and return GMP. + * + * @param object $obj + */ +function unaryOperatorsOnObjectShouldError($obj): void +{ + // Without mutation: extension doesn't activate (GMP not supertype of object) + // With mutation: extension activates (object IS supertype of GMP), returns GMP! + /** @phpstan-ignore unaryOp.invalid */ + assertType('*ERROR*', -$obj); + /** @phpstan-ignore unaryOp.invalid */ + assertType('*ERROR*', +$obj); + /** @phpstan-ignore unaryOp.invalid */ + assertType('*ERROR*', ~$obj); +} From 6cd7a3cbf08419d3eff4175fd30e117f04b8ee10 Mon Sep 17 00:00:00 2001 From: Eric Stern Date: Wed, 25 Mar 2026 18:45:27 -0700 Subject: [PATCH 8/8] Add string type test to catch isNumericString mutation General string has isNumericString()=Maybe, catching the TrinaryLogicMutator that changes .yes() to !.no() on line 53. Co-Authored-By: Claude Opus 4.5 --- .../Type/Php/GmpOperatorTypeSpecifyingExtensionTest.php | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/tests/PHPStan/Type/Php/GmpOperatorTypeSpecifyingExtensionTest.php b/tests/PHPStan/Type/Php/GmpOperatorTypeSpecifyingExtensionTest.php index ec4290848b..97c50184e0 100644 --- a/tests/PHPStan/Type/Php/GmpOperatorTypeSpecifyingExtensionTest.php +++ b/tests/PHPStan/Type/Php/GmpOperatorTypeSpecifyingExtensionTest.php @@ -10,6 +10,7 @@ use PHPStan\Type\IntegerType; use PHPStan\Type\ObjectType; use PHPStan\Type\ObjectWithoutClassType; +use PHPStan\Type\StringType; use PHPStan\Type\Type; use PHPStan\Type\UnionType; use PHPUnit\Framework\Attributes\DataProvider; @@ -109,6 +110,11 @@ public static function dataSpecifyTypeReturnsError(): iterable // When mutation changes .yes() to !.no(), isInteger() incorrectly returns true yield 'GMP + int|stdClass' => ['+', 'GMP', 'int|stdClass']; yield 'int|stdClass + GMP' => ['+', 'int|stdClass', 'GMP']; + + // string has isNumericString()=Maybe - catches line 53 TrinaryLogicMutator + // When mutation changes .yes() to !.no(), isNumericString() incorrectly returns true + yield 'GMP + string' => ['+', 'GMP', 'string']; + yield 'string + GMP' => ['+', 'string', 'GMP']; } #[DataProvider('dataSpecifyTypeReturnsGmp')] @@ -149,6 +155,8 @@ private function createType(string $type): Type return new UnionType([new ObjectType('GMP'), new IntegerType()]); case 'int|stdClass': return new UnionType([new IntegerType(), new ObjectType('stdClass')]); + case 'string': + return new StringType(); default: throw new InvalidArgumentException(sprintf('Unknown type: %s', $type)); }