search term:

scruff, a Scala code formatter

I’ve released scruff, a fast and normalizing Scala code formatter, written in Rust.

philosophy: normalize

Inspired by Python code formatters Black and Ruff, Scruff is a normalizing code formatter. This means that the scruffed up code looks the same regardless of how it started. Here’s an example:

// original input
object A:
  val a = List(1, 2)
  val b = List(
    1,
    2
  )
  val c =
    List(1, 2)

Using Scalafmt, which is the defacto standard formatter for Scala, the above code is kept as-is after formatting. Scruff ignores previous formatting, and applies uniform whitespace as follows:

// scruffed
object A:
  val a = List(1, 2)
  val b = List(1, 2)
  val c = List(1, 2)

When the line goes over the max-column (default: 100), Scruff will break them down vertically:

// original input
object A:
  val a = List("scala-library-2.13.16.jar", "scala3-library_3-3.8.4.jar", "sbt-launch-2.0.0-RC2.jar")

becomes:

// scruffed
object A:
  val a = List(
    "scala-library-2.13.16.jar",
    "scala3-library_3-3.8.4.jar",
    "sbt-launch-2.0.0-RC2.jar",
  )

philosophy: fast

Built on top of Tree-Sitter Scala using Rust, Scruff parses and formats the Scala code in parallel, in native code. When formatting unformatted 207k sloc scala/scala3 repo, Scruff runs in 1.50s:

So out-of-box, Scruff is about 6x faster than Scalafmt to format code mostly due to the default parallelism. Interestingly, on code that’s already formatted, Scruff finishes in 0.60s. This is because when a file’s output matches its input, Scruff skips re-parsing it and the equivalence check, so each file is parsed only once:

This means that Scruff would be 3 ~ 10x faster, depending on how Scalafmt is configured. If it takes 0.6s to run for the whole repo, we can enable it all the time.

philosophy: lunch A or lunch B

Scruff 0.1.0 offers minimal configurations:

# .scruff.toml

max-column = 100
dialect = "scala3"

exclude-paths = [
  "glob:**/src/sbt-test/**",
]

There are no other knobs.

usage

The native binaries are published to GitHub Releases https://github.com/eed3si9n/scruff/releases/tag/v0.1.0. For convenience, I’ve also created an sbt plugin that adds scruffFmt, scruffFmtCheck, and scruffFmtOnCompile.

// project/plugins.sbt
addSbtPlugin("com.eed3si9n" % "sbt-scruff" % "0.1.0")

The first time you call scruffFmt, it will download the binary:

sbt:scruff-root> scruffFmt
[info] downloading scruff 0.1.0 for universal-apple-darwin
[info] 4 files left unchanged
[success] elapsed time: 1 s

To enable formatting on-compile:

ThisBuild / scruffFmtOnCompile := true

theory of formatting

Scruff owes its theoretical backing to Philip Wadler’s 1997/1998 paper ‘A prettier printer’ (https://homepages.inf.ed.ac.uk/wadler/papers/prettier/prettier.pdf). There’s a Scala implementation of the paper called typelevel/paiges, so some might have heard about the concept.

The Wadler paper describes a data type called Doc, which lets us describe the code by appending small chunks of text. When we are ready to format the code, the max-column specification is passed to the printer, which determines the line wrapping and indentation. This printer scales at linear performance.

For comments etc, we do need to retain all whitespaces during parsing, so first we do construct a lossless concrete syntax tree (CST), which we internally call Tree. Formatting and rewriting mostly is the process of turning the original tree into a Doc. Afterwards, we need to re-parse the formatted code to check that the scruffed up code is still equivalent. This equivalence check ignores things that could change during a rewrite like implicit parameter vs using parameter.

let me know how it works

There might be bugs in parsing, formatting etc. Let me know if you find something weird. Tree-Sitter Scala has mostly been used for syntax highlighting purpose, but now that it’s used for formatting, it has exposed dozens of parsing issues, some which fixed in the latest release.

AI disclosure

Scruff was developed using Claude Code. I’ve steered the direction of the code, and checked the tests, but actual Rust code itself was written mostly by an LLM.