Skip to content

Commit 0a655aa

Browse files
phpstan-botclaude
andcommitted
Narrow division to int when modulo is known to be zero
Instead of making all integer range divisions return benevolent unions, detect when the scope knows that `$a % $b === 0` and narrow `$a / $b` to just the integer type (removing float from the union). This is more precise: it only removes float when mathematically guaranteed, rather than broadly making all integer range divisions benevolent. The check works by constructing a synthetic modulo expression with the same operands and querying the scope for its type. When a condition like `0 === ($a % $b)` has narrowed the modulo to ConstantIntegerType(0), the division result is guaranteed to be int. Reverts the BenevolentUnionType propagation changes from the previous commit since the modulo narrowing is the correct fix. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
1 parent 7fab77e commit 0a655aa

5 files changed

Lines changed: 57 additions & 48 deletions

File tree

src/Reflection/InitializerExprTypeResolver.php

Lines changed: 25 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -1260,7 +1260,16 @@ public function getDivType(Expr $left, Expr $right, callable $getTypeCallback):
12601260
$leftType = $getTypeCallback($left);
12611261
$rightType = $getTypeCallback($right);
12621262

1263-
return $this->getDivTypeFromTypes($left, $right, $leftType, $rightType);
1263+
$result = $this->getDivTypeFromTypes($left, $right, $leftType, $rightType);
1264+
1265+
if ($leftType->isInteger()->yes() && $rightType->isInteger()->yes()) {
1266+
$modType = $getTypeCallback(new BinaryOp\Mod($left, $right));
1267+
if ($modType instanceof ConstantIntegerType && $modType->getValue() === 0) {
1268+
return TypeCombinator::remove($result, new FloatType());
1269+
}
1270+
}
1271+
1272+
return $result;
12641273
}
12651274

12661275
public function getDivTypeFromTypes(Expr $left, Expr $right, Type $leftType, Type $rightType): Type
@@ -2094,23 +2103,18 @@ private function resolveCommonMath(Expr\BinaryOp $expr, Type $leftType, Type $ri
20942103
);
20952104
} elseif ($leftNumberType instanceof UnionType) {
20962105
$unionParts = [];
2097-
$hasBenevolentPart = false;
20982106

20992107
foreach ($leftNumberType->getTypes() as $type) {
21002108
$numberType = $type->toNumber();
21012109
if ($numberType instanceof IntegerRangeType || $numberType instanceof ConstantIntegerType) {
2102-
$part = $this->integerRangeMath($numberType, $expr, $rightNumberType);
2103-
if ($part instanceof BenevolentUnionType) {
2104-
$hasBenevolentPart = true;
2105-
}
2106-
$unionParts[] = $part;
2110+
$unionParts[] = $this->integerRangeMath($numberType, $expr, $rightNumberType);
21072111
} else {
21082112
$unionParts[] = $numberType;
21092113
}
21102114
}
21112115

21122116
$union = TypeCombinator::union(...$unionParts);
2113-
if ($leftNumberType instanceof BenevolentUnionType || $hasBenevolentPart) {
2117+
if ($leftNumberType instanceof BenevolentUnionType) {
21142118
return TypeUtils::toBenevolentUnion($union)->toNumber();
21152119
}
21162120

@@ -2179,23 +2183,18 @@ private function integerRangeMath(Type $range, BinaryOp $node, Type $operand): T
21792183
if ($operand instanceof UnionType) {
21802184

21812185
$unionParts = [];
2182-
$hasBenevolentPart = false;
21832186

21842187
foreach ($operand->getTypes() as $type) {
21852188
$numberType = $type->toNumber();
21862189
if ($numberType instanceof IntegerRangeType || $numberType instanceof ConstantIntegerType) {
2187-
$part = $this->integerRangeMath($range, $node, $numberType);
2188-
if ($part instanceof BenevolentUnionType) {
2189-
$hasBenevolentPart = true;
2190-
}
2191-
$unionParts[] = $part;
2190+
$unionParts[] = $this->integerRangeMath($range, $node, $numberType);
21922191
} else {
21932192
$unionParts[] = $type->toNumber();
21942193
}
21952194
}
21962195

21972196
$union = TypeCombinator::union(...$unionParts);
2198-
if ($operand instanceof BenevolentUnionType || $hasBenevolentPart) {
2197+
if ($operand instanceof BenevolentUnionType) {
21992198
return TypeUtils::toBenevolentUnion($union)->toNumber();
22002199
}
22012200

@@ -2315,7 +2314,11 @@ private function integerRangeMath(Type $range, BinaryOp $node, Type $operand): T
23152314
$this->integerRangeMath($range, $node, $positiveOperand),
23162315
)->toNumber();
23172316

2318-
return TypeUtils::toBenevolentUnion($result);
2317+
if ($result->equals(new UnionType([new IntegerType(), new FloatType()]))) {
2318+
return new BenevolentUnionType([new IntegerType(), new FloatType()]);
2319+
}
2320+
2321+
return $result;
23192322
}
23202323
if (
23212324
($rangeMin < 0 || $rangeMin === null)
@@ -2331,7 +2334,11 @@ private function integerRangeMath(Type $range, BinaryOp $node, Type $operand): T
23312334
$this->integerRangeMath($positiveRange, $node, $operand),
23322335
)->toNumber();
23332336

2334-
return TypeUtils::toBenevolentUnion($result);
2337+
if ($result->equals(new UnionType([new IntegerType(), new FloatType()]))) {
2338+
return new BenevolentUnionType([new IntegerType(), new FloatType()]);
2339+
}
2340+
2341+
return $result;
23352342
}
23362343

23372344
$rangeMinSign = ($rangeMin ?? -INF) <=> 0;
@@ -2375,7 +2382,7 @@ private function integerRangeMath(Type $range, BinaryOp $node, Type $operand): T
23752382
return new BenevolentUnionType([new IntegerType(), new FloatType()]);
23762383
}
23772384

2378-
return TypeUtils::toBenevolentUnion(TypeCombinator::union(IntegerRangeType::fromInterval($min, $max), new FloatType()));
2385+
return TypeCombinator::union(IntegerRangeType::fromInterval($min, $max), new FloatType());
23792386
} elseif ($node instanceof Expr\BinaryOp\ShiftLeft) {
23802387
if (!$operand instanceof ConstantIntegerType) {
23812388
return new IntegerType();

tests/PHPStan/Analyser/nsrt/bug-9724.php

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -11,14 +11,16 @@ class HelloWorld
1111
public function thisWorks(?int $limit, int $offset = 0): void
1212
{
1313
if ($limit && 0 === ($offset % $limit)) {
14-
assertType('(float|int)', ($offset / $limit) + 1);
14+
assertType('int', $offset / $limit);
15+
assertType('int', ($offset / $limit) + 1);
1516
}
1617
}
1718

1819
public function thisDoesntWork(?int $limit, int $offset = 0): void
1920
{
2021
if ($limit && $offset && (0 === ($offset % $limit))) {
21-
assertType('(float|int<min, 0>|int<2, max>)', ($offset / $limit) + 1);
22+
assertType('int<min, -1>|int<1, max>', $offset / $limit);
23+
assertType('int<min, 0>|int<2, max>', ($offset / $limit) + 1);
2224
}
2325
}
2426

@@ -27,7 +29,7 @@ public function withRange(int $limit, int $offset, int $offsetRange): void
2729
{
2830
if ($limit) {
2931
assertType('(float|int)', $offset / $limit);
30-
assertType('(float|int<-2, 2>)', $offsetRange / $limit);
32+
assertType('float|int<-2, 2>', $offsetRange / $limit);
3133
}
3234
}
3335

tests/PHPStan/Analyser/nsrt/div-by-zero.php

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -13,9 +13,9 @@ class Foo
1313
*/
1414
public function doFoo(int $range1, int $range2, int $int): void
1515
{
16-
assertType('(float|int<1, 5>)', 5 / $range1);
17-
assertType('(float|int<-5, -1>)', 5 / $range2);
18-
assertType('(float|int<min, 0>)', $range1 / $range2);
16+
assertType('float|int<1, 5>', 5 / $range1);
17+
assertType('float|int<-5, -1>', 5 / $range2);
18+
assertType('float|int<min, 0>', $range1 / $range2);
1919
assertType('(float|int)', 5 / $int);
2020

2121
assertType('*ERROR*', 5 / 0);

tests/PHPStan/Analyser/nsrt/integer-range-types.php

Lines changed: 20 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -218,70 +218,70 @@ public function math($i, $j, $z, $pi, $r1, $r2, $r3, $rMin, $rMax, $x, $y) {
218218
assertType('int<2, 13>', $r1 + $j);
219219
assertType('int<-2, 9>', $r1 - $j);
220220
assertType('int<1, 30>', $r1 * $j);
221-
assertType('(float|int<1, 10>)', $r1 / $j);
221+
assertType('float|int<1, 10>', $r1 / $j);
222222
assertType('int<min, 15>', $rMin * $j);
223223
assertType('int<5, max>', $rMax * $j);
224224

225225
assertType('int<2, 13>', $j + $r1);
226226
assertType('int<-9, 2>', $j - $r1);
227227
assertType('int<1, 30>', $j * $r1);
228-
assertType('(float|int<1, 3>)', $j / $r1);
228+
assertType('float|int<1, 3>', $j / $r1);
229229
assertType('int<min, 15>', $j * $rMin);
230230
assertType('int<5, max>', $j * $rMax);
231231

232232
assertType('int<-19, -10>|int<2, 13>', $r1 + $z);
233233
assertType('int<-2, 9>|int<21, 30>', $r1 - $z);
234234
assertType('int<-200, -20>|int<1, 30>', $r1 * $z);
235-
assertType('(float|int<1, 10>)', $r1 / $z);
235+
assertType('float|int<1, 10>', $r1 / $z);
236236
assertType('int', $rMin * $z);
237237
assertType('int<min, -100>|int<5, max>', $rMax * $z);
238238

239239
assertType('int<2, max>', $pi + 1);
240240
assertType('int<-1, max>', $pi - 2);
241241
assertType('int<2, max>', $pi * 2);
242-
assertType('(float|int<1, max>)', $pi / 2);
242+
assertType('float|int<1, max>', $pi / 2);
243243
assertType('int<2, max>', 1 + $pi);
244244
assertType('int<min, 2>', 2 - $pi);
245245
assertType('int<2, max>', 2 * $pi);
246-
assertType('(float|int<1, 2>)', 2 / $pi);
246+
assertType('float|int<1, 2>', 2 / $pi);
247247

248248
assertType('int<5, 14>', $r1 + 4);
249249
assertType('int<-3, 6>', $r1 - 4);
250250
assertType('int<4, 40>', $r1 * 4);
251-
assertType('(float|int<1, 2>)', $r1 / 4);
251+
assertType('float|int<1, 2>', $r1 / 4);
252252
assertType('int<9, max>', $rMax + 4);
253253
assertType('int<1, max>', $rMax - 4);
254254
assertType('int<20, max>', $rMax * 4);
255-
assertType('(float|int<2, max>)', $rMax / 4);
255+
assertType('float|int<2, max>', $rMax / 4);
256256

257257
assertType('int<6, 20>', $r1 + $r2);
258258
assertType('int<-9, 5>', $r1 - $r2);
259259
assertType('int<5, 100>', $r1 * $r2);
260-
assertType('(float|int<1, 2>)', $r1 / $r2);
260+
assertType('float|int<1, 2>', $r1 / $r2);
261261

262262
assertType('int<-99, 19>', $r1 - $r3);
263263

264264
assertType('int<min, 15>', $r1 + $rMin);
265265
assertType('int<-4, max>', $r1 - $rMin);
266266
assertType('int<min, 50>', $r1 * $rMin);
267-
assertType('(float|int<-10, -1>|int<1, 10>)', $r1 / $rMin);
267+
assertType('float|int<-10, -1>|int<1, 10>', $r1 / $rMin);
268268
assertType('int<min, 15>', $rMin + $r1);
269269
assertType('int<min, 4>', $rMin - $r1);
270270
assertType('int<min, 50>', $rMin * $r1);
271-
assertType('(float|int<min, 5>)', $rMin / $r1);
271+
assertType('float|int<min, 5>', $rMin / $r1);
272272

273273
assertType('int<6, max>', $r1 + $rMax);
274274
assertType('int', $r1 - $rMax);
275275
assertType('int<5, max>', $r1 * $rMax);
276-
assertType('(float|int<1, 2>)', $r1 / $rMax);
276+
assertType('float|int<1, 2>', $r1 / $rMax);
277277
assertType('int<6, max>', $rMax + $r1);
278278
assertType('int<-5, max>', $rMax - $r1);
279279
assertType('int<5, max>', $rMax * $r1);
280-
assertType('(float|int<1, max>)', $rMax / $r1);
280+
assertType('float|int<1, max>', $rMax / $r1);
281281

282282
assertType('5|10|15|20|30', $x / $y);
283283

284-
assertType('(float|int<1, max>)', $rMax / $rMax);
284+
assertType('float|int<1, max>', $rMax / $rMax);
285285
assertType('(float|int)', $rMin / $rMin);
286286
}
287287

@@ -292,9 +292,9 @@ public function math($i, $j, $z, $pi, $r1, $r2, $r3, $rMin, $rMax, $x, $y) {
292292
* @param int<2, 4> $d
293293
*/
294294
function divisionLoosesInformation(int $a, int $b, int $c, int $d): void {
295-
assertType('(float|int<0, max>)', $a / $b);
296-
assertType('(float|int<8, 16>)', $c / 2);
297-
assertType('(float|int<4, 16>)', $c / $d);
295+
assertType('float|int<0, max>', $a / $b);
296+
assertType('float|int<8, 16>', $c / 2);
297+
assertType('float|int<4, 16>', $c / $d);
298298
}
299299

300300
/**
@@ -311,11 +311,11 @@ public function maximaInversion($rMin, $rMax) {
311311
assertType('int<-5, max>', $rMin * -1);
312312
assertType('int<min, -10>', $rMax * -2);
313313

314-
assertType('(-1|1|float)', -1 / $rMin);
314+
assertType('-1|1|float', -1 / $rMin);
315315
assertType('float', -2 / $rMax);
316316

317-
assertType('(float|int<-5, max>)', $rMin / -1);
318-
assertType('(float|int<min, -2>)', $rMax / -2);
317+
assertType('float|int<-5, max>', $rMin / -1);
318+
assertType('float|int<min, -2>', $rMax / -2);
319319
}
320320

321321
/**
@@ -343,7 +343,7 @@ public function sayHello($p, $u): void
343343
assertType('int<-2, 4>', $p + $u);
344344
assertType('int<-3, 3>', $p - $u);
345345
assertType('int<-2, 4>', $p * $u);
346-
assertType('(float|int<-2, 2>)', $p / $u);
346+
assertType('float|int<-2, 2>', $p / $u);
347347
}
348348

349349
/**

tests/PHPStan/Analyser/nsrt/math.php

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -81,18 +81,18 @@ public function doIpsum(int $newLevel): void
8181
$min = min(30, $newLevel);
8282
assertType('int<min, 30>', $min);
8383
$minDivFive = $min / 5;
84-
assertType('(float|int<min, 6>)', $minDivFive);
84+
assertType('float|int<min, 6>', $minDivFive);
8585
$volume = 0x10000000 * $minDivFive;
86-
assertType('(float|int<min, 1610612736>)', $volume);
86+
assertType('float|int<min, 1610612736>', $volume);
8787
}
8888

8989
public function doDolor(int $i): void
9090
{
9191
$chunks = min(200, $i);
9292
assertType('int<min, 200>', $chunks);
9393
$divThirty = $chunks / 30;
94-
assertType('(float|int<min, 6>)', $divThirty);
95-
assertType('(float|int<min, 9>)', $divThirty + 3);
94+
assertType('float|int<min, 6>', $divThirty);
95+
assertType('float|int<min, 9>', $divThirty + 3);
9696
}
9797

9898
public function doSit(int $i, int $j): void

0 commit comments

Comments
 (0)