From 8e9de576c62a2e1625ed385960629104d129454e Mon Sep 17 00:00:00 2001 From: ondrejmirtes <104888+ondrejmirtes@users.noreply.github.com> Date: Thu, 12 Feb 2026 19:28:19 +0000 Subject: [PATCH 1/2] Fix array_count_values key type for general string input - PHP casts numeric strings to integer array keys, so array_count_values on array should return non-empty-array> not non-empty-array> - Added isNumericString() check in ArrayCountValuesDynamicReturnTypeExtension to union IntegerType into key type when string may be numeric - New regression test in tests/PHPStan/Analyser/nsrt/bug-13996.php - Updated existing test expectation for general string case Closes https://github.com/phpstan/phpstan/issues/13996 --- CLAUDE.md | 4 +++ ...yCountValuesDynamicReturnTypeExtension.php | 10 ++++++- .../Analyser/nsrt/array-count-values.php | 2 +- tests/PHPStan/Analyser/nsrt/bug-13996.php | 29 +++++++++++++++++++ 4 files changed, 43 insertions(+), 2 deletions(-) create mode 100644 tests/PHPStan/Analyser/nsrt/bug-13996.php diff --git a/CLAUDE.md b/CLAUDE.md index e33c66a1c2..c96c8954ee 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -323,6 +323,10 @@ Many built-in PHP functions need `DynamicFunctionReturnTypeExtension` implementa - Extensions live in `src/Type/Php/` and are registered in `conf/services.neon` - Each reads the argument types from `Scope::getType()` and returns a more precise `Type` +### toArrayKey() and numeric string coercion + +`Type::toArrayKey()` converts a value type to its array key representation. `StringType::toArrayKey()` returns `string`, but PHP casts numeric strings (like `'123'`) to integer keys. `ConstantStringType::toArrayKey()` correctly handles this (e.g. `'1'` → `int(1)`), and `AccessoryNumericStringType::toArrayKey()` returns `int|numeric-string`. However, a general `string` where `isNumericString()` returns `maybe` needs special handling in extensions that create arrays from values — the key type should be `int|string` (array-key), not just `string`. This applies to `array_count_values`, `array_flip`, and similar functions where input values become output keys. + ### Function signature corrections (`src/Reflection/SignatureMap/`) PHPStan maintains its own signature map for built-in PHP functions in `functionMap.php` and delta files. Fixes involve: diff --git a/src/Type/Php/ArrayCountValuesDynamicReturnTypeExtension.php b/src/Type/Php/ArrayCountValuesDynamicReturnTypeExtension.php index 986f299cd8..0983a9678c 100644 --- a/src/Type/Php/ArrayCountValuesDynamicReturnTypeExtension.php +++ b/src/Type/Php/ArrayCountValuesDynamicReturnTypeExtension.php @@ -54,8 +54,16 @@ public function getTypeFromFunctionCall( continue; } + $keyType = $itemType->toArrayKey(); + + // PHP casts numeric strings to integer keys, so a general string + // that might be numeric should produce int|string keys + if ($itemType->isString()->yes() && $itemType->isNumericString()->maybe()) { + $keyType = TypeCombinator::union($keyType, new IntegerType()); + } + $outputTypes[] = new IntersectionType([ - new ArrayType($itemType->toArrayKey(), IntegerRangeType::fromInterval(1, null)), + new ArrayType($keyType, 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 0be5d86a1c..93c51b80dc 100644 --- a/tests/PHPStan/Analyser/nsrt/array-count-values.php +++ b/tests/PHPStan/Analyser/nsrt/array-count-values.php @@ -25,7 +25,7 @@ function returnsStringOrObjectArray(): array } // Objects are ignored by array_count_values, with a warning emitted. -assertType('non-empty-array>', array_count_values(returnsStringOrObjectArray())); +assertType('non-empty-array>', array_count_values(returnsStringOrObjectArray())); class StringableObject { diff --git a/tests/PHPStan/Analyser/nsrt/bug-13996.php b/tests/PHPStan/Analyser/nsrt/bug-13996.php new file mode 100644 index 0000000000..66419a3adc --- /dev/null +++ b/tests/PHPStan/Analyser/nsrt/bug-13996.php @@ -0,0 +1,29 @@ + $strings + */ +function strings(array $strings): void +{ + assertType('non-empty-array>', array_count_values($strings)); +} + +/** + * @param array $ints + */ +function ints(array $ints): void +{ + assertType('non-empty-array>', array_count_values($ints)); +} + +/** + * @param array $mixed + */ +function intOrString(array $mixed): void +{ + assertType('non-empty-array>', array_count_values($mixed)); +} From 642f3b3b497706ad7782a3f1bc2f87a4c5d33312 Mon Sep 17 00:00:00 2001 From: phpstan-bot Date: Thu, 12 Feb 2026 19:52:41 +0000 Subject: [PATCH 2/2] Fix CI failures [claude-ci-fix] Automated fix attempt 1 for CI failures. --- tests/PHPStan/Analyser/nsrt/bug-12222.php | 8 ++++---- tests/PHPStan/Rules/Classes/data/bug-14097.php | 2 +- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/tests/PHPStan/Analyser/nsrt/bug-12222.php b/tests/PHPStan/Analyser/nsrt/bug-12222.php index 191b8d758a..746280a968 100644 --- a/tests/PHPStan/Analyser/nsrt/bug-12222.php +++ b/tests/PHPStan/Analyser/nsrt/bug-12222.php @@ -1,6 +1,6 @@ = 8.1 -namespace Bug12222; +namespace Bug12222Nsrt; use function PHPStan\Testing\assertType; @@ -51,11 +51,11 @@ public function process(array $data): array 'startDate' => $data['startDate'], ]; - assertType('Bug12222\ContractStatus|null', $contract['status']); + assertType('Bug12222Nsrt\ContractStatus|null', $contract['status']); $contract['isActive'] = $contract['status']?->isActive(); - assertType('Bug12222\ContractStatus|null', $contract['status']); + assertType('Bug12222Nsrt\ContractStatus|null', $contract['status']); $contract['isBeingTerminated'] = $contract['status']?->isBeingTerminated(); - assertType('Bug12222\ContractStatus|null', $contract['status']); + assertType('Bug12222Nsrt\ContractStatus|null', $contract['status']); $contract['isTerminated'] = $contract['status']?->isTerminated(); return $contract; diff --git a/tests/PHPStan/Rules/Classes/data/bug-14097.php b/tests/PHPStan/Rules/Classes/data/bug-14097.php index bd92988c0c..986f708526 100644 --- a/tests/PHPStan/Rules/Classes/data/bug-14097.php +++ b/tests/PHPStan/Rules/Classes/data/bug-14097.php @@ -1,4 +1,4 @@ -= 8.0 namespace Bug14097;