diff --git a/src/Type/Php/CountFunctionReturnTypeExtension.php b/src/Type/Php/CountFunctionReturnTypeExtension.php index b87d34c466..7b3a5e2e30 100644 --- a/src/Type/Php/CountFunctionReturnTypeExtension.php +++ b/src/Type/Php/CountFunctionReturnTypeExtension.php @@ -6,12 +6,14 @@ 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; -use const COUNT_RECURSIVE; +use const COUNT_NORMAL; #[AutowiredService] final class CountFunctionReturnTypeExtension implements DynamicFunctionReturnTypeExtension @@ -28,18 +30,31 @@ public function getTypeFromFunctionCall( Scope $scope, ): ?Type { - if (count($functionCall->getArgs()) < 1) { + $args = $functionCall->getArgs(); + if (count($args) < 1) { 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($args[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(); + return $scope->getType($args[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/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)', ], [ diff --git a/tests/PHPStan/Analyser/nsrt/count-recursive.php b/tests/PHPStan/Analyser/nsrt/count-recursive.php new file mode 100644 index 0000000000..0e7891dc9d --- /dev/null +++ b/tests/PHPStan/Analyser/nsrt/count-recursive.php @@ -0,0 +1,79 @@ + 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)); + } + } + + 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 + { + 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)); + } + } +}