Skip to content

avoid_unmodified_loop_condition

v1.0.0 Warning Control Flow

This rule flags a while or do/while loop whose condition reads only variables that the body never assigns. The condition evaluates the same way forever, so the loop either never runs or never stops.

An infinite loop hangs the isolate and freezes the UI. The cause is usually a forgotten i++, or an increment applied to the wrong variable.

If no variable the condition reads is ever written in the body, no execution can change the outcome — which is what makes this detectable.

See also: Dart: loops

void countdown(int total) {
var remaining = total;
while (remaining > 0) {
print(remaining); // LINT: `remaining` is never decremented
}
}

Advancing the wrong variable is the same bug wearing a disguise:

void countdown(int total) {
var remaining = total;
var printed = 0;
while (remaining > 0) {
printed++; // LINT: `remaining` still never changes
}
}
void countdown(int total) {
var remaining = total;
while (remaining > 0) {
print(remaining);
remaining--;
}
}

Or use a construct that advances for you:

void countdown(int total) {
for (var remaining = total; remaining > 0; remaining--) {
print(remaining);
}
}

Here the body runs once and then spins forever, which makes the bug harder to spot in a log:

void drain(int available) {
var left = available;
do {
print(left); // LINT: `left` is never decremented
} while (left > 0);
}

Advancing a copy is not advancing the condition

Section titled “Advancing a copy is not advancing the condition”

Assigning to a variable the condition does not read leaves it unchanged, whatever the body appears to be doing:

void poll(int attempts) {
var remaining = attempts;
var next = remaining;
while (remaining > 0) {
next = next - 1; // LINT: `remaining` is what the condition reads
}
}

The rule is deliberately narrow, because the cost of a false positive here is high.

while (true) is not reported — it is the idiomatic infinite loop, ended by a break. Any break, return or throw in the body suppresses the report for the same reason: the loop has an exit the condition does not control.

Only locals and parameters are tracked. A condition that reads a field, calls a method, accesses a property, or awaits is treated as opaque: those can change without an assignment in the body, so no conclusion is safe. A closure anywhere in the body also suppresses the report, since it may mutate a captured variable when invoked.

This rule is in the core preset, so it is on with preset: core, preset: recommended or preset: opinionated.

To turn it off:

many_lints.yaml
rules:
avoid_unmodified_loop_condition: false

To keep the rule on but skip certain paths, use per-rule exclude.