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:
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")
}
package foo
object Foo{
val value = 123
def main(args: Array[String]): Unit= {
println("hello " + Bar)
}
}
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
package build
import mill._, scalalib._
object `package` extends RootModule with ScalaModule {
def scalaVersion = "2.13.11"
}
# 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")
}
}
If entering the long fully-qualified module name mill.scalalib.scalafmt.ScalafmtModule/
is tedious, you can add
an External Module Alias
to give it a shorter name that’s easier to type
Code Coverage with Scoverage
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><hello></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: