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.

© 2026 Xygeni. All rights reserved