search term:

stricter Scala with -Xlint, -Xfatal-warnings, and Scalafix

Compile, or compile not. There’s no warning. Two of my favorite Scala compiler flags lately are "-Xlint" and "-Xfatal-warnings". Here is an example setting that can be used with subprojects:

ThisBuild / organization := "com.example"
ThisBuild / version      := "0.1.0-SNAPSHOT"
ThisBuild / scalaVersion := "2.12.6"

lazy val commonSettings = List(
  scalacOptions ++= Seq(
    "-encoding", "utf8",
    "-deprecation",
    "-unchecked",
    "-Xlint",
    "-feature",
    "-language:existentials",
    "-language:experimental.macros",
    "-language:higherKinds",
    "-language:implicitConversions",
    "-Ypartial-unification",
    "-Yrangepos",
  ),
  scalacOptions ++= (scalaVersion.value match {
    case VersionNumber(Seq(2, 12, _*), _, _) =>
      List("-Xfatal-warnings")
    case _ => Nil
  }),
  Compile / console / scalacOptions --= Seq("-deprecation", "-Xfatal-warnings", "-Xlint")
)

lazy val foo = (project in file("foo"))
  .settings(
    commonSettings,
    name := "foo",  
  )

what’s -Xlint?

-Xlint enables a bunch of compiler warnings. @smogami contributed a page called Scala Compiler Options so we can now read what’s in -Xlint.

One of them, for instance is -Xlint:infer-any, which warns when a type argument is inferred to be Any.

contains

-Xfatal-warnings

The problem with warnings is that it often gets postponed and then it piles up. -Xfatal-warnings promotes the warnings to compiler error, so it cannot be ignored.

suppress warnings with silencer

There are situations where warnings are unavoidable. For example, you might need to use a deprecated method for backward compatibility reason. It would be nice if we can suppress warnings for a specific expression.

In 2015 Roman Janusz (@rjghik) wrote a compiler plugin called silencer that does exactly that.

The usage looks like this:

import com.github.ghik.silencer.silent

@silent override lazy val ansiCodesSupported = delegate.ansiCodesSupported

This supresses all warnings for the definition.

custom linting using Scalafix

Scalafix is a refactoring and linting tool created by Ólafur (@olafurpg) and others at Scala Center. As the name suggest, it’s good at automated rewrite of code, but recently there’s been more emphasis on using it for linting purpose.

Scalafix 0.8.0-RC1 that came out recently uses Scalameta 4 (well 4.0.0-RC1 to be specific):

scalafix-noinfer

Previous version of Scalafix shipped with a rule to suppress specific type inference called NoInfer. During recent development it got absorbed by another rule called Disable, which eventually got too complex to be included into Scalafix itself. Instead, Scalafix 0.8 seems to be pursuing the plugin ecosystem route. Since -Yno-lub hasn’t picked up traction, and I was looking forward to Disable.

So I implemented a Scalafix rule called scalafix-noinfer myself. Here’s how to use it.

project/build.properties

sbt.version=1.2.3

project/plugins.scala

addSbtPlugin("ch.epfl.scala" % "sbt-scalafix" % "0.8.0-RC1")

build.sbt

ThisBuild / organization := "com.example"
ThisBuild / version      := "0.1.0-SNAPSHOT"
ThisBuild / scalaVersion := "2.12.6"

// Scalafix plugin
ThisBuild / scalafixDependencies +=
  "com.eed3si9n.fix" %% "scalafix-noinfer" % "0.1.0-M1"

lazy val root = (project in file(".")).
  settings(
    name := "hello",
    addCompilerPlugin(scalafixSemanticdb),
    scalacOptions ++= List(
      "-Yrangepos",
      "-P:semanticdb:synthetics:on",

      // you can add the options from the above here too
    ),
    // Compile / scalacOptions += {
    //   val t = crossTarget.value / "meta"
    //   s"-P:semanticdb:targetroot:$t"
    // },
    // Test / scalacOptions += {
    //   val t = crossTarget.value / "test-meta"
    //   s"-P:semanticdb:targetroot:$t"
    // }
  )

.scalafix.conf

rules = [
  NoInfer
]

Main.scala

package example

case class Address()

object Main extends App {
  List(Animal()).contains("1")
}

scalafix-noinfer usage

From sbt shell type scalafix:

sbt:hello> scalafix
[info] Running scalafix on 2 Scala sources
[error] /Users/eed3si9n/work/quicktest/noinfer/Main.scala:7:3: error: [NoInfer.Serializable] Serializable was inferred, butit's forbidden by NoInfer
[error]   List(Animal()).contains("1")
[error]   ^^^^^^^^^^^^^^^^^^^^^^^
[error] (Compile / scalafix) scalafix.sbt.ScalafixFailed: LinterError

Yes! So now we have NoInfer rule that’s catching bad type inference in contains(...). In my opinion, it doesn’t make sense for Scala to lub to java.io.Serializable since the list would never contain "1".

By default this rule forbids the inference of scala.Any, scala.AnyVal, java.io.Serializable, scala.Serializable, and scala.Product. You can customize this using .scalafix.conf as follows:

rules = [
  NoInfer
]
NoInfer.disabledTypes = [
  scala.Any,
  scala.AnyVal,
  scala.Serializable,
  java.io.Serializable,
  scala.Product,
  scala.Predef.any2stringadd
]

Now this will catch scala.Predef.any2stringadd:

[info] Running scalafix on 2 Scala sources
[error] /Users/eed3si9n/work/quicktest/noinfer/Main.scala:8:3: error: [NoInfer.any2stringadd] any2stringadd was inferred, but it's forbidden by NoInfer
[error]   Option(1) + "what"
[error]   ^^^^^^^^^
[error] (Compile / scalafix) scalafix.sbt.ScalafixFailed: LinterError

challenges

First issue that I noticed is that I can’t seem to move the targetroot of semanticdb. This means semanticdb will be shipped with your JAR if you use Scalafix with semantic rules. I should be able to opt out of this. Maybe I need to dig deeper to find out how.

scalafix-noinfer is a progress forward, and it’s more usable than a forked Scala compiler, but it’s not as thorough as -Yno-lub. For instance, it seems to be perfectly ok with the following:

object Main extends App {
  val x = if (true) 1 else false
  val y = 1 match { case 1 => Array(1); case n => Vector(n) }
}

summary

  1. -Xlint and -Xfatal-warnings provide stronger enforcement against common mistakes.
  2. When we need to bail out some code, we can use @silent annotation.
  3. Scalafix allows flexible linting that can be extended through custom rules.