Skip to content

Emit virtual Assign node for List_ destructuring in foreach value position#5504

Merged
ondrejmirtes merged 1 commit intophpstan:2.1.xfrom
phpstan-bot:create-pull-request/patch-dnix257
Apr 21, 2026
Merged

Emit virtual Assign node for List_ destructuring in foreach value position#5504
ondrejmirtes merged 1 commit intophpstan:2.1.xfrom
phpstan-bot:create-pull-request/patch-dnix257

Conversation

@phpstan-bot
Copy link
Copy Markdown
Collaborator

Summary

When array destructuring is used in the foreach value position (e.g. foreach ($arr as ['b' => $val])), PHPStan did not report nonexistent offsets. The same destructuring via regular assignment (['b' => $val] = $arr[0]) or assignment inside the foreach body (['b' => $val] = $item) correctly reported errors.

Changes

  • src/Analyser/NodeScopeResolver.php: In the foreach handling code, added emission of a virtual Assign node when $stmt->valueVar is a List_. This is placed alongside the existing VariableAssignNode emission (before the loop convergence iterations) to avoid duplicate reports. The virtual node pairs the List_ with a GetIterableValueTypeExpr so ArrayDestructuringRule can resolve the iterable value type and check offset existence.

  • tests/PHPStan/Rules/Arrays/ArrayDestructuringRuleTest.php: Added testBug8075 test method.

  • tests/PHPStan/Rules/Arrays/data/bug-8075.php: Regression test data covering:

    • Basic foreach destructuring with nonexistent key
    • Mixed existent/nonexistent keys in foreach
    • PHPDoc-typed arrays with named keys
    • Numeric offset destructuring (tuples)
    • Nested array destructuring in foreach

Root cause

ArrayDestructuringRule is registered as Rule<Assign> — it only fires when the node callback receives an Assign node. For regular assignments like ['b' => $val] = $expr, the parser produces an Assign(List_, expr) which the rule catches. But for foreach ($arr as ['b' => $val]), the parser produces a Foreach_ node with valueVar = List_, and NodeScopeResolver::enterForeach called processVirtualAssign to handle scope updates without ever emitting an Assign node for rules to process.

Analogous cases probed

  • Foreach key destructuring: Not valid PHP syntax — keys must be simple variables
  • list() vs [] syntax: Both produce List_ AST nodes — handled by the same fix
  • Regular assignment destructuring: Already worked correctly (tested in regression test)
  • Assignment inside foreach body: Already worked correctly (tested in regression test)
  • Nested destructuring: ArrayDestructuringRule::getErrors() already recurses for nested List_ nodes — works with the fix
  • Other rules listening for Assign: Only ArrayDestructuringRule does — no unintended side effects

Test

Regression test testBug8075 in ArrayDestructuringRuleTest covers the reported bug and several variations: string keys, integer keys, PHPDoc-typed arrays, nested destructuring, and mixed existent/nonexistent keys.

Fixes phpstan/phpstan#8075

…ue position

- When a `foreach` uses array destructuring in the value position
  (e.g. `foreach ($arr as ['key' => $val])`), `ArrayDestructuringRule`
  was never invoked because no `Assign` node was emitted for it to match.
- In `NodeScopeResolver::processStmtNode`, alongside the existing
  `VariableAssignNode` emission for simple variable foreach values,
  emit a virtual `Assign(List_, GetIterableValueTypeExpr)` node when
  the value var is a `List_`. This causes `ArrayDestructuringRule` to
  fire and report nonexistent offsets.
- The virtual node is emitted once before the loop convergence iterations,
  avoiding duplicate error reports.
- Covers both `[]` and `list()` syntax, string/integer keys, and nested
  destructuring patterns.
- Analogous cases probed and found not broken: foreach key destructuring
  (not valid PHP), regular assignment destructuring (already worked),
  assignment inside foreach body (already worked).
@ondrejmirtes ondrejmirtes merged commit 14856da into phpstan:2.1.x Apr 21, 2026
654 of 655 checks passed
@ondrejmirtes ondrejmirtes deleted the create-pull-request/patch-dnix257 branch April 21, 2026 19:40
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants