Duplicate Branches
ID |
kotlin.duplicate_branches |
Severity |
high |
Remediation Complexity |
medium |
Remediation Risk |
low |
Remediation Effort |
medium |
Resource |
Code Smell |
Language |
Kotlin |
Tags |
code-style, duplicate-code |
Description
Reports a conditional structure whose branches all run exactly the same code: an
if / else pair or an if / else if / else chain where every body is identical, or a when
where every branch is identical. Both when forms are covered — with a subject and without.
Rationale
fun bumpIfVerbose(verbose: Boolean, count: Int): Int {
if (verbose) {
log(count)
return count + 1
} else {
log(count)
return count + 1
}
}
When every branch does the same thing, the condition decides nothing. Either the whole structure is dead weight that should collapse to the body alone, or — far more often — one branch was copy-pasted and the edit that was supposed to follow never happened, so one of the cases is silently mishandled. The second reading is why this is reported as a defect rather than as tidiness: the code compiles, the tests for the copied branch pass, and the branch that was never edited is the one that goes wrong in production.
The trigger is deliberately all branches, not any two of them. Two arms of a long dispatch chain sharing a body is a normal, intentional idiom; every arm sharing a body never is, at any body size.
Notes
The check stays narrow so that idiomatic code never fires:
-
The chain must be complete. An
if / else ifwith no trailingelseis not a candidate — falling through the whole chain does something different from entering any branch, so the conditions still decide something. -
The
whenmust carry anelse, for the same reason. This is a deliberate trade with a known cost: an exhaustivewhenover an enum or a sealed hierarchy needs noelse, and such awhenis not reported even when every branch is identical. An enum-entry branch (Speed.FAST →) is an ordinary expression in Kotlin, indistinguishable at parse time from any other constant, so the rule has no way to establish exhaustiveness without theelse. Fewer false positives is worth more here than the extra recall. -
Empty bodies are left alone. An empty branch may be a deliberate no-op, and it is already reported by
kotlin.empty_when_branchorkotlin.empty_if_body. -
Branches that type-test the subject are excluded. Under
is Int →the subject is smart-cast to a different static type, so two textually identical bodies do not run the same code —value.toString()underis Intand underis Longcall different members. -
Branches carrying a guard are excluded. A guard (
Speed.FAST if level > 0 →) adds a condition the bodies say nothing about.
Bodies are compared as statement lists, so the comparison is brace-agnostic:
if (v) log(c) else { log(c) } is caught even though one side has braces. Equivalence is
computed over the parsed-token sequence, so comments and formatting differences between two
branches do not hide a match.
Remediation
Decide which reading applies, then act on it.
If the branches were meant to differ, fix the branch that was never edited:
fun bumpIfVerbose(verbose: Boolean, count: Int): Int {
if (verbose) {
log(count)
return count + 1
} else {
return count + 1
}
}
If they were always meant to be the same, drop the condition and keep the body:
fun bumpIfVerbose(verbose: Boolean, count: Int): Int {
if (verbose) {
log(count)
}
return count + 1
}
For a when whose branches genuinely share one body, merge the conditions instead of
repeating them:
fun rateFor(speed: Speed): Int =
when (speed) {
Speed.FAST, Speed.SLOW -> 10
}