From b9f6f8c9ea8cac0c46ee9c445a836e8c99ac31bf Mon Sep 17 00:00:00 2001 From: staabm <120441+staabm@users.noreply.github.com> Date: Thu, 19 Feb 2026 17:23:41 +0000 Subject: [PATCH] Fix param-out list type lost during nested array dim assignment - Pass correctly computed valueToWrite type to VariableAssignNode instead of the assignedPropertyExpr expression tree which loses list types on nested array dimension assignments - The assignedPropertyExpr uses setOffsetValueType for non-outermost dims, losing AccessoryArrayListType, while produceArrayDimFetchAssignValueToWrite correctly uses setExistingOffsetValueType via cross-paired dim fetch checks - New regression test in tests/PHPStan/Rules/Variables/data/bug-14124.php Closes https://github.com/phpstan/phpstan/issues/14124 --- src/Analyser/NodeScopeResolver.php | 4 +-- .../ParameterOutAssignedTypeRuleTest.php | 5 ++++ .../Rules/Variables/data/bug-14124.php | 29 +++++++++++++++++++ 3 files changed, 36 insertions(+), 2 deletions(-) create mode 100644 tests/PHPStan/Rules/Variables/data/bug-14124.php diff --git a/src/Analyser/NodeScopeResolver.php b/src/Analyser/NodeScopeResolver.php index bb361d5615..fd374a9e3e 100644 --- a/src/Analyser/NodeScopeResolver.php +++ b/src/Analyser/NodeScopeResolver.php @@ -6297,7 +6297,7 @@ private function processAssignVar( if ($varType->isArray()->yes() || !(new ObjectType(ArrayAccess::class))->isSuperTypeOf($varType)->yes()) { if ($var instanceof Variable && is_string($var->name)) { - $this->callNodeCallback($nodeCallback, new VariableAssignNode($var, $assignedPropertyExpr), $scopeBeforeAssignEval, $storage); + $this->callNodeCallback($nodeCallback, new VariableAssignNode($var, new TypeExpr($valueToWrite)), $scopeBeforeAssignEval, $storage); $scope = $scope->assignVariable($var->name, $valueToWrite, $nativeValueToWrite, TrinaryLogic::createYes()); } else { if ($var instanceof PropertyFetch || $var instanceof StaticPropertyFetch) { @@ -6314,7 +6314,7 @@ private function processAssignVar( } } else { if ($var instanceof Variable) { - $this->callNodeCallback($nodeCallback, new VariableAssignNode($var, $assignedPropertyExpr), $scopeBeforeAssignEval, $storage); + $this->callNodeCallback($nodeCallback, new VariableAssignNode($var, new TypeExpr($valueToWrite)), $scopeBeforeAssignEval, $storage); } elseif ($var instanceof PropertyFetch || $var instanceof StaticPropertyFetch) { $this->callNodeCallback($nodeCallback, new PropertyAssignNode($var, $assignedPropertyExpr, $isAssignOp), $scopeBeforeAssignEval, $storage); if ($var instanceof PropertyFetch && $var->name instanceof Node\Identifier && !$isAssignOp) { diff --git a/tests/PHPStan/Rules/Variables/ParameterOutAssignedTypeRuleTest.php b/tests/PHPStan/Rules/Variables/ParameterOutAssignedTypeRuleTest.php index bdd78d2dfd..db289635f3 100644 --- a/tests/PHPStan/Rules/Variables/ParameterOutAssignedTypeRuleTest.php +++ b/tests/PHPStan/Rules/Variables/ParameterOutAssignedTypeRuleTest.php @@ -81,4 +81,9 @@ public function testBug12754(): void $this->analyse([__DIR__ . '/data/bug-12754.php'], []); } + public function testBug14124(): void + { + $this->analyse([__DIR__ . '/data/bug-14124.php'], []); + } + } diff --git a/tests/PHPStan/Rules/Variables/data/bug-14124.php b/tests/PHPStan/Rules/Variables/data/bug-14124.php new file mode 100644 index 0000000000..59fbd4233c --- /dev/null +++ b/tests/PHPStan/Rules/Variables/data/bug-14124.php @@ -0,0 +1,29 @@ +> $convert + * @param-out array> $convert + */ +function example3a(array &$convert): void +{ + foreach ($convert as &$inner) { + foreach ($inner as &$val) { + $val = strtoupper($val); + } + } +} + +/** + * @param array> $convert + * @param-out array> $convert + */ +function example3b(array &$convert): void +{ + foreach ($convert as $outerKey => $inner) { + foreach ($inner as $key => $val) { + $convert[$outerKey][$key] = strtoupper($val); + } + } +}