Linting Scala Projects

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

Linting and Autofixing with Scalafix

Scalafix is a tool that analyzes your Scala source code, performing intelligent analyses and code quality checks, and is often able to automatically fix the issues that it discovers. It can also perform automated refactoring.

Mill supports Scalafix through the Mill-Scalafix third party module. See the module documentation for more details:

Linting with Acyclic Files Enforcement

Acyclic is a Scala compiler plugin that detects circular dependencies between files within a module. This can be very useful for ensuring code quality at a high-level:

  • While most linters can be concern themselves at a micro-level with formatting and whitespace, acyclic is concerned at a macro-level with how your codebase is structured into different files

  • While most linters satisfied by tweaking whitespace, circular dependencies may need significant refactorings like dependency-injection or interface-implementation-separation to resolve.

As a Scala compiler plugin, Acyclic can be enabled on any ScalaModule by adding its compileMvnDeps, scalacPluginMvnDeps, and scalacOptions as shown below:

build.mill (download, browse)
package build
import mill._, scalalib._

object `package` extends ScalaModule {
  def scalaVersion = "2.13.11"
  def compileMvnDeps = Seq(mvn"com.lihaoyi:::acyclic:0.3.15")
  def scalacPluginMvnDeps = Seq(mvn"com.lihaoyi:::acyclic:0.3.15")
  def scalacOptions = Seq("-P:acyclic:force")
}
src/Foo.scala (browse)
package foo
object Foo {
  val value = 123
  def main(args: Array[String]): Unit = {
    println("hello " + Bar)
  }
}
src/Bar.scala (browse)
package foo
object Bar {
  val value = Foo + " world"
}

Here we have a single ScalaModule with two files: Foo.scala and Bar.scala. Bar and Foo both depend on each other, which usually indicates an issue:

> ./mill compile
error: Unwanted cyclic dependency
...src/Bar.scala...
  val value = Foo + " world"
              ^
symbol: object Foo
...src/Foo.scala...
    println("hello " + Bar)
                       ^
symbol: object Bar

Usually the code should be refactored such that references between files is only one way. For this example, we remove the reference to Foo in Bar.scala, which allows the code to compile:

> sed -i.bak 's/Foo/Bar/g' src/Bar.scala

> ./mill compile
done compiling

Autoformatting with ScalaFmt

build.mill (download, browse)
package build
import mill._, scalalib._

object `package` extends ScalaModule {
  def scalaVersion = "2.13.11"
}
.scalafmt.conf (browse)
# Newer versions won't work with Java 8!
version = "3.7.15"
runner.dialect = scala213

Mill supports code formatting via scalafmt out of the box. You can reformat your project’s code globally with mill mill.scalalib.scalafmt/ command, specific modules via mill mill.scalalib.scalafmt/ '{foo,bar}.sources or only check the code’s format with +mill mill.scalalib.scalafmt/checkFormatAll. By default, ScalaFmt checks for a .scalafmt.conf file at the root of repository.

> cat src/Foo.scala # initial poorly formatted source code
package foo
object Foo{
def main(args:
Array[String
]
):Unit=
{println("hello world")
}
}


> ./mill mill.scalalib.scalafmt/checkFormatAll
error: ...Found 1 misformatted files

> ./mill mill.scalalib.scalafmt/

> cat src/Foo.scala
package foo
object Foo {
  def main(args: Array[String]): Unit = { println("hello world") }
}

> ./mill mill.scalalib.scalafmt/checkFormatAll
Everything is formatted already

You can modify .scalafmt.conf to adjust the formatting as desired:

> echo "maxColumn: 50" >> .scalafmt.conf

> ./mill mill.scalalib.scalafmt/

> cat src/Foo.scala
package foo
object Foo {
  def main(args: Array[String]): Unit = {
    println("hello world")
  }
}

If entering the long fully-qualified module name mill.scalalib.scalafmt/ 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, containing cross Scala modules, that extends SpotlessModule and provide a JSON configuration file with the format specifications.

build.mill (download, browse)
package build

import mill.Cross
import mill.scalalib.CrossScalaModule
import mill.scalalib.spotless.SpotlessModule

object `package` extends SpotlessModule {
  object lib extends Cross[LibModule]("2.13.15", "3.7.0")
  trait LibModule extends CrossScalaModule
}
lib/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();
}
}
lib/src-2/B.scala (browse)
sealed trait B
object B{
case object F extends B
case object T extends B
}
lib/src-3/B.scala (browse)
enum B:
  case F,T
.scalafmt.conf (browse)
version = "3.8.5"
runner.dialect = scala213
.scalafmt3.conf (browse)
version = "3.8.5"
runner.dialect = scala3
.spotless-formats.json (browse)
[
  {
    "includes": [
      "glob:**.java"
    ],
    "steps": [
      {
        "$type": "PalantirJavaFormat"
      }
    ]
  },
  {
    "includes": [
      "glob:**.scala"
    ],
    "excludes": [
      "glob:**/src-3**"
    ],
    "steps": [
      {
        "$type": "ScalaFmt",
        "version": "3.8.5"
      }
    ]
  },
  {
    "includes": [
      "glob:**/src-3**"
    ],
    "steps": [
      {
        "$type": "ScalaFmt",
        "version": "3.8.5",
        "configFile": ".scalafmt3.conf"
      }
    ]
  }
]

As per the specifications:

  • All Java files are to be formatted using PalantirFormatJava step.

  • All Scala 2 files are to be formatted using Scalafmt step with the configuration in .scalafmt.conf.

  • All Scala 3 files are to be formatted using Scalafmt step with the configuration in .scalafmt3.conf.

Most fields have default values and can be omitted in the JSON file. An example is the ScalaFmt.configFile field that references the .scalafmt.conf file.
If no .scalafmt.conf file is provided, the default configuration from scalafmt is used. Otherwise, you must also specify the ScalaFmt.version field in the JSON file.

Next, we run the inherited spotless command to check/apply the format specifications.

> ./mill spotless --check     # check fails initially
checking format in 1 java files
format errors in lib/src/A.java
checking format in 1 scala files
format errors in lib/src-2/B.scala
checking format in 1 scala files
format errors in lib/src-3/B.scala
format errors in 3 files
error: ...format check failed for 3 files

> ./mill spotless             # auto-fix format
formatting lib/src/A.java
formatting lib/src-2/B.scala
formatting lib/src-3/B.scala
formatted 3 files

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

As shown, 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 java files
checking format in 1 scala files
checking format in 1 scala 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 Scoverage

build.mill (download, browse)
//| mvnDeps:
//| - com.lihaoyi::mill-contrib-scoverage:$MILL_VERSION

package build

import mill._, scalalib._

import mill.contrib.scoverage._

object `package` extends ScoverageModule {
  def scoverageVersion = "2.1.0"
  def scalaVersion = "2.13.11"
  def mvnDeps = Seq(
    mvn"com.lihaoyi::scalatags:0.13.1",
    mvn"com.lihaoyi::mainargs:0.6.2"
  )

  object test extends ScoverageTests /*with TestModule.Utest */ {
    def mvnDeps = Seq(mvn"com.lihaoyi::utest:0.8.5")
    def testFramework = "utest.runner.Framework"
  }
}

This is a basic Mill build for a single ScalaModule, enhanced with Scoverage plugin. The root module extends the ScoverageModule and specifies the version of scoverage version to use here: 2.1.0. This version can be changed if there is a newer one. Now you can call the scoverage tasks to produce coverage reports. The sub test module extends ScoverageTests to transform the execution of the various testXXX tasks to use scoverage and produce coverage data. This lets us perform the coverage operations but before that you must first run the test. ./mill test then ./mill scoverage.consoleReport and get your coverage into your console output.

> ./mill test # Run the tests and produce the coverage data
...
+ foo.FooTests...simple ...  <h1>hello</h1>
+ foo.FooTests...escaping ...  <h1>&lt;hello&gt;</h1>

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

> ./mill scoverage.consoleReport
...
Statement coverage.: 16.67%
Branch coverage....: 100.00%

Mill supports Scala code coverage analysis via the Scoverage contrib plugin. See the contrib plugin 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: