Skip to content

Expose the loop that overwrites a variable still in use - #6414

Merged
ondrejmirtes merged 1 commit into
2.3.xfrom
loop-variable-reuse
Sep 11, 2026
Merged

ondrejmirtes merged 1 commit into
2.3.xfrom
loop-variable-reuse

Conversation

@ondrejmirtes

Copy link
Copy Markdown
Member

Needed by phpstan/phpstan-strict-rules#332 (also phpstan/phpstan#9940).

OverwriteVariablesWithForeachRule fires on any foreach whose key/value variable is already defined. Since #6380 tracks definedness precisely, that includes reusing a spent loop variable after a flag-then-check (if (!$found) return;). The rule needs a liveness answer: does the loop take over a variable that was assigned before it and is read after it, with nothing but the loop's own writes in between?

This adds VariableWritesNode::getVariableOverwritingLoop(VariableWrite $write): Foreach_|For_|null, answering exactly that for foreach key/value bindings and for-loop initial assignments (list targets included).

How it works:

  • ForeachHandler / ForHandler wrap the statement's flow in a new VariableFlow::loopStatement() carrying the head bindings and the statement's own writes (bindings plus a for-loop's update, e.g. $i++).
  • In VariableLivenessResolver, when the bound variable is live after the statement, a probe key (a \0-prefixed key no read can produce) enters the live set. It travels backwards through the statement: own writes let it through, other whole-variable writes kill it. Surviving to the statement entry it is armed; the first earlier assignment, by-reference alias, or offset write of the variable then records the loop. unset() kills it without recording.

Examples (see the fixture):

  • foreach ($a as $x) {} foreach ($b as $x) {} — silent
  • $found = false; foreach (...) { ... break; } if (!$found) return; foreach ($a as $x) {} — silent
  • foreach ($outer as $x) { foreach ($inner as $x) {} use($x); } — inner loop reported
  • function (string $x) { foreach ($a as $x) {} echo $x; } — reported
  • for ($i = 0; ...) {} for ($i = 0; ...) {} — silent; reported only if $i is read after the second loop

Strict-rules side follows once this is in the dev phar.

🤖 Generated with Claude Code

https://claude.ai/code/session_01LYiPGg9cpsKLyK6X5BrJTT

phpstan-strict-rules reports a foreach/for that reuses a variable name.
Since definedness became precise, it also fires on reusing a spent loop
variable. The rule needs to know whether the loop takes over a variable
that was assigned before it and is read after it - a liveness question.

VariableWritesNode::getVariableOverwritingLoop() answers it for foreach
key/value bindings and for-loop initial assignments: the statement's
flow is wrapped, and a probe in the live set follows the variable
backwards through the loop (the loop's own bindings and updates let it
through, any other write kills it) and records the assignment before the
loop that the binding replaces.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LYiPGg9cpsKLyK6X5BrJTT
@phpstan-bot

Copy link
Copy Markdown
Collaborator

You've opened the pull request against the latest branch 2.3.x. PHPStan 2.3 is not going to be released for months. If your code is relevant on 2.2.x and you want it to be released sooner, please rebase your pull request and change its target to 2.2.x.

@ondrejmirtes
ondrejmirtes merged commit 6370618 into 2.3.x Sep 11, 2026
747 of 806 checks passed
@ondrejmirtes
ondrejmirtes deleted the loop-variable-reuse branch September 11, 2026 10:18
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