Skip to content

Commit d52db07

Browse files
ondrejmirtesclaude
andcommitted
Do not call range() for ranges longer than ARRAY_COUNT_LIMIT
RangeFunctionReturnTypeExtension called range() for constant arguments and only then checked the length, so range(0, 100000000) allocated the whole array (1.7 GB) and a large enough range exhausted the memory. The length of a numeric range is now computed first, and a range longer than ConstantArrayTypeBuilder::ARRAY_COUNT_LIMIT gets the general list type right away. That also gives the list type to ranges range() itself refuses to create, like range(0, 2000000000), which lost the bounds before. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Dhb4ssXuKVWcVBcNFpsdn7
1 parent 8f4a43c commit d52db07

2 files changed

Lines changed: 89 additions & 29 deletions

File tree

‎src/Type/Php/RangeFunctionReturnTypeExtension.php‎

Lines changed: 74 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -25,8 +25,15 @@
2525
use PHPStan\Type\TypeCombinator;
2626
use PHPStan\Type\UnionType;
2727
use ValueError;
28+
use function abs;
2829
use function count;
30+
use function floor;
2931
use function is_array;
32+
use function is_finite;
33+
use function is_numeric;
34+
use function is_string;
35+
use function max;
36+
use function min;
3037
use function range;
3138

3239
#[AutowiredService]
@@ -71,6 +78,12 @@ public function getTypeFromFunctionCall(FunctionReflection $functionReflection,
7178
continue;
7279
}
7380

81+
// range() would allocate every item before the length could be checked
82+
$rangeLength = self::getRangeLength($startConstant->getValue(), $endConstant->getValue(), $stepConstant->getValue());
83+
if ($rangeLength !== null && $rangeLength > ConstantArrayTypeBuilder::ARRAY_COUNT_LIMIT) {
84+
return self::getLongRangeType($startConstant, $endConstant, $stepConstant, $stepType);
85+
}
86+
7487
try {
7588
$rangeValues = @range($startConstant->getValue(), $endConstant->getValue(), $stepConstant->getValue());
7689
} catch (ValueError) {
@@ -83,35 +96,7 @@ public function getTypeFromFunctionCall(FunctionReflection $functionReflection,
8396
}
8497

8598
if (count($rangeValues) > self::RANGE_LENGTH_THRESHOLD) {
86-
if (
87-
$startConstant instanceof ConstantIntegerType
88-
&& $endConstant instanceof ConstantIntegerType
89-
&& $stepConstant instanceof ConstantIntegerType
90-
) {
91-
if ($startConstant->getValue() > $endConstant->getValue()) {
92-
$tmp = $startConstant;
93-
$startConstant = $endConstant;
94-
$endConstant = $tmp;
95-
}
96-
return self::getNonEmptyListOfType(
97-
IntegerRangeType::fromInterval(
98-
$startConstant->getValue(),
99-
$endConstant->getValue(),
100-
),
101-
);
102-
}
103-
104-
if ($stepType->isFloat()->yes()) {
105-
return self::getNonEmptyListOfType(new FloatType());
106-
}
107-
108-
return self::getNonEmptyListOfType(
109-
TypeCombinator::union(
110-
$startConstant->generalize(GeneralizePrecision::moreSpecific()),
111-
$endConstant->generalize(GeneralizePrecision::moreSpecific()),
112-
$stepType->generalize(GeneralizePrecision::moreSpecific()),
113-
),
114-
);
99+
return self::getLongRangeType($startConstant, $endConstant, $stepConstant, $stepType);
115100
}
116101
$arrayBuilder = ConstantArrayTypeBuilder::createEmpty();
117102
foreach ($rangeValues as $value) {
@@ -162,6 +147,66 @@ public function getTypeFromFunctionCall(FunctionReflection $functionReflection,
162147
);
163148
}
164149

150+
/**
151+
* The number of items range() creates for numeric arguments, or null when
152+
* only calling it tells - for a character range, which has at most 256
153+
* items, and for a zero, infinite or NAN argument, for which it throws.
154+
*/
155+
private static function getRangeLength(int|float|string $start, int|float|string $end, int|float $step): ?float
156+
{
157+
if (is_string($start) && is_string($end) && !is_numeric($start) && !is_numeric($end)) {
158+
return null;
159+
}
160+
161+
// a non-numeric string next to a number is 0
162+
$startNumber = is_numeric($start) ? (float) $start : 0.0;
163+
$endNumber = is_numeric($end) ? (float) $end : 0.0;
164+
$stepNumber = abs((float) $step);
165+
if ($stepNumber === 0.0) {
166+
return null;
167+
}
168+
169+
$length = abs($endNumber - $startNumber) / $stepNumber;
170+
if (!is_finite($length)) {
171+
return null;
172+
}
173+
174+
return floor($length) + 1;
175+
}
176+
177+
private static function getLongRangeType(
178+
ConstantIntegerType|ConstantFloatType|ConstantStringType $startConstant,
179+
ConstantIntegerType|ConstantFloatType|ConstantStringType $endConstant,
180+
ConstantIntegerType|ConstantFloatType $stepConstant,
181+
Type $stepType,
182+
): Type
183+
{
184+
if (
185+
$startConstant instanceof ConstantIntegerType
186+
&& $endConstant instanceof ConstantIntegerType
187+
&& $stepConstant instanceof ConstantIntegerType
188+
) {
189+
return self::getNonEmptyListOfType(
190+
IntegerRangeType::fromInterval(
191+
min($startConstant->getValue(), $endConstant->getValue()),
192+
max($startConstant->getValue(), $endConstant->getValue()),
193+
),
194+
);
195+
}
196+
197+
if ($stepType->isFloat()->yes()) {
198+
return self::getNonEmptyListOfType(new FloatType());
199+
}
200+
201+
return self::getNonEmptyListOfType(
202+
TypeCombinator::union(
203+
$startConstant->generalize(GeneralizePrecision::moreSpecific()),
204+
$endConstant->generalize(GeneralizePrecision::moreSpecific()),
205+
$stepType->generalize(GeneralizePrecision::moreSpecific()),
206+
),
207+
);
208+
}
209+
165210
private static function getNonEmptyListOfType(Type $type): IntersectionType
166211
{
167212
return new IntersectionType([
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
<?php declare(strict_types = 1);
2+
3+
namespace RangeArrayCountLimit;
4+
5+
use function PHPStan\Testing\assertType;
6+
7+
function doFoo(): void
8+
{
9+
assertType('non-empty-list<int<0, 2000000000>>', range(0, 2000000000));
10+
assertType('non-empty-list<int<0, 100000000>>', range(0, 100000000));
11+
assertType('non-empty-list<int<-100000000, 0>>', range(0, -100000000));
12+
assertType('non-empty-list<int<0, 2000000000>>', range(0, 2000000000, 3));
13+
assertType('non-empty-list<float>', range(0, 100000000, 0.5));
14+
assertType('non-empty-list<int<0, 300>>', range(0, 300));
15+
}

0 commit comments

Comments
 (0)