From f2474149846ee3f1f0decbb0ef47c8eab37a2e94 Mon Sep 17 00:00:00 2001 From: Brad Miller <28307684+mad-briller@users.noreply.github.com> Date: Thu, 4 Dec 2025 16:43:34 +0000 Subject: [PATCH 1/2] Add ArrayCountValuesDynamicReturnTypeExtension. --- ...yCountValuesDynamicReturnTypeExtension.php | 72 +++++++++++++++++++ .../Analyser/nsrt/array-count-values.php | 43 +++++++++++ 2 files changed, 115 insertions(+) create mode 100644 src/Type/Php/ArrayCountValuesDynamicReturnTypeExtension.php create mode 100644 tests/PHPStan/Analyser/nsrt/array-count-values.php diff --git a/src/Type/Php/ArrayCountValuesDynamicReturnTypeExtension.php b/src/Type/Php/ArrayCountValuesDynamicReturnTypeExtension.php new file mode 100644 index 0000000000..9994a0394c --- /dev/null +++ b/src/Type/Php/ArrayCountValuesDynamicReturnTypeExtension.php @@ -0,0 +1,72 @@ +getName() === 'array_count_values'; + } + + public function getTypeFromFunctionCall( + FunctionReflection $functionReflection, + FuncCall $functionCall, + Scope $scope, + ): ?Type + { + $args = $functionCall->getArgs(); + + if (!isset($args[0])) { + return null; + } + + $inputType = $scope->getType($args[0]->value); + + $arrayTypes = $inputType->getArrays(); + + $outputTypes = []; + + foreach ($arrayTypes as $arrayType) { + $itemType = $arrayType->getItemType(); + + if ($itemType instanceof UnionType) { + $itemType = $itemType->filterTypes( + static fn ($type) => !$type->toArrayKey() instanceof ErrorType, + ); + } + + if ($itemType->toArrayKey() instanceof ErrorType) { + continue; + } + + $outputTypes[] = new ArrayType( + $itemType, + IntegerRangeType::fromInterval(1, null), + ); + } + + if (count($outputTypes) === 0) { + return new ConstantArrayType([], []); + } + + return TypeCombinator::union(...$outputTypes); + } + +} diff --git a/tests/PHPStan/Analyser/nsrt/array-count-values.php b/tests/PHPStan/Analyser/nsrt/array-count-values.php new file mode 100644 index 0000000000..ebe65fe441 --- /dev/null +++ b/tests/PHPStan/Analyser/nsrt/array-count-values.php @@ -0,0 +1,43 @@ +>', $ints); + +$strings = array_count_values(['one', 'two', 'two', 'three']); + +assertType('array<\'one\'|\'three\'|\'two\', int<1, max>>', $strings); + +$objects = array_count_values([new \stdClass()]); + +assertType('array{}', $objects); + +/** + * @return array + */ +function returnsStringOrObjectArray(): array +{ + +} + +// Objects are ignored by array_count_values, with a warning emitted. +assertType('array>', array_count_values(returnsStringOrObjectArray())); + +class StringableObject +{ + + public function __toString(): string + { + return 'string'; + } + +} + +// Stringable objects are ignored by array_count_values, with a warning emitted. +$stringable = array_count_values([new StringableObject(), 'string', 1]); + +assertType('array<1|\'string\', int<1, max>>', $stringable); From 9b9854039118b3381bbd6c6db304193815ccee04 Mon Sep 17 00:00:00 2001 From: Brad Miller <28307684+mad-briller@users.noreply.github.com> Date: Fri, 5 Dec 2025 10:30:47 +0000 Subject: [PATCH 2/2] Make array_count_values return non-empty-array types. --- .../Php/ArrayCountValuesDynamicReturnTypeExtension.php | 7 ++++--- tests/PHPStan/Analyser/nsrt/array-count-values.php | 8 ++++---- 2 files changed, 8 insertions(+), 7 deletions(-) diff --git a/src/Type/Php/ArrayCountValuesDynamicReturnTypeExtension.php b/src/Type/Php/ArrayCountValuesDynamicReturnTypeExtension.php index 9994a0394c..2cc1df4870 100644 --- a/src/Type/Php/ArrayCountValuesDynamicReturnTypeExtension.php +++ b/src/Type/Php/ArrayCountValuesDynamicReturnTypeExtension.php @@ -6,6 +6,7 @@ use PHPStan\Analyser\Scope; use PHPStan\DependencyInjection\AutowiredService; use PHPStan\Reflection\FunctionReflection; +use PHPStan\Type\Accessory\NonEmptyArrayType; use PHPStan\Type\ArrayType; use PHPStan\Type\Constant\ConstantArrayType; use PHPStan\Type\DynamicFunctionReturnTypeExtension; @@ -56,9 +57,9 @@ public function getTypeFromFunctionCall( continue; } - $outputTypes[] = new ArrayType( - $itemType, - IntegerRangeType::fromInterval(1, null), + $outputTypes[] = TypeCombinator::intersect( + new ArrayType($itemType, IntegerRangeType::fromInterval(1, null)), + new NonEmptyArrayType(), ); } diff --git a/tests/PHPStan/Analyser/nsrt/array-count-values.php b/tests/PHPStan/Analyser/nsrt/array-count-values.php index ebe65fe441..f752bac1ad 100644 --- a/tests/PHPStan/Analyser/nsrt/array-count-values.php +++ b/tests/PHPStan/Analyser/nsrt/array-count-values.php @@ -6,11 +6,11 @@ $ints = array_count_values([1, 2, 2, 3]); -assertType('array<1|2|3, int<1, max>>', $ints); +assertType('non-empty-array<1|2|3, int<1, max>>', $ints); $strings = array_count_values(['one', 'two', 'two', 'three']); -assertType('array<\'one\'|\'three\'|\'two\', int<1, max>>', $strings); +assertType('non-empty-array<\'one\'|\'three\'|\'two\', int<1, max>>', $strings); $objects = array_count_values([new \stdClass()]); @@ -25,7 +25,7 @@ function returnsStringOrObjectArray(): array } // Objects are ignored by array_count_values, with a warning emitted. -assertType('array>', array_count_values(returnsStringOrObjectArray())); +assertType('non-empty-array>', array_count_values(returnsStringOrObjectArray())); class StringableObject { @@ -40,4 +40,4 @@ public function __toString(): string // Stringable objects are ignored by array_count_values, with a warning emitted. $stringable = array_count_values([new StringableObject(), 'string', 1]); -assertType('array<1|\'string\', int<1, max>>', $stringable); +assertType('non-empty-array<1|\'string\', int<1, max>>', $stringable);