ClosureCapturedValueWrite
warning native off by defaultGroup: Probable bugs
Detect a closure writing a scalar captured by value when that closure is invoked and the outer variable is subsequently read without an intervening assignment.
Example
<?php
$n = 0; $f = function () use ($n) { ++$n; }; $f(); echo $n;Reported:
- line 2: Capture the scalar by reference when updating outer state.
Detection
- Report a closure writing a scalar captured by value when that closure is invoked and the outer variable is subsequently read without an intervening assignment.
- Resolve builtin calls and named arguments before checking the contract. Project-local callees are considered only when their bodies or immutable summaries establish the required facts. The default is disabled; enable only when the stated intent matches the application.
- For flow-dependent conditions, require a reachable source-to-use path with no intervening invalidation. Unknown calls, ambiguous aliases or recursion cannot establish definite facts. When the proof is inconclusive, nothing is reported.
Exceptions (no report)
Ignore writes or transformations after unconditional termination or inside a literal false if branch; these statements cannot establish a missing update or mapped value.
Unknown invocations, object member mutation, explicit references, and deliberate inner-local results are excluded.
The closure uses its copy as a working local, so capturing by reference would change behavior. No report when, in reachable closure code, any of these holds:
- the captured variable is mentioned anywhere other than as the target of a write to itself (for example echoed, interpolated, passed to a call, or captured or used by a nested function or class body);
- the value of a write to it is consumed rather than discarded, i.e. the assignment or increment/decrement is not a whole expression statement (
while ($n-- > 0),echo $n = 4, aforstep); - the closure returns a value.
A statement-level write whose right-hand side reads the same variable (
$n = $n + 1;,$s .= 'x';) is still a lost update and stays reported when nothing else reads the copy. This deliberately gives up closures that update the copy and then also read it (++$n; echo $n;): intent cannot be told apart there.Unresolved targets, incomplete required facts, suppressed findings and PHP versions lacking the referenced API or language feature produce no report. User declarations shadowing builtin names are not builtin contracts.
Fix
No automatic fix: changing capture semantics can affect lifetime and subsequent calls.
PHP versions
Apply only on PHP versions supporting the referenced APIs and syntax (within PHP 5.3–8.5). Builtin behavior and argument contracts follow the configured PHP version. Extension-specific calls require resolved extension API symbols; do not treat same-named application functions as extension calls.
Options
None. Enable or disable this rule through normal rule configuration.
Configure
In custos.json:
{
"rules": {
"ClosureCapturedValueWrite": {
"enabled": true
}
}
}Suppress
Before the statement or declaration (or the first statement of the file):
// @custos-ignore ClosureCapturedValueWrite