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 compileIvyDeps, scalacPluginIvyDeps, and scalacOptions as shown below:

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

object `package` extends RootModule with ScalaModule {
  def scalaVersion = "2.13.11"
  def compileIvyDeps = Agg(ivy"com.lihaoyi:::acyclic:0.3.15")
  def scalacPluginIvyDeps = Agg(ivy"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 RootModule with 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.ScalafmtModule/ command, specific modules via mill mill.scalalib.scalafmt.ScalafmtModule/ '{foo,bar}.sources or only check the code’s format with +mill mill.scalalib.scalafmt.ScalafmtModule/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.ScalafmtModule/checkFormatAll
error: ...Found 1 misformatted files

> mill mill.scalalib.scalafmt.ScalafmtModule/

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

> mill mill.scalalib.scalafmt.ScalafmtModule/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.ScalafmtModule/

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

Code Coverage with Scoverage

build.mill (download, browse)
package build
import mill._, scalalib._
import $ivy.`com.lihaoyi::mill-contrib-scoverage:`

import mill.contrib.scoverage._

object `package` extends RootModule with ScoverageModule {
  def scoverageVersion = "2.1.0"
  def scalaVersion = "2.13.11"
  def ivyDeps = Agg(
    ivy"com.lihaoyi::scalatags:0.12.0",
    ivy"com.lihaoyi::mainargs:0.6.2"
  )

  object test extends ScoverageTests /*with TestModule.Utest */{
    def ivyDeps = Agg(ivy"com.lihaoyi::utest:0.8.4")
    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: