1. Introduction
Modern programming languages come with many different features, letting us implement the same feature in many different ways. Nonetheless, some features are more harmful than others.
Scala is no different. For example, it allows for null values and the use of the Any type. Although not always dangerous, such features often hide a more profound code smell.
WartRemover is a linting tool that warns us about some of the nastiest Scala features. In this tutorial, we’ll look at how to integrate it into our projects.
2. Setup
First of all, let’s add WartRemover to our SBT plugins (project/plugins.sbt):
addSbtPlugin("org.wartremover" % "sbt-wartremover" % "3.0.11")
Version 3 of WartRemover requires at least SBT 1.0 and Scala 2.12.
2.1. Configuration
Most of the configuration happens in build.sbt. We refer to the rules enforced by WartRemover as “warts”. The default configuration disables all warts. Let’s see how to turn them all on:
wartremoverErrors ++= Warts.all
The setting above will turn every WartRemover finding into a compilation error. Nonetheless, some rules might have false positives. Let’s see how to enable only the stable ones:
wartremoverErrors ++= Warts.unsafe
If we want WartRemover to issue warnings instead of errors, we can use wartremoverWarnings instead of wartremoverErrors.
In some cases, however, we might want to enable a few warts instead of all of them. Let’s see, for example, how to detect usages of asInstanceOf and null:
wartremoverWarnings ++= Seq(Wart.AsInstanceOf, Wart.Null)
WartRemover has a rich list of built-in warts defined within the org.wartremover.warts package.
Similarly, we can also explicitly exclude warts from being detected:
wartremoverErrors ++= Warts.allBut(Wart.asInstanceOf, Wart.null)
Lastly, we can enable WartRemover only on selected scopes:
Compile / compile / wartremoverErrors ++= Warts.all
3. WartRemover in Practice
After enabling WartRemover in our project, it’ll inspect our code during compilation. Let’s see an example, assuming we have configured it to generate warnings if we use null values:
object Main extends App {
val test = null
}
When compiling the example above, WartRemover will generate a warning, as expected:
sbt:wart-remover> compile
[info] compiling 1 Scala source to /.../scala-sbt/wart-remover/target/scala-2.13/classes ...
[warn] /.../scala-sbt/wart-remover/src/main/scala/com/baeldung/scala/wartremover/Main.scala:4:14: [wartremover:Null] null is disabled
[warn] val test = null
[warn] ^
[warn] one warning found
[success] Total time: 3 s
On the other hand, if we configured WartRemover to generate errors instead of warnings, the compilation would fail:
sbt:wart-remover> compile
[info] compiling 1 Scala source to /.../scala-sbt/wart-remover/target/scala-2.13/classes ...
[error] /.../scala-sbt/wart-remover/src/main/scala/com/baeldung/scala/wartremover/Main.scala:4:14: [wartremover:Null] null is disabled
[error] val test = null
[error] ^
[error] one error found
[error] (Compile / compileIncremental) Compilation failed
[error] Total time: 0 s
4. Suppressing Errors and Warnings
As we saw above, WartRemover is excellent at spotting code smells in our projects. Sometimes, however, it might be helpful to disable the inspection. We usually do so when we know that a given piece of code violates a specific rule, but we have no way to work around it. For example, we might use a Java library that allows null values.
In such cases, we can resort to the @SuppressWarnings annotation. Let’s add a new null value to our example, suppressing the inspection just for it:
object Main extends App {
val test = null
@SuppressWarnings(Array("org.wartremover.warts.Null"))
val suppressed = null
}
If we compile the example above, we’ll get only one error (or warning, depending on our configuration):
sbt:wart-remover> compile
[info] compiling 1 Scala source to /.../scala-sbt/wart-remover/target/scala-2.13/classes ...
[warn] /.../scala-sbt/wart-remover/src/main/scala/com/baeldung/scala/wartremover/Main.scala:4:14: [wartremover:Null] null is disabled
[warn] val test = null
[warn] ^
[warn] one warning found
[success] Total time: 3 s
@SuppressWarnings is very useful when we want to disable the inspection in narrowed pieces of code, but it cannot be used to exclude entire directories. To that end, we can resort to the wartremoverExcluded setting in build.sbt:
wartremoverExcluded += baseDirectory.value / "src" / "main" / "scala" / "com" / "baeldung" / "scala" / "wartremover" / "FileName.scala"
5. User-Defined Wart Rules
Besides all built-in warts, sometimes it’s useful to define our own rules. In this section, we’ll see how to define and import custom rules.
5.1. Defining a Rule
A rule is an object extending WartTraverser and defining an apply() method whose input is an instance of WartUniverse, and output is an instance of WartUniverse#universe#Traverser.
WartUniverse defines two methods, error and warning, that we can call to add, as a side-effect, errors and warning to the inspection. Let’s see how to define a wart detecting the literal “Baeldung”:
package com.baeldung.scala.customwarts
import org.wartremover.{ WartTraverser, WartUniverse }
object BaeldungWart extends WartTraverser {
def apply(u: WartUniverse): u.Traverser = {
import u.universe._
new Traverser {
override def traverse(tree: Tree): Unit = {
tree match {
case t if hasWartAnnotation(u)(t) => //Ignore trees with the SuppressWarnings annotation
case Literal(Constant("Baeldung")) => error(u)(tree.pos, "Baeldung literal is disabled")
case _ => super.traverse(tree)
}
}
}
}
}
To define a rule, we use the Scala Reflection API, which is out of the scope of this article.
In a few words, a rule is defined over a path on the abstract syntax tree representing a program. In our case, we first check if the tree is annotated with @SuppressWarnings. We do that by calling the WartTraverser::hasWartAnnotation method. If so, we stop the traversal.
The second branch of the match looks for a literal with the value Baeldung. When we find one, we report a violation with the WartTraverser::error method. Lastly, if none of the previous branches produced a match, we keep traversing the tree.
5.2. Defining an SBT Project for the Rules
Wart rules must be added to the classpath for WartRemover to see them.
We can do that by creating an SBT sub-project. Hence, let’s assume we saved our custom rule in “*./custom-warts/src/main/scala/com/baeldung/scala/customwarts/BaeldungWart.scala*“. In the build.sbt file, let’s define the sub-project:
lazy val customWarts = (project in file("custom-warts")).settings(
scalaVersion := "2.13.10",
name := "CustomWarts",
version := "1.0.0",
exportJars := true,
libraryDependencies += "org.wartremover" % "wartremover" % wartremover.Wart.PluginVersion cross CrossVersion.full
)
The definition above basically declares the name of the project and tells SBT to generate a JAR file each time we compile the project. Lastly, we add a dependency to the wartremover library, using the same version of the SBT plugin and enabling cross-building.
5.3. Importing Our Custom Rules
After defining a sub-project for our custom wart rules, let’s add it to the main SBT project:
lazy val root = (project in file("."))
.dependsOn(customWarts)
.settings(
wartremoverClasspaths ++= {
(Compile / dependencyClasspath).value.files
.find(_.name.contains("customwarts"))
.map(_.toURI.toString)
.toList
},
wartremoverWarnings += Wart.custom(
"com.baeldung.scala.customwarts.BaeldungWart"
)
)
First, we set a dependency on customWarts, defined above. Then, we tell WartRemover to look for additional rules in the JAR file produced by customWarts. Lastly, we enable our BaeldungWart rule using Wart.custom.
5.4. Custom Rules in Action
We can now finally test our rule out. Let’s add a couple of violating vals:
object Main extends App {
val baeldungViolation = "Baeldung"
@SuppressWarnings(Array("com.baeldung.scala.customwarts.BaeldungWart"))
val baeldungSuppressed = "Baeldung"
}
If we compile the program above, we’ll get only one warning for baeldungViolation, but none for baeldungSuppressed, as expected:
sbt:wart-remover> compile
[info] compiling 2 Scala sources to /.../scala-sbt/wart-remover/target/scala-2.13/classes ...
[warn] /.../scala-sbt/wart-remover/src/main/scala/com/baeldung/scala/wartremover/Main.scala:10:27: [wartremover:BaeldungWart] Baeldung literal is disabled
[warn] val baeldungViolation = "Baeldung"
[warn] ^
[warn] one warnings found
[success] Total time: 0 s
6. Conclusion
In this article, we explored WartRemover, a linting tool for Scala. First, we saw how to import and configure it. Then, we experimented with the built-in rules. Finally, we defined a rule of our own.
As usual, the source code for the examples can be found over on GitHub.