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
.
-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.
Scala compiler plugin for warning suppression: https://t.co/iPT7AKDq1i
— Roman Janusz (@rjghik) April 14, 2015
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 v0.8.0-RC1 is out with new documentation, improved sbt plugin, better semantic APIs, improved support for custom rules and more https://t.co/sEpy7U9diD
— Ólafur Páll Geirsson (@olafurpg) September 20, 2018
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
-Xlint
and-Xfatal-warnings
provide stronger enforcement against common mistakes.- When we need to bail out some code, we can use
@silent
annotation. - Scalafix allows flexible linting that can be extended through custom rules.