Else If Without Else
ID |
go.else_if_without_else |
Severity |
high |
Remediation Complexity |
trivial |
Remediation Risk |
low |
Remediation Effort |
low |
Resource |
Code Smell |
Language |
Go |
Tags |
code-style, conditional, control-flow |
Description
Reports an if / else if chain that has no final else and whose branches can all fall through.
At least one else if is required: a lone if with no else is an ordinary guard and is never
reported. A chain whose every branch ends the flow — return, break, continue, goto,
panic(…) or os.Exit(…) — is also not reported; see Configuration.
Rationale
func tally(level string, c *counters) {
if level == "warn" { // FLAW — no final else
c.warn++
} else if level == "fail" {
c.fail++
}
}
A chain of conditions with no catch-all leaves one case unwritten: the one where none of the conditions hold. Nothing in the source says whether doing nothing is the intended behaviour for that case or whether the branch was forgotten, so a reader cannot tell a deliberate no-op from an oversight, and neither can a reviewer.
The gap gets worse over time. When a value is added to whatever level enumerates, the new value
falls through the whole chain silently — the counter is simply never incremented, with no error and
no log line. A chain that ends in else fails loudly instead, at the one place that was written to
catch exactly this.
Remediation
End the chain with an else that states the remaining case, even when the statement is that nothing
should happen.
func tally(level string, c *counters) {
if level == "warn" {
c.warn++
} else if level == "fail" {
c.fail++
} else {
c.other++
}
}
When the remaining case really is unreachable, say so where a reader and a test will both see it:
} else {
panic(fmt.Sprintf("unhandled level %q", level))
}
If the chain is dispatching on a single value, a switch with a default says the same thing more
directly — see go.missing_switch_default.
Configuration
This rule has no configurable properties. Two shapes are excluded by construction.
Chains whose every branch terminates. When the if body and every else if body end in
return, break, continue, goto, panic(…) or os.Exit(…), control provably leaves the
chain in each branch, and the statements after the chain are the else case:
if n > 0 {
return "positive"
} else if n < 0 {
return "negative"
}
return "zero" // OK — this is the else, unindented
That is the idiomatic Go early return, and go.superfluous_else asks for exactly it: writing the
else back in would earn a finding from that rule. Reporting it here would put the two rules in
direct contradiction on the same code, so both decide what "terminates" means the same way.
A lone if. One guard is not an enumeration, so if x { … } and if x { … } else { … }
are outside the rule. What is left is the shape the rule is about: a ladder of conditions that only
assigns or calls, and silently does nothing when no condition matches.