Duplicate Condition In Branch Chain
ID |
kotlin.duplicate_condition_in_if_chain |
Severity |
high |
Remediation Complexity |
trivial |
Remediation Risk |
low |
Remediation Effort |
low |
Resource |
Reliability |
Language |
Kotlin |
Tags |
control-flow, dead-code, reliability |
Description
Reports a condition that is structurally identical to an earlier condition in the same
if / else if chain, or in the same when. Both run top to bottom and stop at the first
branch that matches, so the later, duplicated branch can never be reached.
Rationale
fun label(a: Int): String {
if (a == 1) {
return "one"
} else if (a == 2) {
return "two"
} else if (a == 1) { // unreachable
return "also one"
}
return "other"
}
fun rate(a: Int): Int =
when (a) {
1 -> 10
2 -> 20
1 -> 30 // unreachable
else -> 0
}
Anything matching the repeated condition triggers the earlier branch, so the code in the later one never runs. Almost always the condition was copy-pasted and one operand was left unchanged: the case the author meant to add is missing, and the branch that took its place is dead. Both halves of that mistake are silent — the code compiles and every existing test still passes.
Notes
-
Comparison is scoped to a single chain or a single
when. The same condition written in two unrelatedifstatements, or in an inner chain nested inside an outer one, is ordinary code and is not reported. -
Comparison is structural, over the parsed-token sequence, so it does not confuse operand order:
a == bandb == aare not flagged even though they are logically equivalent, and neither area && bandb && a. Formatting and comments make no difference. -
In a
when, conditions are compared one by one rather than branch by branch, so in a comma list the individual repeated value is reported. With1, 2 →followed by2 →, the second branch is not entirely dead, but its condition is. Every condition form is compared the same way — a constant, anistype test, aninrange test, and the bare boolean expressions of a subject-lesswhen. -
A
whenbranch carrying a guard (1 if level > 0 →) is skipped in either position: the guard adds a condition of its own, so the same value may legitimately appear again on a branch that applies when the guard does not hold. -
A condition that contains a call is excluded in either position. A call can legitimately return a different value on each evaluation — a counter, an iterator, a mutable property — so two textually identical calls do not make the later branch provably dead.
Remediation
Write the condition that was meant to be there:
fun label(a: Int): String {
if (a == 1) {
return "one"
} else if (a == 2) {
return "two"
} else if (a == 3) {
return "three"
}
return "other"
}
When the two branches really do share one outcome, merge the conditions instead of repeating them:
fun rate(a: Int): Int =
when (a) {
1, 2 -> 10
else -> 0
}