Skip to content

Commit c4cc640

Browse files
Limit int ranges when narrowing arrays via count()
Co-authored-by: Ondrej Mirtes <ondrej@mirtes.cz>
1 parent c5cf14b commit c4cc640

File tree

4 files changed

+78
-14
lines changed

4 files changed

+78
-14
lines changed

src/Analyser/TypeSpecifier.php

Lines changed: 22 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1087,10 +1087,7 @@ private function specifyTypesForCountFuncCall(
10871087
if (
10881088
$sizeType instanceof ConstantIntegerType
10891089
&& $sizeType->getValue() < ConstantArrayTypeBuilder::ARRAY_COUNT_LIMIT
1090-
&& (
1091-
$isList->yes()
1092-
|| $isConstantArray->yes() && $arrayType->getKeyType()->isSuperTypeOf(IntegerRangeType::fromInterval(0, $sizeType->getValue() - 1))->yes()
1093-
)
1090+
&& $arrayType->getKeyType()->isSuperTypeOf(IntegerRangeType::fromInterval(0, $sizeType->getValue() - 1))->yes()
10941091
) {
10951092
// turn optional offsets non-optional
10961093
$valueTypesBuilder = ConstantArrayTypeBuilder::createEmpty();
@@ -1105,21 +1102,23 @@ private function specifyTypesForCountFuncCall(
11051102
if (
11061103
$sizeType instanceof IntegerRangeType
11071104
&& $sizeType->getMin() !== null
1108-
&& (
1109-
$isList->yes()
1110-
|| $isConstantArray->yes() && $arrayType->getKeyType()->isSuperTypeOf(IntegerRangeType::fromInterval(0, $sizeType->getMin() - 1))->yes()
1111-
)
1105+
&& $sizeType->getMin() < ConstantArrayTypeBuilder::ARRAY_COUNT_LIMIT
1106+
&& $arrayType->getKeyType()->isSuperTypeOf(IntegerRangeType::fromInterval(0, ($sizeType->getMax() ?? $sizeType->getMin()) - 1))->yes()
11121107
) {
1108+
$builderData = [];
11131109
// turn optional offsets non-optional
1114-
$valueTypesBuilder = ConstantArrayTypeBuilder::createEmpty();
11151110
for ($i = 0; $i < $sizeType->getMin(); $i++) {
11161111
$offsetType = new ConstantIntegerType($i);
1117-
$valueTypesBuilder->setOffsetValueType($offsetType, $arrayType->getOffsetValueType($offsetType));
1112+
$builderData[] = [$offsetType, $arrayType->getOffsetValueType($offsetType), false];
11181113
}
11191114
if ($sizeType->getMax() !== null) {
1115+
if ($sizeType->getMax() - $sizeType->getMin() > ConstantArrayTypeBuilder::ARRAY_COUNT_LIMIT) {
1116+
$resultTypes[] = $arrayType;
1117+
continue;
1118+
}
11201119
for ($i = $sizeType->getMin(); $i < $sizeType->getMax(); $i++) {
11211120
$offsetType = new ConstantIntegerType($i);
1122-
$valueTypesBuilder->setOffsetValueType($offsetType, $arrayType->getOffsetValueType($offsetType), true);
1121+
$builderData[] = [$offsetType, $arrayType->getOffsetValueType($offsetType), true];
11231122
}
11241123
} elseif ($arrayType->isConstantArray()->yes()) {
11251124
for ($i = $sizeType->getMin();; $i++) {
@@ -1128,14 +1127,24 @@ private function specifyTypesForCountFuncCall(
11281127
if ($hasOffset->no()) {
11291128
break;
11301129
}
1131-
$valueTypesBuilder->setOffsetValueType($offsetType, $arrayType->getOffsetValueType($offsetType), !$hasOffset->yes());
1130+
$builderData[] = [$offsetType, $arrayType->getOffsetValueType($offsetType), !$hasOffset->yes()];
11321131
}
11331132
} else {
11341133
$resultTypes[] = TypeCombinator::intersect($arrayType, new NonEmptyArrayType());
11351134
continue;
11361135
}
11371136

1138-
$resultTypes[] = $valueTypesBuilder->getArray();
1137+
if (count($builderData) > ConstantArrayTypeBuilder::ARRAY_COUNT_LIMIT) {
1138+
$resultTypes[] = $arrayType;
1139+
continue;
1140+
}
1141+
1142+
$builder = ConstantArrayTypeBuilder::createEmpty();
1143+
foreach ($builderData as [$offsetType, $valueType, $optional]) {
1144+
$builder->setOffsetValueType($offsetType, $valueType, $optional);
1145+
}
1146+
1147+
$resultTypes[] = $builder->getArray();
11391148
continue;
11401149
}
11411150

tests/PHPStan/Analyser/AnalyserIntegrationTest.php

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1542,6 +1542,12 @@ public function testBug12159(): void
15421542
$this->assertNoErrors($errors);
15431543
}
15441544

1545+
public function testBug12787(): void
1546+
{
1547+
$errors = $this->runAnalyse(__DIR__ . '/data/bug-12787.php');
1548+
$this->assertNoErrors($errors);
1549+
}
1550+
15451551
/**
15461552
* @param string[]|null $allAnalysedFiles
15471553
* @return Error[]
Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
<?php declare(strict_types = 1);
2+
3+
namespace Bug12787;
4+
5+
class HelloWorld
6+
{
7+
protected const MAX_COUNT = 100000000;
8+
9+
/**
10+
* @return string[]
11+
*/
12+
public function accumulate(): array
13+
{
14+
$items = [];
15+
16+
do {
17+
$items[] = 'something';
18+
} while (count($items) < self::MAX_COUNT);
19+
20+
return $items;
21+
}
22+
}

tests/PHPStan/Analyser/nsrt/list-count.php

Lines changed: 28 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -341,12 +341,15 @@ protected function testOptionalKeysInUnionArray($row): void
341341

342342
/**
343343
* @param array{string}|list{0: int, 1?: string|null, 2?: int|null, 3?: float|null} $row
344+
* @param list<string> $listRow
344345
* @param int<2, 3> $twoOrThree
345346
* @param int<2, max> $twoOrMore
346347
* @param int<min, 3> $maxThree
347348
* @param int<10, 11> $tenOrEleven
349+
* @param int<3, 32> $threeOrMoreInRangeLimit
350+
* @param int<3, 512> $threeOrMoreOverRangeLimit
348351
*/
349-
protected function testOptionalKeysInUnionListWithIntRange($row, $twoOrThree, $twoOrMore, int $maxThree, $tenOrEleven): void
352+
protected function testOptionalKeysInUnionListWithIntRange($row, $listRow, $twoOrThree, $twoOrMore, int $maxThree, $tenOrEleven, $threeOrMoreInRangeLimit, $threeOrMoreOverRangeLimit): void
350353
{
351354
if (count($row) >= $twoOrThree) {
352355
assertType('array{0: int, 1: string|null, 2?: int|null}', $row);
@@ -371,6 +374,30 @@ protected function testOptionalKeysInUnionListWithIntRange($row, $twoOrThree, $t
371374
} else {
372375
assertType('array{string}|list{0: int, 1?: string|null, 2?: int|null, 3?: float|null}', $row);
373376
}
377+
378+
if (count($row) >= $threeOrMoreInRangeLimit) {
379+
assertType('list{0: int, 1?: string|null, 2?: int|null, 3?: float|null}', $row);
380+
} else {
381+
assertType('array{string}|list{0: int, 1?: string|null, 2?: int|null, 3?: float|null}', $row);
382+
}
383+
384+
if (count($listRow) >= $threeOrMoreInRangeLimit) {
385+
assertType('list{0: string, 1: string, 2: string, 3?: string, 4?: string, 5?: string, 6?: string, 7?: string, 8?: string, 9?: string, 10?: string, 11?: string, 12?: string, 13?: string, 14?: string, 15?: string, 16?: string, 17?: string, 18?: string, 19?: string, 20?: string, 21?: string, 22?: string, 23?: string, 24?: string, 25?: string, 26?: string, 27?: string, 28?: string, 29?: string, 30?: string, 31?: string}', $listRow);
386+
} else {
387+
assertType('list<string>', $listRow);
388+
}
389+
390+
if (count($row) >= $threeOrMoreOverRangeLimit) {
391+
assertType('list{0: int, 1?: string|null, 2?: int|null, 3?: float|null}', $row);
392+
} else {
393+
assertType('array{string}|list{0: int, 1?: string|null, 2?: int|null, 3?: float|null}', $row);
394+
}
395+
396+
if (count($listRow) >= $threeOrMoreOverRangeLimit) {
397+
assertType('non-empty-list<string>', $listRow);
398+
} else {
399+
assertType('list<string>', $listRow);
400+
}
374401
}
375402

376403
/**

0 commit comments

Comments
 (0)