diff --git a/src/Type/IntersectionType.php b/src/Type/IntersectionType.php index 16cb33d2dd..ce66ee1c7b 100644 --- a/src/Type/IntersectionType.php +++ b/src/Type/IntersectionType.php @@ -53,6 +53,7 @@ use function in_array; use function is_int; use function ksort; +use function max; use function md5; use function sprintf; use function strcasecmp; @@ -680,7 +681,22 @@ public function isIterableAtLeastOnce(): TrinaryLogic public function getArraySize(): Type { - return $this->intersectTypes(static fn (Type $type): Type => $type->getArraySize()); + $arraySize = $this->intersectTypes(static fn (Type $type): Type => $type->getArraySize()); + + if ($arraySize instanceof IntegerRangeType) { + $knownOffsets = []; + foreach ($this->types as $type) { + if (!($type instanceof HasOffsetValueType) && !($type instanceof HasOffsetType)) { + continue; + } + + $knownOffsets[$type->getOffsetType()->getValue()] = true; + } + + return IntegerRangeType::fromInterval(max(count($knownOffsets), $arraySize->getMin()), $arraySize->getMax()); + } + + return $arraySize; } public function getIterableKeyType(): Type 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..e64d14c4a2 --- /dev/null +++ b/tests/PHPStan/Analyser/nsrt/count-recursive.php @@ -0,0 +1,159 @@ +> $muliDimArr + * @return void + */ + public function countMultiDim(array $muliDimArr, $mixed): void + { + if (count($muliDimArr, $mixed) > 2) { + assertType('int<1, max>', count($muliDimArr)); + assertType('int<3, max>', count($muliDimArr, $mixed)); + assertType('int<1, max>', count($muliDimArr, COUNT_NORMAL)); + assertType('int<1, max>', count($muliDimArr, COUNT_RECURSIVE)); + } + } + + public function countUnknownArray(array $arr): void + { + assertType('array', $arr); + assertType('int<0, max>', count($arr)); + assertType('int<0, max>', count($arr, COUNT_NORMAL)); + assertType('int<0, max>', count($arr, COUNT_RECURSIVE)); + } + + public function countEmptyArray(array $arr): void + { + if (count($arr) == 0) { + assertType('array{}', $arr); + assertType('0', count($arr)); + assertType('0', count($arr, COUNT_NORMAL)); + assertType('0', count($arr, COUNT_RECURSIVE)); + } + } + + public function countArray(array $arr): void + { + if (count($arr) > 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)); + } + } + + public function countConstantArray(array $anotherArray): void { + $arr = [1, 2, 3, [4, 5]]; + assertType('4', count($arr)); + assertType('4', count($arr, COUNT_NORMAL)); + assertType('int<1, max>', count($arr, COUNT_RECURSIVE)); + + $arr = [1, 2, 3, $anotherArray]; + assertType('array{1, 2, 3, array}', $arr); + assertType('4', count($arr)); + assertType('4', count($arr, COUNT_NORMAL)); + assertType('int<1, max>', count($arr, COUNT_RECURSIVE)); // could be int<4, max> + + if (rand(0,1)) { + $arr[] = 10; + } + assertType('array{0: 1, 1: 2, 2: 3, 3: array, 4?: 10}', $arr); + assertType('int<4, 5>', count($arr)); + assertType('int<4, 5>', count($arr, COUNT_NORMAL)); + assertType('int<1, max>', count($arr, COUNT_RECURSIVE)); // could be int<4, max> + + $arr = [1, 2, 3] + $anotherArray; + assertType('non-empty-array&hasOffsetValue(0, 1)&hasOffsetValue(1, 2)&hasOffsetValue(2, 3)', $arr); + assertType('int<3, max>', count($arr)); + assertType('int<3, max>', count($arr, COUNT_NORMAL)); + assertType('int<1, max>', count($arr, COUNT_RECURSIVE)); // could be int<3, max> + } + + public function countAfterKeyExists(array $array, int $i): void { + if (array_key_exists(5, $array)) { + assertType('non-empty-array&hasOffset(5)', $array); + assertType('int<1, max>', count($array)); + } + + if ($array !== []) { + assertType('non-empty-array', $array); + assertType('int<1, max>', count($array)); + if (array_key_exists(5, $array)) { + assertType('non-empty-array&hasOffset(5)', $array); + assertType('int<1, max>', count($array)); + + if (array_key_exists(15, $array)) { + assertType('non-empty-array&hasOffset(15)&hasOffset(5)', $array); + assertType('int<2, max>', count($array)); + } + } + } + } +}