Scala Code Linting via WartRemover 0.4
I was recently on a panel with some smart people discussing Rod Johnsons’ infamous “Scala 2018” keynote. Most agreed that we should start enforcing code standards. Sadly, the tools for doing that are a bit lacking.
For a while I’ve had an idea on a flexible tool for writing lint rules over Scala code. I already had WartRemover which was a linter for things I considered bad. I decided to rewrite WartRemover into the flexible tool I imagined.
The idea is to write lint rules using a Traverser over the Scala AST. WartRemover then provides the wrappers to run rules from:
- Macros
- Command-line tool
- Compiler plugin
To run a rule as a macro:
scala> import language.experimental.macros
import language.experimental.macros
scala> import org.brianmckenna.wartremover.warts.Unsafe
import org.brianmckenna.wartremover.warts.Unsafe
scala> def safe(expr: Any) = macro Unsafe.asMacro
safe: (expr: Any)Any
scala> safe { null }
<console>:10: error: null is disabled
safe { null }
Using the command-line tool, which reports errors and gives an exit code of 1 on failures:
java -classpath target/classes:wartremover-assembly-0.4-SNAPSHOT.jar \
org.brianmckenna.wartremover.Main \
-traverser org.brianmckenna.wartremover.warts.Unsafe \
src/main/scala/Main.scala
Or, best for last, as a compiler plugin by adding the following to
build.sbt
:
resolvers += Resolver.sonatypeRepo("releases")
addCompilerPlugin("org.brianmckenna" % "wartremover" % "0.4" cross CrossVersion.full)
scalacOptions += "-P:wartremover:traverser:org.brianmckenna.wartremover.warts.Unsafe"
Which will check all code under the project for errors.
The provided rules are:
- No any2stringadd
- No non-unit statements
- No null
- No var
- No Nothing type
- No unsafe (all of the above except Nothing is allowed)
But WartRemover now allows you to write your own rules. Here is a complete example:
package unimplemented
import org.brianmckenna.wartremover.{WartTraverser, WartUniverse}
object Unimplemented extends WartTraverser {
def apply(u: WartUniverse): u.Traverser = {
import u.universe._
val NotImplementedName: TermName = "$qmark$qmark$qmark" // ???
new Traverser {
override def traverse(tree: Tree) {
tree match {
case Select(_, NotImplementedName) =>
u.error(tree.pos, "There was something left unimplemented")
case _ =>
}
super.traverse(tree)
}
}
}
}
The above disables usages of ???
- you could make that above rule
execute before committing, for example.
You can also combine rules like so:
package org.brianmckenna.wartremover
package warts
object Unsafe extends WartTraverser {
val safeTraversers = List(NonUnitStatements, Var, Null, Any2StringAdd)
def apply(u: WartUniverse): u.Traverser =
WartTraverser.sumList(u)(safeTraversers)
}
If you’re interested in contributing rules (aka warts) or just want to have a play, the repository is on GitHub.