Skip to content

Commit 6e90da1

Browse files
authored
Rework IntegerRange math
1 parent b105b9e commit 6e90da1

File tree

7 files changed

+250
-92
lines changed

7 files changed

+250
-92
lines changed

src/Analyser/MutatingScope.php

Lines changed: 135 additions & 79 deletions
Original file line numberDiff line numberDiff line change
@@ -1268,95 +1268,32 @@ private function resolveType(Expr $node): Type
12681268
!($node instanceof Node\Expr\BinaryOp\Pow || $node instanceof Node\Expr\AssignOp\Pow)) {
12691269

12701270
if ($leftType instanceof ConstantIntegerType) {
1271-
$leftMin = $leftType->getValue();
1272-
$leftMax = $leftType->getValue();
1271+
return $this->integerRangeMath(
1272+
$leftType,
1273+
$node,
1274+
$rightType
1275+
);
12731276
} elseif ($leftType instanceof UnionType) {
1274-
$leftMin = null;
1275-
$leftMax = null;
12761277

1277-
foreach ($leftType->getTypes() as $type) {
1278-
if ($type instanceof IntegerRangeType) {
1279-
$leftMin = $leftMin !== null ? min($leftMin, $type->getMin()) : $type->getMin();
1280-
$leftMax = max($leftMax, $type->getMax());
1281-
} elseif ($type instanceof ConstantIntegerType) {
1282-
if ($node instanceof Node\Expr\BinaryOp\Minus || $node instanceof Node\Expr\AssignOp\Minus ||
1283-
$node instanceof Node\Expr\BinaryOp\Div || $node instanceof Node\Expr\AssignOp\Div) {
1284-
$leftMin = max($leftMin, $type->getValue());
1285-
$leftMax = $leftMax !== null ? min($leftMax, $type->getValue()) : $type->getValue();
1286-
} else {
1287-
$leftMin = $leftMin !== null ? min($leftMin, $type->getValue()) : $type->getValue();
1288-
$leftMax = max($leftMax, $type->getValue());
1289-
}
1290-
}
1291-
}
1292-
} else {
1293-
$leftMin = $leftType->getMin();
1294-
$leftMax = $leftType->getMax();
1295-
}
1278+
$unionParts = [];
12961279

1297-
if ($rightType instanceof ConstantIntegerType) {
1298-
$rightMin = $rightType->getValue();
1299-
$rightMax = $rightType->getValue();
1300-
} elseif ($rightType instanceof UnionType) {
1301-
$rightMin = null;
1302-
$rightMax = null;
1303-
1304-
foreach ($rightType->getTypes() as $type) {
1305-
if ($type instanceof IntegerRangeType) {
1306-
$rightMin = $rightMin !== null ? min($rightMin, $type->getMin()) : $type->getMin();
1307-
$rightMax = max($rightMax, $type->getMax());
1308-
} elseif ($type instanceof ConstantIntegerType) {
1309-
if ($node instanceof Node\Expr\BinaryOp\Minus || $node instanceof Node\Expr\AssignOp\Minus ||
1310-
$node instanceof Node\Expr\BinaryOp\Div || $node instanceof Node\Expr\AssignOp\Div) {
1311-
$rightMin = max($rightMin, $type->getValue());
1312-
$rightMax = $rightMax !== null ? min($rightMax, $type->getValue()) : $type->getValue();
1313-
} else {
1314-
$rightMin = $rightMin !== null ? min($rightMin, $type->getValue()) : $type->getValue();
1315-
$rightMax = max($rightMax, $type->getValue());
1316-
}
1280+
foreach ($leftType->getTypes() as $type) {
1281+
if ($type instanceof IntegerRangeType || $type instanceof ConstantIntegerType) {
1282+
$unionParts[] = $this->integerRangeMath($type, $node, $rightType);
1283+
} else {
1284+
$unionParts[] = $type;
13171285
}
13181286
}
1319-
} else {
1320-
$rightMin = $rightType->getMin();
1321-
$rightMax = $rightType->getMax();
1322-
}
1323-
1324-
if ($node instanceof Node\Expr\BinaryOp\Plus || $node instanceof Node\Expr\AssignOp\Plus) {
1325-
$min = $leftMin !== null && $rightMin !== null ? $leftMin + $rightMin : null;
1326-
$max = $leftMax !== null && $rightMax !== null ? $leftMax + $rightMax : null;
1327-
} elseif ($node instanceof Node\Expr\BinaryOp\Minus || $node instanceof Node\Expr\AssignOp\Minus) {
1328-
$min = $leftMin !== null && $rightMin !== null ? $leftMin - $rightMin : null;
1329-
$max = $leftMax !== null && $rightMax !== null ? $leftMax - $rightMax : null;
13301287

1331-
if ($min !== null && $max !== null && $min > $max) {
1332-
[$min, $max] = [$max, $min];
1288+
$union = TypeCombinator::union(...$unionParts);
1289+
if ($leftType instanceof BenevolentUnionType) {
1290+
return TypeUtils::toBenevolentUnion($union)->toNumber();
13331291
}
1334-
} elseif ($node instanceof Node\Expr\BinaryOp\Mul || $node instanceof Node\Expr\AssignOp\Mul) {
1335-
$min = $leftMin !== null && $rightMin !== null ? $leftMin * $rightMin : null;
1336-
$max = $leftMax !== null && $rightMax !== null ? $leftMax * $rightMax : null;
1337-
} else {
1338-
$min = $leftMin !== null && $rightMin !== null && $rightMin !== 0 ? (int) ($leftMin / $rightMin) : null;
1339-
$max = $leftMax !== null && $rightMax !== null && $rightMax !== 0 ? (int) ($leftMax / $rightMax) : null;
13401292

1341-
if ($min !== null && $max !== null && $min > $max) {
1342-
[$min, $max] = [$max, $min];
1343-
}
1293+
return $union->toNumber();
13441294
}
13451295

1346-
if ($min !== null || $max !== null) {
1347-
$integerRange = IntegerRangeType::fromInterval($min, $max);
1348-
1349-
if ($node instanceof Node\Expr\BinaryOp\Div || $node instanceof Node\Expr\AssignOp\Div) {
1350-
if ($min === $max && $min === 0) {
1351-
// division of upper and lower bound turns into a tiny 0.x fraction, which casted to int turns into 0.
1352-
// this leads to a useless 0|float type; we return only float instead.
1353-
return new FloatType();
1354-
}
1355-
return TypeCombinator::union($integerRange, new FloatType());
1356-
}
1357-
1358-
return $integerRange;
1359-
}
1296+
return $this->integerRangeMath($leftType, $node, $rightType);
13601297
}
13611298

13621299
$operatorSigil = null;
@@ -5175,4 +5112,123 @@ private function propertyFetchType(Type $fetchedOnType, string $propertyName, Ex
51755112
return $propertyReflection->getReadableType();
51765113
}
51775114

5115+
/**
5116+
* @param ConstantIntegerType|IntegerRangeType $range
5117+
* @param \PhpParser\Node\Expr\AssignOp\Div|\PhpParser\Node\Expr\AssignOp\Minus|\PhpParser\Node\Expr\AssignOp\Mul|\PhpParser\Node\Expr\AssignOp\Plus|\PhpParser\Node\Expr\BinaryOp\Div|\PhpParser\Node\Expr\BinaryOp\Minus|\PhpParser\Node\Expr\BinaryOp\Mul|\PhpParser\Node\Expr\BinaryOp\Plus $node
5118+
* @param IntegerRangeType|ConstantIntegerType|UnionType $operand
5119+
*/
5120+
private function integerRangeMath(Type $range, Expr $node, Type $operand): Type
5121+
{
5122+
if ($range instanceof IntegerRangeType) {
5123+
$rangeMin = $range->getMin();
5124+
$rangeMax = $range->getMax();
5125+
} else {
5126+
$rangeMin = $range->getValue();
5127+
$rangeMax = $rangeMin;
5128+
}
5129+
5130+
if ($operand instanceof UnionType) {
5131+
5132+
$unionParts = [];
5133+
5134+
foreach ($operand->getTypes() as $type) {
5135+
if ($type instanceof IntegerRangeType || $type instanceof ConstantIntegerType) {
5136+
$unionParts[] = $this->integerRangeMath($range, $node, $type);
5137+
} else {
5138+
$unionParts[] = $type->toNumber();
5139+
}
5140+
}
5141+
5142+
$union = TypeCombinator::union(...$unionParts);
5143+
if ($operand instanceof BenevolentUnionType) {
5144+
return TypeUtils::toBenevolentUnion($union)->toNumber();
5145+
}
5146+
5147+
return $union->toNumber();
5148+
}
5149+
5150+
if ($node instanceof Node\Expr\BinaryOp\Plus || $node instanceof Node\Expr\AssignOp\Plus) {
5151+
if ($operand instanceof ConstantIntegerType) {
5152+
$min = $rangeMin !== null ? $rangeMin + $operand->getValue() : null;
5153+
$max = $rangeMax !== null ? $rangeMax + $operand->getValue() : null;
5154+
} else {
5155+
$min = $rangeMin !== null && $operand->getMin() !== null ? $rangeMin + $operand->getMin() : null;
5156+
$max = $rangeMax !== null && $operand->getMax() !== null ? $rangeMax + $operand->getMax() : null;
5157+
}
5158+
} elseif ($node instanceof Node\Expr\BinaryOp\Minus || $node instanceof Node\Expr\AssignOp\Minus) {
5159+
if ($operand instanceof ConstantIntegerType) {
5160+
$min = $rangeMin !== null ? $rangeMin - $operand->getValue() : null;
5161+
$max = $rangeMax !== null ? $rangeMax - $operand->getValue() : null;
5162+
} else {
5163+
if ($rangeMin === $rangeMax && $rangeMin !== null
5164+
&& ($operand->getMin() === null || $operand->getMax() === null)) {
5165+
$min = null;
5166+
$max = $rangeMin;
5167+
} else {
5168+
if ($operand->getMin() === null) {
5169+
$min = null;
5170+
} elseif ($rangeMin !== null) {
5171+
$min = $rangeMin - $operand->getMin();
5172+
} else {
5173+
$min = null;
5174+
}
5175+
5176+
if ($operand->getMax() === null) {
5177+
$max = null;
5178+
} elseif ($rangeMax !== null) {
5179+
$max = $rangeMax - $operand->getMax();
5180+
} else {
5181+
$max = null;
5182+
}
5183+
5184+
if ($min !== null && $max !== null && $min > $max) {
5185+
[$min, $max] = [$max, $min];
5186+
}
5187+
}
5188+
}
5189+
} elseif ($node instanceof Node\Expr\BinaryOp\Mul || $node instanceof Node\Expr\AssignOp\Mul) {
5190+
if ($operand instanceof ConstantIntegerType) {
5191+
$min = $rangeMin !== null ? $rangeMin * $operand->getValue() : null;
5192+
$max = $rangeMax !== null ? $rangeMax * $operand->getValue() : null;
5193+
} else {
5194+
$min = $rangeMin !== null && $operand->getMin() !== null ? $rangeMin * $operand->getMin() : null;
5195+
$max = $rangeMax !== null && $operand->getMax() !== null ? $rangeMax * $operand->getMax() : null;
5196+
}
5197+
5198+
if ($min !== null && $max !== null && $min > $max) {
5199+
[$min, $max] = [$max, $min];
5200+
}
5201+
5202+
} else {
5203+
if ($operand instanceof ConstantIntegerType) {
5204+
$min = $rangeMin !== null && $operand->getValue() !== 0 ? $rangeMin / $operand->getValue() : null;
5205+
$max = $rangeMax !== null && $operand->getValue() !== 0 ? $rangeMax / $operand->getValue() : null;
5206+
} else {
5207+
$min = $rangeMin !== null && $operand->getMin() !== null && $operand->getMin() !== 0 ? $rangeMin / $operand->getMin() : null;
5208+
$max = $rangeMax !== null && $operand->getMax() !== null && $operand->getMax() !== 0 ? $rangeMax / $operand->getMax() : null;
5209+
}
5210+
5211+
if ($operand instanceof IntegerRangeType
5212+
&& ($operand->getMin() === null || $operand->getMax() === null)
5213+
|| ($rangeMin === null || $rangeMax === null)
5214+
|| is_float($min) || is_float($max)
5215+
) {
5216+
if (is_float($min)) {
5217+
$min = (int) $min;
5218+
}
5219+
if (is_float($max)) {
5220+
$max = (int) $max;
5221+
}
5222+
5223+
if ($min !== null && $max !== null && $min > $max) {
5224+
[$min, $max] = [$max, $min];
5225+
}
5226+
5227+
return TypeCombinator::union(IntegerRangeType::fromInterval($min, $max), new FloatType());
5228+
}
5229+
}
5230+
5231+
return IntegerRangeType::fromInterval($min, $max);
5232+
}
5233+
51785234
}

src/File/ParentDirectoryRelativePathHelper.php

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -54,6 +54,11 @@ public function getFilenameParts(string $filename): array
5454
}
5555

5656
$dotsCount = $parentPartsCount - $i;
57+
58+
if ($dotsCount < 0) {
59+
throw new \PHPStan\ShouldNotHappenException();
60+
}
61+
5762
return array_merge(array_fill(0, $dotsCount, '..'), array_slice($filenameParts, $i));
5863
}
5964

src/Type/IntegerRangeType.php

Lines changed: 0 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -400,11 +400,6 @@ public function getGreaterOrEqualType(): Type
400400
return TypeCombinator::remove(new MixedType(), TypeCombinator::union(...$subtractedTypes));
401401
}
402402

403-
public function toNumber(): Type
404-
{
405-
return new parent();
406-
}
407-
408403
public function toBoolean(): BooleanType
409404
{
410405
$isZero = (new ConstantIntegerType(0))->isSuperTypeOf($this);

tests/PHPStan/Analyser/NodeScopeResolverTest.php

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -487,13 +487,16 @@ public function dataFileAsserts(): iterable
487487
yield from $this->gatherAssertTypes(__DIR__ . '/data/sizeof.php');
488488

489489
yield from $this->gatherAssertTypes(__DIR__ . '/data/div-by-zero.php');
490+
490491
yield from $this->gatherAssertTypes(__DIR__ . '/data/bug-5072.php');
491492
yield from $this->gatherAssertTypes(__DIR__ . '/data/bug-5530.php');
492493
yield from $this->gatherAssertTypes(__DIR__ . '/data/bug-1861.php');
493494
yield from $this->gatherAssertTypes(__DIR__ . '/data/bug-4843.php');
494495
yield from $this->gatherAssertTypes(__DIR__ . '/data/bug-4602.php');
495496
yield from $this->gatherAssertTypes(__DIR__ . '/data/bug-4499.php');
496497
yield from $this->gatherAssertTypes(__DIR__ . '/data/bug-2142.php');
498+
499+
yield from $this->gatherAssertTypes(__DIR__ . '/data/math.php');
497500
}
498501

499502
/**

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

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -11,10 +11,11 @@ class Foo
1111
* @param int<0, max> $range1
1212
* @param int<min, 0> $range2
1313
*/
14-
public function doFoo(int $range1, int $range2): void
14+
public function doFoo(int $range1, int $range2, int $int): void
1515
{
16-
assertType('(float|int)', 5 / $range1);
17-
assertType('(float|int)', 5 / $range2);
16+
assertType('float|int', 5 / $range1);
17+
assertType('float|int', 5 / $range2);
18+
assertType('(float|int)', 5 / $int);
1819
assertType('*ERROR*', 5 / 0);
1920
}
2021

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

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -228,10 +228,10 @@ public function math($i, $j, $z, $pi, $r1, $r2, $rMin, $rMax, $x, $y) {
228228
assertType('int<min, 15>', $j * $rMin);
229229
assertType('int<5, max>', $j * $rMax);
230230

231-
assertType('int<-19, 13>', $r1 + $z);
232-
assertType('int<-2, 30>', $r1 - $z);
233-
assertType('int<-20, 30>', $r1 * $z);
234-
assertType('float', $r1 / $z);
231+
assertType('int<-19, -10>|int<2, 13>', $r1 + $z);
232+
assertType('int<-2, 9>|int<21, 30>', $r1 - $z);
233+
assertType('int<-200, -20>|int<1, 30>', $r1 * $z);
234+
assertType('float|int<0, 10>', $r1 / $z);
235235
assertType('int<min, 15>', $rMin * $z);
236236
assertType('int<-100, max>', $rMax * $z);
237237

@@ -240,7 +240,7 @@ public function math($i, $j, $z, $pi, $r1, $r2, $rMin, $rMax, $x, $y) {
240240
assertType('int<2, max>', $pi * 2);
241241
assertType('float|int<0, max>', $pi / 2);
242242
assertType('int<2, max>', 1 + $pi);
243-
assertType('int<1, max>', 2 - $pi);
243+
assertType('int<min, 2>', 2 - $pi);
244244
assertType('int<2, max>', 2 * $pi);
245245
assertType('float|int<2, max>', 2 / $pi);
246246

0 commit comments

Comments
 (0)