Writing your first Detekt rule

Detekt is a static analyze tool for the Kotlin language. It’s open source, in active development and friendly to first time contributors. If you know PMD or Findbugs it’s very familiar and specialises in code smells for Kotlin. Extending Detekt with your own custom rule is super straightforward.

Use case

Almost never should you import types from an internal package like it is done below:

import com.foo.bar.internal.InternalClass
class MyClass(val internalClass: InternalClass)

We don’t want to bother with things like this every time we review pull requests. Instead — a machine is much better at things like this. Hence we can write a Detekt rule that will detect internal imports.

Set up

It’s best to put your rules into a separate module (e.g. custom-detekt-rules).

apply plugin: "kotlin"
repositories {
jcenter()
}
dependencies {
compileOnly "io.gitlab.arturbosch.detekt:detekt-api:1.0.0.RC7–3"
  testCompile "junit:junit:4.12"
testCompile "org.assertj:assertj-core:3.10.0"
testCompile "io.gitlab.arturbosch.detekt:detekt-api:1.0.0.RC7–3"
testCompile "io.gitlab.arturbosch.detekt:detekt-test:1.0.0.RC7–3"
}

We’ll use the detekt-api for writing our tests and for testing we can leverage the detekt-test module.

Rule

Almost all code analyze tools use the visitor pattern. The code that should be analyzed gets represented in classes and those classes can be visited.

class NoInternalImportRule(
config: Config = Config.empty
) : Rule(config) {
override val issue = Issue("NoInternalImport",
Severity.Maintainability, "Don’t import from an internal "
+ "package as they are subject to change.", Debt.TWENTY_MINS)
  override fun visitImportDirective(
importDirective: KtImportDirective
) {
val import = importDirective.importPath?.pathStr
    if (import?.contains(“internal”) == true) {
report(CodeSmell(issue, Entity.from(importDirective),
"Importing '$import' which is an internal import."))
}
}
}

This is all the code that’s needed for our custom rule. Configuration isn’t needed for this rule. We’ll create an issue, NoInternalImport which is in the Maintainability category with our description. Debt is an estimated guess how long it would take to solve this particular issue.

For every import in the code that will be analyzed, detekt will call the visitImportDirective function. If the string importPath contains the internal word we can report it as a code smell.

Testing

It’s effortless to test your rule as you can see below:

class NoInternalImportRuleTest {
@Test fun noWildcardImportsRule() {
val findings = NoInternalImportRule().lint("""
import a.b.c
import a.internal.foo
""".trimIndent())
    assertThat(findings).hasSize(1)
assertThat(findings[0].message).isEqualTo(
"Importing ‘a.internal.foo’ which is an internal import.”)
}
}

We pass our testing file as a string and can lint it. We verify that there’s only one finding and that the message is equal to the one we’re emitting in our rule. It’s that easy.

RuleSetProvider

We need to expose our rule. Detekt has a concept of a RuleSetProvider.

class CustomRuleSetProvider : RuleSetProvider {
override val ruleSetId: String = "detekt-custom-rules"
  override fun instance(config: Config)
= RuleSet(ruleSetId, listOf(NoInternalImportRule(config)))
}

We give it an id and return an instance of the RuleSet with our custom rule.

To let Detekt know about this class we need to create a META-INF service file: custom-detekt-rules/src/main/resources/META-INF/services/io.gitlab.arturbosch.detekt.api.RuleSetProvider and contains the fully qualified name of our CustomRuleSetProvider: com.vanniktech.detektcustomrules.CustomRuleSetProvider.

This way detekt can look up our class, instantiate it and use it via the RuleSetProvider interface.

Wiring things up

Either you’re using one of the many Gradle plugins for Detekt or the Gradle Plugin from Detekt itself. Internally — they all create a Gradle configuration. We know Gradle configurations already as api, implementation, testImplementation, etc. Typically — they create one called detekt.

In your module where you want to use Detekt with your custom rule you’d add it like this:

dependencies {
detekt project(":custom-detekt-rules")
}

This is all that needs to be done. When running from the command line it will look like this:

Conclusion

Writing your own rules is done in a breeze. There’s an initial work that needs to be done for setting up your environment but even that is quickly done.

For more complex things you can always look at the existing Detekt rules and see how they are able to retrieve information about your code.

If you have something that’s generally applicable create an issue on Detekt and let’s talk whether it should be baked as a default rule — so everyone can benefit from it. I hope I’ll see some new faces contributing to Detekt.

You can find the entire example from this post in the GitHub repository below. Be sure to check it out.