diff --git a/src/Analyser/TypeSpecifier.php b/src/Analyser/TypeSpecifier.php index 8965b6a7eb..5601c024af 100644 --- a/src/Analyser/TypeSpecifier.php +++ b/src/Analyser/TypeSpecifier.php @@ -246,11 +246,16 @@ public function specifyTypesInCondition( ) { $argType = $scope->getType($expr->right->getArgs()[0]->value); - if ($argType instanceof UnionType && $leftType instanceof ConstantIntegerType) { - if ($orEqual) { - $sizeType = IntegerRangeType::createAllGreaterThanOrEqualTo($leftType->getValue()); - } else { - $sizeType = IntegerRangeType::createAllGreaterThan($leftType->getValue()); + if ($argType instanceof UnionType) { + $sizeType = null; + if ($leftType instanceof ConstantIntegerType) { + if ($orEqual) { + $sizeType = IntegerRangeType::createAllGreaterThanOrEqualTo($leftType->getValue()); + } else { + $sizeType = IntegerRangeType::createAllGreaterThan($leftType->getValue()); + } + } elseif ($leftType instanceof IntegerRangeType) { + $sizeType = $leftType; } $narrowed = $this->narrowUnionByArraySize($expr->right, $argType, $sizeType, $context, $scope, $rootExpr); @@ -943,8 +948,12 @@ public function specifyTypesInCondition( return new SpecifiedTypes([], [], false, [], $rootExpr); } - private function narrowUnionByArraySize(FuncCall $countFuncCall, UnionType $argType, Type $sizeType, TypeSpecifierContext $context, Scope $scope, ?Expr $rootExpr): ?SpecifiedTypes + private function narrowUnionByArraySize(FuncCall $countFuncCall, UnionType $argType, ?Type $sizeType, TypeSpecifierContext $context, Scope $scope, ?Expr $rootExpr): ?SpecifiedTypes { + if ($sizeType === null) { + return null; + } + if (count($countFuncCall->getArgs()) === 1) { $isNormalCount = TrinaryLogic::createYes(); } else { @@ -1011,6 +1020,37 @@ private function turnListIntoConstantArray(FuncCall $countFuncCall, Type $type, return $valueTypesBuilder->getArray(); } + if ( + $isNormalCount->yes() + && $type->isList()->yes() + && $sizeType instanceof IntegerRangeType + && $sizeType->getMin() !== null + ) { + // turn optional offsets non-optional + $valueTypesBuilder = ConstantArrayTypeBuilder::createEmpty(); + for ($i = 0; $i < $sizeType->getMin(); $i++) { + $offsetType = new ConstantIntegerType($i); + $valueTypesBuilder->setOffsetValueType($offsetType, $type->getOffsetValueType($offsetType)); + } + if ($sizeType->getMax() !== null) { + for ($i = $sizeType->getMin(); $i < $sizeType->getMax(); $i++) { + $offsetType = new ConstantIntegerType($i); + $valueTypesBuilder->setOffsetValueType($offsetType, $type->getOffsetValueType($offsetType), true); + } + } else { + for ($i = $sizeType->getMin();; $i++) { + $offsetType = new ConstantIntegerType($i); + $hasOffset = $type->hasOffsetValueType($offsetType); + if ($hasOffset->no()) { + break; + } + $valueTypesBuilder->setOffsetValueType($offsetType, $type->getOffsetValueType($offsetType), !$hasOffset->yes()); + } + + } + return $valueTypesBuilder->getArray(); + } + return null; } diff --git a/tests/PHPStan/Analyser/nsrt/bug-4700.php b/tests/PHPStan/Analyser/nsrt/bug-4700.php index 45f09eaf9f..078ea41b12 100644 --- a/tests/PHPStan/Analyser/nsrt/bug-4700.php +++ b/tests/PHPStan/Analyser/nsrt/bug-4700.php @@ -21,8 +21,8 @@ function(array $array, int $count): void { assertType('int<1, 5>', count($a)); assertType('array{0: mixed~null, 1?: mixed~null, 2?: mixed~null, 3?: mixed~null, 4?: mixed~null}', $a); } else { - assertType('int<0, 5>', count($a)); - assertType('array{}|array{0: mixed~null, 1?: mixed~null, 2?: mixed~null, 3?: mixed~null, 4?: mixed~null}', $a); + assertType('0', count($a)); + assertType('array{}', $a); } }; @@ -40,10 +40,10 @@ function(array $array, int $count): void { if (isset($array['d'])) $a[] = $array['d']; if (isset($array['e'])) $a[] = $array['e']; if (count($a) > $count) { - assertType('int<2, 5>', count($a)); + assertType('int<1, 5>', count($a)); assertType('array{0: mixed~null, 1?: mixed~null, 2?: mixed~null, 3?: mixed~null, 4?: mixed~null}', $a); } else { - assertType('int<0, 5>', count($a)); - assertType('array{}|array{0: mixed~null, 1?: mixed~null, 2?: mixed~null, 3?: mixed~null, 4?: mixed~null}', $a); + assertType('0', count($a)); + assertType('array{}', $a); } }; diff --git a/tests/PHPStan/Analyser/nsrt/bug11480.php b/tests/PHPStan/Analyser/nsrt/bug11480.php index 08a220a624..f4d3898790 100644 --- a/tests/PHPStan/Analyser/nsrt/bug11480.php +++ b/tests/PHPStan/Analyser/nsrt/bug11480.php @@ -24,7 +24,7 @@ public function arrayGreatherThan(): void assertType("array{}|array{'xy'}|array{0: 'ab', 1?: 'xy'}", $x); if (count($x) > 1) { - assertType("array{0: 'ab', 1?: 'xy'}", $x); + assertType("array{'ab', 'xy'}", $x); } else { assertType("array{}|array{'xy'}|array{0: 'ab', 1?: 'xy'}", $x); } @@ -58,7 +58,7 @@ public function arraySmallerThan(): void if (count($x) <= 1) { assertType("array{}|array{'xy'}|array{0: 'ab', 1?: 'xy'}", $x); } else { - assertType("array{0: 'ab', 1?: 'xy'}", $x); + assertType("array{'ab', 'xy'}", $x); } assertType("array{}|array{'xy'}|array{0: 'ab', 1?: 'xy'}", $x); } @@ -106,7 +106,7 @@ public function intRangeCount($count): void if (count($x) >= $count) { assertType("array{'xy'}|array{0: 'ab', 1?: 'xy'}", $x); } else { - assertType("array{}|array{'xy'}|array{0: 'ab', 1?: 'xy'}", $x); + assertType("array{}", $x); } assertType("array{}|array{'xy'}|array{0: 'ab', 1?: 'xy'}", $x); } diff --git a/tests/PHPStan/Analyser/nsrt/list-count.php b/tests/PHPStan/Analyser/nsrt/list-count.php index 006bf149d8..caf0a17c87 100644 --- a/tests/PHPStan/Analyser/nsrt/list-count.php +++ b/tests/PHPStan/Analyser/nsrt/list-count.php @@ -339,4 +339,51 @@ protected function testOptionalKeysInUnionArray($row): void } } + /** + * @param array{string}|list{0: int, 1?: string|null, 2?: int|null, 3?: float|null} $row + * @param int<2, 3> $twoOrThree + * @param int<2, max> $twoOrMore + * @param int $maxThree + * @param int<10, 11> $tenOrEleven + */ + protected function testOptionalKeysInUnionListWithIntRange($row, $twoOrThree, $twoOrMore, int $maxThree, $tenOrEleven): void + { + if (count($row) >= $twoOrThree) { + assertType('array{0: int, 1: string|null, 2?: int|null}', $row); + } else { + assertType('(array{0: int, 1?: string|null, 2?: int|null, 3?: float|null}&list)|array{string}', $row); + } + + if (count($row) >= $tenOrEleven) { + assertType('*NEVER*', $row); + } else { + assertType('(array{0: int, 1?: string|null, 2?: int|null, 3?: float|null}&list)|array{string}', $row); + } + + if (count($row) >= $twoOrMore) { + assertType('array{0: int, 1: string|null, 2?: int|null, 3?: float|null}&list', $row); + } else { + assertType('(array{0: int, 1?: string|null, 2?: int|null, 3?: float|null}&list)|array{string}', $row); + } + + if (count($row) >= $maxThree) { + assertType('(array{0: int, 1?: string|null, 2?: int|null, 3?: float|null}&list)|array{string}', $row); + } else { + assertType('array{0: int, 1?: string|null, 2?: int|null, 3?: float|null}&list', $row); + } + } + + /** + * @param array{string}|array{0: int, 1?: string|null, 2?: int|null, 3?: float|null} $row + * @param int<2, 3> $twoOrThree + */ + protected function testOptionalKeysInUnionArrayWithIntRange($row, $twoOrThree): void + { + // doesn't narrow because no list + if (count($row) >= $twoOrThree) { + assertType('array{0: int, 1?: string|null, 2?: int|null, 3?: float|null}', $row); + } else { + assertType('array{0: int, 1?: string|null, 2?: int|null, 3?: float|null}|array{string}', $row); + } + } }