Skip to content

Commit 38ec2fc

Browse files
phpstan-botVincentLangletclaude
authored
Do not track string keys of an unpacked non-literal expression in DuplicateKeysInLiteralArraysRule (#6635)
Co-authored-by: VincentLanglet <9052536+VincentLanglet@users.noreply.github.com> Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
1 parent 0ff0c25 commit 38ec2fc

3 files changed

Lines changed: 132 additions & 0 deletions

File tree

‎src/Rules/Arrays/DuplicateKeysInLiteralArraysRule.php‎

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,9 +11,12 @@
1111
use PHPStan\Rules\RuleErrorBuilder;
1212
use PHPStan\Type\ArrayUnpackingHelper;
1313
use PHPStan\Type\Constant\ConstantIntegerType;
14+
use PHPStan\Type\Type;
15+
use function array_filter;
1416
use function array_key_first;
1517
use function array_keys;
1618
use function array_search;
19+
use function array_values;
1720
use function count;
1821
use function implode;
1922
use function is_bool;
@@ -101,6 +104,15 @@ public function processNode(Node $node, Scope $scope): array
101104
? $unpackedKeyType->getValue()
102105
: max($autoGeneratedIndex, $unpackedKeyType->getValue());
103106
}
107+
108+
// unpacking an expression works like array_merge() - its string keys are
109+
// meant to override or to be overridden by the other keys
110+
if (!$item->value instanceof Node\Expr\Array_) {
111+
$keyTypes = array_values(array_filter(
112+
$keyTypes,
113+
static fn (Type $keyType): bool => !$keyType->isString()->yes(),
114+
));
115+
}
104116
} elseif ($key === null) {
105117
if ($autoGeneratedIndex === false) {
106118
continue;

‎tests/PHPStan/Rules/Arrays/DuplicateKeysInLiteralArraysRuleTest.php‎

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -180,4 +180,35 @@ public function testBug15247Php81(): void
180180
]);
181181
}
182182

183+
#[RequiresPhp('>= 8.1.0')]
184+
public function testBug15295(): void
185+
{
186+
$this->analyse([__DIR__ . '/data/bug-15295.php'], [
187+
[
188+
"Array has 2 duplicate keys with value 'start' ('start', 'start').",
189+
47,
190+
],
191+
[
192+
"Array has 2 duplicate keys with value 'start' ('start', 'start').",
193+
53,
194+
],
195+
[
196+
'Array has 2 duplicate keys with value 0 (0, 0).',
197+
59,
198+
],
199+
[
200+
"Array has 2 duplicate keys with value 0 (0, '0').",
201+
65,
202+
],
203+
[
204+
"Array has 2 duplicate keys with value 0 (0, '0').",
205+
71,
206+
],
207+
[
208+
"Array has 2 duplicate keys with value 0 (0, '0').",
209+
88,
210+
],
211+
]);
212+
}
213+
183214
}
Lines changed: 89 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,89 @@
1+
<?php // lint >= 8.1
2+
3+
declare(strict_types = 1);
4+
5+
namespace Bug15295;
6+
7+
/** @param array{heading: string, start: float} $section */
8+
function resetStart(array $section): array
9+
{
10+
return [...$section, 'start' => 0.0];
11+
}
12+
13+
function resetStartOfLocalVariable(): array
14+
{
15+
$section = ['heading' => 'h', 'start' => 1.0];
16+
return [...$section, 'start' => 0.0];
17+
}
18+
19+
/** @param array{heading: string, start: float} $section */
20+
function defaultsOverriddenBySpread(array $section): array
21+
{
22+
return ['start' => 0.0, ...$section];
23+
}
24+
25+
/**
26+
* @param array{heading: string, start: float} $a
27+
* @param array{start: float} $b
28+
*/
29+
function twoSpreads(array $a, array $b): array
30+
{
31+
return [...$a, ...$b];
32+
}
33+
34+
/** @return array{heading: string, start: float} */
35+
function getSection(): array
36+
{
37+
return ['heading' => 'h', 'start' => 1.0];
38+
}
39+
40+
function resetStartOfCall(): array
41+
{
42+
return [...getSection(), 'start' => 0.0];
43+
}
44+
45+
function spreadOfLiteral(): array
46+
{
47+
return [...['heading' => 'h', 'start' => 1.0], 'start' => 0.0];
48+
}
49+
50+
/** @param array{heading: string, start: float} $section */
51+
function duplicateExplicitKeysAfterSpread(array $section): array
52+
{
53+
return [...$section, 'start' => 0.0, 'start' => 1.0];
54+
}
55+
56+
function intKeysOfVariable(): array
57+
{
58+
$list = [1, 2];
59+
return [...$list, 0 => 'x'];
60+
}
61+
62+
function numericStringKeyAfterSpreadOfList(): array
63+
{
64+
$list = [1, 2];
65+
return [...$list, '0' => 'x'];
66+
}
67+
68+
/** @param array{'0': string, start: float} $section */
69+
function numericStringKeyAfterSpread(array $section): array
70+
{
71+
return [...$section, '0' => 'x'];
72+
}
73+
74+
/** @param array{'0': string, start: float} $section */
75+
function stringKeyAfterSpreadWithNumericStringKey(array $section): array
76+
{
77+
return [...$section, 'start' => 0.0];
78+
}
79+
80+
/** @param array{'0': string, start: float} $section */
81+
function numericStringKeyBeforeSpread(array $section): array
82+
{
83+
return ['0' => 'x', ...$section];
84+
}
85+
86+
function spreadOfLiteralWithNumericStringKey(): array
87+
{
88+
return [...['0' => 'a'], '0' => 'x'];
89+
}

0 commit comments

Comments
 (0)