diff --git a/src/Type/IntersectionType.php b/src/Type/IntersectionType.php index 16cb33d2dd..cd66832682 100644 --- a/src/Type/IntersectionType.php +++ b/src/Type/IntersectionType.php @@ -680,7 +680,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()); + + $knownOffsets = []; + foreach ($this->types as $type) { + if (!($type instanceof HasOffsetValueType) && !($type instanceof HasOffsetType)) { + continue; + } + + $knownOffsets[$type->getOffsetType()->getValue()] = true; + } + + if ($knownOffsets !== []) { + return TypeCombinator::intersect($arraySize, IntegerRangeType::fromInterval(count($knownOffsets), null)); + } + + 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..1725b929fd --- /dev/null +++ b/tests/PHPStan/Analyser/nsrt/count-recursive.php @@ -0,0 +1,229 @@ +> $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)); + } + } + + public function countImplicitNormal($mode): void + { + $arr = [1, 2, 3]; + if (count($arr, $mode) > 2) { + assertType('3', count($arr)); + assertType('3', count($arr, $mode)); + assertType('3', count($arr, COUNT_NORMAL)); + assertType('3', count($arr, COUNT_RECURSIVE)); + } + } + + public function countMixed($arr, $mode): void + { + if (count($arr, $mode) > 2) { + assertType('int<0, max>', count($arr)); + assertType('int<3, max>', count($arr, $mode)); + assertType('int<0, max>', count($arr, COUNT_NORMAL)); + assertType('int<0, max>', count($arr, 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)); + } + } + + /** @param arary $array */ + public function countListRecursiveOnUnionOfRanges($array): void + { + if (!array_key_exists(5, $array)) { + return; + } + assertType('non-empty-array&hasOffset(5)', $array); + assertType('int<1, max>', count($array)); + + if ( + (count($array) > 2 && count($array) < 5) + || (count($array) > 20 && count($array) < 50) + ) { + assertType('int<3, 4>|int<21, 49>', count($array)); + } + } + + + 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)); + } + } + } + } + + public function unionIntegerCountAfterKeyExists(array $array, int $i): void { + if ($array === []) { + return; + } + + assertType('non-empty-array', $array); + if (count($array) === 3 || count($array) === 4) { + assertType('3|4', count($array)); + if (array_key_exists(5, $array)) { + assertType('non-empty-array&hasOffset(5)', $array); + assertType('3|4', count($array)); + } + } + } + + public function countMaybeCountable(array $arr, bool $b, int $i) { + $c = rand(0,1) ? $arr : $b; + assertType('array|bool', $c); + assertType('int<0, max>', count($c, $i)); + + if ($arr === []) { + return; + } + assertType('int<1, max>', count($arr, $i)); + + $c = rand(0,1) ? $arr : $b; + assertType('non-empty-array|bool', $c); + assertType('int<0, max>', count($c, $i)); + + } +}