Skip to content

Do not track string keys of an unpacked non-literal expression in DuplicateKeysInLiteralArraysRule - #6635

Merged
VincentLanglet merged 2 commits into
phpstan:2.2.xfrom
phpstan-bot:create-pull-request/patch-d5tnwsy
Sep 30, 2026
Merged

VincentLanglet merged 2 commits into
phpstan:2.2.xfrom
phpstan-bot:create-pull-request/patch-d5tnwsy

Conversation

@phpstan-bot

Copy link
Copy Markdown
Collaborator

Summary

Since #6462, with bleedingEdge, DuplicateKeysInLiteralArraysRule reported [...$section, 'start' => 0.0] when $section is a sealed array{heading: string, start: float}. Copying an array and overriding one of its keys is a common, intentional idiom, and spreading is effectively array_merge(). This PR stops reporting string keys contributed by an unpacked non-literal expression.

Changes

  • src/Rules/Arrays/DuplicateKeysInLiteralArraysRule.php: when the unpacked item is not an Expr\Array_ literal, the string keys returned by ArrayUnpackingHelper::getKeyTypes() are dropped before duplicate tracking. Integer keys are kept, both to advance the implicit index and for duplicate detection.
  • Covered in the same change (all go through the same code path):
    • explicit key after spread: [...$section, 'start' => 0.0]
    • explicit key before spread (defaults pattern): ['start' => 0.0, ...$section]
    • two spreads sharing a key: [...$a, ...$b]
    • spread of a local variable holding a constant array and of a function call result
  • Still reported on purpose:
    • spread of an array literal: [...['start' => 1.0], 'start' => 0.0], ['a' => 1, ...['a' => 2]]
    • duplicate explicit keys after a spread: [...$section, 'start' => 0.0, 'start' => 1.0]
    • explicit integer key overriding a renumbered integer key of a spread variable: [...$list, 0 => 'x']. This one is unlike array_merge(), which appends integer keys.

Root cause

#6462 made the rule add every key an unpacked item contributes to the seen keys, including string keys of arbitrary expressions whose type is a sealed constant array. The rule works on types, so any sealed shape (from PHPDoc or a local variable) made an intentional override look like a duplicate literal key. The fix limits string-key tracking for spreads to array literals, where the key is written twice in the source.

Test

Added tests/PHPStan/Rules/Arrays/data/bug-15295.php with testBug15295 (PHP >= 8.1). It covers the reported case, the local-variable and function-call variants, the defaults-first and two-spread variants (no errors), and the literal-spread, repeated explicit key and integer-key variants (still reported). The test failed before the fix with 5 false positives. Existing tests for #6462 (bug-15247*, bug-15244, bug-15248) pass unchanged.

Fixes phpstan/phpstan#15295

…plicateKeysInLiteralArraysRule`

- Unpacking an expression (`...$section`, `...getSection()`) behaves like array_merge():
  its string keys are meant to override earlier keys or be overridden by later ones,
  so they no longer count as duplicates, whichever side they are on and also between
  two unpacked expressions (`[...$a, ...$b]`).
- Unpacked array literals (`['a' => 1, ...['a' => 2]]`) are still reported, since the
  key is written twice in the source.
- Integer keys of an unpacked expression are still tracked - they are renumbered, so an
  explicit integer key overriding one (`[...$list, 0 => 'x']`) is unlike array_merge()
  and still reported. They also keep advancing the implicit index.

@staabm staabm left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

add test-coverage for numeric-string array keys like '0'

…tests

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@phpstan-bot

Copy link
Copy Markdown
Collaborator Author

I added tests for numeric-string keys like '0' to tests/PHPStan/Rules/Arrays/data/bug-15295.php and pushed them as commit d592d174d. make tests and make phpstan both pass. The rule itself didn't need any changes.

PHP turns a key like '0' into the integer 0, so the rule treats these keys like the integer keys already in the tests. When an array is spread, its integer keys are renumbered from the current position, and a later explicit '0' overwrites the first one. I checked this at runtime: [...['0' => 'a', 'start' => 1.0], '0' => 'x'] produces [0 => 'x', 'start' => 1.0].

Case Result
[...$list, '0' => 'x'] with $list = [1, 2] reported: value 0 (0, '0')
[...$section, '0' => 'x'] with array{'0': string, start: float} reported
[...$section, 'start' => 0.0] with that same shape not reported (named key overridden, as in the original fix)
['0' => 'x', ...$section] not reported (the spread's 0 is renumbered to 1)
[...['0' => 'a'], '0' => 'x'] (spread of a literal) reported

These tests also confirm that the fix's filter only drops non-numeric string keys from a spread variable. Numeric-string keys arrive as integers, so they are still tracked.

@VincentLanglet
VincentLanglet merged commit 38ec2fc into phpstan:2.2.x Sep 30, 2026
863 of 875 checks passed
@VincentLanglet
VincentLanglet deleted the create-pull-request/patch-d5tnwsy branch September 30, 2026 11:13
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants