From 2d73870a1febaa031fab4d2d56977492d339d3ff Mon Sep 17 00:00:00 2001 From: Markus Staab Date: Tue, 13 Aug 2024 09:36:02 +0200 Subject: [PATCH 1/9] Narrow arrays in union based on count() with smaller/greater operator --- src/Analyser/TypeSpecifier.php | 43 +++++++++++++++- tests/PHPStan/Analyser/nsrt/bug11480.php | 65 ++++++++++++++++++++++++ 2 files changed, 106 insertions(+), 2 deletions(-) create mode 100644 tests/PHPStan/Analyser/nsrt/bug11480.php diff --git a/src/Analyser/TypeSpecifier.php b/src/Analyser/TypeSpecifier.php index 276d219dce..ae6affff2b 100644 --- a/src/Analyser/TypeSpecifier.php +++ b/src/Analyser/TypeSpecifier.php @@ -244,12 +244,51 @@ public function specifyTypesInCondition( && in_array(strtolower((string) $expr->right->name), ['count', 'sizeof'], true) && $leftType->isInteger()->yes() ) { + $argType = $scope->getType($expr->right->getArgs()[0]->value); + + if (count($expr->right->getArgs()) === 1) { + $isNormalCount = TrinaryLogic::createYes(); + } else { + $mode = $scope->getType($expr->right->getArgs()[1]->value); + $isNormalCount = (new ConstantIntegerType(COUNT_NORMAL))->isSuperTypeOf($mode)->or($argType->getIterableValueType()->isArray()->negate()); + } + + if ( + $isNormalCount->yes() + && $argType instanceof UnionType + && $leftType instanceof ConstantIntegerType + ) { + if ($orEqual) { + $constantType = IntegerRangeType::createAllGreaterThanOrEqualTo($leftType->getValue()); + } else { + $constantType = IntegerRangeType::createAllGreaterThan($leftType->getValue()); + } + + $result = []; + foreach ($argType->getTypes() as $innerType) { + $arraySize = $innerType->getArraySize(); + $isSize = $constantType->isSuperTypeOf($arraySize); + if ($context->truthy()) { + if ($isSize->no()) { + continue; + } + } + if ($context->falsey()) { + if (!$isSize->yes()) { + continue; + } + } + + $result[] = $innerType; + } + + return $this->create($expr->right->getArgs()[0]->value, TypeCombinator::union(...$result), $context, false, $scope, $rootExpr); + } + if ( $context->true() && (IntegerRangeType::createAllGreaterThanOrEqualTo(1 - $offset)->isSuperTypeOf($leftType)->yes()) || ($context->false() && (new ConstantIntegerType(1 - $offset))->isSuperTypeOf($leftType)->yes()) ) { - $argType = $scope->getType($expr->right->getArgs()[0]->value); - if ($context->truthy() && $argType->isArray()->maybe()) { $countables = []; if ($argType instanceof UnionType) { diff --git a/tests/PHPStan/Analyser/nsrt/bug11480.php b/tests/PHPStan/Analyser/nsrt/bug11480.php new file mode 100644 index 0000000000..348806be0d --- /dev/null +++ b/tests/PHPStan/Analyser/nsrt/bug11480.php @@ -0,0 +1,65 @@ + 0) { + assertType("array{'xy'}|array{0: 'ab', 1?: 'xy'}", $x); + } else { + assertType("array{}", $x); + } + assertType("array{}|array{'xy'}|array{0: 'ab', 1?: 'xy'}", $x); + + if (count($x) > 1) { + assertType("array{0: 'ab', 1?: 'xy'}", $x); + } else { + assertType("array{}|array{'xy'}|array{0: 'ab', 1?: 'xy'}", $x); + } + assertType("array{}|array{'xy'}|array{0: 'ab', 1?: 'xy'}", $x); + + if (count($x) >= 1) { + assertType("array{'xy'}|array{0: 'ab', 1?: 'xy'}", $x); + } else { + assertType("array{}", $x); + } + assertType("array{}|array{'xy'}|array{0: 'ab', 1?: 'xy'}", $x); + } + + public function arraySmallerThan(): void + { + $x = []; + if (rand(0, 1)) { + $x[] = 'ab'; + } + if (rand(0, 1)) { + $x[] = 'xy'; + } + + if (count($x) < 1) { + assertType("array{}", $x); + } else { + assertType("array{'xy'}|array{0: 'ab', 1?: 'xy'}", $x); + } + assertType("array{}|array{'xy'}|array{0: 'ab', 1?: 'xy'}", $x); + + if (count($x) <= 1) { + assertType("array{}|array{'xy'}|array{0: 'ab', 1?: 'xy'}", $x); + } else { + assertType("array{0: 'ab', 1?: 'xy'}", $x); + } + assertType("array{}|array{'xy'}|array{0: 'ab', 1?: 'xy'}", $x); + } +} From 98bb312daa2cf9c8427d97d47b7995a932b2e963 Mon Sep 17 00:00:00 2001 From: Markus Staab Date: Tue, 13 Aug 2024 09:38:29 +0200 Subject: [PATCH 2/9] Update bug-3558.php --- tests/PHPStan/Analyser/nsrt/bug-3558.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/PHPStan/Analyser/nsrt/bug-3558.php b/tests/PHPStan/Analyser/nsrt/bug-3558.php index 8c2e198013..2f71cf07d2 100644 --- a/tests/PHPStan/Analyser/nsrt/bug-3558.php +++ b/tests/PHPStan/Analyser/nsrt/bug-3558.php @@ -28,6 +28,6 @@ function (): void { } if(count($idGroups) > 1){ - assertType('array{1, array{1, 2}, array{1, 2}, array{1, 2}}|array{1}', $idGroups); + assertType('array{1, array{1, 2}, array{1, 2}, array{1, 2}}', $idGroups); } }; From 406203be504bc79781161dd5488d33d6c1e1709c Mon Sep 17 00:00:00 2001 From: Markus Staab Date: Tue, 13 Aug 2024 09:47:07 +0200 Subject: [PATCH 3/9] fix build --- src/Analyser/TypeSpecifier.php | 1 + 1 file changed, 1 insertion(+) diff --git a/src/Analyser/TypeSpecifier.php b/src/Analyser/TypeSpecifier.php index ae6affff2b..d79ef2ef17 100644 --- a/src/Analyser/TypeSpecifier.php +++ b/src/Analyser/TypeSpecifier.php @@ -256,6 +256,7 @@ public function specifyTypesInCondition( if ( $isNormalCount->yes() && $argType instanceof UnionType + && $argType->isConstantArray()->yes() && $leftType instanceof ConstantIntegerType ) { if ($orEqual) { From 33a2248eb432346095cd8b6795820e21623acc48 Mon Sep 17 00:00:00 2001 From: Markus Staab Date: Tue, 13 Aug 2024 10:07:59 +0200 Subject: [PATCH 4/9] extract narrowUnionBySize --- src/Analyser/TypeSpecifier.php | 80 ++++++++++++++++++++-------------- 1 file changed, 47 insertions(+), 33 deletions(-) diff --git a/src/Analyser/TypeSpecifier.php b/src/Analyser/TypeSpecifier.php index d79ef2ef17..26a965ac0b 100644 --- a/src/Analyser/TypeSpecifier.php +++ b/src/Analyser/TypeSpecifier.php @@ -246,44 +246,17 @@ public function specifyTypesInCondition( ) { $argType = $scope->getType($expr->right->getArgs()[0]->value); - if (count($expr->right->getArgs()) === 1) { - $isNormalCount = TrinaryLogic::createYes(); - } else { - $mode = $scope->getType($expr->right->getArgs()[1]->value); - $isNormalCount = (new ConstantIntegerType(COUNT_NORMAL))->isSuperTypeOf($mode)->or($argType->getIterableValueType()->isArray()->negate()); - } - - if ( - $isNormalCount->yes() - && $argType instanceof UnionType - && $argType->isConstantArray()->yes() - && $leftType instanceof ConstantIntegerType - ) { + if ($argType instanceof UnionType && $leftType instanceof ConstantIntegerType) { if ($orEqual) { - $constantType = IntegerRangeType::createAllGreaterThanOrEqualTo($leftType->getValue()); + $sizeType = IntegerRangeType::createAllGreaterThanOrEqualTo($leftType->getValue()); } else { - $constantType = IntegerRangeType::createAllGreaterThan($leftType->getValue()); + $sizeType = IntegerRangeType::createAllGreaterThan($leftType->getValue()); } - $result = []; - foreach ($argType->getTypes() as $innerType) { - $arraySize = $innerType->getArraySize(); - $isSize = $constantType->isSuperTypeOf($arraySize); - if ($context->truthy()) { - if ($isSize->no()) { - continue; - } - } - if ($context->falsey()) { - if (!$isSize->yes()) { - continue; - } - } - - $result[] = $innerType; + $narrowed = $this->narrowUnionBySize($expr->right, $argType, $sizeType, $context, $scope, $rootExpr); + if ($narrowed !== null) { + return $narrowed; } - - return $this->create($expr->right->getArgs()[0]->value, TypeCombinator::union(...$result), $context, false, $scope, $rootExpr); } if ( @@ -976,6 +949,47 @@ public function specifyTypesInCondition( return new SpecifiedTypes([], [], false, [], $rootExpr); } + private function narrowUnionBySize(FuncCall $countFuncCall, UnionType $argType, Type $sizeType, TypeSpecifierContext $context, Scope $scope, ?Expr $rootExpr): ?SpecifiedTypes + { + if (!$sizeType->isInteger()->yes()) { + return null; + } + + if (count($countFuncCall->getArgs()) === 1) { + $isNormalCount = TrinaryLogic::createYes(); + } else { + $mode = $scope->getType($countFuncCall->getArgs()[1]->value); + $isNormalCount = (new ConstantIntegerType(COUNT_NORMAL))->isSuperTypeOf($mode)->or($argType->getIterableValueType()->isArray()->negate()); + } + + if ( + $isNormalCount->yes() + && $argType->isConstantArray()->yes() + ) { + $result = []; + foreach ($argType->getTypes() as $innerType) { + $arraySize = $innerType->getArraySize(); + $isSize = $sizeType->isSuperTypeOf($arraySize); + if ($context->truthy()) { + if ($isSize->no()) { + continue; + } + } + if ($context->falsey()) { + if (!$isSize->yes()) { + continue; + } + } + + $result[] = $innerType; + } + + return $this->create($countFuncCall->getArgs()[0]->value, TypeCombinator::union(...$result), $context, false, $scope, $rootExpr); + } + + return null; + } + private function specifyTypesForConstantBinaryExpression( Expr $exprNode, ConstantScalarType $constantType, From add6977b3df13aa6e4d525030a45bcc00c554199 Mon Sep 17 00:00:00 2001 From: Markus Staab Date: Tue, 13 Aug 2024 10:23:27 +0200 Subject: [PATCH 5/9] re-use narrowUnionBySize --- src/Analyser/TypeSpecifier.php | 40 ++++++++++------------------------ 1 file changed, 11 insertions(+), 29 deletions(-) diff --git a/src/Analyser/TypeSpecifier.php b/src/Analyser/TypeSpecifier.php index 26a965ac0b..2f3840b5f9 100644 --- a/src/Analyser/TypeSpecifier.php +++ b/src/Analyser/TypeSpecifier.php @@ -1040,36 +1040,11 @@ private function specifyTypesForConstantBinaryExpression( ) { $argType = $scope->getType($exprNode->getArgs()[0]->value); - if (count($exprNode->getArgs()) === 1) { - $isNormalCount = TrinaryLogic::createYes(); - } else { - $mode = $scope->getType($exprNode->getArgs()[1]->value); - $isNormalCount = (new ConstantIntegerType(COUNT_NORMAL))->isSuperTypeOf($mode)->or($argType->getIterableValueType()->isArray()->negate()); - } - - if ( - $isNormalCount->yes() - && $argType instanceof UnionType - ) { - $result = []; - foreach ($argType->getTypes() as $innerType) { - $arraySize = $innerType->getArraySize(); - $isSize = $constantType->isSuperTypeOf($arraySize); - if ($context->truthy()) { - if ($isSize->no()) { - continue; - } - } - if ($context->falsey()) { - if (!$isSize->yes()) { - continue; - } - } - - $result[] = $innerType; + if ($argType instanceof UnionType) { + $narrowed = $this->narrowUnionBySize($exprNode, $argType, $constantType, $context, $scope, $rootExpr); + if ($narrowed !== null) { + return $narrowed; } - - return $this->create($exprNode->getArgs()[0]->value, TypeCombinator::union(...$result), $context, false, $scope, $rootExpr); } if ($context->truthy() || $constantType->getValue() === 0) { @@ -1079,6 +1054,13 @@ private function specifyTypesForConstantBinaryExpression( } if ($argType->isArray()->yes()) { + if (count($exprNode->getArgs()) === 1) { + $isNormalCount = TrinaryLogic::createYes(); + } else { + $mode = $scope->getType($exprNode->getArgs()[1]->value); + $isNormalCount = (new ConstantIntegerType(COUNT_NORMAL))->isSuperTypeOf($mode)->or($argType->getIterableValueType()->isArray()->negate()); + } + $funcTypes = $this->create($exprNode, $constantType, $context, false, $scope, $rootExpr); if ($isNormalCount->yes() && $argType->isList()->yes() && $context->truthy() && $constantType->getValue() < ConstantArrayTypeBuilder::ARRAY_COUNT_LIMIT) { $valueTypesBuilder = ConstantArrayTypeBuilder::createEmpty(); From 028fdcaa7c2985848469239bc29fd61379340123 Mon Sep 17 00:00:00 2001 From: Markus Staab Date: Tue, 13 Aug 2024 20:37:29 +0200 Subject: [PATCH 6/9] Update bug11480.php --- tests/PHPStan/Analyser/nsrt/bug11480.php | 26 ++++++++++++++++++++++++ 1 file changed, 26 insertions(+) diff --git a/tests/PHPStan/Analyser/nsrt/bug11480.php b/tests/PHPStan/Analyser/nsrt/bug11480.php index 348806be0d..f3b215058f 100644 --- a/tests/PHPStan/Analyser/nsrt/bug11480.php +++ b/tests/PHPStan/Analyser/nsrt/bug11480.php @@ -62,4 +62,30 @@ public function arraySmallerThan(): void } assertType("array{}|array{'xy'}|array{0: 'ab', 1?: 'xy'}", $x); } + + public function intRangeCount(): void + { + $count = 1; + if (rand(0, 1)) { + $count++; + } + + $x = []; + if (rand(0, 1)) { + $x[] = 'ab'; + } + if (rand(0, 1)) { + $x[] = 'xy'; + } + + assertType('1|2', $count); + + assertType("array{}|array{'xy'}|array{0: 'ab', 1?: 'xy'}", $x); + 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{}|array{'xy'}|array{0: 'ab', 1?: 'xy'}", $x); + } } From d8d22b81e93c72bc564c266b9c1615409c8baa56 Mon Sep 17 00:00:00 2001 From: Markus Staab Date: Tue, 13 Aug 2024 20:43:38 +0200 Subject: [PATCH 7/9] Update bug11480.php --- tests/PHPStan/Analyser/nsrt/bug11480.php | 24 +++++++++++++++++++++++- 1 file changed, 23 insertions(+), 1 deletion(-) diff --git a/tests/PHPStan/Analyser/nsrt/bug11480.php b/tests/PHPStan/Analyser/nsrt/bug11480.php index f3b215058f..08a220a624 100644 --- a/tests/PHPStan/Analyser/nsrt/bug11480.php +++ b/tests/PHPStan/Analyser/nsrt/bug11480.php @@ -63,7 +63,7 @@ public function arraySmallerThan(): void assertType("array{}|array{'xy'}|array{0: 'ab', 1?: 'xy'}", $x); } - public function intRangeCount(): void + public function intUnionCount(): void { $count = 1; if (rand(0, 1)) { @@ -88,4 +88,26 @@ public function intRangeCount(): void } assertType("array{}|array{'xy'}|array{0: 'ab', 1?: 'xy'}", $x); } + + /** + * @param int<1,2> $count + */ + public function intRangeCount($count): void + { + $x = []; + if (rand(0, 1)) { + $x[] = 'ab'; + } + if (rand(0, 1)) { + $x[] = 'xy'; + } + + assertType("array{}|array{'xy'}|array{0: 'ab', 1?: 'xy'}", $x); + 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{}|array{'xy'}|array{0: 'ab', 1?: 'xy'}", $x); + } } From c48a535b191542912e1a53255d7f0f3b4ac96d60 Mon Sep 17 00:00:00 2001 From: Markus Staab Date: Tue, 13 Aug 2024 20:53:19 +0200 Subject: [PATCH 8/9] simplify --- src/Analyser/TypeSpecifier.php | 4 ---- 1 file changed, 4 deletions(-) diff --git a/src/Analyser/TypeSpecifier.php b/src/Analyser/TypeSpecifier.php index 2f3840b5f9..f4f643fa28 100644 --- a/src/Analyser/TypeSpecifier.php +++ b/src/Analyser/TypeSpecifier.php @@ -951,10 +951,6 @@ public function specifyTypesInCondition( private function narrowUnionBySize(FuncCall $countFuncCall, UnionType $argType, Type $sizeType, TypeSpecifierContext $context, Scope $scope, ?Expr $rootExpr): ?SpecifiedTypes { - if (!$sizeType->isInteger()->yes()) { - return null; - } - if (count($countFuncCall->getArgs()) === 1) { $isNormalCount = TrinaryLogic::createYes(); } else { From fe581fa967dd6fb6e7448cda46b73d4ae55bab51 Mon Sep 17 00:00:00 2001 From: Markus Staab Date: Tue, 13 Aug 2024 20:54:47 +0200 Subject: [PATCH 9/9] Update TypeSpecifier.php --- 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 f4f643fa28..08e0c65f0c 100644 --- a/src/Analyser/TypeSpecifier.php +++ b/src/Analyser/TypeSpecifier.php @@ -253,7 +253,7 @@ public function specifyTypesInCondition( $sizeType = IntegerRangeType::createAllGreaterThan($leftType->getValue()); } - $narrowed = $this->narrowUnionBySize($expr->right, $argType, $sizeType, $context, $scope, $rootExpr); + $narrowed = $this->narrowUnionByArraySize($expr->right, $argType, $sizeType, $context, $scope, $rootExpr); if ($narrowed !== null) { return $narrowed; } @@ -949,7 +949,7 @@ public function specifyTypesInCondition( return new SpecifiedTypes([], [], false, [], $rootExpr); } - private function narrowUnionBySize(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 (count($countFuncCall->getArgs()) === 1) { $isNormalCount = TrinaryLogic::createYes(); @@ -1037,7 +1037,7 @@ private function specifyTypesForConstantBinaryExpression( $argType = $scope->getType($exprNode->getArgs()[0]->value); if ($argType instanceof UnionType) { - $narrowed = $this->narrowUnionBySize($exprNode, $argType, $constantType, $context, $scope, $rootExpr); + $narrowed = $this->narrowUnionByArraySize($exprNode, $argType, $constantType, $context, $scope, $rootExpr); if ($narrowed !== null) { return $narrowed; }