Skip to content
Merged
74 changes: 74 additions & 0 deletions src/Type/Php/GmpOperatorTypeSpecifyingExtension.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
<?php declare(strict_types = 1);

namespace PHPStan\Type\Php;

use PHPStan\DependencyInjection\AutowiredService;
use PHPStan\Type\BooleanType;
use PHPStan\Type\IntegerRangeType;
use PHPStan\Type\NeverType;
use PHPStan\Type\ObjectType;
use PHPStan\Type\OperatorTypeSpecifyingExtension;
use PHPStan\Type\Type;
use function in_array;

#[AutowiredService]
final class GmpOperatorTypeSpecifyingExtension implements OperatorTypeSpecifyingExtension
{

public function isOperatorSupported(string $operatorSigil, Type $leftSide, Type $rightSide): bool
{
if ($leftSide instanceof NeverType || $rightSide instanceof NeverType) {
return false;
}

if (!in_array($operatorSigil, ['+', '-', '*', '/', '**', '%', '&', '|', '^', '<<', '>>', '<', '<=', '>', '>=', '==', '!=', '<=>'], true)) {
return false;
}

$gmpType = new ObjectType('GMP');
$leftIsGmp = $gmpType->isSuperTypeOf($leftSide)->yes();

Check warning on line 29 in src/Type/Php/GmpOperatorTypeSpecifyingExtension.php

View workflow job for this annotation

GitHub Actions / Mutation Testing (8.3, ubuntu-latest)

Escaped Mutant for Mutator "PHPStan\Infection\TrinaryLogicMutator": @@ @@ } $gmpType = new ObjectType('GMP'); - $leftIsGmp = $gmpType->isSuperTypeOf($leftSide)->yes(); + $leftIsGmp = !$gmpType->isSuperTypeOf($leftSide)->no(); $rightIsGmp = $gmpType->isSuperTypeOf($rightSide)->yes(); // At least one side must be GMP

Check warning on line 29 in src/Type/Php/GmpOperatorTypeSpecifyingExtension.php

View workflow job for this annotation

GitHub Actions / Mutation Testing (8.3, ubuntu-latest)

Escaped Mutant for Mutator "PHPStan\Infection\IsSuperTypeOfCalleeAndArgumentMutator": @@ @@ } $gmpType = new ObjectType('GMP'); - $leftIsGmp = $gmpType->isSuperTypeOf($leftSide)->yes(); + $leftIsGmp = $leftSide->isSuperTypeOf($gmpType)->yes(); $rightIsGmp = $gmpType->isSuperTypeOf($rightSide)->yes(); // At least one side must be GMP

Check warning on line 29 in src/Type/Php/GmpOperatorTypeSpecifyingExtension.php

View workflow job for this annotation

GitHub Actions / Mutation Testing (8.4, ubuntu-latest)

Escaped Mutant for Mutator "PHPStan\Infection\TrinaryLogicMutator": @@ @@ } $gmpType = new ObjectType('GMP'); - $leftIsGmp = $gmpType->isSuperTypeOf($leftSide)->yes(); + $leftIsGmp = !$gmpType->isSuperTypeOf($leftSide)->no(); $rightIsGmp = $gmpType->isSuperTypeOf($rightSide)->yes(); // At least one side must be GMP

Check warning on line 29 in src/Type/Php/GmpOperatorTypeSpecifyingExtension.php

View workflow job for this annotation

GitHub Actions / Mutation Testing (8.4, ubuntu-latest)

Escaped Mutant for Mutator "PHPStan\Infection\IsSuperTypeOfCalleeAndArgumentMutator": @@ @@ } $gmpType = new ObjectType('GMP'); - $leftIsGmp = $gmpType->isSuperTypeOf($leftSide)->yes(); + $leftIsGmp = $leftSide->isSuperTypeOf($gmpType)->yes(); $rightIsGmp = $gmpType->isSuperTypeOf($rightSide)->yes(); // At least one side must be GMP
$rightIsGmp = $gmpType->isSuperTypeOf($rightSide)->yes();

Check warning on line 30 in src/Type/Php/GmpOperatorTypeSpecifyingExtension.php

View workflow job for this annotation

GitHub Actions / Mutation Testing (8.3, ubuntu-latest)

Escaped Mutant for Mutator "PHPStan\Infection\TrinaryLogicMutator": @@ @@ $gmpType = new ObjectType('GMP'); $leftIsGmp = $gmpType->isSuperTypeOf($leftSide)->yes(); - $rightIsGmp = $gmpType->isSuperTypeOf($rightSide)->yes(); + $rightIsGmp = !$gmpType->isSuperTypeOf($rightSide)->no(); // At least one side must be GMP if (!$leftIsGmp && !$rightIsGmp) {

Check warning on line 30 in src/Type/Php/GmpOperatorTypeSpecifyingExtension.php

View workflow job for this annotation

GitHub Actions / Mutation Testing (8.3, ubuntu-latest)

Escaped Mutant for Mutator "PHPStan\Infection\IsSuperTypeOfCalleeAndArgumentMutator": @@ @@ $gmpType = new ObjectType('GMP'); $leftIsGmp = $gmpType->isSuperTypeOf($leftSide)->yes(); - $rightIsGmp = $gmpType->isSuperTypeOf($rightSide)->yes(); + $rightIsGmp = $rightSide->isSuperTypeOf($gmpType)->yes(); // At least one side must be GMP if (!$leftIsGmp && !$rightIsGmp) {

Check warning on line 30 in src/Type/Php/GmpOperatorTypeSpecifyingExtension.php

View workflow job for this annotation

GitHub Actions / Mutation Testing (8.4, ubuntu-latest)

Escaped Mutant for Mutator "PHPStan\Infection\TrinaryLogicMutator": @@ @@ $gmpType = new ObjectType('GMP'); $leftIsGmp = $gmpType->isSuperTypeOf($leftSide)->yes(); - $rightIsGmp = $gmpType->isSuperTypeOf($rightSide)->yes(); + $rightIsGmp = !$gmpType->isSuperTypeOf($rightSide)->no(); // At least one side must be GMP if (!$leftIsGmp && !$rightIsGmp) {

Check warning on line 30 in src/Type/Php/GmpOperatorTypeSpecifyingExtension.php

View workflow job for this annotation

GitHub Actions / Mutation Testing (8.4, ubuntu-latest)

Escaped Mutant for Mutator "PHPStan\Infection\IsSuperTypeOfCalleeAndArgumentMutator": @@ @@ $gmpType = new ObjectType('GMP'); $leftIsGmp = $gmpType->isSuperTypeOf($leftSide)->yes(); - $rightIsGmp = $gmpType->isSuperTypeOf($rightSide)->yes(); + $rightIsGmp = $rightSide->isSuperTypeOf($gmpType)->yes(); // At least one side must be GMP if (!$leftIsGmp && !$rightIsGmp) {

// 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()) {

Check warning on line 44 in src/Type/Php/GmpOperatorTypeSpecifyingExtension.php

View workflow job for this annotation

GitHub Actions / Mutation Testing (8.3, ubuntu-latest)

Escaped Mutant for Mutator "PHPStan\Infection\TrinaryLogicMutator": @@ @@ private function isGmpCompatible(Type $type, ObjectType $gmpType): bool { - if ($gmpType->isSuperTypeOf($type)->yes()) { + if (!$gmpType->isSuperTypeOf($type)->no()) { return true; } if ($type->isInteger()->yes()) {

Check warning on line 44 in src/Type/Php/GmpOperatorTypeSpecifyingExtension.php

View workflow job for this annotation

GitHub Actions / Mutation Testing (8.3, ubuntu-latest)

Escaped Mutant for Mutator "PHPStan\Infection\IsSuperTypeOfCalleeAndArgumentMutator": @@ @@ private function isGmpCompatible(Type $type, ObjectType $gmpType): bool { - if ($gmpType->isSuperTypeOf($type)->yes()) { + if ($type->isSuperTypeOf($gmpType)->yes()) { return true; } if ($type->isInteger()->yes()) {

Check warning on line 44 in src/Type/Php/GmpOperatorTypeSpecifyingExtension.php

View workflow job for this annotation

GitHub Actions / Mutation Testing (8.4, ubuntu-latest)

Escaped Mutant for Mutator "PHPStan\Infection\TrinaryLogicMutator": @@ @@ private function isGmpCompatible(Type $type, ObjectType $gmpType): bool { - if ($gmpType->isSuperTypeOf($type)->yes()) { + if (!$gmpType->isSuperTypeOf($type)->no()) { return true; } if ($type->isInteger()->yes()) {

Check warning on line 44 in src/Type/Php/GmpOperatorTypeSpecifyingExtension.php

View workflow job for this annotation

GitHub Actions / Mutation Testing (8.4, ubuntu-latest)

Escaped Mutant for Mutator "PHPStan\Infection\IsSuperTypeOfCalleeAndArgumentMutator": @@ @@ private function isGmpCompatible(Type $type, ObjectType $gmpType): bool { - if ($gmpType->isSuperTypeOf($type)->yes()) { + if ($type->isSuperTypeOf($gmpType)->yes()) { return true; } if ($type->isInteger()->yes()) {
return true;
}
if ($type->isInteger()->yes()) {

Check warning on line 47 in src/Type/Php/GmpOperatorTypeSpecifyingExtension.php

View workflow job for this annotation

GitHub Actions / Mutation Testing (8.3, ubuntu-latest)

Escaped Mutant for Mutator "PHPStan\Infection\TrinaryLogicMutator": @@ @@ if ($gmpType->isSuperTypeOf($type)->yes()) { return true; } - if ($type->isInteger()->yes()) { + if (!$type->isInteger()->no()) { return true; } if ($type->isNumericString()->yes()) {

Check warning on line 47 in src/Type/Php/GmpOperatorTypeSpecifyingExtension.php

View workflow job for this annotation

GitHub Actions / Mutation Testing (8.4, ubuntu-latest)

Escaped Mutant for Mutator "PHPStan\Infection\TrinaryLogicMutator": @@ @@ if ($gmpType->isSuperTypeOf($type)->yes()) { return true; } - if ($type->isInteger()->yes()) { + if (!$type->isInteger()->no()) { return true; } if ($type->isNumericString()->yes()) {
return true;
}
if ($type->isNumericString()->yes()) {

Check warning on line 50 in src/Type/Php/GmpOperatorTypeSpecifyingExtension.php

View workflow job for this annotation

GitHub Actions / Mutation Testing (8.3, ubuntu-latest)

Escaped Mutant for Mutator "PHPStan\Infection\TrinaryLogicMutator": @@ @@ if ($type->isInteger()->yes()) { return true; } - if ($type->isNumericString()->yes()) { + if (!$type->isNumericString()->no()) { return true; } return false;

Check warning on line 50 in src/Type/Php/GmpOperatorTypeSpecifyingExtension.php

View workflow job for this annotation

GitHub Actions / Mutation Testing (8.4, ubuntu-latest)

Escaped Mutant for Mutator "PHPStan\Infection\TrinaryLogicMutator": @@ @@ if ($type->isInteger()->yes()) { return true; } - if ($type->isNumericString()->yes()) { + if (!$type->isNumericString()->no()) { return true; } return false;
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;
}

}
35 changes: 35 additions & 0 deletions src/Type/Php/GmpUnaryOperatorTypeSpecifyingExtension.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
<?php declare(strict_types = 1);

namespace PHPStan\Type\Php;

use PHPStan\DependencyInjection\AutowiredService;
use PHPStan\Type\NeverType;
use PHPStan\Type\ObjectType;
use PHPStan\Type\Type;
use PHPStan\Type\UnaryOperatorTypeSpecifyingExtension;
use function in_array;

#[AutowiredService]
final class GmpUnaryOperatorTypeSpecifyingExtension implements UnaryOperatorTypeSpecifyingExtension
{

public function isOperatorSupported(string $operatorSigil, Type $operand): bool
{
if ($operand instanceof NeverType) {
return false;
}

if (!in_array($operatorSigil, ['-', '+', '~'], true)) {
return false;
}

$gmpType = new ObjectType('GMP');
return $gmpType->isSuperTypeOf($operand)->yes();

Check warning on line 27 in src/Type/Php/GmpUnaryOperatorTypeSpecifyingExtension.php

View workflow job for this annotation

GitHub Actions / Mutation Testing (8.3, ubuntu-latest)

Escaped Mutant for Mutator "PHPStan\Infection\TrinaryLogicMutator": @@ @@ } $gmpType = new ObjectType('GMP'); - return $gmpType->isSuperTypeOf($operand)->yes(); + return !$gmpType->isSuperTypeOf($operand)->no(); } public function specifyType(string $operatorSigil, Type $operand): Type

Check warning on line 27 in src/Type/Php/GmpUnaryOperatorTypeSpecifyingExtension.php

View workflow job for this annotation

GitHub Actions / Mutation Testing (8.3, ubuntu-latest)

Escaped Mutant for Mutator "PHPStan\Infection\IsSuperTypeOfCalleeAndArgumentMutator": @@ @@ } $gmpType = new ObjectType('GMP'); - return $gmpType->isSuperTypeOf($operand)->yes(); + return $operand->isSuperTypeOf($gmpType)->yes(); } public function specifyType(string $operatorSigil, Type $operand): Type

Check warning on line 27 in src/Type/Php/GmpUnaryOperatorTypeSpecifyingExtension.php

View workflow job for this annotation

GitHub Actions / Mutation Testing (8.4, ubuntu-latest)

Escaped Mutant for Mutator "PHPStan\Infection\TrinaryLogicMutator": @@ @@ } $gmpType = new ObjectType('GMP'); - return $gmpType->isSuperTypeOf($operand)->yes(); + return !$gmpType->isSuperTypeOf($operand)->no(); } public function specifyType(string $operatorSigil, Type $operand): Type

Check warning on line 27 in src/Type/Php/GmpUnaryOperatorTypeSpecifyingExtension.php

View workflow job for this annotation

GitHub Actions / Mutation Testing (8.4, ubuntu-latest)

Escaped Mutant for Mutator "PHPStan\Infection\IsSuperTypeOfCalleeAndArgumentMutator": @@ @@ } $gmpType = new ObjectType('GMP'); - return $gmpType->isSuperTypeOf($operand)->yes(); + return $operand->isSuperTypeOf($gmpType)->yes(); } public function specifyType(string $operatorSigil, Type $operand): Type
Copy link
Copy Markdown
Contributor

@staabm staabm Mar 27, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

the

- return $gmpType->isSuperTypeOf($operand)->yes(); 
+ return !$gmpType->isSuperTypeOf($operand)->no();

mutation can be killed by testing with a "maybe GMP operand", like

function maybeGmp(GMP $a, GMP $b) {
	if (rand(0,1)) {
		$a = 5;
	}
	assertType(..., $a + $b));
}

same for the other extension

}

public function specifyType(string $operatorSigil, Type $operand): Type
{
return new ObjectType('GMP');
}

}
193 changes: 193 additions & 0 deletions tests/PHPStan/Analyser/nsrt/gmp-operators.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,193 @@
<?php

namespace GmpOperatorsTest;

use function PHPStan\Testing\assertType;

// =============================================================================
// Operator overloads
// =============================================================================

function gmpArithmeticOperators(\GMP $a, \GMP $b): void
{
assertType('GMP', $a + $b);
assertType('GMP', $a - $b);
assertType('GMP', $a * $b);
assertType('GMP', $a / $b);
assertType('GMP', $a % $b);
assertType('GMP', $a ** $b);
assertType('GMP', -$a);
}

function gmpWithIntOperators(\GMP $a, int $i): void
{
// GMP on left
assertType('GMP', $a + $i);
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);
assertType('GMP', $i / $a);
assertType('GMP', $i % $a);
// Note: $i ** $a is not supported by GMP - exponent must be int
}

function gmpBitwiseOperators(\GMP $a, \GMP $b, int $i): void
{
// GMP bitwise with GMP
assertType('GMP', $a & $b);
assertType('GMP', $a | $b);
assertType('GMP', $a ^ $b);
assertType('GMP', ~$a);
assertType('GMP', $a << $b);
assertType('GMP', $a >> $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'));
}
Loading