ForeachReferenceSurvivesLoop
warning native on by defaultGroup: Probable bugs
A by-reference foreach leaves its value variable attached to the last array element. Unset that reference before reusing the variable.
Example
php
<?php
$xs=[1,2]; foreach($xs as &$x) {} $x=9;Reported:
- line 2: Unset the foreach reference before reusing the variable.
Detection
- A foreach binds a simple local variable by reference; after the loop, a reachable assignment to that same variable occurs before unset. Require the same lexical scope and no intervening control-flow ambiguity. Highlight the subsequent assignment.
- Exclude a statically proven empty iterable: no iteration then binds the reference. Also exclude a binding that is cleared with unset or rebound in the loop body; only a surviving element reference can justify the later-write finding. Unknown binding mutations discard survival proof.
- Known emptiness includes a directly written empty array and an immediately preceding ordinary non-reference assignment of an empty array to the same simple iterable local. Any unset of the binding or by-reference rebinding inside the loop body excludes the finding, even if the operation is conditional.
- D1f. Exclude an iterable that is a fresh array value no other code can reach, because a write through the leftover reference then changes nothing the program still holds. Two forms qualify, looked at directly in the foreach header (parentheses ignored): an array literal whose items are all plain values, and a function, method or static-method call that resolves to a declaration returning by value whose result type is known to be only an array. Everything else stays reportable: variables, properties, array elements and static properties; literals with a by-reference or spread item (those slots can alias live variables); calls declared with a by-reference return (including by-reference generators); calls whose result may be an object, a generator, an iterable or is unknown; unresolved or dynamic calls; and compound expressions such as ternaries. A literal assigned to a local first and then iterated is not fresh: the local keeps the array.
- Resolve builtin functions, classes, constants and methods; user-defined lookalikes are excluded. Follow only bounded local proof in one lexical scope.
- Emit one finding per violating operation, not per supporting evidence statement. When the proof is inconclusive, nothing is reported.
Exceptions (no report)
- Do not report a reference never reused, a by-value foreach, or a proven unset/rebinding before reuse.
- Do not report when the loop iterates a fresh array value as defined by D1f.
- Both standard suppression forms suppress this native ID.
Fix
None. Choosing a repair requires intent or additional runtime information.
PHP versions
Available throughout supported PHP 5.3–8.5 where the referenced builtin/extension is available. Respect syntax/version-specific receiver contracts; unavailable APIs are not this rule’s concern.
Configure
In custos.json:
json
{
"rules": {
"ForeachReferenceSurvivesLoop": {
"enabled": false
}
}
}Suppress
Before the statement or declaration (or the first statement of the file):
php
// @custos-ignore ForeachReferenceSurvivesLoop