From 81db5f5b6f660942c1e3fa1a83e67233d0987ad6 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 1/5] 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); + } + } +} From 44b20311d8adb0f8ce970e6a5d9230983b21318e Mon Sep 17 00:00:00 2001 From: Markus Staab Date: Fri, 20 Feb 2026 09:24:58 +0100 Subject: [PATCH 2/5] more tests --- .../Analyser/NodeScopeResolverTest.php | 2 + .../ParameterOutAssignedTypeRuleTest.php | 5 +++ .../Rules/Variables/data/bug-14124.php | 4 ++ .../Rules/Variables/data/bug-14124b.php | 37 +++++++++++++++++++ 4 files changed, 48 insertions(+) create mode 100644 tests/PHPStan/Rules/Variables/data/bug-14124b.php diff --git a/tests/PHPStan/Analyser/NodeScopeResolverTest.php b/tests/PHPStan/Analyser/NodeScopeResolverTest.php index da0f571127..d0983dbb84 100644 --- a/tests/PHPStan/Analyser/NodeScopeResolverTest.php +++ b/tests/PHPStan/Analyser/NodeScopeResolverTest.php @@ -253,6 +253,8 @@ private static function findTestFiles(): iterable yield __DIR__ . '/../Rules/Arrays/data/narrow-superglobal.php'; yield __DIR__ . '/../Rules/Methods/data/bug-12927.php'; yield __DIR__ . '/../Rules/Properties/data/bug-14012.php'; + yield __DIR__ . '/../Rules/Variables/data/bug-14124.php'; + yield __DIR__ . '/../Rules/Variables/data/bug-14124b.php'; } /** diff --git a/tests/PHPStan/Rules/Variables/ParameterOutAssignedTypeRuleTest.php b/tests/PHPStan/Rules/Variables/ParameterOutAssignedTypeRuleTest.php index db289635f3..6da45dd248 100644 --- a/tests/PHPStan/Rules/Variables/ParameterOutAssignedTypeRuleTest.php +++ b/tests/PHPStan/Rules/Variables/ParameterOutAssignedTypeRuleTest.php @@ -86,4 +86,9 @@ public function testBug14124(): void $this->analyse([__DIR__ . '/data/bug-14124.php'], []); } + public function testBug14124b(): void + { + $this->analyse([__DIR__ . '/data/bug-14124b.php'], []); + } + } diff --git a/tests/PHPStan/Rules/Variables/data/bug-14124.php b/tests/PHPStan/Rules/Variables/data/bug-14124.php index 59fbd4233c..be3ad1a8de 100644 --- a/tests/PHPStan/Rules/Variables/data/bug-14124.php +++ b/tests/PHPStan/Rules/Variables/data/bug-14124.php @@ -2,6 +2,8 @@ namespace Bug14124; +use function PHPStan\Testing\assertType; + /** * @param array> $convert * @param-out array> $convert @@ -13,6 +15,7 @@ function example3a(array &$convert): void $val = strtoupper($val); } } + assertType('array>', $convert); } /** @@ -26,4 +29,5 @@ function example3b(array &$convert): void $convert[$outerKey][$key] = strtoupper($val); } } + assertType('array>', $convert); } diff --git a/tests/PHPStan/Rules/Variables/data/bug-14124b.php b/tests/PHPStan/Rules/Variables/data/bug-14124b.php new file mode 100644 index 0000000000..0ad84eb7a3 --- /dev/null +++ b/tests/PHPStan/Rules/Variables/data/bug-14124b.php @@ -0,0 +1,37 @@ +>> $convert + * @param-out array>> $convert + */ +function example3a(array &$convert): void +{ + foreach ($convert as &$inner) { + foreach ($inner as &$val) { + foreach ($val as &$val2) { + $val2 = strtoupper($val2); + } + } + } + assertType('array>>', $convert); +} + +/** + * @param array>> $convert + * @param-out array>> $convert + */ +function example3b(array &$convert): void +{ + foreach ($convert as $outerKey => $inner) { + foreach ($inner as $key => $val) { + foreach ($val as $key2 => $val2) { + $convert[$outerKey][$key][$key2] = strtoupper($val); + } + } + } + assertType('array>>', $convert); +} From 0d142ab04ad12b3e0ef57e6f01cf2de03c88b6f8 Mon Sep 17 00:00:00 2001 From: Markus Staab Date: Fri, 20 Feb 2026 09:34:45 +0100 Subject: [PATCH 3/5] try one more fix --- src/Analyser/NodeScopeResolver.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Analyser/NodeScopeResolver.php b/src/Analyser/NodeScopeResolver.php index fd374a9e3e..f082fb183a 100644 --- a/src/Analyser/NodeScopeResolver.php +++ b/src/Analyser/NodeScopeResolver.php @@ -6627,7 +6627,7 @@ private function processAssignVar( } if ($var instanceof Variable && is_string($var->name)) { - $this->callNodeCallback($nodeCallback, new VariableAssignNode($var, $assignedPropertyExpr), $scope, $storage); + $this->callNodeCallback($nodeCallback, new VariableAssignNode($var, new TypeExpr($valueToWrite)), $scope, $storage); $scope = $scope->assignVariable($var->name, $valueToWrite, $nativeValueToWrite, TrinaryLogic::createYes()); } else { if ($var instanceof PropertyFetch || $var instanceof StaticPropertyFetch) { From 16a20023ffbf96b5d309b9bd933d9f213e3d773f Mon Sep 17 00:00:00 2001 From: Markus Staab Date: Fri, 20 Feb 2026 15:59:06 +0100 Subject: [PATCH 4/5] Revert "try one more fix" This reverts commit 0d142ab04ad12b3e0ef57e6f01cf2de03c88b6f8. --- src/Analyser/NodeScopeResolver.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Analyser/NodeScopeResolver.php b/src/Analyser/NodeScopeResolver.php index f082fb183a..fd374a9e3e 100644 --- a/src/Analyser/NodeScopeResolver.php +++ b/src/Analyser/NodeScopeResolver.php @@ -6627,7 +6627,7 @@ private function processAssignVar( } if ($var instanceof Variable && is_string($var->name)) { - $this->callNodeCallback($nodeCallback, new VariableAssignNode($var, new TypeExpr($valueToWrite)), $scope, $storage); + $this->callNodeCallback($nodeCallback, new VariableAssignNode($var, $assignedPropertyExpr), $scope, $storage); $scope = $scope->assignVariable($var->name, $valueToWrite, $nativeValueToWrite, TrinaryLogic::createYes()); } else { if ($var instanceof PropertyFetch || $var instanceof StaticPropertyFetch) { From 502a4b6cdf6f5137802a883e937fc85bff17c32b Mon Sep 17 00:00:00 2001 From: Markus Staab Date: Fri, 20 Feb 2026 16:00:46 +0100 Subject: [PATCH 5/5] Update NodeScopeResolver.php --- src/Analyser/NodeScopeResolver.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Analyser/NodeScopeResolver.php b/src/Analyser/NodeScopeResolver.php index fd374a9e3e..4751e203a9 100644 --- a/src/Analyser/NodeScopeResolver.php +++ b/src/Analyser/NodeScopeResolver.php @@ -6314,7 +6314,7 @@ private function processAssignVar( } } else { if ($var instanceof Variable) { - $this->callNodeCallback($nodeCallback, new VariableAssignNode($var, new TypeExpr($valueToWrite)), $scopeBeforeAssignEval, $storage); + $this->callNodeCallback($nodeCallback, new VariableAssignNode($var, $assignedPropertyExpr), $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) {