Kotlin: when statement, when expression… oh my! or How we created our custom Detekt rule
Important update: as of April 2021 there is an official detekt rule for checking when cases — MissingWhenCase. Please use it
Back in 2015 when I only started trying Kotlin (many thanks to Jake Wharton for his thorough investigation) and then later, when Kotlin 1.0 was released and year after announced by Google as a priority language for development on Android, in the long list of shiny Kotlin features, presented by the articles and blog posts related to the new language, sealed classes were one of the most interesting for me. They were advertised as better enums, enums with super power etc. And one of the answers to the question why it’s better enum, was the ability of compiler to complain if you forgot to handle one of the cases while using when
on sealed class.
If you try to compile this code, you will receive the error: 'when' expression must be exhaustive. Add remaining branches or 'else' branch
.
I remembered that piece of information and, until recently, completely believed into it. Now you could imagine, how I was surprised when I found that it’s not true. Or being precise, not always true.
When
expression and when
statement
So, recently we started a new project here in AUTO1 and of course Kotlin was our choice. And we decided to use sealed classes to represent the set of states of our screens. Something like this:
Inside the fragment I created render(viewState: EmailLoginViewState)
function and started implementing it:
Surprisingly, I didn’t get any compiler warnings at this point, although I knew that I haven’t covered other cases of this sealed class. How could it be possible? So I decided to start my investigation from reading documentation.

So, that’s what I found in Kotlin sealed classes documentation:
The key benefit of using sealed classes comes into play when you use them in a
when
expression. If it's possible to verify that the statement covers all cases, you don't need to add anelse
clause to the statement. However, this works only if you usewhen
as an expression (using the result) and not as a statement.
In my case I was using when
as a statement, I didn’t use the result of the when
anywhere in my code, that’s why it wasn’t expression. And what’s why compiler wasn’t complaining about not covered cases in my sealed class.
Make when expression out of statement
The recipe was obvious: I need to use the result of when
expression in order to enable compiler warnings. But how to make it in the most idiomatic way?
We considered a number of options:
- Assign the result of the
when
expression to some variable:
2. Return the result of the when
exression out of the function:
3. Add empty let
block after when
expression:
All these methods transform when
statement to when
expression, enabling compiler warnings. But they all are not perfect: they are not readable; they bring some layer of ambiguity into our code; they hide the knowledge since the purpose of extra let
block or storing result of when
expression in some variable was unclear.
There’s no perfect solution to this problem. We decided to follow one of the advices and to create an extension property:
It works similar to empty let
block, but it’s more readable — it’s name is self-explanatory:
Now compiler checks our when
expressions and that could have been the happy end of our story…
So far so good as now we have compiler warnings. But still part of the problem is present — that is a hidden knowledge: new team member having no idea about exhaustive
property would never use it.
Of course, we can add it to the documentation of our project, we can make this topic part of the on-boarding process… But still there’s left some place for human factor. Maybe it’s better to delegate this to machine? So, let’s do it!
We are using Detekt for static analysis of our Kotlin code so we decided to create a custom Detekt rule to check if when
block was used in pair with exhaustive
property.
For those of you who haven’t had experience with Detekt I could recommend great intro into Detekt by Mohit Sarveiya (slides here):
The article of Niklas Baudy was also kind of inspiration and guide for us.
PSI
Detekt uses a Kotlin PSI to perform checks. PSI states for Program Structure Interface, and each JetBrains IDE uses it for code completion, syntax highlighting etc. Every program could be represented as a hierarchy of PSI elements. For example, this simple program:
will give us the following PSI tree (very simplified):

This tree helps Detekt and other static analysis tools to assess any element of the code. And we will use it to check our when
directives. And from the PSI point of view there could be 4 possible situations:
- If we are assigning the result of our
when
expression to some variable it is considered to be aKtProperty:
2. If we are returning the result of our when
expression from the function it’s the KtBlockExpression:
3. If we are chaining some extension after our when
block, for PSI it becomes KtDotQualifiedExpression:
4. And our initial when
statement (if we are not using the result) in terms of PSI is KtWhenExpression:
Having this, we can formulate what our check should do: it should visit content of our Kotlin classes and look for instances of KtWhenExpession
and yell at us if so.
Rule implementation
Let’s start with creating a new module in our project and name it detekt-rules
. And we need to add to our module’s build.gradle
detekt-api
artifact as a dependency:
Now we can create class for our Detekt rule. Rule class describes the issue and also contains the logic of detecting it. Let’s call our class NonExhaustiveWhen
:
We are passing number of parameters to the constructor of the superclass:
- We need to specify severity of our issue. Here we chose
Defect
. - We need to provide a description of the issue
- We need to specify time estimate to fix the issue, so that Detekt could calculate total amount of time required to fix all issues in the codebase. Since this fix only requires adding one method call, we estimated it to minimum amount of
FIVE_MINS.
We are extending here Rule
class. Its constructor parameters, as we’ve already seen, describe the issue. But in order to check our code for the issue Rule
class also inherits from KtVisitor
class . It provides us with a huge API: more than one hundred methods — each one for visiting specific PSI element, such as visitFile(file: PsiFile?)
, visitClassBody(classBody: KtClassBody)
, visitArgument(argument: KtValueArgument)
etc. And we can override any of these methods to add our checks. And we will override visitNamedFunction()
method to add our check. For each KtNamedFunction
that is detected while traversing the PSI, this method will be called exactly once:
Here we are filtering children of the KtNamedFunction
looking out for instances of KtWhenExpession
. If it’s empty, everything is fine. Otherwise, we use report
method to signal our Finding.
We are calling report
method and passing some message, providing some hints on how to fix this issue.
Our class now looks like this:
UPD: As pointed by Xavier Rubio Jansana visitNamedFunction
visits only named function, so code of anonymous lambdas wouldn’t be checked. To deal with lambdas we need to implement visitLambdaExpression
function:
Testing our rule
In order to believe in our rule veracity let’s write some unit test for it. To test rules we need add detekt-test
artifact in our module’s build.gradle
:
There’s an extension function lint()
for BaseRule
class, which executes the check and we will use it for our tests.
We need to create some inputs for our tests. For the sake of simplicity let’s create simple string values for our test inputs:
Now, we can test our rule. First, let’s check non-compliant input, representing when
statement:
Then, let’s test compliant inputs:
Since we have 3 different inputs, which should provide us the same result, I am using JUnit 5 ParameterizedTest
feature to run the same test with different inputs. I use MethodSource
as inputs provider:
Thanks for JUnit 5 annotations ParameterizedTest
and DisplayName
we can get meaningful outputs in the test results:

RulesetProvider
Now we have our rule and tests running, our rule is ready to check kotlin files. In order to make our check accessible by Detekt we need to create instance of RuleSetProvider:
RulesetProvider
is responsible for storing the registry of all the rules, accessible by Detekt. Since it’s just a hook for Detekt to look for rules there’s nothing special happens here. We just need to provide unique id for our ruleset and include our custom check classes into the list of rules.
Other important step here is to notify Detekt about our ruleset provider. For that we need to create a file with the name io.gitlab.arturbosch.detekt.api.RuleSetProvider
and place it in the directory src/main/resources/META-INF/servises
. The content of this file should be the fully qualified class name of our custom ruleset provider:
Enabling our check
By default Detekt has a number of built-in rulesets. Before enabling our ruleset we need to add dependency of our detekt-rules
module:
Then we should include our ruleset and rule into Detekt configuration:
Here on top level we list our ruleset name, the next level is there we list individual rules from this ruleset. Each of them we can turn on or off.
Tying things together
Now let’s try run Detekt on our code and you will see the output like this:
./gradlew detekt> Task :app:detekt FAILED
.....5 kotlin files were analyzed.
Ruleset: detekt-rules - 5min debt
NonExhaustiveWhen - [render] at /Users/oleg.osipenko/StudioProjects/KoinTestApp/app/src/main/java/com/github/olegosipenko/kointestsample/EmailLoginFragment.kt:30:3Overall debt: 5minComplexity Report:
- 102 lines of code (loc)
- 80 source lines of code (sloc)
- 40 logical lines of code (lloc)
- 1 comment lines of code (cloc)
- 8 McCabe complexity (mcc)
- 1 number of total code smells
- 1 % comment source ratio
- 200 mcc per 1000 lloc
- 25 code smells per 1000 llocProject Statistics:
- number of properties: 4
- number of functions: 7
- number of classes: 5
- number of packages: 1
- number of kt files: 5detekt finished in 1200 ms.
Successfully generated HTML report at /Users/oleg.osipenko/StudioProjects/KoinTestApp/app/build/reports/detekt/detekt.html
Build failed with 1 weighted issues (threshold defined was 0).
io.gitlab.arturbosch.detekt.cli.BuildFailure: Build failed with 1 weighted issues (threshold defined was 0).
Learning a new programming language is always exciting and could be much more useful if combined with reading documentation. That could help to better understand the subtle nuances of technology you are trying to master. And if you want to establish some practice of using some language feature, it’s better to offload this responsibility to static analysis tool, such as Detekt.
Detekt offers great API to create custom rules and it’s not very difficult to write and add your rules. This will improve your code and make your life easier.
You can check the source code for the sample here.