Writing custom lint rules for your Kotlin project with detekt

One of the disadvantage of Kotlin over Java is that there aren’t many code analysis tools around that work for Kotlin. I recently came across detekt, a static code analysis tool for Kotlin, and found it very helpful. It has all sorts of rules that cover problems in styling, code complexity, potential bugs, performance issues and more. The repository itself has very good documentation on its features, so in this post I’m just going to give a detailed walk through of how you can create and use custom rules for your Kotlin project.
Clone the repo
Custom rules depend on other parts of the detekt repo. It also contains a very nice sample custom ruleset which I’ll be walking you through in this post, so it’s nice to have the repo ready on your laptop:
git clone https://github.com/arturbosch/detekt.git
How to write a rule
Let’s take a look at the TooManyFunctions
rule in detekt/detekt-sample-ruleset
:
Detekt operates on the abstract syntax tree provided by the Kotlin compiler, which means you can override the visitXxx()
methods to let them do your bidding. The call to super.visitXxx()
would traverse the child nodes of Xxx in the AST. You can also do this by implementing your own DetektVisitor
. Take the implementation of NestedBlockDepth
rule, for example:
In this use case since each function needs to count its own depth, it’s much cleaner to have a Visitor
hold the count rather than having a global counter like TooManyFunctions
does. Notice that you can also make your rule configurable by simply adding config: Config
as a parameter in its constructor.
Testing your rule
You can use either Spek or JUnit to test rules. The example tests are pretty straight-forward:
I just want to highlight two things:
- If you use Spek, the superclass
SubjectSpek
and thesubject
closure need to know which rule you’re testing for. - In both tests,
subject/rule.lint(String)
compiles the String you defined and tests against it. There’s also asubject/rule.lint(path: Path)
function if you want to pass in thePath
to your test file instead. Another extension function that you might find useful isRule.format(String/Path)
. It would format the code you pass in with the detekt formatting rules.
Using your rule
cd detekt/detekt-sample-ruleset/
gradle build
You’ll see two jars appeared in detekt-sample-ruleset/build/libs
. The one we want is detekt-sample-ruleset-[version].jar
. We can test the rules out on the detekt repo itself. Go to detekt/build.gradle
, and at the end of the file you’ll find a detekt
block like this:
detekt {
// ...
profile("main") {
input = "$project.projectDir"
filters = '.*/test/.*, .*/resources/.*, .*/build/.*'
config = "$project.projectDir/detekt-cli/src/main/resources/default-detekt-config.yml"
baseline = "$project.projectDir/reports/baseline.xml"
}
// ...
}
Add a line ruleSets = “$projectDir/detekt-sample-ruleset/build/libs/detekt-sample-ruleset-[version].jar”
in the profile(“main”)
block. This tells detekt it should include the rules in the sample jar. Now run:
// In detekt/
gradle detektCheck
You should see that the build failed because of something like this:
Ruleset: sample
TooManyFunctions - [Configurations.kt] at detekt-cli/src/main/kotlin/io/gitlab/arturbosch/detekt/cli/Configurations.kt:1:1
That’s it!
If you decide to create your own ruleset module, here are some things you need to keep in mind:
- Include your module in
detekt/settings.gradle
:
rootProject.name = 'detekt'
include 'detekt-api'
// ...
include 'detekt-migration'
include 'my-awesome-ruleset' //<--- your ruleset here
- When you make a new rule, remember to add it to the RuleSet in your RuleSetProvider:
class MyAwesomeProvider(override val ruleSetId: String = "awesome") : RuleSetProvider {
override fun instance(config: Config): RuleSet {
return RuleSet(ruleSetId, listOf(
MyRule1(),
MyRule2() ))
}
}
- detekt uses a
ServiceLoader
to load all the rule sets. Make sure you have aresources/META-INF/services/io.gitlab.arturbosch.detekt.api.RuleSetProvider
file that contains the fully qualified names of all your RuleSetProviders (e.g.io.gitlab.arturbosch.detekt.sampleruleset.SampleProvider
)
Hopefully that was helpful. Happy linting!