Alexandru Nedelcu =<<

Fatal Warnings and Linting in Scala

| 9 minute

The best practices are those enforced by the build tools, as part of the build process. Don’t annoy your colleagues in code reviews; let the build tools do that for you.

The Scala compiler has multiple linting options available and emits some warnings out of the box that would be more useful as errors. Let’s see how we can rely on the Scala compiler to strengthen our code and piss off your colleagues with clean code requirements.

Best Practice: Stop Ignoring Warnings! #

Some of it might be noise; however, in that noise, some real gems might be missed, warnings that signal bugs. For example, the compiler can do exhaustiveness checks when pattern matching:

def size(list: List[_]): Int =
  list match {
    case _ :: rest => 1 + size(rest)
  }
// On line 2: warning: match may not be exhaustive.
//        It would fail on the following input: Nil  

Here’s what happens next:

size(List(1,2,3))
// scala.MatchError: List() (of class scala.collection.immutable.Nil$)

It shouldn’t be just a warning. Even if you’re diligent, this warning can get lost in a sea of other warnings, like it often does. And here we were lucky, because the runtime exception was triggered on the happy path, but our luck would eventually run out and end up with such exceptions in production.

1. Activate -Xfatal-warnings #

To turn all compiler warnings into errors, you can activate -Xfatal-warnings. In build.sbt:

scalacOptions ++= Seq(
  "-Xfatal-warnings",
  //...
)

NOTE: it’s probably a good idea to exclude this from the console configuration, see section 2.3.

Now if we try out the code above, we get an error like this:

[error] .../Example.scala:10:5: match may not be exhaustive.
[error] It would fail on the following input: Nil
[error]     list match {
[error]     ^
[error] one error found

Thus we can no longer ignore it.

1.1. Make only some warnings fatal (Scala 2.13) #

There’s a new -Wconf option in Scala 2.13.2 (see PR). With it, we can still keep some warnings as warnings.

Use-case: say you’re upgrading an Akka project. It’s going to have a ton of @deprecated warnings that you may not want to fix right now, but you still want to keep the exhaustiveness checks as errors.

scalacOptions ++= Seq(
  // "-Xfatal-warnings",
  "-Wconf:cat=deprecation:ws,any:e",
)

What this does is to turn all warnings into errors, except for deprecation messages, which are still left as warnings. To break it down:

  • cat=deprecation refers to deprecation messages (classes/methods marked with @deprecated being called) and :ws says that for these warnings a “warning summary” should be shown
  • any:e says that for any other kind of warning, signal it via an error

Run this in a terminal for more help:

scalac -Wconf:help

2. Activate All Linting Options #

There are many useful compiler options that you could activate. You can find a (possibly non-complete) list on docs.scala-lang.org.

WARNING: the compiler evolves and it's good to keep this list up to date (see next tip)!

For Scala 2.13 at the time of writing, here’s my list of scalac options:

scalacOptions := Seq(
  // Feature options
  "-encoding", "utf-8",
  "-explaintypes",
  "-feature",
  "-language:existentials",
  "-language:experimental.macros",
  "-language:higherKinds",
  "-language:implicitConversions",
  "-Ymacro-annotations",

  // Warnings as errors!
  "-Xfatal-warnings",

  // Linting options
  "-unchecked",
  "-Xcheckinit",
  "-Xlint:adapted-args",
  "-Xlint:constant",
  "-Xlint:delayedinit-select",
  "-Xlint:deprecation",
  "-Xlint:doc-detached",
  "-Xlint:inaccessible",
  "-Xlint:infer-any",
  "-Xlint:missing-interpolator",
  "-Xlint:nullary-override",
  "-Xlint:nullary-unit",
  "-Xlint:option-implicit",
  "-Xlint:package-object-classes",
  "-Xlint:poly-implicit-overload",
  "-Xlint:private-shadow",
  "-Xlint:stars-align",
  "-Xlint:type-parameter-shadow",
  "-Wdead-code",
  "-Wextra-implicit",
  "-Wnumeric-widen",
  "-Wunused:implicits",
  "-Wunused:imports",
  "-Wunused:locals",
  "-Wunused:params",
  "-Wunused:patvars",
  "-Wunused:privates",
  "-Wvalue-discard",
)

There are many useful options in there, from disallowing “adapted args”, detecting inaccessible code, inferred Any, shadowing of values, to unused imports and params and others.

2.1. Use the sbt-tpolecat plugin #

Keeping that list of compiler options up to date is exhausting, new useful options get added all the time, others are deprecated, and especially for libraries, you have to deal with multiple Scala versions in the same project.

A better option is to include sbt-tpolecat in your project.

2.2. Exclude annoying linting options #

Some linting options can trigger false positives that are too annoying. It’s fine to remove them from your project or specific configurations (e.g. Test, Console).

For example, I do not like the -Wunused:privates option because it triggers an annoying false positive when defining (unused) default values in case class definitions.

If using sbt-tpolecat, as mentioned above, you can include something like this in your build definition:

scalacOptions in Compile ~= { options: Seq[String] =>
  options.filterNot(
    Set(
      "-Wunused:privates"
    )
  )
}

2.3. Relax the console configuration #

When playing around in the console, it’s a good idea to deactivate Xfatal-warnings, along with some of the more annoying warnings.

You could use sbt-tpolecat, which already does this automatically, or you could add this to build.sbt:

val filterConsoleScalacOptions = { options: Seq[String] =>
  options.filterNot(Set(
    "-Xfatal-warnings",
    "-Werror",
    "-Wdead-code",
    "-Wunused:imports",
    "-Ywarn-unused:imports",
    "-Ywarn-unused-import",
    "-Ywarn-dead-code",
  ))
}

// ...
scalacOptions.in(Compile, console) ~= filterConsoleScalacOptions,
scalacOptions.in(Test, console) ~= filterConsoleScalacOptions

Credit for this snippet: DavidGregory084/sbt-tpolecat.

3. Silence warnings locally #

Sometimes you want to ignore a certain warning:

  • maybe it’s a false positive
  • maybe you want something “unused”, as a placeholder

3.1. Silencer plugin (Scala < 2.13) #

You can use the ghik/silencer compiler plugin.

For our sample above, if we want to silence that exhaustiveness check:

import com.github.ghik.silencer.silent

@silent("not.*?exhaustive")
def size(list: List[_]): Int =
  list match {
    case _ :: rest => 1 + size(rest)
  }

You can give via this annotation a regular expression that matches the warning being silenced.

We can also silence the source files on a path using a compiler option in build.sbt:

scalacOptions += "-P:silencer:pathFilters=.*[/]src_managed[/].*"

3.2. Using @nowarn in Scala 2.13.2 #

Scala 2.13 has added the @nowarn annotation for local suppression.

@nowarn can be more fine grained. We could do just like the above and silence with a pattern matcher:

import scala.annotation.nowarn

@nowarn("msg=not.*?exhaustive")
def size(list: List[_]): Int =
  list match {
    case _ :: rest => 1 + size(rest)
  }

But we can do better, because the actual error messages are brittle. We can silence based on the “category” of the warning. To find the category of a warning, we can (temporarily) enable extra verbosity in these messages via this compiler option in build.sbt:

scalacOptions += "-Wconf:any:warning-verbose"

That error will then look like this:

[error] ... [other-match-analysis @ size] match may not be exhaustive.

The category is “other-match-analysis”, so we can silence it like this:

@nowarn("cat=other-match-analysis")
def size(list: List[_]): Int =
  list match {
    case _ :: rest => 1 + size(rest)
  }

NOTE: for forward compatibility in older Scala versions, with the Silencer plugin, coupled with scala-library-compat, you can use the new @nowarn annotation with older Scala versions, however only the @nowarn("msg=<pattern>") filtering is supported.

4. Other linters #

You shouldn’t stop at Scala’s linting options. There are other sbt plugins available that can enforce certain best practices. Off the top of my head:

With these, you could ban null or Any from your project, among other beneficial options.

Any useful plugins that I’m missing?

Final words #

Scala is a static language, but sometimes it isn’t static or opinionated enough. The more you can prove about your code at compile-time, the less defects you can have at runtime.

Now go forth and annoy your colleagues with actually useful compiler errors!

| Written by
Tags: scala | code