Skip to content

Commit 9e394e1

Browse files
phpstan-botclaude
andcommitted
Fix nested dim fetch incorrectly preserving list type after container reset
When processing nested array dim fetch assignments like `$arr[$key][$int] = $value` where the container was previously reset (`$arr[$key] = []`), the list type was incorrectly preserved. This happened because `produceArrayDimFetchAssignValueToWrite` used `setExistingOffsetValueType` (which preserves list structure) based on the general type from the parent's type system, while the actual tracked container type was an empty array where the offset couldn't exist. The fix adds a check at container levels (inner dimensions of nested dim fetches) to verify that the scope's actual tracked type of the container also supports the offset before using `setExistingOffsetValueType`. This correctly falls through to `setOffsetValueType` when the container was just reset to `[]`, which properly degrades the list type. This addresses the actual reproducer from phpstan/phpstan#14336 where the previous IntersectionType::setOffsetValueType fix alone was insufficient. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
1 parent e2022ca commit 9e394e1

File tree

2 files changed

+50
-0
lines changed

2 files changed

+50
-0
lines changed

src/Analyser/ExprHandler/AssignHandler.php

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1057,11 +1057,13 @@ private function produceArrayDimFetchAssignValueToWrite(array $dimFetchStack, ar
10571057
}
10581058

10591059
$arrayDimFetch = $dimFetchStack[$i] ?? null;
1060+
$isContainerLevel = $i + 1 < count($dimFetchStack);
10601061
if (
10611062
$offsetType !== null
10621063
&& $arrayDimFetch !== null
10631064
&& $scope->hasExpressionType($arrayDimFetch)->yes()
10641065
&& !$offsetValueType->hasOffsetValueType($offsetType)->no()
1066+
&& (!$isContainerLevel || !$scope->getType($arrayDimFetch)->hasOffsetValueType($offsetType)->no())
10651067
) {
10661068
$hasOffsetType = null;
10671069
if ($offsetType instanceof ConstantStringType || $offsetType instanceof ConstantIntegerType) {

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

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,54 @@
44

55
use function PHPStan\Testing\assertType;
66

7+
/**
8+
* Actual reproducer from the issue: nested dim fetch with arbitrary int key
9+
* after resetting array element to [].
10+
*
11+
* @param array<string, list<array{xmlNamespace: string, namespace: string, name: string}>> $xsdFiles
12+
* @param array<string, list<array{xmlNamespace: string, namespace: string, name: string}>> $groupedByNamespace
13+
* @param array<string, list<string>> $extraNamespaces
14+
*/
15+
function testIssueReproducer(array $xsdFiles, array $groupedByNamespace, array $extraNamespaces, int $int): void
16+
{
17+
foreach ($extraNamespaces as $mergedNamespace) {
18+
if (count($mergedNamespace) < 2) {
19+
continue;
20+
}
21+
22+
$targetNamespace = end($mergedNamespace);
23+
if (!isset($groupedByNamespace[$targetNamespace])) {
24+
continue;
25+
}
26+
$xmlNamespace = $groupedByNamespace[$targetNamespace][0]['xmlNamespace'];
27+
28+
assertType('string', $xmlNamespace);
29+
assertType('non-empty-list<string>&hasOffsetValue(1, string)', $mergedNamespace);
30+
31+
$xsdFiles[$xmlNamespace] = [];
32+
foreach ($mergedNamespace as $namespace) {
33+
foreach ($groupedByNamespace[$namespace] ?? [] as $viewHelper) {
34+
$xsdFiles[$xmlNamespace][$int] = $viewHelper;
35+
}
36+
}
37+
// After assigning any int, $xsdFiles[$xmlNamespace] should NOT be a list
38+
assertType('array<int, array{xmlNamespace: string, namespace: string, name: string}>', $xsdFiles[$xmlNamespace]);
39+
$xsdFiles[$xmlNamespace] = array_values($xsdFiles[$xmlNamespace]);
40+
}
41+
}
42+
43+
/**
44+
* Simplified: nested dim fetch after reset with arbitrary int key.
45+
*
46+
* @param array<string, list<array{name: string}>> $arr
47+
*/
48+
function testNestedDimFetchAfterReset(array $arr, int $int, string $key): void
49+
{
50+
$arr[$key] = [];
51+
$arr[$key][$int] = ['name' => 'test'];
52+
assertType("non-empty-array<int, array{name: string}>", $arr[$key]);
53+
}
54+
755
/**
856
* Assigning with arbitrary int key in a loop should degrade list to array.
957
*

0 commit comments

Comments
 (0)