Nested When
ID |
kotlin.no_nested_when |
Severity |
high |
Remediation Complexity |
medium |
Remediation Risk |
low |
Remediation Effort |
medium |
Resource |
Code Smell |
Language |
Kotlin |
Tags |
code-style, conditional, control-flow |
Description
Reports a when written inside a branch of another when. The inner when is the one
reported, because that is the one to extract.
Both when forms are covered: the one with a subject (when (x) { … }) and the
subject-less one (when { cond → … }), which is an if / else if ladder in when
clothing. A when used as an expression counts the same as one used as a statement — a
val label = when { … } inside a branch of an outer when is still a second decision
table in the same place.
Rationale
fun dispatch(kind: Int, tier: Tier): String =
when (kind) {
1 ->
when (tier) { // two tables to hold in mind at once
Tier.GOLD -> "1-gold"
Tier.SILVER -> "1-silver"
}
else -> "other"
}
Each when is already a table of conditions the reader has to hold in mind at once.
Nesting one inside a branch multiplies the paths through the function and hides the inner
table behind a branch arrow, where nobody looks for it — which is why the combination is a
frequent source of branches that were never handled.
In a three-deep stack the middle and innermost when are each reported against their own
nearest enclosing one: each is independently a level too many.
Notes
Nesting is lexical but scoped to one unit of code:
-
An inner
whenwrapped in anifor aforinside the branch still counts — the reader still meets it while reading the outer branch. -
A
wheninside a lambda passed tolet,run,maporforEach, inside a local or anonymous function, inside a property accessor, or inside an object expression is not reported. That body is a separate unit the reader already considers on its own, and moving the table there is exactly the fix this rule asks for.
Branching depth for if is measured separately, by kotlin.nested_if.
Remediation
Give the second dimension a name: lift the inner when into its own function, so each
table can be read on its own.
fun dispatch(kind: Int, tier: Tier): String =
when (kind) {
1 -> forTier(tier)
else -> "other"
}
private fun forTier(tier: Tier): String =
when (tier) {
Tier.GOLD -> "1-gold"
Tier.SILVER -> "1-silver"
}
When the two subjects are genuinely independent, a single when over a pair is often
clearer still:
fun dispatch(kind: Int, tier: Tier): String =
when (kind to tier) {
1 to Tier.GOLD -> "1-gold"
1 to Tier.SILVER -> "1-silver"
else -> "other"
}