From 781a5dafa18ef07aa8bb0e0724ea781b8114fa5d Mon Sep 17 00:00:00 2001 From: Markus Staab Date: Thu, 22 Aug 2024 10:23:06 +0200 Subject: [PATCH 1/3] Narrow arrays in union based on count() with IntegerRangeType --- src/Analyser/TypeSpecifier.php | 59 ++++++++++++++++++---- tests/PHPStan/Analyser/nsrt/bug-4700.php | 10 ++-- tests/PHPStan/Analyser/nsrt/bug11480.php | 6 +-- tests/PHPStan/Analyser/nsrt/list-count.php | 47 +++++++++++++++++ 4 files changed, 105 insertions(+), 17 deletions(-) diff --git a/src/Analyser/TypeSpecifier.php b/src/Analyser/TypeSpecifier.php index 1c1732089d..1750903c61 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); @@ -949,8 +954,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 { @@ -971,7 +980,7 @@ private function narrowUnionByArraySize(FuncCall $countFuncCall, UnionType $argT continue; } - $constArray = $this->turnListIntoConstantArray($countFuncCall, $innerType, $sizeType, $scope); + $constArray = $this->turnListIntoConstantArray($countFuncCall, $innerType, $sizeType, $context, $scope); if ($constArray !== null) { $innerType = $constArray; } @@ -991,7 +1000,7 @@ private function narrowUnionByArraySize(FuncCall $countFuncCall, UnionType $argT return null; } - private function turnListIntoConstantArray(FuncCall $countFuncCall, Type $type, Type $sizeType, Scope $scope): ?Type + private function turnListIntoConstantArray(FuncCall $countFuncCall, Type $type, Type $sizeType, TypeSpecifierContext $context, Scope $scope): ?Type { $argType = $scope->getType($countFuncCall->getArgs()[0]->value); @@ -1017,6 +1026,38 @@ private function turnListIntoConstantArray(FuncCall $countFuncCall, Type $type, return $valueTypesBuilder->getArray(); } + if ( + $context->truthy() + && $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; } @@ -1093,7 +1134,7 @@ private function specifyTypesForConstantBinaryExpression( } $funcTypes = $this->create($exprNode, $constantType, $context, false, $scope, $rootExpr); - $constArray = $this->turnListIntoConstantArray($exprNode, $argType, $constantType, $scope); + $constArray = $this->turnListIntoConstantArray($exprNode, $argType, $constantType, $context, $scope); if ($context->truthy() && $constArray !== null) { $valueTypes = $this->create($exprNode->getArgs()[0]->value, $constArray, $context, false, $scope, $rootExpr); } else { 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); + } + } } From 74c6de1fd8873632735ad7084827a96e3c0aa6ca Mon Sep 17 00:00:00 2001 From: Markus Staab Date: Thu, 22 Aug 2024 11:46:59 +0200 Subject: [PATCH 2/3] simplify --- src/Analyser/TypeSpecifier.php | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/Analyser/TypeSpecifier.php b/src/Analyser/TypeSpecifier.php index 1750903c61..3689dba035 100644 --- a/src/Analyser/TypeSpecifier.php +++ b/src/Analyser/TypeSpecifier.php @@ -1027,8 +1027,7 @@ private function turnListIntoConstantArray(FuncCall $countFuncCall, Type $type, } if ( - $context->truthy() - && $isNormalCount->yes() + $isNormalCount->yes() && $type->isList()->yes() && $sizeType instanceof IntegerRangeType && $sizeType->getMin() !== null From b04d7b7216e50997aed30b8c82be5397e9535f38 Mon Sep 17 00:00:00 2001 From: Markus Staab Date: Thu, 22 Aug 2024 11:49:36 +0200 Subject: [PATCH 3/3] simplify --- src/Analyser/TypeSpecifier.php | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/Analyser/TypeSpecifier.php b/src/Analyser/TypeSpecifier.php index 3689dba035..5a8e742448 100644 --- a/src/Analyser/TypeSpecifier.php +++ b/src/Analyser/TypeSpecifier.php @@ -980,7 +980,7 @@ private function narrowUnionByArraySize(FuncCall $countFuncCall, UnionType $argT continue; } - $constArray = $this->turnListIntoConstantArray($countFuncCall, $innerType, $sizeType, $context, $scope); + $constArray = $this->turnListIntoConstantArray($countFuncCall, $innerType, $sizeType, $scope); if ($constArray !== null) { $innerType = $constArray; } @@ -1000,7 +1000,7 @@ private function narrowUnionByArraySize(FuncCall $countFuncCall, UnionType $argT return null; } - private function turnListIntoConstantArray(FuncCall $countFuncCall, Type $type, Type $sizeType, TypeSpecifierContext $context, Scope $scope): ?Type + private function turnListIntoConstantArray(FuncCall $countFuncCall, Type $type, Type $sizeType, Scope $scope): ?Type { $argType = $scope->getType($countFuncCall->getArgs()[0]->value); @@ -1133,7 +1133,7 @@ private function specifyTypesForConstantBinaryExpression( } $funcTypes = $this->create($exprNode, $constantType, $context, false, $scope, $rootExpr); - $constArray = $this->turnListIntoConstantArray($exprNode, $argType, $constantType, $context, $scope); + $constArray = $this->turnListIntoConstantArray($exprNode, $argType, $constantType, $scope); if ($context->truthy() && $constArray !== null) { $valueTypes = $this->create($exprNode->getArgs()[0]->value, $constArray, $context, false, $scope, $rootExpr); } else {