BAM Weblog

Scala Code Linting via WartRemover 0.4

Brian McKenna2013-09-16

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.

Please enable JavaScript to view the comments powered by Disqus.