Skip to content

Commit bfe7fd3

Browse files
Firehedclaude
andauthored
Implement GMP operator type specifying extension (#5223)
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
1 parent a3bf054 commit bfe7fd3

File tree

5 files changed

+504
-4
lines changed

5 files changed

+504
-4
lines changed
Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,62 @@
1+
<?php declare(strict_types = 1);
2+
3+
namespace PHPStan\Type\Php;
4+
5+
use PHPStan\DependencyInjection\AutowiredService;
6+
use PHPStan\Type\BooleanType;
7+
use PHPStan\Type\ErrorType;
8+
use PHPStan\Type\IntegerRangeType;
9+
use PHPStan\Type\NeverType;
10+
use PHPStan\Type\ObjectType;
11+
use PHPStan\Type\OperatorTypeSpecifyingExtension;
12+
use PHPStan\Type\Type;
13+
use function in_array;
14+
15+
#[AutowiredService]
16+
final class GmpOperatorTypeSpecifyingExtension implements OperatorTypeSpecifyingExtension
17+
{
18+
19+
public function isOperatorSupported(string $operatorSigil, Type $leftSide, Type $rightSide): bool
20+
{
21+
if ($leftSide instanceof NeverType || $rightSide instanceof NeverType) {
22+
return false;
23+
}
24+
25+
$gmpType = new ObjectType('GMP');
26+
27+
return in_array($operatorSigil, ['+', '-', '*', '/', '**', '%', '&', '|', '^', '<<', '>>', '<', '<=', '>', '>=', '==', '!=', '<=>'], true)
28+
&& (
29+
$gmpType->isSuperTypeOf($leftSide)->yes()
30+
|| $gmpType->isSuperTypeOf($rightSide)->yes()
31+
);
32+
}
33+
34+
public function specifyType(string $operatorSigil, Type $leftSide, Type $rightSide): Type
35+
{
36+
$gmpType = new ObjectType('GMP');
37+
$otherSide = $gmpType->isSuperTypeOf($leftSide)->yes()
38+
? $rightSide
39+
: $leftSide;
40+
41+
// Comparison operators return bool or int (for spaceship)
42+
if (in_array($operatorSigil, ['<', '<=', '>', '>=', '==', '!='], true)) {
43+
return new BooleanType();
44+
}
45+
46+
if ($operatorSigil === '<=>') {
47+
return IntegerRangeType::fromInterval(-1, 1);
48+
}
49+
50+
// GMP can operate with: GMP, int, or numeric-string
51+
if (
52+
$otherSide->isInteger()->yes()
53+
|| $otherSide->isNumericString()->yes()
54+
|| $gmpType->isSuperTypeOf($otherSide)->yes()
55+
) {
56+
return $gmpType;
57+
}
58+
59+
return new ErrorType();
60+
}
61+
62+
}
Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
<?php declare(strict_types = 1);
2+
3+
namespace PHPStan\Type\Php;
4+
5+
use PHPStan\DependencyInjection\AutowiredService;
6+
use PHPStan\Type\NeverType;
7+
use PHPStan\Type\ObjectType;
8+
use PHPStan\Type\Type;
9+
use PHPStan\Type\UnaryOperatorTypeSpecifyingExtension;
10+
use function in_array;
11+
12+
#[AutowiredService]
13+
final class GmpUnaryOperatorTypeSpecifyingExtension implements UnaryOperatorTypeSpecifyingExtension
14+
{
15+
16+
public function isOperatorSupported(string $operatorSigil, Type $operand): bool
17+
{
18+
if ($operand instanceof NeverType) {
19+
return false;
20+
}
21+
22+
if (!in_array($operatorSigil, ['-', '+', '~'], true)) {
23+
return false;
24+
}
25+
26+
$gmpType = new ObjectType('GMP');
27+
return $gmpType->isSuperTypeOf($operand)->yes();
28+
}
29+
30+
public function specifyType(string $operatorSigil, Type $operand): Type
31+
{
32+
return new ObjectType('GMP');
33+
}
34+
35+
}
Lines changed: 237 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,237 @@
1+
<?php
2+
3+
namespace GmpOperatorsTest;
4+
5+
use function PHPStan\Testing\assertType;
6+
7+
// =============================================================================
8+
// Operator overloads
9+
// =============================================================================
10+
11+
function gmpArithmeticOperators(\GMP $a, \GMP $b): void
12+
{
13+
assertType('GMP', $a + $b);
14+
assertType('GMP', $a - $b);
15+
assertType('GMP', $a * $b);
16+
assertType('GMP', $a / $b);
17+
assertType('GMP', $a % $b);
18+
assertType('GMP', $a ** $b);
19+
assertType('GMP', -$a);
20+
}
21+
22+
function gmpWithIntOperators(\GMP $a, int $i): void
23+
{
24+
// GMP on left
25+
assertType('GMP', $a + $i);
26+
assertType('GMP', $a - $i);
27+
assertType('GMP', $a * $i);
28+
assertType('GMP', $a / $i);
29+
assertType('GMP', $a % $i);
30+
assertType('GMP', $a ** $i);
31+
32+
// int on left (GMP on right)
33+
assertType('GMP', $i + $a);
34+
assertType('GMP', $i - $a);
35+
assertType('GMP', $i * $a);
36+
assertType('GMP', $i / $a);
37+
assertType('GMP', $i % $a);
38+
// Note: $i ** $a is not supported by GMP - exponent must be int
39+
}
40+
41+
function gmpBitwiseOperators(\GMP $a, \GMP $b, int $i): void
42+
{
43+
// GMP bitwise with GMP
44+
assertType('GMP', $a & $b);
45+
assertType('GMP', $a | $b);
46+
assertType('GMP', $a ^ $b);
47+
assertType('GMP', ~$a);
48+
assertType('GMP', $a << $b);
49+
assertType('GMP', $a >> $b);
50+
51+
// GMP on left, int on right
52+
assertType('GMP', $a & $i);
53+
assertType('GMP', $a | $i);
54+
assertType('GMP', $a ^ $i);
55+
assertType('GMP', $a << $i);
56+
assertType('GMP', $a >> $i);
57+
58+
// int on left, GMP on right
59+
assertType('GMP', $i & $a);
60+
assertType('GMP', $i | $a);
61+
assertType('GMP', $i ^ $a);
62+
}
63+
64+
function gmpComparisonOperators(\GMP $a, \GMP $b, int $i): void
65+
{
66+
// GMP compared with GMP
67+
assertType('bool', $a < $b);
68+
assertType('bool', $a <= $b);
69+
assertType('bool', $a > $b);
70+
assertType('bool', $a >= $b);
71+
assertType('bool', $a == $b);
72+
assertType('bool', $a != $b);
73+
assertType('int<-1, 1>', $a <=> $b);
74+
75+
// GMP on left, int on right
76+
assertType('bool', $a < $i);
77+
assertType('bool', $a <= $i);
78+
assertType('bool', $a > $i);
79+
assertType('bool', $a >= $i);
80+
assertType('bool', $a == $i);
81+
assertType('bool', $a != $i);
82+
assertType('int<-1, 1>', $a <=> $i);
83+
84+
// int on left, GMP on right
85+
assertType('bool', $i < $a);
86+
assertType('bool', $i <= $a);
87+
assertType('bool', $i > $a);
88+
assertType('bool', $i >= $a);
89+
assertType('bool', $i == $a);
90+
assertType('bool', $i != $a);
91+
assertType('int<-1, 1>', $i <=> $a);
92+
}
93+
94+
function gmpAssignmentOperators(\GMP $a, int $i): void
95+
{
96+
$x = $a;
97+
$x += $i;
98+
assertType('GMP', $x);
99+
100+
$y = $a;
101+
$y -= $i;
102+
assertType('GMP', $y);
103+
104+
$z = $a;
105+
$z *= $i;
106+
assertType('GMP', $z);
107+
}
108+
109+
// =============================================================================
110+
// gmp_* functions (corresponding to operator overloads)
111+
// =============================================================================
112+
113+
function gmpArithmeticFunctions(\GMP $a, \GMP $b, int $i): void
114+
{
115+
// gmp_add corresponds to +
116+
assertType('GMP', gmp_add($a, $b));
117+
assertType('GMP', gmp_add($a, $i));
118+
assertType('GMP', gmp_add($i, $a));
119+
120+
// gmp_sub corresponds to -
121+
assertType('GMP', gmp_sub($a, $b));
122+
assertType('GMP', gmp_sub($a, $i));
123+
assertType('GMP', gmp_sub($i, $a));
124+
125+
// gmp_mul corresponds to *
126+
assertType('GMP', gmp_mul($a, $b));
127+
assertType('GMP', gmp_mul($a, $i));
128+
assertType('GMP', gmp_mul($i, $a));
129+
130+
// gmp_div_q corresponds to /
131+
assertType('GMP', gmp_div_q($a, $b));
132+
assertType('GMP', gmp_div_q($a, $i));
133+
134+
// gmp_div is alias of gmp_div_q
135+
assertType('GMP', gmp_div($a, $b));
136+
137+
// gmp_mod corresponds to %
138+
assertType('GMP', gmp_mod($a, $b));
139+
assertType('GMP', gmp_mod($a, $i));
140+
141+
// gmp_pow corresponds to **
142+
assertType('GMP', gmp_pow($a, 2));
143+
assertType('GMP', gmp_pow($a, $i));
144+
145+
// gmp_neg corresponds to unary -
146+
assertType('GMP', gmp_neg($a));
147+
148+
// gmp_abs (no direct operator)
149+
assertType('GMP', gmp_abs($a));
150+
}
151+
152+
function gmpBitwiseFunctions(\GMP $a, \GMP $b): void
153+
{
154+
// gmp_and corresponds to &
155+
assertType('GMP', gmp_and($a, $b));
156+
157+
// gmp_or corresponds to |
158+
assertType('GMP', gmp_or($a, $b));
159+
160+
// gmp_xor corresponds to ^
161+
assertType('GMP', gmp_xor($a, $b));
162+
163+
// gmp_com corresponds to ~
164+
assertType('GMP', gmp_com($a));
165+
}
166+
167+
function gmpComparisonFunctions(\GMP $a, \GMP $b, int $i): void
168+
{
169+
// gmp_cmp returns -1, 0, or 1 in practice, but stubs say int
170+
// TODO: Could be improved to int<-1, 1> like the <=> operator
171+
assertType('int', gmp_cmp($a, $b));
172+
assertType('int', gmp_cmp($a, $i));
173+
}
174+
175+
function gmpFromInit(): void
176+
{
177+
$x = gmp_init('1');
178+
assertType('GMP', $x);
179+
180+
// Operator with gmp_init result
181+
$y = $x * 2;
182+
assertType('GMP', $y);
183+
184+
$z = $x + gmp_init('5');
185+
assertType('GMP', $z);
186+
}
187+
188+
function gmpWithNumericString(\GMP $a, string $s): void
189+
{
190+
// GMP functions accept numeric strings
191+
assertType('GMP', gmp_add($a, '123'));
192+
assertType('GMP', gmp_mul($a, '456'));
193+
194+
// General string (not numeric-string) has isNumericString()=Maybe
195+
// This catches TrinaryLogicMutator on line 53: isNumericString()->yes() → !isNumericString()->no()
196+
// Without mutation: Maybe.yes()=false → ErrorType
197+
// With mutation: !Maybe.no()=true → GMP (incorrect!)
198+
/** @phpstan-ignore binaryOp.invalid */
199+
assertType('*ERROR*', $a + $s);
200+
/** @phpstan-ignore binaryOp.invalid */
201+
assertType('*ERROR*', $s + $a);
202+
}
203+
204+
/**
205+
* @param object $obj
206+
*/
207+
function nonGmpObjectsDoNotGetGmpTreatment($obj, int $i): void
208+
{
209+
// Generic object should NOT be treated as GMP - the extension should not activate
210+
// (object is a supertype of GMP, but GMP is not a supertype of object)
211+
/** @phpstan-ignore binaryOp.invalid */
212+
assertType('*ERROR*', $obj + $i);
213+
/** @phpstan-ignore binaryOp.invalid */
214+
assertType('*ERROR*', $i + $obj);
215+
}
216+
217+
/**
218+
* Tests for unary operators on non-GMP objects.
219+
* This catches IsSuperTypeOfCalleeAndArgumentMutator and TrinaryLogicMutator
220+
* on GmpUnaryOperatorTypeSpecifyingExtension line 27.
221+
*
222+
* When mutation swaps $gmpType->isSuperTypeOf($operand) to $operand->isSuperTypeOf($gmpType),
223+
* `object` would incorrectly activate the extension and return GMP.
224+
*
225+
* @param object $obj
226+
*/
227+
function unaryOperatorsOnObjectShouldError($obj): void
228+
{
229+
// Without mutation: extension doesn't activate (GMP not supertype of object)
230+
// With mutation: extension activates (object IS supertype of GMP), returns GMP!
231+
/** @phpstan-ignore unaryOp.invalid */
232+
assertType('*ERROR*', -$obj);
233+
/** @phpstan-ignore unaryOp.invalid */
234+
assertType('*ERROR*', +$obj);
235+
/** @phpstan-ignore unaryOp.invalid */
236+
assertType('*ERROR*', ~$obj);
237+
}

tests/PHPStan/Analyser/nsrt/pow.php

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -20,11 +20,12 @@ function (\GMP $a, \GMP $b): void {
2020
};
2121

2222
function (\stdClass $a, \GMP $b): void {
23-
assertType('GMP|stdClass', pow($a, $b));
24-
assertType('GMP|stdClass', $a ** $b);
23+
// stdClass is not a valid GMP operand, these should error
24+
assertType('*ERROR*', pow($a, $b));
25+
assertType('*ERROR*', $a ** $b);
2526

26-
assertType('GMP|stdClass', pow($b, $a));
27-
assertType('GMP|stdClass', $b ** $a);
27+
assertType('*ERROR*', pow($b, $a));
28+
assertType('*ERROR*', $b ** $a);
2829
};
2930

3031
function (): void {

0 commit comments

Comments
 (0)