Skip to content

Commit a3a94dc

Browse files
staabmphpstan-bot
authored andcommitted
Fix incorrect narrowing of nested array after assignment with non-constant key
When assigning to a nested array with a non-constant key like `$array[$id]['state'] = 'foo'`, PHPStan incorrectly inferred that ALL elements of the outer array had the narrowed type, not just the one being modified. For example, `array<int, array{state: string}>` became `non-empty-array<int, array{state: 'foo'}>` instead of the correct `non-empty-array<int, array{state: string}>`. The fix ensures that when the outer array key is non-constant (no constant scalar decomposition), the new value type is unioned with the existing item type rather than replacing it entirely, since we cannot know which specific element was modified. Fixes phpstan/phpstan#13857
1 parent d32efcb commit a3a94dc

File tree

9 files changed

+36
-10
lines changed

9 files changed

+36
-10
lines changed

src/Analyser/ExprHandler/AssignHandler.php

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -980,7 +980,9 @@ private function produceArrayDimFetchAssignValueToWrite(array $dimFetchStack, ar
980980
}
981981

982982
} else {
983-
$valueToWrite = $offsetValueType->setOffsetValueType($offsetType, $valueToWrite, $i === 0);
983+
$unionValues = $i === 0
984+
|| ($offsetType !== null && count($offsetType->getConstantScalarTypes()) === 0);
985+
$valueToWrite = $offsetValueType->setOffsetValueType($offsetType, $valueToWrite, $unionValues);
984986
}
985987

986988
if ($arrayDimFetch === null || !$offsetValueType->isList()->yes()) {

src/DependencyInjection/AutowiredAttributeServicesExtension.php

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -139,7 +139,7 @@ public function loadConfiguration(): void
139139

140140
/**
141141
* @param class-string $className
142-
* @param array<lowercase-string, non-empty-list<TargetMethodParameter<AutowiredParameter>>> $constructorParameters
142+
* @param array<lowercase-string, list<TargetMethodParameter<AutowiredParameter>>> $constructorParameters
143143
*/
144144
private function processConstructorParameters(string $className, ServiceDefinition $definition, array $constructorParameters): void
145145
{

tests/PHPStan/Analyser/nsrt/assign-nested-arrays.php

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@ public function doFoo(int $i)
1414
$array[$i]['bar'] = 1;
1515
$array[$i]['baz'] = 2;
1616

17-
assertType('non-empty-array<int, array{bar: 1, baz: 2}>', $array);
17+
assertType('non-empty-array<int, array{bar: 1, baz?: 2}>', $array);
1818
}
1919

2020
public function doBar(int $i, int $j)
@@ -27,7 +27,7 @@ public function doBar(int $i, int $j)
2727
echo $array[$i][$j]['bar'];
2828
echo $array[$i][$j]['baz'];
2929

30-
assertType('non-empty-array<int, non-empty-array<int, array{bar: 1, baz: 2}>>', $array);
30+
assertType('non-empty-array<int, non-empty-array<int, array{bar: 1, baz?: 2}>>', $array);
3131
}
3232

3333
}

tests/PHPStan/Analyser/nsrt/bug-10438.php

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,7 @@ public function extract(SimpleXMLElement $data, string $type = 'Meta'): array
2525
$meta[$key] = [];
2626
assertType('array{}', $meta[$key]);
2727
foreach ($tag->{$valueName} as $value) {
28-
assertType('list<string>', $meta[$key]);
28+
assertType('list<string>|string', $meta[$key]);
2929
$meta[$key][] = (string)$value;
3030
}
3131
}
Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
<?php declare(strict_types = 1);
2+
3+
namespace Bug13857;
4+
5+
use function PHPStan\Testing\assertType;
6+
7+
/**
8+
* @param array<int, array{state: string}> $array
9+
*/
10+
function test(array $array, int $id): void {
11+
$array[$id]['state'] = 'foo';
12+
// only one element was set to 'foo', not all of them.
13+
// correct type would be: non-empty-array<int, array{state: string}>
14+
assertType('non-empty-array<int, array{state: string}>', $array);
15+
}
16+
17+
/**
18+
* @param array<string, array{name: string, age: int}> $people
19+
*/
20+
function test2(array $people, string $key): void {
21+
$people[$key]['name'] = 'John';
22+
// age becomes optional because $key might not exist yet, creating array{name: 'John'} without age
23+
assertType('non-empty-array<string, array{name: string, age?: int}>', $people);
24+
}

tests/PHPStan/Analyser/nsrt/pr-4390.php

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,6 @@ function (string $s): void {
1313
}
1414
}
1515

16-
assertType('non-empty-array<int<0, 9>, non-empty-array<int<0, 9>, string>>', $locations);
17-
assertType('non-empty-array<int<0, 9>, string>', $locations[0]);
16+
assertType('non-empty-array<int<0, 9>, list<string>>', $locations);
17+
assertType('list<string>', $locations[0]);
1818
};

tests/PHPStan/Rules/Arrays/data/bug-11679.php

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,7 @@ public function sayHello(int $index): bool
3131
assertType('array<int, array{foo?: bool}>', $this->arr);
3232
if (!isset($this->arr[$index]['foo'])) {
3333
$this->arr[$index]['foo'] = true;
34-
assertType('non-empty-array<int, array{foo: true}>', $this->arr);
34+
assertType('non-empty-array<int, array{foo?: bool}>', $this->arr);
3535
}
3636
assertType('array<int, array{foo?: bool}>', $this->arr);
3737
return $this->arr[$index]['foo']; // PHPStan does not realize 'foo' is set

tests/PHPStan/Rules/Arrays/data/slevomat-foreach-array-key-exists-bug.php

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@ public function doFoo(array $percentageIntervals, array $changes): void
1818
assertType('non-empty-array<array{itemsCount: mixed, interval: mixed}>', $intervalResults);
1919
assertType('array{itemsCount: mixed, interval: mixed}', $intervalResults[$key]);
2020
$intervalResults[$key]['itemsCount'] += $itemsCount;
21-
assertType('non-empty-array<array{itemsCount: (array|float|int), interval: mixed}>', $intervalResults);
21+
assertType('non-empty-array<array{itemsCount: mixed, interval: mixed}>', $intervalResults);
2222
assertType('array{itemsCount: (array|float|int), interval: mixed}', $intervalResults[$key]);
2323
} else {
2424
assertType('array<array{itemsCount: mixed, interval: mixed}>', $intervalResults);

tests/PHPStan/Rules/Methods/data/bug-12927.php

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -40,7 +40,7 @@ public function sayFoo2(array $list): void
4040
{
4141
foreach($list as $k => $v) {
4242
$list[$k]['abc'] = 'world';
43-
assertType("non-empty-list<non-empty-array<string, string>&hasOffsetValue('abc', 'world')>", $list);
43+
assertType("non-empty-list<array<string, string>>", $list);
4444
assertType("non-empty-array<string, string>&hasOffsetValue('abc', 'world')", $list[$k]);
4545
}
4646
assertType("list<non-empty-array<string, string>&hasOffsetValue('abc', 'world')>", $list);

0 commit comments

Comments
 (0)