Loop At Most One Iteration
ID |
csharp.loop_at_most_one_iteration |
Severity |
high |
Remediation Complexity |
trivial |
Remediation Risk |
low |
Remediation Effort |
low |
Resource |
Reliability |
Language |
CSharp |
Tags |
control-flow, loop, reliability |
Description
Reports a for, foreach, while or do loop whose body ends in an unconditional break,
return, throw or continue. The first pass always transfers control, so the loop cannot
iterate as written.
Rationale
A loop is written to repeat. When the last statement of its body is a jump that no condition
guards, the repetition never happens: the body runs once and control leaves. The usual cause is a
jump that belongs inside an if but was typed one indentation level too high, which turns "handle
every element" into "handle the first element" without any compiler complaint.
A trailing continue is a milder variant of the same mistake — it does not cut the loop short,
it simply has no effect, because control was already about to reach the end of the body. Either
way the statement misleads whoever reads the loop next.
Only the last statement of the body is examined, so a guarded exit — the ordinary idiom — is never
reported. That includes a break inside an if, a break that closes a switch section, a
throw inside a try that catches, and a jump that belongs to an inner loop. Wrappers that guard
nothing are looked through: a jump ending a using block, a lock block or a try with no catch
clause still ends the first iteration and is reported.
A loop whose body holds a conditional continue targeting it is never reported at all. That
continue is direct evidence that the loop was written to run more than once, so the trailing jump
is the deliberate end of the find-first idiom rather than a misplaced statement.
public string FirstHandler(string[] handlers)
{
foreach (var handler in handlers) // FLAW — returns on the first element; the rest are dead
{
return handler;
}
return null;
}
public void ScanOnce(int[] values)
{
for (int i = 0; i < values.Length; i++) // FLAW — the break was meant to be conditional
{
Log(values[i]);
break;
}
}
public string ReadFirst(string[] paths)
{
foreach (var path in paths) // FLAW — the using block guards nothing
{
using (var reader = new StreamReader(path))
{
return reader.ReadLine();
}
}
return null;
}
public void ScanUntilNegative(int[] values)
{
foreach (var value in values) // OK — the break is guarded
{
if (value < 0) break;
Log(value);
}
}
public string FirstAccepted(Handler[] handlers, string key)
{
foreach (var handler in handlers) // OK — the guarded continue proves the loop iterates
{
if (!handler.Accepts(key)) continue;
return handler.Name;
}
return null;
}
public void Classify(int[] values)
{
foreach (var value in values) // OK — the break closes the switch section
{
switch (value)
{
case 0:
Log("zero");
break;
default:
Log("other");
break;
}
}
}
Remediation
Decide what the jump was meant to do. If the loop should stop early on some condition, move the
jump into an if that expresses it. If only the first element was ever wanted, drop the loop and
index or query the collection directly (for example a FirstOrDefault call), which states the
intent plainly. A trailing continue can simply be deleted.