Linting Java Projects

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

Linting with ErrorProne

Error Prone augments the Java compiler’s type checker and detect common mistakes at compile time. Mill supports ErrorProne via the ErrorProneModule. Mix the ErrorProneModule trait into your JavaModule and it will automatically run with every compilation.

build.mill (download, browse)
package build

import mill._, javalib._, errorprone._

object `package` extends JavaModule with ErrorProneModule {
  def errorProneOptions = Seq("-XepAllErrorsAsWarnings")
}
src/example/ShortSet.java (browse)
package example;

import java.util.HashSet;
import java.util.Set;

public class ShortSet {
  public static void main(String[] args) {
    Set<Short> s = new HashSet<>();
    for (short i = 0; i < 100; i++) {
      s.add(i);
      s.remove(i - 1);
    }
    System.out.println(s.size());
  }
}

When adding the ErrorProneModule to your JavaModule, the error-prone compiler plugin automatically detects various kind of programming errors.

> ./mill show errorProneOptions
[
  "-XepAllErrorsAsWarnings"
]

> ./mill compile
[warn] .../src/example/ShortSet.java:11:15:  [CollectionIncompatibleType] Argument 'i - 1' should not be passed to this method; its type int is not compatible with its collection's type argument Short
[warn]       s.remove(i - 1);
[warn]               ^    (see https://errorprone.info/bugpattern/CollectionIncompatibleType)
[warn] 1 warning
[warn]               ^

Configuration

The following configuration options exist:

def errorProneVersion: T[String]

The error-prone version to use. Find the list of versions and changlog at https://github.com/google/error-prone/releases

def errorProneOptions: T[Seq[String]]

Options directly given to the error-prone processor. Those are documented as "flags" at https://errorprone.info/docs/flags

Linting with Checkstyle

CheckstyleModule Performs quality checks on Java source files using Checkstyle and generates reports from these checks.

build.mill (download, browse)
package build
import mill._, javalib._, checkstyle._

object `package` extends CheckstyleModule {
  def checkstyleVersion = "9.3"
}

To use this plugin in a Java/Scala module,

  1. Extend mill.contrib.checkstyle.CheckstyleModule.

  2. Define a configuration file checkstyle-config.xml.

  3. Run the checkstyle command.

> ./mill checkstyle # run checkstyle to produce a report, defaults to warning without error
...src/InputWhitespaceCharacters.java:3:23: Line contains a tab character...
...src/InputWhitespaceCharacters.java:16:3: Line contains a tab character...
...src/InputFileName1.java:2:1: Top-level class MyAnnotation1 has to reside in its own source file...
...src/InputFileName1.java:13:1: Top-level class Enum1 has to reside in its own source file...
...src/InputFileName1.java:26:1: Top-level class TestRequireThisEnum has to reside in its own source file...
Audit done.

> sed -i.bak 's/warning/error/g' checkstyle-config.xml # make checkstyle error on violations

> ./mill checkstyle
error: ...src/InputWhitespaceCharacters.java:3:23: Line contains a tab character...
...src/InputWhitespaceCharacters.java:16:3: Line contains a tab character...
...src/InputFileName1.java:2:1: Top-level class MyAnnotation1 has to reside in its own source file...
...src/InputFileName1.java:13:1: Top-level class Enum1 has to reside in its own source file...
...src/InputFileName1.java:26:1: Top-level class TestRequireThisEnum has to reside in its own source file...
Audit done.

> sed -i.bak 's/\t/    /g' src/InputWhitespaceCharacters.java

> rm src/InputFileName1.java

> ./mill checkstyle # after fixing the violations, checkstyle no longer errors
Audit done.

Checkstyle Flags

// if an exception should be raised when violations are found
./mill checkstyle --check

// if Checkstyle output report should be written to System.out
./mill checkstyle --stdout

Checkstyle Sources (optional)

// incorrect paths will cause a command failure
./mill checkstyle a/b

// you can specify paths relative to moduleDir
./mill checkstyle src/a/b

// process a single file
./mill checkstyle src/a/B.java

// process multiple sources
./mill checkstyle src/a/b src/c/d src/e/F.java

// process with flags
./mill checkstyle --check --stdout src/a/b src/c/d

// process all module sources
./mill checkstyle

Shared Checkstyle Configuration

To share checkstyle-config.xml across modules, adapt the following example.

import mill._
import mill.contrib.checkstyle.CheckstyleModule
import mill.scalalib._

object foo extends Module {

  object bar extends MyModule
  object baz extends Module {
    object fizz extends MyModule
    object buzz extends MyModule
  }

  trait MyModule extends JavaModule with CheckstyleModule {

    override def checkstyleConfig = Task {
      api.PathRef(mill.define.BuildCtx.workspaceRoot / "checkstyle-config.xml")
    }
  }
}

Limitations

  • Version 6.3 or above is required for plain and xml formats.

  • Setting checkstyleOptions might cause failures with legacy versions.

CheckstyleXsltModule

This plugin extends the mill.contrib.checkstyle.CheckstyleModule with the ability to generate reports by applying XSL Transformations on a Checkstyle output report.

Auto detect XSL Transformations

XSLT files are detected automatically provided a prescribed directory structure is followed.

/**
 * checkstyle-xslt
 *  ├─ html
 *  │   ├─ xslt0.xml
 *  │   └─ xslt1.xml
 *  └─ pdf
 *      ├─ xslt1.xml
 *      └─ xslt2.xml
 *
 * html/xslt0.xml -> xslt0.html
 * html/xslt1.xml -> xslt1.html
 * pdf/xslt1.xml  -> xslt1.pdf
 * pdf/xslt2.xml  -> xslt2.pdf
 */

Specify XSL Transformations manually

For a custom setup, adapt the following example.

import mill._
import mill.define.PathRef
import mill.contrib.checkstyle.CheckstyleXsltModule
import mill.contrib.checkstyle.CheckstyleXsltReport
import mill.scalalib._

object foo extends JavaModule with CheckstyleXsltModule {

    override def checkstyleXsltReports = Task {
      Set(
        CheckstyleXsltReport(
          PathRef(moduleDir / "checkstyle-no-frames.xml"),
          PathRef(Task.dest / "checkstyle-no-frames.html"),
        )
      )
  }
}

AutoFormatting with Palantir Java Format

Mill supports auto-formatting Java code via the Palantir Java Format project.

build.mill (download, browse)
package build

import mill._
import mill.javalib.palantirformat._

object `package` extends PalantirFormatModule
src/A.java (browse)
public class A {

    public static void main(String[] args) {
    System.out.println("hello"); // indentation should be fixed
    }
}

Palantir Java Format can be used on a per-module basis by inheriting from PalantirFormatModule and running the palanatirformat command on that module You can also use --check if you wish to error if the code is not formatted, which is useful in CI validation jobs to ensure code is formatted before merging.

> ./mill palantirformat --check    # check should fail initially
...checking format in 1 java sources ...
...src/A.java
error: ...palantirformat aborted due to format error(s) (or invalid plugin settings/palantirformat options)

> ./mill palantirformat    # format all Java source files
...formatting 1 java sources ...

> ./mill palantirformat --check    # check should succeed now
...checking format in 1 java sources ...

You can also use Palantir Java Format globally on all JavaModules in your build by running mill.javalib.palantirformat/.

> ./mill mill.javalib.palantirformat/ # alternatively, use external module to check/format
...formatting 1 java sources ...

If entering the long fully-qualified module name mill.javalib.palantirformat/ is tedious, you can add an External Module Alias to give it a shorter name that’s easier to type

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.

build.mill (download, browse)
package build

import mill.javalib.JavaModule
import mill.scalalib.spotless.*

object `package` extends JavaModule with SpotlessModule
src/A.java (browse)
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();
}
}
resources/app.properties (browse)
foo=bar
LICENSE (browse)
// MIT
.spotless-formats.json (browse)
[
  {
    "includes": [
      "glob:build.mill"
    ],
    "steps": [
      {
        "$type": "ScalaFmt"
      }
    ]
  },
  {
    "includes": [
      "glob:resources/app.properties"
    ],
    "steps": [
      {
        "$type": "TrimTrailingWhitespace"
      }
    ]
  },
  {
    "includes": [
      "glob:**.java"
    ],
    "steps": [
      {
        "$type": "PalantirJavaFormat"
      },
      {
        "$type": "LicenseHeader",
        "delimiter": "(package|import|public|class|module) "
      }
    ]
  }
]

As per the specifications:

  • The build.mill is to be formatted using ScalaFmt step.

  • The resources/app.properties file is to be formatted using TrimTrailingWhitespace step.

  • All Java files are to be formatted using PalantirFormatJava and LicenseHeader 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 properties files
format errors in resources/app.properties
checking format in 1 java files
format errors in src/A.java
format errors in 3 files
error: ...format check failed for 3 files

> ./mill spotless           # auto-fix format
formatting build.mill
formatting resources/app.properties
formatting src/A.java
formatted 3 files
format completed

> ./mill spotless           # fast incremental format
1 mill files are already formatted
1 properties files are already formatted
1 java 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 properties files
checking format in 1 java 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.

build.mill (download, browse)
package build

import mill.javalib.JavaModule
import mill.scalalib.spotless.SpotlessModule

object `package` extends JavaModule with SpotlessModule
src/A.java (browse)
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 Jacoco

Mill supports Java code coverage analysis via the mill-jacoco plugin. See the plugin repository documentation for more details:

Binary Compatibility Enforcement

If you want to lint against binary compatibility breakages, e.g. when developing an upstream library that downstream libraries may compile against, you can use the Lightbend Migration Manager (MiMa) tool via the mill-mima plugin. See the mill-mima documentation for more details: