For Bad Increment
ID |
php.for_bad_increment |
Severity |
high |
Remediation Complexity |
trivial |
Remediation Risk |
low |
Remediation Effort |
low |
Resource |
Reliability |
Language |
Php |
Tags |
control-flow, loop, reliability |
Description
Reports a classic for loop whose update clause moves the counter away from the bound its condition
tests.
The finding sits on the update clause, which is usually the half that is wrong. Only clauses whose direction is certain are compared — see Configuration.
Rationale
<?php
function emitAll(int $limit): void
{
for ($i = 0; $i < $limit; $i--) { // FLAW — capped from above, counter goes down
emit($i);
}
}
The condition caps $i from above while the update lowers it, so the counter never gets closer to
the bound. Either the loop never ends, or — when the condition cannot hold on the first evaluation,
as in for ($i = 10; $i > 0; $i++) reversed — it never runs a single iteration.
PHP offers no rescue here. An int that grows past PHP_INT_MAX silently becomes a float rather
than wrapping around, and the comparison keeps holding, so the loop runs until max_execution_time
kills the request or the worker is reaped. In a CLI script there is no time limit at all.
Each clause is correct read on its own, which is why the mistake survives review: it is only the combination of init, condition and update that is wrong.
Remediation
Decide which direction the loop is meant to run in and make the two clauses agree.
<?php
for ($i = 0; $i < $limit; $i++) { // ascending
emit($i);
}
for ($i = $limit; $i > 0; $i--) { // descending
emit($i);
}
When the loop is walking an array, foreach removes the counter — and the chance of this mistake —
altogether; see php.foreach_for_arrays.
Configuration
This rule has no configurable properties. It reports only when both directions are certain, and several shapes are therefore excluded.
Update clauses whose direction is resolved: $i`, `$i--`, `$i, --$i,
$i += literal, $i -= literal, $i = $i + literal and $i = $i - literal, where the literal is
a positive integer. A step of unknown sign is left alone:
for ($i = 0; $i < $limit; $i += $step) { // OK — the sign of $step is not knowable here
emit($i);
}
Conditions whose direction is resolved: <, ⇐, > and >= comparing the counter directly
against a bound, in either operand position. != and !== set no bound to move towards, and a
comparison against an expression built from the counter is not a direct comparison:
for ($i = 0; $i !== $limit; $i--) { // OK — no bound to reach
for ($i = 0; $i + 1 < $limit; $i--) { // OK — the counter is not compared directly
A single update expression. A for with several updates — the converging idiom
for ($i = 0, $j = $n; $i < $j; $i++, $j--) — is skipped entirely, because which counter the
condition is about would then be a guess.
A plain variable counter. A property or an array element ($this→cursor--, $row['i']--) is
not reported: what such an expression reads on the next iteration is not settled by the update
clause alone.
An empty condition (for ($i = 0; ; $i--)) or an empty update clause gives nothing to compare and
is never reported. The alternative syntax (for (…): … endfor;) is analysed exactly like the braced
form.
Related: php.no_update_loop_vars reports a loop that updates its control variable in the body as
well as the header — a different defect that does not overlap with this one.