From 6810d228c8679a6a597912f5032a13f31431f11f Mon Sep 17 00:00:00 2001 From: phpstan-bot <79867460+phpstan-bot@users.noreply.github.com> Date: Tue, 21 Jul 2026 10:40:50 +0000 Subject: [PATCH] Normalize array keys through `toArrayKey()` in `array_fill_keys` and `array_combine` return types - `ArrayType::fillKeysArray()` now passes the resulting key type through `toArrayKey()`, so `decimal-int-string` (and other integer-like string keys) collapse to `int`, matching how PHP coerces array keys. This mirrors what `ArrayType::flipArray()` already does. - `MixedType::fillKeysArray()` normalizes the key type the same way, so `array_fill_keys` on a `mixed` array no longer yields a `mixed` key type (array keys can only be `int|string`). - `ArrayCombineHelper` (the non-constant path of `array_combine`) had the identical missing normalization and is fixed the same way. - Probed the other value-as-key functions: `array_flip`, `array_count_values` and `array_column` already normalize via `toArrayKey()` and were correct; no change needed there. - Updated existing `array-fill-keys` / `array-combine` expectations that encoded the un-normalized keys (float keys now include `int`, `bool` keys collapse to `int|string`, decimal-int-string keys collapse to `int`). --- src/Type/ArrayType.php | 4 +- src/Type/MixedType.php | 2 +- src/Type/Php/ArrayCombineHelper.php | 4 +- .../Analyser/nsrt/array-combine-php8.php | 4 +- .../Analyser/nsrt/array-fill-keys-php8.php | 2 +- .../PHPStan/Analyser/nsrt/array-fill-keys.php | 8 +- tests/PHPStan/Analyser/nsrt/bug-14980.php | 82 +++++++++++++++++++ 7 files changed, 94 insertions(+), 12 deletions(-) create mode 100644 tests/PHPStan/Analyser/nsrt/bug-14980.php diff --git a/src/Type/ArrayType.php b/src/Type/ArrayType.php index a951f13c24b..b24fbc64fcf 100644 --- a/src/Type/ArrayType.php +++ b/src/Type/ArrayType.php @@ -484,10 +484,10 @@ public function fillKeysArray(Type $valueType): Type return $stringKeyType; } - return new ArrayType($stringKeyType, $valueType); + return new ArrayType($stringKeyType->toArrayKey(), $valueType); } - return new ArrayType($itemType, $valueType); + return new ArrayType($itemType->toArrayKey(), $valueType); } public function flipArray(): Type diff --git a/src/Type/MixedType.php b/src/Type/MixedType.php index 9b07022d5e4..4633760d19d 100644 --- a/src/Type/MixedType.php +++ b/src/Type/MixedType.php @@ -223,7 +223,7 @@ public function fillKeysArray(Type $valueType): Type return new ErrorType(); } - return new ArrayType($this->getIterableValueType(), $valueType); + return new ArrayType($this->getIterableValueType()->toArrayKey(), $valueType); } public function flipArray(): Type diff --git a/src/Type/Php/ArrayCombineHelper.php b/src/Type/Php/ArrayCombineHelper.php index b8bb3f445af..d4744cfe025 100644 --- a/src/Type/Php/ArrayCombineHelper.php +++ b/src/Type/Php/ArrayCombineHelper.php @@ -98,9 +98,9 @@ public function getReturnAndThrowType(Expr $firstArg, Expr $secondArg, Scope $sc return [new NeverType(), TrinaryLogic::createNo()]; } - $keyType = $itemType->toString(); + $keyType = $itemType->toString()->toArrayKey(); } else { - $keyType = $itemType; + $keyType = $itemType->toArrayKey(); } } else { $keyType = new MixedType(); diff --git a/tests/PHPStan/Analyser/nsrt/array-combine-php8.php b/tests/PHPStan/Analyser/nsrt/array-combine-php8.php index 1a1a78c411b..76da9400e5d 100644 --- a/tests/PHPStan/Analyser/nsrt/array-combine-php8.php +++ b/tests/PHPStan/Analyser/nsrt/array-combine-php8.php @@ -110,7 +110,7 @@ function withUnionConstArraysDifferentArraysCount(): void } } - assertType("non-empty-array<1|'2'|'3', 'apple'|'avocado'|'banana'|'pear'>", array_combine($a, $b)); + assertType("non-empty-array<1|2|3, 'apple'|'avocado'|'banana'|'pear'>", array_combine($a, $b)); } function withUnionConstArraysAndDifferentFiniteKeysCount(bool $bool): void @@ -123,7 +123,7 @@ function withUnionConstArraysAndDifferentFiniteKeysCount(bool $bool): void $b = ['apple', 'banana']; } - assertType("non-empty-array<''|'1'|'2', 'apple'|'avocado'|'banana'>", array_combine($a, $b)); + assertType("non-empty-array<1|2|'', 'apple'|'avocado'|'banana'>", array_combine($a, $b)); } /** diff --git a/tests/PHPStan/Analyser/nsrt/array-fill-keys-php8.php b/tests/PHPStan/Analyser/nsrt/array-fill-keys-php8.php index 47104825342..07c06367dc7 100644 --- a/tests/PHPStan/Analyser/nsrt/array-fill-keys-php8.php +++ b/tests/PHPStan/Analyser/nsrt/array-fill-keys-php8.php @@ -7,7 +7,7 @@ function mixedAndSubtractedArray($mixed): void { if (is_array($mixed)) { - assertType("array", array_fill_keys($mixed, 'b')); + assertType("array<'b'>", array_fill_keys($mixed, 'b')); } else { assertType("*NEVER*", array_fill_keys($mixed, 'b')); } diff --git a/tests/PHPStan/Analyser/nsrt/array-fill-keys.php b/tests/PHPStan/Analyser/nsrt/array-fill-keys.php index 231b2dcbe5b..eabf069c861 100644 --- a/tests/PHPStan/Analyser/nsrt/array-fill-keys.php +++ b/tests/PHPStan/Analyser/nsrt/array-fill-keys.php @@ -91,17 +91,17 @@ function withNotConstantArray(array $foo, array $bar, array $baz, array $floats, assertType("array", array_fill_keys($foo, null)); assertType("array", array_fill_keys($bar, null)); assertType("array<'foo', null>", array_fill_keys($baz, null)); - assertType("array", array_fill_keys($floats, null)); - assertType("array", array_fill_keys($mixed, null)); + assertType("array", array_fill_keys($floats, null)); + assertType("array", array_fill_keys($mixed, null)); assertType('array', array_fill_keys($list, null)); assertType('*ERROR*', array_fill_keys($objectsWithoutToString, null)); if (array_key_exists(17, $mixed)) { - assertType('non-empty-array', array_fill_keys($mixed, null)); + assertType('non-empty-array', array_fill_keys($mixed, null)); } if (array_key_exists(17, $mixed) && $mixed[17] === 'foo') { - assertType('non-empty-array', array_fill_keys($mixed, null)); + assertType('non-empty-array', array_fill_keys($mixed, null)); } } diff --git a/tests/PHPStan/Analyser/nsrt/bug-14980.php b/tests/PHPStan/Analyser/nsrt/bug-14980.php new file mode 100644 index 00000000000..eb62e95e2a3 --- /dev/null +++ b/tests/PHPStan/Analyser/nsrt/bug-14980.php @@ -0,0 +1,82 @@ += 8.0 + +declare(strict_types = 1); + +namespace Bug14980; + +use function PHPStan\Testing\assertType; + +/** + * @param list $ints + */ +function decimalIntStringKeys(array $ints): void +{ + $keys = []; + foreach ($ints as $int) { + $keys[] = $int; + $keys[] = (string) $int; + } + + assertType('list', $keys); + assertType('array', array_fill_keys($keys, true)); +} + +/** + * @param list $keys + */ +function numericStringKeys(array $keys): void +{ + assertType('array', array_fill_keys($keys, true)); +} + +function fillKeysMixedArray(mixed $m): void +{ + if (is_array($m)) { + assertType('array', array_fill_keys($m, true)); + } +} + +/** + * @param list $ints + */ +function combineDecimalIntStringKeys(array $ints): void +{ + $keys = []; + foreach ($ints as $int) { + $keys[] = $int; + $keys[] = (string) $int; + } + + assertType('array', array_combine($keys, $keys)); +} + +/** + * @param list $keys + * @param list $values + */ +function combineNumericStringKeys(array $keys, array $values): void +{ + assertType('array', array_combine($keys, $values)); +} + +class DecimalIntBug +{ + + /** @var array */ + private array $data = []; + + /** + * @param list $ints + */ + public function set(array $ints): void + { + $keys = []; + foreach ($ints as $int) { + $keys[] = $int; + $keys[] = (string) $int; + } + + $this->data = array_fill_keys($keys, true); + } + +}