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"

}
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"))

}
> ./mill ktlint # 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

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"

}
> ./mill ktfmt --format=false # run ktfmt to produce a list of files which should be formatter
...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.FooTestskotlin - 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...