Catch bugs with Scalafix v0.5

Monday 11 September 2017

Ólafur Páll Geirsson

BLOG

I am excited to announce the release of Scalafix v0.5.0-RC3. This release introduces new features in addition to several bug fixes and integration improvements.

If you are not familiar with Scalafix, Scalafix is a rewrite and linting tool for Scala. The project is developed at the Scala Center with the mission to help automate migration between different Scala compiler and library versions.

This is the third post on Scalafix and scalameta. You might be interested in reading the previous post here.

Linting

Scalafix v0.5 supports linters. Linters are programs that analyze source code for usages of language constructs that could unwittingly lead to bugs, and emit warnings when these potential issues are found. Linters are particularly useful for defining and enforcing coding standards across teams.

Before v0.5, Scalafix rules could only provide rewrite instructions to “fix” code. Now, it’s possible to implement Scalafix rules that only report messages instead of rewrite instructions. Each category of lint messages is attached to a default severity level (info/warn/error), which users can override via configuration.

The first Scalafix linter is NoInfer. NoInfer reports errors when the Scala compiler infers one of the following types: Any, AnyVal, Product or Serializable. It’s usually a bad sign when the compiler infers such a generic type. In those cases, it’s better to explicitly type annotate the expected type.

Building a custom linter

It’s simple to implement custom Scalafix linters. To demonstrate this, let’s implement a FinalCaseClass linter that reports an error if a case class is not marked as final. We start our project with the scalacenter/scalafix.g8 template.

sbt new scalacenter/scalafix.g8 --rule="FinalCaseClass" --version="v1.0"
cd scalafix
sbt tests/test

Rules are implemented with Scalafix and Scalameta.

// rules/src/main/scala/fix/FinalCaseClass_v1.scala
package fix
import scalafix._
import scala.meta._, contrib._

case object FinalCaseClass extends Rule("FinalCaseClass") {
  val error = LintCategory.error(
    "Extending a case classes can have surprising equals/hashCode behavior."
  )
  override def check(ctx: RuleCtx): List[LintMessage] = ctx.tree.collect {
    case cls: Defn.Class if cls.hasMod(mod"case") && !cls.hasMod(mod"final") =>
      error.at(s"case class $name must be final", cls.name.pos)
  }
}

When FinalCaseClass encounters a non-final case class it reports a message like this

scala/test/FinalCaseClass.scala:8: error: [FinalCaseClass] case class Foo must be final
  case class Foo(a: Int)
             ^

We can use scalafix-testkit to make sure our rewrite works as expected. The top of the file contains a comment including configuration about which rule to run.

/*
rule = "class:fix.FinalCaseClass"
*/
class FinalCaseClass {
  case class Foo(a: Int) // assert: FinalCaseClass
  final case class Bar(a: Int)
}

The // assert: FinalCaseClass comment asserts that a linter message with id “FinalCaseClass” is reported on line 7. The test fails if

  • a message is not reported on that line
  • a message is reported on that line but the category id doesn’t match,
  • a message is reported at a line without an assert

FinalCaseClass is ready for a release! It is not necessary to fiddle with cross-building or publishing to Maven Central, Scalafix can run custom rules from source. Our friends can try out FinalCaseClass by installing sbt-scalafix and execute from the sbt shell

sbt> scalafix github:olafurpg/FinalCaseClass/v1

The full source code for FinalCaseClass is available in olafurpg/FinalCaseClass. To learn more about implementing custom rules, refer to our documentation.

Note. Scalafix has not at all shifted away its focus from rewriting. The idea to use Scalafix for linting initially started as a discussion in Scala Contributors. It turned out to be simple to add basic linting capabilities with the existing Scalafix infrastructure.

sbtfix

Another notable new feature in Scalafix v0.5 is the ability to rewrite sbt build sources with access to the semantic API. The first available sbt rewrite is Sbt1, which migrates several deprecated sbt “fishy operators” such as <++= to the recommended .value DSL. To run Sbt1 on your 0.13 build, follow the installation instructions for the sbt-scalafix plugin and execute > sbtfix Sbt1.

Custom sbtfix rules are implemented just like regular Scalafix rules. No custom setup is required from rule authors to fix sbt source files. The semantic API to resolve names and symbols signatures is supported, but advanced sections such as Synthetics (which NoInfer use) are not. I am excited to see what the community can build with this new functionality. Some ideas that come to my mind include

  • update library dependency versions
  • install new library dependencies
  • automatically enforce sbt best-practices

To learn more about how Scalafix supports semantic analysis of sbt 0.13 and Scala 2.10 sources, see semanticdb-sbt.

Note. Semantic API support for *.sbt files is still only at a “proof-of-concept” stage. The current implementation requires more engineering work to reach the same level of coverage as the semantic API for 2.11/2.12 *.scala source files.

Tab completion

Scalafix v0.5 improves the sbt-scalafix plugin and scalafix-cli in many ways. Most of these bug fixes and improvements involve fairly boring behind-the-scenes mechanics. However, I believe one small quality-of-life improvement is worth highlighting: tab completion of Scalafix rules.

Scalafix tab completion

Tab completion is supported in sbt-scalafix as well as for scalafix-cli in bash and zsh. Tab completion makes it easier to discover which rewrites are available and help prevents typos when spelling out rewrite names. To install completions for scalafix-cli, follow the instructions in --help.

What’s next

I believe we will soon reach a tipping point for Scala tooling. Semantic analysis of Scala programs has traditionally been tricky business, requiring intimate familiarity with compiler internals. Scalameta and Scalafix involve none of that. For example, the core logic of the NoInfer rule is implemented in 10 lines of fairly straightforward, immutable and functional code.

Next steps for Scalafix include:

  • more comprehensive understanding of “synthetics” such as inferred implicit arguments, #266. This is important for Dotty migration rewrites.
  • more linting rules. I would love to unite efforts with Scalastyle and Wartremover to offer an extensive set of linting rules in a single tool. Scalafix can definitely benefit from their experience in this space.
  • more complete Sbt1 rewrite to accellerate sbt 1.0 migration, see sbtfix issues.
  • more polished integrations. Currently, running Scalafix in a large project typically results in many spurious errors and warnings.

I encourage everyone to get involved if they want to improve the state of Scala tooling. If you are interested in contributing, don’t hesitate to stop by the Scalafix Gitter channel.

PS. I want to shout out to Gabriele Petronella, who made major contributions to Scalafix v0.4 and v0.5 via PRs and discussions. If you use cats, then you might be interested in his cats v1.0 migration rewrites.