search term:

scruff、Scala コード・フォーマッター

scruff という高速に正規化を行う Scala のコード・フォーマッターを Rust で作ってリリースした。

設計方針: 正規化

Scruff は、Python のコードフォーマッターの Black と Ruff にインスパイヤされた正規化を行うコード・フォーマッターだ。これは Scruff をかけたコードは、どのような形から始めても同じになることを意味する。と言っても分かりづらいので、具体例で説明する:

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

デファクト・スタンダードな Scala フォーマッターである Scalafmt を使った場合、上記のコードはフォーマット後もそのままだ。一方 Scruff は以前のフォーマットを無視して、統一した空白文字を以下のように適用する:

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

もし行が max-column (デフォルト: 100) を上回った場合は、Scruff は縦に分割する:

// 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")

は以下のようになる:

// 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",
  )

設計方針: 高速

Scruff は、Tree-Sitter Scala を利用して Rust で書かれており、Scala コードのパースとフォーマットを、並列に、ネイティブコードで行う。未処理の scala/scala3 リポジトリは、207k sloc のコードがあるが、Scruff はこれを 1.50s でフォーマットする:

これは、設定をいじらない状態だと Scruff は Scalafmt に比べて約6x 速いことになるが、これはデフォルトの並列度の設定に依る所が大きい。興味深いことに、既にフォーマット済みのコードに再実行すると、Scruff は 0.60s で終わる。これは、ファイル出力が入力と同一の場合、Scruff は再パースと等価性の検査を省略しているためだ:

ということで、Scalafmt の設定次第で Scruff は 3~10 倍高速だということだ。もし、リポジトリ全体を 0.6秒でフォーマット化できるなら、いつでもフォーマット化するべきだろう。

設計方針: 定食 A か定食 B

Scruff 0.1.0 は、最小限の設定しか持たない:

# .scruff.toml

max-column = 100
dialect = "scala3"

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

他にいじる設定は無し。

使い方

ネイティブのバイナリファイルは GitHub Releases https://github.com/eed3si9n/scruff/releases/tag/v0.1.0 に公開している。便宜のため、sbt プラグインも作って、scruffFmt、scruffFmtCheck、scruffFmtOnCompile をビルドに追加する。

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

scruffFmt を最初に使ったときにバイナリのダウンロードが行われる:

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

コンパイル時に自動でフォーマットを行いたい場合は:

ThisBuild / scruffFmtOnCompile := true

フォーマット理論

Scruff はその理論的基礎を Philip Wadler 氏の 1997/1998 の論文 ‘A prettier printer’ (https://homepages.inf.ed.ac.uk/wadler/papers/prettier/prettier.pdf) に負っている。このペーパーの Scala 実装として typelevel/paiges があるので、コンセプトとして知っている人もいるかもしれない。

Wadler ペーパーは、Doc というデータ型を説明していて、これはコードをテキストの小さな「かたまり」を追加することによって記述できる。フォーマット化を行うときは、プリンターに max-column を渡して、改行やインデントは、プリンターが受け持つ。このプリンターは線形にスケールする。

実際には、パース時にコメントなど全部の空文字も保持する必要があるので、まずは内部で Tree と呼んでいるロスレスな具象構文木 (CST) を構築する。フォーマット化と書き換えは、この Tree を Doc に変換する作業となる。その後、フォーマット化されたコードは再パースして、処理されたコードが等価かの検査を行う。この等価性の検査は、implicit パラメータと using パラメータなど、書き換えで変わるものは無視する。

ご感想お待ちしております

できたばかりなので、パースやフォーマットなどまだバグもあるかもしれない。何か変な挙動が見つかったら報告してください。Tree-Sitter Scala は今まで構文のハイライト目的で使われることが多かったが、フォーマット化に使うことで何十ものパースの不具合が発見され、最新のリリースではそのいくつかの修正が既に入った。

AI 利用開示

Scruff は Claude Code を用いて開発した。コードの方向性は自分で細かく操作して、テストなども確認したが、実際の Rust のコードそのものの大半は LLM によって書かれた。