Linting Kotlin Projects

This page will discuss common topics around enforcing the code quality of Kotlin codebases using the Mill build tool

Linting with Detekt

build.mill (download, browse)
package build

import mill._
import kotlinlib.KotlinModule
import kotlinlib.detekt.DetektModule

object `package` extends RootModule with KotlinModule with DetektModule {
  def kotlinVersion = "1.9.24"
}

This example shows how to use the Detekt static code analyzer for linting a KotlinModule, by mixing in the trait DetektModule and calling the detekt task:

src/example/Foo.kt (browse)
package example

import java.util.Random

fun main() {
    if (Random.nextBoolean()) {
        if (Random.nextBoolean()) {
            if (Random.nextBoolean()) {
                if (Random.nextBoolean()) {
                    if (Random.nextBoolean()) {
                        if (Random.nextBoolean()) {
                            println("Hello World")
                        }
                    }
                }
            }
        }
    }
}
> ./mill detekt
error: ...Foo.kt:5:5: Function main is nested too deeply. [NestedBlockDepth]

> ./mill detekt --check false
...Foo.kt:5:5: Function main is nested too deeply. [NestedBlockDepth]

Linting with KtLint

build.mill (download, browse)
package build

import mill._
import mill.util.Jvm
import mill.api.Loose
import kotlinlib.KotlinModule
import kotlinlib.ktlint.KtlintModule

object `package` extends RootModule with KotlinModule with KtlintModule {
  def kotlinVersion = "1.9.24"

  def ktlintConfig = Some(PathRef(T.workspace / ".editorconfig"))
}

This example shows how to use the KtLint linter on a KotlinModule, by mixing in the trait KtlintModule and calling the ktlint task. ktlint also supports autoformatting to automatically resolve code formatting violations, via the --format flag shown below:

> ./mill ktlint --check true # run ktlint to produce a report, defaults to warning without error
error: ...src/example/FooWrong.kt:6:28: Missing newline before ")" (standard:parameter-list-wrapping)...
...src/example/FooWrong.kt:6:28: Newline expected before closing parenthesis (standard:function-signature)...
...src/example/FooWrong.kt:6:28: Missing trailing comma before ")" (standard:trailing-comma-on-declaration-site)...

> ./mill ktlint --format true

> ./mill ktlint # after fixing the violations, ktlint no longer errors

> ./mill mill.kotlinlib.ktlint.KtlintModule/ # alternatively, use external module to check/format

Autoformatting with KtFmt

build.mill (download, browse)
package build

import mill._
import mill.util.Jvm
import mill.api.Loose
import kotlinlib.KotlinModule
import kotlinlib.ktfmt.KtfmtModule

object `package` extends RootModule with KotlinModule with KtfmtModule {
  def kotlinVersion = "1.9.24"
}

This example demonstrates how to use the KtFmt autoformatter from Facebook both to enforce and apply formatting to your KotlinModule source files. You can configure a non-default version of KtFmt by overriding def ktfmtVersion

> ./mill ktfmt --format=false # run ktfmt to produce a list of files which should be formatter
error: ...src/example/FooWrong.kt...

> ./mill ktfmt # running without arguments will format all files
Done formatting ...src/example/FooWrong.kt

> ./mill ktfmt # after fixing the violations, ktfmt no longer prints any file

> ./mill mill.kotlinlib.ktfmt.KtfmtModule/ __.sources   # alternatively, use external module to check/format

Code Coverage with Kover

build.mill (download, browse)
package build

import mill._, kotlinlib._
import kotlinlib.kover.KoverModule

object `package` extends RootModule with KotlinModule with KoverModule {

  trait KotestTests extends TestModule.Junit5 {
    override def forkArgs: T[Seq[String]] = Task {
      super.forkArgs() ++ Seq("-Dkotest.framework.classpath.scanning.autoscan.disable=true")

    }
    override def ivyDeps = super.ivyDeps() ++ Agg(
      ivy"io.kotest:kotest-runner-junit5-jvm:5.9.1"
    )
  }

  def kotlinVersion = "1.9.24"

  override def koverVersion = "0.8.3"

  object test extends KotlinTests with KotestTests with KoverTests

  object bar extends KotlinModule with KoverModule {
    def kotlinVersion = "1.9.24"
    object test extends KotlinTests with KotestTests with KoverTests
  }
}

This is a basic Mill build for a single KotlinModule, enhanced with Kover module. The root module extends the KoverModule and specifies the version of Kover version to use here: 0.8.3. This version can be changed if there is a newer one. Now you can call the Kover tasks to produce coverage reports. The sub test module extends KoverTests to transform the execution of the various testXXX tasks to use Kover and produce coverage data. This lets us perform the coverage operations but before that you must first run the test. ./mill test then ./mill show kover.htmlReport and get your coverage in HTML format. Also reports for all modules can be collected in a single place by running ./mill show mill.kotlinlib.kover.Kover/htmlReportAll.

> ./mill test # Run the tests and produce the coverage data
...
... foo.FooTests kotlin - success started

> ./mill resolve kover._ # List what tasks are available to run from kover
...
kover.htmlReport
...
kover.xmlReport
...

> ./mill show kover.htmlReport
...
...out/kover/htmlReport.dest/kover-report...

> cat out/kover/htmlReport.dest/kover-report/index.html
...
...Kover HTML Report: Overall Coverage Summary...

> ./mill show mill.kotlinlib.kover.Kover/htmlReportAll # collect reports from all modules
...
...out/mill/kotlinlib/kover/Kover/htmlReportAll.dest/kover-report...

> cat out/mill/kotlinlib/kover/Kover/htmlReportAll.dest/kover-report/index.html
...
...Kover HTML Report: Overall Coverage Summary...