Skip to content

LoopClosureCapturesReference ​

warning native off by default

Group: Probable bugs

Deferred closures that capture a loop counter by reference observe its later value. Capture by value when each callback needs its own iteration value. This inspection is disabled by default because the intended policy or use can vary.

Example ​

php
<?php
$jobs=[]; for($i=0;$i<3;$i++){ $jobs[]=function() use (&$i){return $i;}; } foreach($jobs as $job){echo $job();}

Reported:

  • line 2: Capture the iteration counter by value for deferred callbacks.

Detection ​

  • A closure appended to a local array inside a for loop explicitly captures the loop counter by reference. The array is consumed after the loop and the closure body reads that counter in its own variable scope. Reads inside nested closures, arrow functions, functions or other variable scopes do not establish this proof. A plain assignment target is a write, not a read, and so are a foreach key or value target, a value slot of a list()/[] destructuring pattern (nested patterns included, in an assignment or a foreach target) and an unset() operand. Compound assignments and increments also read their target, as do a destructuring key expression and an element of an ordinary array value. Require no intervening overwrite or escape of 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 unresolved or shadowed builtin names, unknown values, or incomplete receiver/type provenance. Unknown calls, aliases escaping, and intervening writes invalidate local proof. Similar code outside the stated detection contract is not a finding.
  • A nested callback parameter with the same name does not read the captured outer counter. A closure that only writes the counter without reading it is excluded, whether it assigns it, destructures into it, iterates into it or unsets it.
  • 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": {
    "LoopClosureCapturesReference": {
      "enabled": true
    }
  }
}

Suppress ​

Before the statement or declaration (or the first statement of the file):

php
// @custos-ignore LoopClosureCapturesReference

Released under the MIT License. Native rules and rules modeled on Php Inspections (EA Extended); independent clean-room implementation.