diff --git a/src/Analyser/TypeSpecifier.php b/src/Analyser/TypeSpecifier.php index 52b3d76d45..1e58feac38 100644 --- a/src/Analyser/TypeSpecifier.php +++ b/src/Analyser/TypeSpecifier.php @@ -1087,10 +1087,7 @@ private function specifyTypesForCountFuncCall( if ( $sizeType instanceof ConstantIntegerType && $sizeType->getValue() < ConstantArrayTypeBuilder::ARRAY_COUNT_LIMIT - && ( - $isList->yes() - || $isConstantArray->yes() && $arrayType->getKeyType()->isSuperTypeOf(IntegerRangeType::fromInterval(0, $sizeType->getValue() - 1))->yes() - ) + && $arrayType->getKeyType()->isSuperTypeOf(IntegerRangeType::fromInterval(0, $sizeType->getValue() - 1))->yes() ) { // turn optional offsets non-optional $valueTypesBuilder = ConstantArrayTypeBuilder::createEmpty(); @@ -1105,21 +1102,23 @@ private function specifyTypesForCountFuncCall( if ( $sizeType instanceof IntegerRangeType && $sizeType->getMin() !== null - && ( - $isList->yes() - || $isConstantArray->yes() && $arrayType->getKeyType()->isSuperTypeOf(IntegerRangeType::fromInterval(0, $sizeType->getMin() - 1))->yes() - ) + && $sizeType->getMin() < ConstantArrayTypeBuilder::ARRAY_COUNT_LIMIT + && $arrayType->getKeyType()->isSuperTypeOf(IntegerRangeType::fromInterval(0, ($sizeType->getMax() ?? $sizeType->getMin()) - 1))->yes() ) { + $builderData = []; // turn optional offsets non-optional - $valueTypesBuilder = ConstantArrayTypeBuilder::createEmpty(); for ($i = 0; $i < $sizeType->getMin(); $i++) { $offsetType = new ConstantIntegerType($i); - $valueTypesBuilder->setOffsetValueType($offsetType, $arrayType->getOffsetValueType($offsetType)); + $builderData[] = [$offsetType, $arrayType->getOffsetValueType($offsetType), false]; } if ($sizeType->getMax() !== null) { + if ($sizeType->getMax() - $sizeType->getMin() > ConstantArrayTypeBuilder::ARRAY_COUNT_LIMIT) { + $resultTypes[] = $arrayType; + continue; + } for ($i = $sizeType->getMin(); $i < $sizeType->getMax(); $i++) { $offsetType = new ConstantIntegerType($i); - $valueTypesBuilder->setOffsetValueType($offsetType, $arrayType->getOffsetValueType($offsetType), true); + $builderData[] = [$offsetType, $arrayType->getOffsetValueType($offsetType), true]; } } elseif ($arrayType->isConstantArray()->yes()) { for ($i = $sizeType->getMin();; $i++) { @@ -1128,14 +1127,24 @@ private function specifyTypesForCountFuncCall( if ($hasOffset->no()) { break; } - $valueTypesBuilder->setOffsetValueType($offsetType, $arrayType->getOffsetValueType($offsetType), !$hasOffset->yes()); + $builderData[] = [$offsetType, $arrayType->getOffsetValueType($offsetType), !$hasOffset->yes()]; } } else { $resultTypes[] = TypeCombinator::intersect($arrayType, new NonEmptyArrayType()); continue; } - $resultTypes[] = $valueTypesBuilder->getArray(); + if (count($builderData) > ConstantArrayTypeBuilder::ARRAY_COUNT_LIMIT) { + $resultTypes[] = $arrayType; + continue; + } + + $builder = ConstantArrayTypeBuilder::createEmpty(); + foreach ($builderData as [$offsetType, $valueType, $optional]) { + $builder->setOffsetValueType($offsetType, $valueType, $optional); + } + + $resultTypes[] = $builder->getArray(); continue; } diff --git a/tests/PHPStan/Analyser/AnalyserIntegrationTest.php b/tests/PHPStan/Analyser/AnalyserIntegrationTest.php index e7678b1449..d515fb2379 100644 --- a/tests/PHPStan/Analyser/AnalyserIntegrationTest.php +++ b/tests/PHPStan/Analyser/AnalyserIntegrationTest.php @@ -1542,6 +1542,12 @@ public function testBug12159(): void $this->assertNoErrors($errors); } + public function testBug12787(): void + { + $errors = $this->runAnalyse(__DIR__ . '/data/bug-12787.php'); + $this->assertNoErrors($errors); + } + /** * @param string[]|null $allAnalysedFiles * @return Error[] diff --git a/tests/PHPStan/Analyser/data/bug-12787.php b/tests/PHPStan/Analyser/data/bug-12787.php new file mode 100644 index 0000000000..189d88cc8b --- /dev/null +++ b/tests/PHPStan/Analyser/data/bug-12787.php @@ -0,0 +1,22 @@ + $listRow * @param int<2, 3> $twoOrThree * @param int<2, max> $twoOrMore * @param int $maxThree * @param int<10, 11> $tenOrEleven + * @param int<3, 32> $threeOrMoreInRangeLimit + * @param int<3, 512> $threeOrMoreOverRangeLimit */ - protected function testOptionalKeysInUnionListWithIntRange($row, $twoOrThree, $twoOrMore, int $maxThree, $tenOrEleven): void + protected function testOptionalKeysInUnionListWithIntRange($row, $listRow, $twoOrThree, $twoOrMore, int $maxThree, $tenOrEleven, $threeOrMoreInRangeLimit, $threeOrMoreOverRangeLimit): void { if (count($row) >= $twoOrThree) { assertType('array{0: int, 1: string|null, 2?: int|null}', $row); @@ -371,6 +374,30 @@ protected function testOptionalKeysInUnionListWithIntRange($row, $twoOrThree, $t } else { assertType('array{string}|list{0: int, 1?: string|null, 2?: int|null, 3?: float|null}', $row); } + + if (count($row) >= $threeOrMoreInRangeLimit) { + assertType('list{0: int, 1?: string|null, 2?: int|null, 3?: float|null}', $row); + } else { + assertType('array{string}|list{0: int, 1?: string|null, 2?: int|null, 3?: float|null}', $row); + } + + if (count($listRow) >= $threeOrMoreInRangeLimit) { + 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); + } else { + assertType('list', $listRow); + } + + if (count($row) >= $threeOrMoreOverRangeLimit) { + assertType('list{0: int, 1?: string|null, 2?: int|null, 3?: float|null}', $row); + } else { + assertType('array{string}|list{0: int, 1?: string|null, 2?: int|null, 3?: float|null}', $row); + } + + if (count($listRow) >= $threeOrMoreOverRangeLimit) { + assertType('non-empty-list', $listRow); + } else { + assertType('list', $listRow); + } } /**