1. Introduction

Scalafix lets us define and enforce rules in our Scala code, which is useful for two main reasons. First, it is useful for linting purposes, that is, to statically analyze the code, looking for common code smells of programming errors. Secondly, it helps us refactor the code. In other words, when the linter flags a suspicious construct, we can use Scalafix to rewrite it. In this tutorial, we’ll explore how to integrate it with SBT and examine some useful built-in rules.

2. Installation and Setup

Scalafix supports Scala 2.12, 2.13, and 3. Since SBT 1.3, we can add the Scalafix plugin to the project/plugins.sbt file:

addSbtPlugin("ch.epfl.scala" % "sbt-scalafix" % "0.12.1")

Importing the plugin will give us access to several SBT settings and tasks. The main ones are scalafix and scalafixAll, running the scalafix command on the current SBT module or all modules with Scalafix enabled. The latter is useful, for example, when we have a standalone module for integration tests or when our project is split into multiple sub-modules. The scalafix and scalafixAll tasks have a –check option, ensuring that scalafix was run on all sources. In particular, it makes the build fail if running scalafix produces an error message or a lint finding. This is particularly useful in CI/CD pipelines to ensure our code always passes the Scalafix checks. Another useful setting is scalafixConfig, which lets us specify a file with the extension .scalafix.conf containing the Scalafix rules to run.

2.1. Run on Compilation

If we want to run Scalafix anytime we build the project, we could set the scalafixOnCompile SBT setting to true. However, always running Scalafix on compilation is not considered a best practice as it comes with a few shortcomings:

  • scalafix might get run before scalafix –check in a CI environment, causing the latter to run on dirty sources and thus generating false negatives (that is, a successful run of scalafix –check even if the original code presents some issues);
  • bugs in the implementation of the rules might jeopardize the build;
  • caching is automatically enabled when scalafixOnCompile is true and might lead to issues difficult to debug (for example false positives due to previously cached Scalafix warnings).

2.2. The .scalafix.conf File

As we saw above, the .scalafix.conf file configures the Scalafix rules to be enforced. The configuration should follow the HOCON syntax. Let’s see an example of this file:

rules = [
  DisableSyntax,
  RemoveUnused
]

DisableSyntax.noVars = true
DisableSyntax.noThrows = true
DisableSyntax.noNulls = true

In the example above, we first specified which rules we want Scalafix to enforce (DisableSyntax and RemoveUnused). We’ll see what these rules are about shortly. Secondly, we configured the DisableSyntax one. This is an important aspect of Scalafix. It has many built-in rules, each checking for different code smells in our codebase. For example, our configuration of DisableSyntax tells Scalafix to only warn us about throw, var, or null in our code.

3. Scalafix Rules Overview

In this section, we’ll analyze a few commonly used Scalafix rules and show examples of the same code before and after applying the rule. Scalafix comes with several built-in rules. They are of two types:

  • Syntactic rules do not require code compilation. As such, they are straightforward to run but also limited in the analysis they can perform (having access only to the actual source code and not to any product of the compilation phase);
  • Semantic rules require code compilation. In addition to the Scala compiler, they require the SemanticDB compiler plugin. As they have access to more information w.r.t. syntactic rules, they are more complex but can also be used for more advanced code analysis.

3.1. DisableSyntax

The DisableSyntax Scalafix rule is a syntactic rule reporting errors whenever our code uses certain syntax. By default, the rule disables no syntax. It is our job to choose among the various options provided by Scalafix and configure the rule accordingly. This gives us greater freedom, letting us explicitly choose what we want to disallow. For example, the following configuration disables throw, var, or null. On the other hand, it allows, for instance, return and asInstanceOf:

DisableSyntax.noVars = true
DisableSyntax.noThrows = true
DisableSyntax.noNulls = true

Let’s see how this rule behaves. First, we’ll write a code snippet breaking it:

object DisableSyntaxDemo:
  var myVariable = null

  def validateMyVariable(): Boolean =
    if (myVariable == null) throw Exception("myVariable Is Null")

    return true

The code snippet above contains a lot of disabled syntax. Namely, we’re declaring a var, setting it to null, and throwing an Exception with throw. Additionally, we’re using the return keyword. According to our configuration above, Scalafix will flag the former three violations but not return. Let’s run the scalafix SBT task and see what happens:

[info] Running scalafix on 1 Scala sources
[error] /.../scala-tutorials/scala-sbt/scalafix/src/main/scala/com/baeldung/scala/scalafix/DisableSyntaxDemo.scala:4:3: error: [DisableSyntax.var] mutable state should be avoided
[error]   var myVariable = null
[error]   ^^^
[error] /.../scala-tutorials/scala-sbt/scalafix/src/main/scala/com/baeldung/scala/scalafix/DisableSyntaxDemo.scala:4:20: error: [DisableSyntax.null] null should be avoided, consider using Option instead
[error]   var myVariable = null
[error]                    ^^^^
[error] /.../scala-tutorials/scala-sbt/scalafix/src/main/scala/com/baeldung/scala/scalafix/DisableSyntaxDemo.scala:7:23: error: [DisableSyntax.null] null should be avoided, consider using Option instead
[error]     if (myVariable == null) throw ("myVariable Is Null")
[error]                       ^^^^
[error] /.../scala-tutorials/scala-sbt/scalafix/src/main/scala/com/baeldung/scala/scalafix/DisableSyntaxDemo.scala:7:29: error: [DisableSyntax.throw] exceptions should be avoided, consider encoding the error in the return type instead
[error]     if (myVariable == null) throw ("myVariable Is Null")
[error]                             ^^^^^
[error] (Compile / scalafix) scalafix.sbt.ScalafixFailed: LinterError

As expected, Scalafix flags the three forbidden syntaxes we used: var, null, and throw. On the contrary, it did not report return, as we did not enable it in the configuration file. Scalafix did not rewrite our code in this case, as it’d be pretty complex. Instead, it just warns us about potential code smells. It is our responsibility to rewrite the code to remove the. Let’s see one possible way:

object DisableSyntaxDemoRewritten:
  val myVariable = Option.empty[Unit]

  def validateMyVariable(): Either[String, Unit] =
    myVariable.toRight("myVariable Is Null")

In the snippet above, we replaced var with val. This was safe, as our code was reading myVariable without writing it. Secondly, we used Option.empty[Unit] instead of null. Thirdly, we refactored the exception with the Either type.

3.2. RemoveUnused

The RemoveUnused semantic rule removes unused imports and statements, thus rewriting our code. To work, however, it needs a couple of additional build settings:

  • enable the -Wunused:all compiler option (Scala 3.4+). This is because Scalafix relies on the unused imports and terms flagged by the compiler. This compiler option has slightly different names based on the Scala version. We can use -Ywarn-unused in Scala 2.12 and -Wunused in Scala 2.13.
  • disable the -Xfatal-warnings compiler option to avoid compilation errors before running Scalafix.;
  • enable the SemanticDB compiler plugin.

If we don’t edit our SBT configuration as described above and try to run Scalafix with the RemoveUnused rule, we’ll get two errors:

[error] (Compile / scalafix) scalafix.sbt.InvalidArgument: 2 errors
[error] [E1] The scalac compiler should produce semanticdb files to run semantic rules like DisableSyntax, RemoveUnused.
[error] To fix this problem for this sbt shell session, run `scalafixEnable` and try again.
[error] To fix this problem permanently for your build, add the following settings to build.sbt:
[error] 
[error] inThisBuild(
[error]   List(
[error]     scalaVersion := "3.4.2",
[error]     semanticdbEnabled := true,
[error]     semanticdbVersion := scalafixSemanticdb.revision
[error]   )
[error] )
[error] 
[error] 
[error] [E2] A Scala compiler option is required to use RemoveUnused. To fix this problem,
[error] update your build to add -Ywarn-unused (with 2.12), -Wunused (with 2.13), or 
[error] -Wunused:all (with 3.4+)

Let’s get back to our build.sbt file and edit the configuration:

semanticdbEnabled := true,
semanticdbVersion := scalafixSemanticdb.revision

scalacOptions += "-Wunused:all"

To show how RemoveUnused rule, let’s consider a simple Scala file with an unused import and statement:

import scala.List

object RemoveUnusedDemo:
  val myNumber = 10

  def greeting(name: String): String = {
    val newName = s"$name $myNumber"
    s"Hello, $name!"
  }

The snippet above contains an unused import (import scala.List) and an unused variable instantiation (that of newName). If we run scalafix, we’ll get the following output:

[warn] -- Warning: /.../scala-tutorials/scala-sbt/scalafix/src/main/scala/com/baeldung/scala/scalafix/RemoveUnusedDemo.scala:3:13 
[warn] 3 |import scala.List
[warn]   |             ^^^^
[warn]   |             unused import
[warn] -- Warning: /.../scala-tutorials/scala-sbt/scalafix/src/main/scala/com/baeldung/scala/scalafix/RemoveUnusedDemo.scala:9:8 
[warn] 9 |    val newName = s"$name $myNumber"
[warn]   |        ^^^^^^^
[warn]   |        unused local definition
[warn] two warnings found

And Scalafix will rewrite our code:

object RemoveUnusedDemo:
  val myNumber = 10

  def greeting(name: String): String = {
    s"$name $myNumber"
    s"Hello, $name!"
  }

4. Conclusion

In this article, we provided a brief introduction to Scalafix. We also saw how to set it up in an SBT-based project and some examples of the two types of rules: DisableSyntax as a syntactic rule and RemoveUnused as a semantic rule. We then saw how Scalafix behaves when it finds violations of those rules. As usual, you can find the code over on GitHub.