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
package build
import mill._
import kotlinlib.KotlinModule
import kotlinlib.detekt.DetektModule
object `package` extends 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:
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
package build
import mill._
import mill.util.Jvm
import kotlinlib.KotlinModule
import kotlinlib.ktlint.KtlintModule
import mill.define.BuildCtx
object `package` extends KotlinModule with KtlintModule {
def kotlinVersion = "1.9.24"
def ktlintConfig0 = Task.Source(BuildCtx.workspaceRoot / ".editorconfig")
def ktlintConfig = Some(ktlintConfig0())
}
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/ # alternatively, use external module to check/format
Autoformatting with KtFmt
package build
import mill._
import mill.util.Jvm
import kotlinlib.KotlinModule
import kotlinlib.ktfmt.KtfmtModule
object `package` extends 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/ __.sources # alternatively, use external module to check/format
Auto-formatting with Spotless
If your project has file groups each requiring different formatting, you may want to give Mill’s Spotless plugin a try. It supports formatting all files with a single command as opposed to using a different plugin/command for each group.
We define a module that extends SpotlessModule and provide a JSON configuration file with the format specifications.
package build
import mill.kotlinlib.KotlinModule
import mill.scalalib.spotless.SpotlessModule
object `package` extends KotlinModule with SpotlessModule {
def kotlinVersion = "2.1.20"
}
import a.*
import kotlinx.android.synthetic.main.layout_name.*
import a.b.c.*
import a.b
fun main() {
fun name() { a(); return b }
println(";")
println();
}
import mylib.Unused;
import mylib.UsedB;
import mylib.UsedA;
public class A {
/**
* Some javadoc.
*/
public static void main(String[] args) {
System.out.println("A very very very very very very very very very very very very very very very very very very very very very long string that goes beyond the 100-character line length.");
UsedB.someMethod();
UsedA.someMethod();
}
}
// GPL
[
{
"includes": [
"glob:build.mill"
],
"steps": [
{
"$type": "ScalaFmt"
}
]
},
{
"includes": [
"glob:**.java"
],
"steps": [
{
"$type": "PalantirJavaFormat"
},
{
"$type": "LicenseHeader",
"delimiter": "(package|import|public|class|module) "
}
]
},
{
"includes": [
"glob:**.{kt,kts}"
],
"steps": [
{
"$type": "Ktfmt"
},
{
"$type": "LicenseHeader",
"delimiter": "(package |@file|import )"
}
]
}
]
As per the specifications:
-
The
build.mill
is to be formatted usingScalaFmt
step. -
All Java files are to be formatted using
PalantirFormatJava
andLicenseHeader
steps. -
All Kotlin files are to be formatted using
Ktfmt
andLicenseHeader
steps.
Most fields have default values and can be omitted in the JSON file.
An example is the LicenseHeader.header field that references the LICENSE file.
|
Next, we run the inherited spotless
command to check/apply the format specifications.
> ./mill spotless --check # check fails initially
checking format in 1 mill files
format errors in build.mill
checking format in 1 java files
format errors in src/B.java
checking format in 1 kt files
format errors in src/A.kt
format errors in 3 files
error: ...format check failed for 3 files
> ./mill spotless # auto-fix format
formatting build.mill
formatting src/B.java
formatting src/A.kt
formatted 3 files
> ./mill spotless # fast incremental format
1 mill files are already formatted
1 java files are already formatted
1 kt files are already formatted
This demonstrates how different file groups can be formatted with a single command.
For the full list of format steps and configuration options, please refer to the API documentation.
For a multi-module project, it is sufficient to extend SpotlessModule in your
build root module and define a format specification for each use-case.
|
You can also run spotless
globally
if you prefer not to have to extend SpotlessModule
.
> ./mill mill.scalalib.spotless.SpotlessModule/ --check
checking format in 1 mill files
checking format in 1 java files
checking format in 1 kt files
format check completed
Ratchet
Similar to the Spotless Gradle and Maven plugins, Mill provides the ability to enforce formatting gradually aka ratchet.
We define a module with an incorrectly formatted file.
package build
import mill.javalib.JavaModule
import mill.scalalib.spotless.SpotlessModule
object `package` extends JavaModule with SpotlessModule
import mylib.Unused;
import mylib.UsedB;
import mylib.UsedA;
public class A {
/**
* Some javadoc.
*/
public static void main(String[] args) {
System.out.println("A very very very very very very very very very very very very very very very very very very very very very long string that goes beyond the 100-character line length.");
UsedB.someMethod();
UsedA.someMethod();
}
}
A Git repository is initialized and a base commit is created with the incorrectly formatted file.
Since no .spotless-formats.json file is present, a default list of format specifications is used.
The default is meant for use cases where some basic formatting is sufficient.
|
> git init . -b main
> git add .gitignore build.mill src/A.java
> git commit -a -m "1"
Next, we create a new file with format errors.
> echo " module hello {}" > src/module-info.java # content has leading space at start
> ./mill spotless --check
format errors in build.mill
format errors in src/A.java
format errors in src/module-info.java
error: ...format check failed for 3 files
The spotless
command finds format errors in all files.
But we do not want to fix the formatting in files that were committed previously.
Instead, we use the ratchet
command to identify and format files that differ between Git trees.
> ./mill ratchet --check # format changes in working tree since HEAD commit
ratchet found changes in 1 files
format errors in src/module-info.java
error: ...format check failed for 1 files
> ./mill ratchet # auto-fix formatting in changeset
ratchet found changes in 1 files
formatting src/module-info.java
formatted 1 files
This demonstrates how to introduce formatting incrementally into your project.
You can also set up actions on CI systems that compare and check/fix formatting between 2 revisions.
> git add src/module-info.java # stage and
> git commit -a -m "2" # commit changes
> ./mill ratchet --check HEAD^ HEAD # format changes between last 2 commits
ratchet found changes in 1 files
1 java files are already formatted
CI actions may require additional setup. |
The ratchet
command is also available globally.
> ./mill mill.scalalib.spotless.SpotlessModule/ratchet --check HEAD^ HEAD
ratchet found changes in 1 files
Code Coverage with Kover
package build
import mill._, kotlinlib._
import kotlinlib.kover.KoverModule
object `package` extends 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 mvnDeps = super.mvnDeps() ++ Seq(
mvn"io.kotest:kotest-runner-junit5: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/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/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...