Writing your first ktlint rule

Niklas Baudy
4 min readAug 11, 2018

ktlint is an anti-bikeshedding linter with built-in formatter for the Kotlin language. It’s open source, in active development and friendly to first time contributors. If you know Checkstyle it’s very familiar and specialises in formatting for Kotlin. Extending ktlint with your own custom rule is super straightforward.

As a matter of fact writing a rule for Detekt — another tool for analyzing Kotlin code — is very similar and this article is based on https://medium.com/@vanniktech/writing-your-first-detekt-rule-ee940e56428d

Use case

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

import com.foo.bar.internal.InternalClassclass 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 ktlint rule that will detect internal imports.

Set up

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

apply plugin: "kotlin"repositories {
jcenter()
}
dependencies {
compileOnly "com.github.shyiko.ktlint:ktlint-core:0.27.0"
testCompile "junit:junit:4.12"
testCompile "org.assertj:assertj-core:3.10.0"
testCompile "com.github.shyiko.ktlint:ktlint-core:0.27.0"
testCompile "com.github.shyiko.ktlint:ktlint-test:0.27.0"
}

We’ll use the ktlint-api for writing our tests and for testing we can leverage the ktlint-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 : Rule("no-internal-import") {
override fun visit(
node: ASTNode,
autoCorrect: Boolean,
emit: (offset: Int, errorMessage: String, canBeAutoCorrected:
Boolean) -> Unit
) {
if (node.elementType == KtStubElementTypes.IMPORT_DIRECTIVE) {
val importDirective = node.psi as KtImportDirective
val path = importDirective.importPath?.pathStr
if (path != null && path.contains("internal")) {
emit(node.startOffset, "Importing from an internal package",
false)
}
}
}
}

This is all the code that’s needed for our custom rule. We’ll subclass from Rule and give it our name.

We’ll visit every ASTNode. If it’s of the KtImportDirective type we know we’re visiting an import statement. If the string importPath contains the internal word we can report it.

Testing

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

class NoInternalImportRuleTest {
@Test fun noWildcardImportsRule() {
assertThat(NoInternalImportRule().lint("""
import a.b.c
import a.internal.foo
""".trimIndent()
)).containsExactly(
LintError(2, 1, "no-internal-import",
"Importing from an internal package")
)
}
}

We pass our testing file as a string and can lint it. We verify that there’s exactly one Lint Error on the given file. It’s that easy.

RuleSetProvider

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

class CustomRuleSetProvider : RuleSetProvider {
override fun get()
= RuleSet("custom-ktlint-rules", NoInternalImportRule())
}

In the get method we return a RuleSet instance with our rule set id and our rule implementation.

To let ktlint know about this class we need to create a META-INF service file: custom-ktlint-rules/src/main/resources/META-INF/services/com.github.shyiko.ktlint.core.RuleSetProvider and contains the fully qualified name of our CustomRuleSetProvider: com.vanniktech.ktlintcustomrules.CustomRuleSetProvider.

This way ktlint 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 ktlint or you’re wiring it up yourself. Internally — they all create a Gradle configuration. We know Gradle configurations already as api, implementation, testImplementation, etc. Typically — they create one called ktlint.

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

dependencies {
ktlint project(":custom-ktlint-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 ktlint 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 ktlint and let’s talk whether it should be baked as a ktlint rule — so everyone can benefit from it. I hope I’ll see some new faces contributing to ktlint.

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

--

--