Skip to content

Commit 56d2e30

Browse files
hrachondrejmirtes
authored andcommitted
type combinator: fix integer ranges operations
1 parent 7faf11b commit 56d2e30

File tree

7 files changed

+143
-47
lines changed

7 files changed

+143
-47
lines changed

src/Type/TypeCombinator.php

Lines changed: 62 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -44,8 +44,6 @@ public static function remove(Type $fromType, Type $typeToRemove): Type
4444
$isSuperType = $typeToRemove->isSuperTypeOf($fromType);
4545
if ($isSuperType->yes()) {
4646
return new NeverType();
47-
} elseif ($isSuperType->no()) {
48-
return $fromType;
4947
}
5048

5149
if ($fromType instanceof BooleanType) {
@@ -62,29 +60,47 @@ public static function remove(Type $fromType, Type $typeToRemove): Type
6260
return $arrayType;
6361
}
6462
} elseif ($fromType instanceof IntegerRangeType || $fromType instanceof IntegerType) {
65-
$minA = $fromType instanceof IntegerRangeType ? $fromType->getMin() : PHP_INT_MIN;
66-
$maxA = $fromType instanceof IntegerRangeType ? $fromType->getMax() : PHP_INT_MAX;
63+
if ($fromType instanceof ConstantIntegerType) {
64+
$minA = $fromType->getValue();
65+
$maxA = $fromType->getValue();
66+
} else {
67+
$minA = $fromType instanceof IntegerRangeType ? $fromType->getMin() : PHP_INT_MIN;
68+
$maxA = $fromType instanceof IntegerRangeType ? $fromType->getMax() : PHP_INT_MAX;
69+
}
6770

6871
if ($typeToRemove instanceof IntegerRangeType) {
69-
if ($typeToRemove->getMax() >= $maxA) {
70-
return IntegerRangeType::fromInterval(
71-
$minA,
72-
$typeToRemove->getMin() - 1
72+
$removeValueMin = $typeToRemove->getMin();
73+
$removeValueMax = $typeToRemove->getMax();
74+
if ($minA < $removeValueMin && $removeValueMax < $maxA) {
75+
return self::union(
76+
IntegerRangeType::fromInterval($minA, $removeValueMin - 1),
77+
IntegerRangeType::fromInterval($removeValueMax + 1, $maxA)
7378
);
7479
}
75-
76-
if ($typeToRemove->getMin() <= $minA) {
80+
if ($removeValueMin <= $minA && $minA <= $removeValueMax) {
7781
return IntegerRangeType::fromInterval(
78-
$typeToRemove->getMax() + 1,
82+
$removeValueMax === PHP_INT_MAX ? PHP_INT_MAX : $removeValueMax + 1,
7983
$maxA
8084
);
8185
}
86+
if ($removeValueMin <= $maxA && $maxA <= $removeValueMax) {
87+
return IntegerRangeType::fromInterval(
88+
$minA,
89+
$removeValueMin === PHP_INT_MIN ? PHP_INT_MIN : $removeValueMin - 1
90+
);
91+
}
8292
} elseif ($typeToRemove instanceof ConstantIntegerType) {
83-
if ($typeToRemove->getValue() === $minA) {
93+
$removeValue = $typeToRemove->getValue();
94+
if ($minA < $removeValue && $removeValue < $maxA) {
95+
return self::union(
96+
IntegerRangeType::fromInterval($minA, $removeValue - 1),
97+
IntegerRangeType::fromInterval($removeValue + 1, $maxA)
98+
);
99+
}
100+
if ($removeValue === $minA) {
84101
return IntegerRangeType::fromInterval($minA + 1, $maxA);
85102
}
86-
87-
if ($typeToRemove->getValue() === $maxA) {
103+
if ($removeValue === $maxA) {
88104
return IntegerRangeType::fromInterval($minA, $maxA - 1);
89105
}
90106
}
@@ -269,22 +285,36 @@ public static function union(Type ...$types): Type
269285
for ($i = 0; $i < count($types); $i++) {
270286
for ($j = $i + 1; $j < count($types); $j++) {
271287
if ($types[$i] instanceof IntegerRangeType) {
272-
if ($types[$j] instanceof IntegerRangeType && $types[$i]->isSuperTypeOf($types[$j])->maybe()) {
273-
$types[$i] = IntegerRangeType::fromInterval(
274-
min($types[$i]->getMin(), $types[$j]->getMin()),
275-
max($types[$i]->getMax(), $types[$j]->getMax())
276-
);
277-
array_splice($types, $j, 1);
278-
continue 2;
288+
if ($types[$j] instanceof IntegerRangeType) {
289+
if (
290+
$types[$i]->isSuperTypeOf($types[$j])->maybe() ||
291+
$types[$i]->getMax() + 1 === $types[$j]->getMin() ||
292+
$types[$j]->getMax() + 1 === $types[$i]->getMin()
293+
) {
294+
$types[$i] = IntegerRangeType::fromInterval(
295+
min($types[$i]->getMin(), $types[$j]->getMin()),
296+
max($types[$i]->getMax(), $types[$j]->getMax())
297+
);
298+
$i--;
299+
array_splice($types, $j, 1);
300+
continue 2;
301+
}
279302
}
280303

281304
if ($types[$j] instanceof ConstantIntegerType) {
282-
$types[$i] = IntegerRangeType::fromInterval(
283-
min($types[$i]->getMin(), $types[$j]->getValue()),
284-
max($types[$i]->getMax(), $types[$j]->getValue())
285-
);
286-
array_splice($types, $j, 1);
287-
continue 2;
305+
$value = $types[$j]->getValue();
306+
if ($types[$i]->getMin() === $value + 1) {
307+
$types[$i] = IntegerRangeType::fromInterval($value, $types[$i]->getMax());
308+
$i--;
309+
array_splice($types, $j, 1);
310+
continue 2;
311+
}
312+
if ($types[$i]->getMax() === $value - 1) {
313+
$types[$i] = IntegerRangeType::fromInterval($types[$i]->getMin(), $value);
314+
$i--;
315+
array_splice($types, $j, 1);
316+
continue 2;
317+
}
288318
}
289319
}
290320

@@ -629,10 +659,7 @@ public static function intersect(Type ...$types): Type
629659
$isSuperTypeA = $types[$j]->isSuperTypeOf($types[$i]);
630660
}
631661

632-
if ($isSuperTypeA->no()) {
633-
return new NeverType();
634-
635-
} elseif ($isSuperTypeA->yes()) {
662+
if ($isSuperTypeA->yes()) {
636663
array_splice($types, $j--, 1);
637664
continue;
638665
}
@@ -659,6 +686,10 @@ public static function intersect(Type ...$types): Type
659686
array_splice($types, $i--, 1);
660687
continue 2;
661688
}
689+
690+
if ($isSuperTypeA->no()) {
691+
return new NeverType();
692+
}
662693
}
663694
}
664695

tests/PHPStan/Analyser/NodeScopeResolverTest.php

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2339,7 +2339,7 @@ public function dataBinaryOperations(): array
23392339
'@$stringOrNull ?: 12',
23402340
],
23412341
[
2342-
'int',
2342+
'int<1, max>|int<min, -1>',
23432343
'$integer ?: 12',
23442344
],
23452345
[
@@ -5236,7 +5236,7 @@ public function dataArrayFunctions(): array
52365236
'array_filter($union)',
52375237
],
52385238
[
5239-
'array<int, int|true>',
5239+
'array<int, int<1, max>|int<min, -1>|true>',
52405240
'array_filter($withPossiblyFalsey)',
52415241
],
52425242
[

tests/PHPStan/Analyser/data/bug-2648.php

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,7 @@ public function doBar(array $list): void
2828
if (count($list) > 1) {
2929
assertType('int<2, max>', count($list));
3030
foreach ($list as $key => $item) {
31-
assertType('int', count($list));
31+
assertType('int<2, max>|int<min, 0>', count($list));
3232
if ($item === false) {
3333
unset($list[$key]);
3434
assertType('int', count($list));

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

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,8 @@ function (int $i) {
2121
assertType('int<min, 1>', $i);
2222
}
2323

24+
assertType('int<3, max>|int<min, 1>', $i);
25+
2426
if ($i < 3 && $i > 5) {
2527
assertType('*NEVER*', $i);
2628
} else {
@@ -29,9 +31,8 @@ function (int $i) {
2931

3032
if ($i > 3 && $i < 5) {
3133
assertType('4', $i);
32-
3334
} else {
34-
assertType('int<3, max>|int<min, 1>', $i);
35+
assertType('3|int<5, max>|int<min, 1>', $i);
3536
}
3637

3738
if ($i >= 3 && $i <= 5) {
@@ -65,7 +66,7 @@ function () {
6566

6667
$i = 0;
6768
while ($i++ < 5) {
68-
assertType('int<min, 5>', $i); // should improved to be int<0, 4>
69+
assertType('int<min, 5>', $i); // should improved to be int<1, 5>
6970
}
7071

7172
$i = 0;

tests/PHPStan/Analyser/data/isset.php

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -10,16 +10,16 @@ class Foo
1010
*/
1111
public function doFoo(array $integers, string $string, $mixedIsset, $mixedArrayKeyExists)
1212
{
13-
if (rand(0, 1) === 0) {
13+
if (rand(0, 10) === 0) {
1414
$array = [
1515
'a' => 1,
1616
'b' => 2,
1717
];
18-
} elseif (rand(0, 1) === 0) {
18+
} elseif (rand(0, 11) === 0) {
1919
$array = [
2020
'a' => 2,
2121
];
22-
} elseif (rand(0, 1) === 0) {
22+
} elseif (rand(0, 12) === 0) {
2323
$array = [
2424
'a' => 3,
2525
'b' => 3,

tests/PHPStan/Rules/Comparison/StrictComparisonOfDifferentTypesRuleTest.php

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -163,11 +163,11 @@ public function testStrictComparison(): void
163163
438,
164164
],
165165
[
166-
'Strict comparison using === between int|string and 1.0 will always evaluate to false.',
166+
'Strict comparison using === between int<2, max>|int<min, 0>|string and 1.0 will always evaluate to false.',
167167
464,
168168
],
169169
[
170-
'Strict comparison using === between int|string and stdClass will always evaluate to false.',
170+
'Strict comparison using === between int<2, max>|int<min, 0>|string and stdClass will always evaluate to false.',
171171
466,
172172
],
173173
[
@@ -341,11 +341,11 @@ public function testStrictComparisonWithoutAlwaysTrue(): void
341341
438,
342342
],
343343
[
344-
'Strict comparison using === between int|string and 1.0 will always evaluate to false.',
344+
'Strict comparison using === between int<2, max>|int<min, 0>|string and 1.0 will always evaluate to false.',
345345
464,
346346
],
347347
[
348-
'Strict comparison using === between int|string and stdClass will always evaluate to false.',
348+
'Strict comparison using === between int<2, max>|int<min, 0>|string and stdClass will always evaluate to false.',
349349
466,
350350
],
351351
[

tests/PHPStan/Type/TypeCombinatorTest.php

Lines changed: 67 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1366,6 +1366,14 @@ public function dataUnion(): array
13661366
IntegerRangeType::class,
13671367
'int<1, 5>',
13681368
],
1369+
[
1370+
[
1371+
IntegerRangeType::fromInterval(1, 2),
1372+
IntegerRangeType::fromInterval(3, 5),
1373+
],
1374+
IntegerRangeType::class,
1375+
'int<1, 5>',
1376+
],
13691377
[
13701378
[
13711379
IntegerRangeType::fromInterval(1, 3),
@@ -1395,8 +1403,19 @@ public function dataUnion(): array
13951403
IntegerRangeType::fromInterval(1, 3),
13961404
new ConstantIntegerType(5),
13971405
],
1398-
IntegerRangeType::class,
1399-
'int<1, 5>',
1406+
UnionType::class,
1407+
'5|int<1, 3>',
1408+
],
1409+
[
1410+
[
1411+
new UnionType([
1412+
IntegerRangeType::fromInterval(null, 1),
1413+
IntegerRangeType::fromInterval(3, null),
1414+
]),
1415+
new ConstantIntegerType(2),
1416+
],
1417+
IntegerType::class,
1418+
'int',
14001419
],
14011420
[
14021421
[
@@ -2823,7 +2842,7 @@ public function dataRemove(): array
28232842
new BenevolentUnionType([new IntegerType(), new StringType()]),
28242843
new ConstantIntegerType(1),
28252844
UnionType::class,
2826-
'int|string',
2845+
'int<2, max>|int<min, 0>|string',
28272846
],
28282847
[
28292848
new BenevolentUnionType([new IntegerType(), new StringType()]),
@@ -2970,6 +2989,51 @@ public function dataRemove(): array
29702989
IntegerRangeType::class,
29712990
'int<8, max>',
29722991
],
2992+
[
2993+
IntegerRangeType::fromInterval(0, 2),
2994+
IntegerRangeType::fromInterval(-1, 3),
2995+
NeverType::class,
2996+
'*NEVER*',
2997+
],
2998+
[
2999+
IntegerRangeType::fromInterval(0, 2),
3000+
IntegerRangeType::fromInterval(0, 3),
3001+
NeverType::class,
3002+
'*NEVER*',
3003+
],
3004+
[
3005+
IntegerRangeType::fromInterval(0, 2),
3006+
IntegerRangeType::fromInterval(-1, 2),
3007+
NeverType::class,
3008+
'*NEVER*',
3009+
],
3010+
[
3011+
IntegerRangeType::fromInterval(0, 2),
3012+
new IntegerType(),
3013+
NeverType::class,
3014+
'*NEVER*',
3015+
],
3016+
[
3017+
IntegerRangeType::fromInterval(null, 1),
3018+
IntegerRangeType::fromInterval(4, null),
3019+
IntegerRangeType::class,
3020+
'int<min, 1>',
3021+
],
3022+
[
3023+
IntegerRangeType::fromInterval(1, null),
3024+
IntegerRangeType::fromInterval(null, -4),
3025+
IntegerRangeType::class,
3026+
'int<1, max>',
3027+
],
3028+
[
3029+
new UnionType([
3030+
IntegerRangeType::fromInterval(3, null),
3031+
IntegerRangeType::fromInterval(null, 1),
3032+
]),
3033+
IntegerRangeType::fromInterval(4, null),
3034+
UnionType::class,
3035+
'3|int<min, 1>',
3036+
],
29733037
];
29743038
}
29753039

0 commit comments

Comments
 (0)