Skip to content

Commit 3651338

Browse files
committed
Scope generalization for integer range types
Closes phpstan/phpstan#3153
1 parent d4557a6 commit 3651338

File tree

7 files changed

+238
-4
lines changed

7 files changed

+238
-4
lines changed

‎src/Analyser/MutatingScope.php‎

Lines changed: 80 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -133,6 +133,8 @@
133133
use function strtolower;
134134
use function substr;
135135
use function usort;
136+
use const PHP_INT_MAX;
137+
use const PHP_INT_MIN;
136138

137139
class MutatingScope implements Scope
138140
{
@@ -4679,6 +4681,7 @@ private static function generalizeType(Type $a, Type $b): Type
46794681
$constantStrings = ['a' => [], 'b' => []];
46804682
$constantArrays = ['a' => [], 'b' => []];
46814683
$generalArrays = ['a' => [], 'b' => []];
4684+
$integerRanges = ['a' => [], 'b' => []];
46824685
$otherTypes = [];
46834686

46844687
foreach ([
@@ -4710,6 +4713,10 @@ private static function generalizeType(Type $a, Type $b): Type
47104713
$generalArrays[$key][] = $type;
47114714
continue;
47124715
}
4716+
if ($type instanceof IntegerRangeType) {
4717+
$integerRanges[$key][] = $type;
4718+
continue;
4719+
}
47134720

47144721
$otherTypes[] = $type;
47154722
}
@@ -4857,6 +4864,79 @@ private static function generalizeType(Type $a, Type $b): Type
48574864
$resultTypes[] = TypeCombinator::union(...$constantIntegers['b']);
48584865
}
48594866

4867+
if (count($integerRanges['a']) > 0) {
4868+
if (count($integerRanges['b']) === 0) {
4869+
$resultTypes[] = TypeCombinator::union(...$integerRanges['a']);
4870+
} else {
4871+
$integerRangesA = TypeCombinator::union(...$integerRanges['a']);
4872+
$integerRangesB = TypeCombinator::union(...$integerRanges['b']);
4873+
4874+
if ($integerRangesA->equals($integerRangesB)) {
4875+
$resultTypes[] = $integerRangesA;
4876+
} else {
4877+
$min = null;
4878+
$max = null;
4879+
foreach ($integerRanges['a'] as $range) {
4880+
if ($range->getMin() === null) {
4881+
$rangeMin = PHP_INT_MIN;
4882+
} else {
4883+
$rangeMin = $range->getMin();
4884+
}
4885+
if ($range->getMax() === null) {
4886+
$rangeMax = PHP_INT_MAX;
4887+
} else {
4888+
$rangeMax = $range->getMax();
4889+
}
4890+
4891+
if ($min === null || $rangeMin < $min) {
4892+
$min = $rangeMin;
4893+
}
4894+
if ($max !== null && $rangeMax <= $max) {
4895+
continue;
4896+
}
4897+
4898+
$max = $rangeMax;
4899+
}
4900+
4901+
$gotGreater = false;
4902+
$gotSmaller = false;
4903+
foreach ($integerRanges['b'] as $range) {
4904+
if ($range->getMin() === null) {
4905+
$rangeMin = PHP_INT_MIN;
4906+
} else {
4907+
$rangeMin = $range->getMin();
4908+
}
4909+
if ($range->getMax() === null) {
4910+
$rangeMax = PHP_INT_MAX;
4911+
} else {
4912+
$rangeMax = $range->getMax();
4913+
}
4914+
4915+
if ($rangeMax > $max) {
4916+
$gotGreater = true;
4917+
}
4918+
if ($rangeMin >= $min) {
4919+
continue;
4920+
}
4921+
4922+
$gotSmaller = true;
4923+
}
4924+
4925+
if ($gotGreater && $gotSmaller) {
4926+
$resultTypes[] = new IntegerType();
4927+
} elseif ($gotGreater) {
4928+
$resultTypes[] = IntegerRangeType::fromInterval($min, null);
4929+
} elseif ($gotSmaller) {
4930+
$resultTypes[] = IntegerRangeType::fromInterval(null, $max);
4931+
} else {
4932+
$resultTypes[] = TypeCombinator::union($integerRangesA, $integerRangesB);
4933+
}
4934+
}
4935+
}
4936+
} elseif (count($integerRanges['b']) > 0) {
4937+
$resultTypes[] = TypeCombinator::union(...$integerRanges['b']);
4938+
}
4939+
48604940
return TypeCombinator::union(...$resultTypes, ...$otherTypes);
48614941
}
48624942

‎tests/PHPStan/Analyser/LegacyNodeScopeResolverTest.php‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7097,7 +7097,7 @@ public function dataForLoopVariables(): array
70977097
"'end'",
70987098
],
70997099
[
7100-
'int<0, 10>',
7100+
'int<0, max>',
71017101
'$i',
71027102
"'afterLoop'",
71037103
],

‎tests/PHPStan/Analyser/ScopeTest.php‎

Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99
use PHPStan\Type\Constant\ConstantBooleanType;
1010
use PHPStan\Type\Constant\ConstantIntegerType;
1111
use PHPStan\Type\Constant\ConstantStringType;
12+
use PHPStan\Type\IntegerRangeType;
1213
use PHPStan\Type\ObjectType;
1314
use PHPStan\Type\Type;
1415
use PHPStan\Type\UnionType;
@@ -155,6 +156,72 @@ public function dataGeneralize(): array
155156
]),
156157
'array<literal-string&non-empty-string, int<1, max>>',
157158
],
159+
[
160+
new UnionType([
161+
new ConstantIntegerType(0),
162+
new ConstantIntegerType(1),
163+
]),
164+
new UnionType([
165+
new ConstantIntegerType(-1),
166+
new ConstantIntegerType(0),
167+
new ConstantIntegerType(1),
168+
]),
169+
'int<min, 1>',
170+
],
171+
[
172+
new UnionType([
173+
new ConstantIntegerType(0),
174+
new ConstantIntegerType(2),
175+
]),
176+
new UnionType([
177+
new ConstantIntegerType(0),
178+
new ConstantIntegerType(1),
179+
new ConstantIntegerType(2),
180+
]),
181+
'0|1|2',
182+
],
183+
[
184+
new UnionType([
185+
new ConstantIntegerType(0),
186+
new ConstantIntegerType(1),
187+
new ConstantIntegerType(2),
188+
]),
189+
new UnionType([
190+
new ConstantIntegerType(0),
191+
new ConstantIntegerType(2),
192+
]),
193+
'0|1|2',
194+
],
195+
[
196+
IntegerRangeType::fromInterval(0, 16),
197+
IntegerRangeType::fromInterval(1, 17),
198+
'int<0, max>',
199+
],
200+
[
201+
IntegerRangeType::fromInterval(0, 16),
202+
IntegerRangeType::fromInterval(-1, 15),
203+
'int<min, 16>',
204+
],
205+
[
206+
IntegerRangeType::fromInterval(0, 16),
207+
IntegerRangeType::fromInterval(1, null),
208+
'int<0, max>',
209+
],
210+
[
211+
IntegerRangeType::fromInterval(0, 16),
212+
IntegerRangeType::fromInterval(null, 15),
213+
'int<min, 16>',
214+
],
215+
[
216+
IntegerRangeType::fromInterval(0, 16),
217+
IntegerRangeType::fromInterval(0, null),
218+
'int<0, max>',
219+
],
220+
[
221+
IntegerRangeType::fromInterval(0, 16),
222+
IntegerRangeType::fromInterval(null, 16),
223+
'int<min, 16>',
224+
],
158225
];
159226
}
160227

‎tests/PHPStan/Analyser/data/for-loop-i-type.php‎

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -14,14 +14,14 @@ public function doBar() {
1414
assertType('int<1, 49>', $i);
1515
}
1616

17-
assertType('50', $i);
17+
assertType('int<50, max>', $i);
1818
assertType(\stdClass::class, $foo);
1919

2020
for($i = 50; $i > 0; $i--) {
2121
assertType('int<1, 50>', $i);
2222
}
2323

24-
assertType('0', $i);
24+
assertType('int<min, 0>', $i);
2525
}
2626

2727
public function doCount(array $a) {
@@ -59,7 +59,7 @@ public function doLOrem() {
5959
break;
6060
}
6161

62-
assertType('int<1, 50>', $i);
62+
assertType('int<1, max>', $i);
6363
}
6464

6565
}

‎tests/PHPStan/Rules/Comparison/NumberComparisonOperatorsConstantConditionRuleTest.php‎

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -73,4 +73,14 @@ public function testBug3867(): void
7373
$this->analyse([__DIR__ . '/data/bug-3867.php'], []);
7474
}
7575

76+
public function testIntegerRangeGeneralization(): void
77+
{
78+
$this->analyse([__DIR__ . '/data/integer-range-generalization.php'], []);
79+
}
80+
81+
public function testBug3153(): void
82+
{
83+
$this->analyse([__DIR__ . '/data/bug-3153.php'], []);
84+
}
85+
7686
}
Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
<?php
2+
3+
namespace Bug3153;
4+
5+
class Foo
6+
{
7+
8+
public function doFoo()
9+
{
10+
$x = random_int(1, 1000);
11+
12+
if($x < 7){
13+
while(true){
14+
$x++;
15+
16+
if( $x > 10 ){
17+
break;
18+
}
19+
}
20+
}
21+
}
22+
23+
public function doBar()
24+
{
25+
$rows = [1,2,3,4,5,6,7,8,9,10,11,12,13,14,15];
26+
$added_rows = 0;
27+
$limit = random_int(1, 20);
28+
29+
foreach($rows as $row){
30+
31+
if( $added_rows >= $limit ){
32+
break;
33+
}
34+
$added_rows++;
35+
}
36+
37+
if( $added_rows < 3 ){
38+
foreach($rows as $row){
39+
40+
$added_rows++;
41+
42+
if( $added_rows > 10 ){
43+
break;
44+
}
45+
}
46+
}
47+
}
48+
49+
}
Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
<?php
2+
3+
namespace IntegerRangeGenerealization;
4+
5+
class Foo
6+
{
7+
8+
public function doFoo(string $array): string
9+
{
10+
$result = str_repeat("\x00", 4096);
11+
if($array !== $result){
12+
$i = 0;
13+
for($x = 0; $x < 16; ++$x){
14+
$zM = $x + 256;
15+
for($z = $x; $z < $zM; $z += 16){
16+
$yM = $z + 4096;
17+
for($y = $z; $y < $yM; $y += 256){
18+
$result[$i] = $array[$y];
19+
++$i;
20+
}
21+
}
22+
}
23+
}
24+
25+
return $result;
26+
}
27+
28+
}

0 commit comments

Comments
 (0)