diff --git a/src/Rules/Arrays/DuplicateKeysInLiteralArraysRule.php b/src/Rules/Arrays/DuplicateKeysInLiteralArraysRule.php index 7feeea4726c..4f91110f1a9 100644 --- a/src/Rules/Arrays/DuplicateKeysInLiteralArraysRule.php +++ b/src/Rules/Arrays/DuplicateKeysInLiteralArraysRule.php @@ -11,9 +11,12 @@ use PHPStan\Rules\RuleErrorBuilder; use PHPStan\Type\ArrayUnpackingHelper; use PHPStan\Type\Constant\ConstantIntegerType; +use PHPStan\Type\Type; +use function array_filter; use function array_key_first; use function array_keys; use function array_search; +use function array_values; use function count; use function implode; use function is_bool; @@ -101,6 +104,15 @@ public function processNode(Node $node, Scope $scope): array ? $unpackedKeyType->getValue() : max($autoGeneratedIndex, $unpackedKeyType->getValue()); } + + // unpacking an expression works like array_merge() - its string keys are + // meant to override or to be overridden by the other keys + if (!$item->value instanceof Node\Expr\Array_) { + $keyTypes = array_values(array_filter( + $keyTypes, + static fn (Type $keyType): bool => !$keyType->isString()->yes(), + )); + } } elseif ($key === null) { if ($autoGeneratedIndex === false) { continue; diff --git a/tests/PHPStan/Rules/Arrays/DuplicateKeysInLiteralArraysRuleTest.php b/tests/PHPStan/Rules/Arrays/DuplicateKeysInLiteralArraysRuleTest.php index e32bd5ebecb..d98e48385c5 100644 --- a/tests/PHPStan/Rules/Arrays/DuplicateKeysInLiteralArraysRuleTest.php +++ b/tests/PHPStan/Rules/Arrays/DuplicateKeysInLiteralArraysRuleTest.php @@ -180,4 +180,35 @@ public function testBug15247Php81(): void ]); } + #[RequiresPhp('>= 8.1.0')] + public function testBug15295(): void + { + $this->analyse([__DIR__ . '/data/bug-15295.php'], [ + [ + "Array has 2 duplicate keys with value 'start' ('start', 'start').", + 47, + ], + [ + "Array has 2 duplicate keys with value 'start' ('start', 'start').", + 53, + ], + [ + 'Array has 2 duplicate keys with value 0 (0, 0).', + 59, + ], + [ + "Array has 2 duplicate keys with value 0 (0, '0').", + 65, + ], + [ + "Array has 2 duplicate keys with value 0 (0, '0').", + 71, + ], + [ + "Array has 2 duplicate keys with value 0 (0, '0').", + 88, + ], + ]); + } + } diff --git a/tests/PHPStan/Rules/Arrays/data/bug-15295.php b/tests/PHPStan/Rules/Arrays/data/bug-15295.php new file mode 100644 index 00000000000..49e76aaa52a --- /dev/null +++ b/tests/PHPStan/Rules/Arrays/data/bug-15295.php @@ -0,0 +1,89 @@ += 8.1 + +declare(strict_types = 1); + +namespace Bug15295; + +/** @param array{heading: string, start: float} $section */ +function resetStart(array $section): array +{ + return [...$section, 'start' => 0.0]; +} + +function resetStartOfLocalVariable(): array +{ + $section = ['heading' => 'h', 'start' => 1.0]; + return [...$section, 'start' => 0.0]; +} + +/** @param array{heading: string, start: float} $section */ +function defaultsOverriddenBySpread(array $section): array +{ + return ['start' => 0.0, ...$section]; +} + +/** + * @param array{heading: string, start: float} $a + * @param array{start: float} $b + */ +function twoSpreads(array $a, array $b): array +{ + return [...$a, ...$b]; +} + +/** @return array{heading: string, start: float} */ +function getSection(): array +{ + return ['heading' => 'h', 'start' => 1.0]; +} + +function resetStartOfCall(): array +{ + return [...getSection(), 'start' => 0.0]; +} + +function spreadOfLiteral(): array +{ + return [...['heading' => 'h', 'start' => 1.0], 'start' => 0.0]; +} + +/** @param array{heading: string, start: float} $section */ +function duplicateExplicitKeysAfterSpread(array $section): array +{ + return [...$section, 'start' => 0.0, 'start' => 1.0]; +} + +function intKeysOfVariable(): array +{ + $list = [1, 2]; + return [...$list, 0 => 'x']; +} + +function numericStringKeyAfterSpreadOfList(): array +{ + $list = [1, 2]; + return [...$list, '0' => 'x']; +} + +/** @param array{'0': string, start: float} $section */ +function numericStringKeyAfterSpread(array $section): array +{ + return [...$section, '0' => 'x']; +} + +/** @param array{'0': string, start: float} $section */ +function stringKeyAfterSpreadWithNumericStringKey(array $section): array +{ + return [...$section, 'start' => 0.0]; +} + +/** @param array{'0': string, start: float} $section */ +function numericStringKeyBeforeSpread(array $section): array +{ + return ['0' => 'x', ...$section]; +} + +function spreadOfLiteralWithNumericStringKey(): array +{ + return [...['0' => 'a'], '0' => 'x']; +}