Skip to content

Commit 29d5146

Browse files
committed
Limit int ranges when narrowing arrays via count()
And guard against non-existent offsets
1 parent 0401189 commit 29d5146

File tree

4 files changed

+66
-1
lines changed

4 files changed

+66
-1
lines changed

src/Analyser/TypeSpecifier.php

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1105,6 +1105,8 @@ private function specifyTypesForCountFuncCall(
11051105
if (
11061106
$sizeType instanceof IntegerRangeType
11071107
&& $sizeType->getMin() !== null
1108+
&& $sizeType->getMin() <= ConstantArrayTypeBuilder::ARRAY_COUNT_LIMIT
1109+
&& ($sizeType->getMax() === null || $sizeType->getMax() - $sizeType->getMin() <= ConstantArrayTypeBuilder::ARRAY_COUNT_LIMIT)
11081110
&& (
11091111
$isList->yes()
11101112
|| $isConstantArray->yes() && $arrayType->getKeyType()->isSuperTypeOf(IntegerRangeType::fromInterval(0, $sizeType->getMin() - 1))->yes()
@@ -1114,11 +1116,19 @@ private function specifyTypesForCountFuncCall(
11141116
$valueTypesBuilder = ConstantArrayTypeBuilder::createEmpty();
11151117
for ($i = 0; $i < $sizeType->getMin(); $i++) {
11161118
$offsetType = new ConstantIntegerType($i);
1119+
$hasOffset = $arrayType->hasOffsetValueType($offsetType);
1120+
if ($hasOffset->no()) {
1121+
break;
1122+
}
11171123
$valueTypesBuilder->setOffsetValueType($offsetType, $arrayType->getOffsetValueType($offsetType));
11181124
}
11191125
if ($sizeType->getMax() !== null) {
11201126
for ($i = $sizeType->getMin(); $i < $sizeType->getMax(); $i++) {
11211127
$offsetType = new ConstantIntegerType($i);
1128+
$hasOffset = $arrayType->hasOffsetValueType($offsetType);
1129+
if ($hasOffset->no()) {
1130+
break;
1131+
}
11221132
$valueTypesBuilder->setOffsetValueType($offsetType, $arrayType->getOffsetValueType($offsetType), true);
11231133
}
11241134
} elseif ($arrayType->isConstantArray()->yes()) {

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('array{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)