From 54d135437b607aa07ac74657a95d9f539b3268ed Mon Sep 17 00:00:00 2001 From: Markus Staab Date: Sun, 2 Nov 2025 18:24:17 +0100 Subject: [PATCH 1/4] `count(non-empty-array, COUNT_RECURSIVE)` is `int<1, max>` --- .../Php/CountFunctionReturnTypeExtension.php | 22 +++++-- .../PHPStan/Analyser/nsrt/count-recursive.php | 66 +++++++++++++++++++ 2 files changed, 84 insertions(+), 4 deletions(-) create mode 100644 tests/PHPStan/Analyser/nsrt/count-recursive.php diff --git a/src/Type/Php/CountFunctionReturnTypeExtension.php b/src/Type/Php/CountFunctionReturnTypeExtension.php index b87d34c466..4b257a00f7 100644 --- a/src/Type/Php/CountFunctionReturnTypeExtension.php +++ b/src/Type/Php/CountFunctionReturnTypeExtension.php @@ -6,8 +6,10 @@ use PHPStan\Analyser\Scope; use PHPStan\DependencyInjection\AutowiredService; use PHPStan\Reflection\FunctionReflection; +use PHPStan\TrinaryLogic; use PHPStan\Type\Constant\ConstantIntegerType; use PHPStan\Type\DynamicFunctionReturnTypeExtension; +use PHPStan\Type\IntegerRangeType; use PHPStan\Type\Type; use function count; use function in_array; @@ -32,14 +34,26 @@ public function getTypeFromFunctionCall( return null; } - if (count($functionCall->getArgs()) > 1) { - $mode = $scope->getType($functionCall->getArgs()[1]->value); - if ($mode->isSuperTypeOf(new ConstantIntegerType(COUNT_RECURSIVE))->yes()) { - return null; + $arrayType = $scope->getType($functionCall->getArgs()[0]->value); + if (!$this->isNormalCount($functionCall, $arrayType, $scope)->yes()) { + if ($arrayType->isIterableAtLeastOnce()->yes()) { + return IntegerRangeType::fromInterval(1, null); } + return null; } return $scope->getType($functionCall->getArgs()[0]->value)->getArraySize(); } + private function isNormalCount(FuncCall $countFuncCall, Type $countedType, Scope $scope,): TrinaryLogic + { + if (count($countFuncCall->getArgs()) === 1) { + $isNormalCount = TrinaryLogic::createYes(); + } else { + $mode = $scope->getType($countFuncCall->getArgs()[1]->value); + $isNormalCount = (new ConstantIntegerType(COUNT_NORMAL))->isSuperTypeOf($mode)->result->or($countedType->getIterableValueType()->isArray()->negate()); + } + return $isNormalCount; + } + } diff --git a/tests/PHPStan/Analyser/nsrt/count-recursive.php b/tests/PHPStan/Analyser/nsrt/count-recursive.php new file mode 100644 index 0000000000..283fbbff9c --- /dev/null +++ b/tests/PHPStan/Analyser/nsrt/count-recursive.php @@ -0,0 +1,66 @@ + 2) { + assertType('non-empty-array', $arr); + assertType('int<3, max>', count($arr)); + assertType('int<1, max>', count($arr, COUNT_NORMAL)); // could be int<3, max> + assertType('int<1, max>', count($arr, COUNT_RECURSIVE)); + } + } + + public function countArrayNormal(array $arr): void + { + if (count($arr, COUNT_NORMAL) > 2) { + assertType('non-empty-array', $arr); + assertType('int<1, max>', count($arr)); // could be int<3, max> + assertType('int<3, max>', count($arr, COUNT_NORMAL)); + assertType('int<1, max>', count($arr, COUNT_RECURSIVE)); + } + } + + public function countArrayRecursive(array $arr): void + { + if (count($arr, COUNT_RECURSIVE) > 2) { + assertType('non-empty-array', $arr); + assertType('int<1, max>', count($arr)); + assertType('int<1, max>', count($arr, COUNT_NORMAL)); + assertType('int<3, max>', count($arr, COUNT_RECURSIVE)); + } + } + + /** @param list $list */ + public function countList($list): void + { + if (count($list) > 2) { + assertType('int<3, max>', count($list)); + assertType('int<1, max>', count($list, COUNT_NORMAL)); + assertType('int<1, max>', count($list, COUNT_RECURSIVE)); + } + } + + /** @param list $list */ + public function countListNormal($list): void + { + if (count($list, COUNT_NORMAL) > 2) { + assertType('int<1, max>', count($list)); + assertType('int<3, max>', count($list, COUNT_NORMAL)); + assertType('int<1, max>', count($list, COUNT_RECURSIVE)); + } + } + + /** @param list $list */ + public function countListRecursive($list): void + { + if (count($list, COUNT_RECURSIVE) > 2) { + assertType('int<1, max>', count($list)); + assertType('int<1, max>', count($list, COUNT_NORMAL)); + assertType('int<3, max>', count($list, COUNT_RECURSIVE)); + } + } +} From b363706c05a6b4012bec06efe86f31368f809d0d Mon Sep 17 00:00:00 2001 From: Markus Staab Date: Sun, 2 Nov 2025 18:28:02 +0100 Subject: [PATCH 2/4] Update count-recursive.php --- tests/PHPStan/Analyser/nsrt/count-recursive.php | 2 ++ 1 file changed, 2 insertions(+) diff --git a/tests/PHPStan/Analyser/nsrt/count-recursive.php b/tests/PHPStan/Analyser/nsrt/count-recursive.php index 283fbbff9c..9795bdecc2 100644 --- a/tests/PHPStan/Analyser/nsrt/count-recursive.php +++ b/tests/PHPStan/Analyser/nsrt/count-recursive.php @@ -1,5 +1,7 @@ Date: Sun, 2 Nov 2025 18:34:07 +0100 Subject: [PATCH 3/4] cs --- src/Type/Php/CountFunctionReturnTypeExtension.php | 11 ++++++----- .../PHPStan/Analyser/LegacyNodeScopeResolverTest.php | 2 +- 2 files changed, 7 insertions(+), 6 deletions(-) diff --git a/src/Type/Php/CountFunctionReturnTypeExtension.php b/src/Type/Php/CountFunctionReturnTypeExtension.php index 4b257a00f7..7b3a5e2e30 100644 --- a/src/Type/Php/CountFunctionReturnTypeExtension.php +++ b/src/Type/Php/CountFunctionReturnTypeExtension.php @@ -13,7 +13,7 @@ use PHPStan\Type\Type; use function count; use function in_array; -use const COUNT_RECURSIVE; +use const COUNT_NORMAL; #[AutowiredService] final class CountFunctionReturnTypeExtension implements DynamicFunctionReturnTypeExtension @@ -30,11 +30,12 @@ public function getTypeFromFunctionCall( Scope $scope, ): ?Type { - if (count($functionCall->getArgs()) < 1) { + $args = $functionCall->getArgs(); + if (count($args) < 1) { return null; } - $arrayType = $scope->getType($functionCall->getArgs()[0]->value); + $arrayType = $scope->getType($args[0]->value); if (!$this->isNormalCount($functionCall, $arrayType, $scope)->yes()) { if ($arrayType->isIterableAtLeastOnce()->yes()) { return IntegerRangeType::fromInterval(1, null); @@ -42,10 +43,10 @@ public function getTypeFromFunctionCall( return null; } - return $scope->getType($functionCall->getArgs()[0]->value)->getArraySize(); + return $scope->getType($args[0]->value)->getArraySize(); } - private function isNormalCount(FuncCall $countFuncCall, Type $countedType, Scope $scope,): TrinaryLogic + private function isNormalCount(FuncCall $countFuncCall, Type $countedType, Scope $scope): TrinaryLogic { if (count($countFuncCall->getArgs()) === 1) { $isNormalCount = TrinaryLogic::createYes(); diff --git a/tests/PHPStan/Analyser/LegacyNodeScopeResolverTest.php b/tests/PHPStan/Analyser/LegacyNodeScopeResolverTest.php index c9fe4f942a..843786a0e5 100644 --- a/tests/PHPStan/Analyser/LegacyNodeScopeResolverTest.php +++ b/tests/PHPStan/Analyser/LegacyNodeScopeResolverTest.php @@ -2464,7 +2464,7 @@ public static function dataBinaryOperations(): array 'count($arrayOfIntegers)', ], [ - 'int<0, max>', + '3', 'count($arrayOfIntegers, \COUNT_RECURSIVE)', ], [ From 3d92b39cdeddf1ff32ac9085019d1514b7804ec3 Mon Sep 17 00:00:00 2001 From: Markus Staab Date: Mon, 3 Nov 2025 10:56:07 +0100 Subject: [PATCH 4/4] test union --- tests/PHPStan/Analyser/nsrt/count-recursive.php | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/tests/PHPStan/Analyser/nsrt/count-recursive.php b/tests/PHPStan/Analyser/nsrt/count-recursive.php index 9795bdecc2..0e7891dc9d 100644 --- a/tests/PHPStan/Analyser/nsrt/count-recursive.php +++ b/tests/PHPStan/Analyser/nsrt/count-recursive.php @@ -36,6 +36,17 @@ public function countArrayRecursive(array $arr): void } } + public function countArrayUnionMode(array $arr): void + { + $mode = rand(0,1) ? COUNT_NORMAL : COUNT_RECURSIVE; + if (count($arr, $mode) > 2) { + assertType('non-empty-array', $arr); + assertType('int<3, max>', count($arr, $mode)); + assertType('int<1, max>', count($arr, COUNT_NORMAL)); + assertType('int<1, max>', count($arr, COUNT_RECURSIVE)); + } + } + /** @param list $list */ public function countList($list): void {