Scalafixを
使いこなして楽しよう
2020.6.16 Taisuke Oe
あなた誰?

Scalafixとは?
- リファクタリング用のツール
- 日々の開発だったり、マイグレーションだったり
- Scalaソースコードの書き換えコマンドラインツール
- sbt-scalafixは、コマンドラインツール(の実体)に引数を渡す
- 二つのモード
- scalafix : ruleに従って、ソースコードを書き換える
- scalafix --check : ruleに従っているか、チェックする
- 利用するruleと、その設定は .scalafix.conf に定義
どんな感じかチラ見
使い方 初めの一歩
前準備
//project/plugins.sbt
addSbtPlugin("ch.epfl.scala" % "sbt-scalafix" % "0.9.17")
//build.sbt
ThisBuild / semanticdbEnabled := true
ThisBuild / semanticdbVersion := scalafixSemanticdb.revision
Scalafixの使い方
.scalafix.conf を利用
// Lint
sbt:myProject> scalafix --check
// Rewrite
sbt:myProject> scalafix
引数でRuleを指定
//Rewrite
sbt:myProject> scalafix RemoveUnused
Scalafixの使い方
.scalafix.conf の例
//.scalafix.conf
rules = [
DisableSyntax
ProcedureSyntax
RemoveUnused
]
DisableSyntax.keywords = [
return
]
とあるScalaのスニペット
//src/main/scala/Sample.scala
import scala.util.Try
class Sample {
private val x = 1
def run() {
return
}
}
Q. 間違い?探し
このScalaのスニペットには、変更した方がいいところが幾つかあります。
分かりますか?
とあるScalaのスニペット
//src/main/scala/Sample.scala
import scala.util.Try
class Sample {
private val x = 1
def run() {
return
}
}
A. 回答
- return
- procedure syntax
- unused private variable
- unused import
//.scalafix.conf
rules = [
DisableSyntax
ProcedureSyntax
RemoveUnused
]
DisableSyntax.keywords = [
return
]
以下をカバーする.scalafix.conf
- return
- procedure syntax
- unused private variable
- unused import
[error] /Users/taisukeoe/Workspace/ScalafixSandbox/src/main/scala/Sample.scala:5:5: error: [DisableSyntax.return] return should be avoided, consider using if/else instead
[error] return
[error] ^^^^^^
- Lint専用Rule。Rewriteはしない。
- 様々な構文(Syntax)の使用を検出して、エラーを報告する
- return, while, varなど
- コメントにはマッチしない
- 正規表現による検出も可能
- return, while, varなど
- procedure syntaxの戻り値を明記するようrewriteするRule
- `scalafix --check` だと、期待される書き換えをエラーメッセージとして表示する
- 使われていないimport、private変数、ローカル変数を削除するようrewriteするrule
- scalacOptionsに以下の制約がある
- `-Ywarn-unused` や、`-Wunused` (2.13以上)を使う
- 使われていないシンボルの検出に、エラーメッセージを利用している
- `-Xfatal-warnings` を使わない。
- scalafixの実行前にコンパイルする必要があるため、警告有りでもコンパイルできる必要がある
- `-Ywarn-unused` や、`-Wunused` (2.13以上)を使う
[error] /Users/taisukeoe/Workspace/ScalafixSandbox/src/main/scala/Sample.scala:6:5: error: [DisableSyntax.return] return should be avoided, consider using if/else instead
[error] return
[error] ^^^^^^
--- /Users/taisukeoe/Workspace/ScalafixSandbox/src/main/scala/Sample.scala
+++ <expected fix>
@@ -1,8 +1,8 @@ / Compile / scalafix 1s
-import scala.util.Try
+
class Sample {
- private val x = 1
- def run() {
+
+ def run(): Unit = {
return
}
}
Lintの結果
sbt:myProject> scalafix --check
--- /Users/taisukeoe/Workspace/ScalafixSandbox/src/main/scala/Sample.scala
+++ <expected fix>
@@ -1,8 +1,8 @@
-import scala.util.Try
+ | => refactor / Compile / scalafix 0s
class Sample {
- private val x = 1
- def run() {
+
+ def run(): Unit = {
return //scalafix:ok
}
}
[error] (refactor / Compile / scalafix) scalafix.sbt.ScalafixFailed: TestError
[error] Total time: 1 s, completed 2020/03/23 17:32:04
- //scalafix:ok
- 指定した式のみ抑制
- //scalafix:off
- //scalafix:on するまでのブロックを抑制
- @SuppressWarnings(Array("scalafix:DisableSyntax.return"))
[error] /Users/taisukeoe/Workspace/ScalafixSandbox/src/main/scala/Sample.scala:6:5: error: [DisableSyntax.return] return should be avoided, consider using if/else instead
[error] return
[error] ^^^^^^
[error] (refactor / Compile / scalafix) scalafix.sbt.ScalafixFailed: LinterError
[error] Total time: 6 s, completed 2020/03/23 4:47:51
Rewriteの結果
sbt:myProject> scalafix
// src/main/scala/Sample.scala
class Sample {
def run(): Unit = {
return
}
}
Scalafixの使い所
よく使われているScalaのLinter
コンパイラオプション
- scalac options
- -Xlint, -Xfatal-warnings, -Ywarn-unused, ...
コンパイラプラグイン
- WartRemover
- scapegoat
Scalafixと、既存のLinterとの比較
うれしいところ
苦手なところ
Scalafixのうれしいところ
- Scalafixの警告の大半は、半自動で修正可能
- 設定をgit管理できる
- 設定に個人差が出やすい、IDEに頼らなくていい
- 新規で導入するときも、警告の修正で圧倒的に楽ができる
- 設定をgit管理できる
Scalafixのメリット
- 設計上、Scala 3もサポート可能
- dottyは既にSemanticDBを生成可能。
- あとはScala 3のソースコードを構文解析できれば……
- Scalametaが一部対応済みだが、Dotty最新版には追従していない
- 極一部の、コンパイラ内部に密結合しているRuleは対応が大変?

Scalafixが苦手なところ
- (単純に比較はできないが)実行に時間がかかる
- コンパイル時間のoverheadがある可能性もある
- (その他のLinterに比べて) Ruleは少なめ
- 一部のscalac optionsへの依存や、共存が難しい問題がある
- Ruleごとに設定フォーマットが違う
- 大量のRuleを入れると、設定ファイルが爆発しそう?
個人的なScalafixの使い方
- scalac optionやlinterによる警告を、完全に代替しない
- 修正方法を決定できないものを、Scalafixで警告するメリットはあまりない
- lintのみのScalafix ruleがほとんど無い理由
- コンパイラとは別途、構文解析している分、時間がかかる
- コンパイル毎に実行させるのは厳しい
- 修正方法を決定できないものを、Scalafixで警告するメリットはあまりない
- scalacOptionsやlinterと併用するのが便利
- Scalafixで修正可能なものは、可能な限りScalafixに任せる
- 修正が難しいもの、コンパイル毎に警告したいものはscalac optionやlinterを使う
便利な常用Ruleの例
-
RemoveUnused
- 前述
-
ExplicitResultTypes
- public、もしくはprotectedなメンバに型注釈をつける
- Literalなど自明な場合は型注釈をつけない
- 現在の実装はScala compilerに依存しており、2.12.11でしか動かない
- public、もしくはprotectedなメンバに型注釈をつける
Scalafix repo外のRule
scalacenter/scalafix に含まれないRuleは、なんらかの方法で指定する必要がある
継続して使いたい外部Rule
- コードの質を保つために有用なもの。
- build.sbtのscalafixDependenciesに追記するのがオススメ。
ThisBuild / scalafixDependencies += "com.github.vovapolu" %% "scaluzzi" % "0.1.8"
便利な常用Ruleの例(外部)
-
OrganizeImports
- 書き換え内容
- 設定に従ったimport文の並び替え
- wildcardやimplicitのimportを特別扱い
- Unused importの削除 (RemoveUnusedのruleから一部取り込み)
- 設定に従ったimport文の並び替え
- もちろんIDEにも同種の機能はあるが…
- 気分次第でたまにやると、不必要に差分が大きくなりがち
- こまめな整理を強制したい
- まだ安定版ではないが、使えている
- 期待大
- 書き換え内容
ThisBuild / scalafixDependencies += "com.github.liancheng" %% "organize-imports" % "0.3.1-RC2"
便利な常用Ruleの例(外部)
-
MissingFinals
- 書き換え内容
- finalが付いていない具象case classにfinalをつける
- leaking sealedを警告する
- よく見る(やる)、かつ後々修正が大変なのでこまめに潰したい
- 書き換え内容
ThisBuild / scalafixDependencies += "com.github.vovapolu" %% "scaluzzi" % "0.1.8"
Scala 2.12 -> 2.13
Scala 2.x -> Scala3 migration guide
- 単純なものはdotty -source 3.0-migrationでカバー
- 複雑なもの(non-trivial)は scala-rewrites でホスト予定 (6/16現在なし)
library
マイグレーション用のRuleの例
マイグレーション用のRule
-
基本的に、一度使えば終わり
-
build.sbtに追記せず使うことが多い
publishされているartifactを利用
- repositoryにpublishされているもの
- local repositoryにpublishLocalされているもの
sbt:myProject> scalafix dependency:RULE@GROUP:ARTIFACT:VERSION
sbt:myProject> scalafix github:ORG/REPO/FILENAME?sha=BRANCH
マイグレーション用のRule
scala-steward repositoryに登録されているmigration用のrulesは、自動でバージョンアップとともにRewriteするPullRequestが飛んでくる(!!)
参考: scala-steward Scalafix Migrations
libraryのupdateには対応しているが、scalaVersionのupdateには対応していない
- https://github.com/fthomas/scala-steward/issues/145
- https://github.com/fthomas/scala-steward/issues/589
- https://twitter.com/fst9000/status/1141443347741061120
マイグレーション用のRule
Scalafix 書き換えの仕組み
Scalafixの仕組みを学ぶ
Rule と Patch
Rule
- 書き換えおよびLintの「ルール」
- .scalafix.conf に記述、もしくはコマンドライン引数に渡す
- Ruleごとに以下のものがある
- 利用に必要な追加のbuild設定
- カスタマイズ用の設定
Rule
Ruleは大別して2種
- SyntacticRule
- SemanticRule
のどちらかを継承する
abstract class SyntacticRule(name: RuleName) extends Rule(name) {
def fix(implicit doc: SyntacticDocument): Patch = Patch.empty
}
abstract class SemanticRule(name: RuleName) extends Rule(name) {
def fix(implicit doc: SemanticDocument): Patch = Patch.empty
}
Rule
それぞれのfixメソッドにおいて
- ソースコードの情報(Syntactic or SemanticDocument)を受け取り
- どこをどのように書き換える/Lintする(Patch)のか決めて返す
abstract class SyntacticRule(name: RuleName) extends Rule(name) {
def fix(implicit doc: SyntacticDocument): Patch = Patch.empty
}
abstract class SemanticRule(name: RuleName) extends Rule(name) {
def fix(implicit doc: SemanticDocument): Patch = Patch.empty
}
Ruleの種類
SyntacticRule (構文的ルール)
- ソースコードを、構文解析(だけ)で書き換えるRule
- ソースの構文さえ合っていれば、compileできなくてもlintやrewriteができる
- SyntacticRuleの例
- DisableSyntax
- LeakingImplicitClassVal
- NoValInForComprehension
- ProcedureSyntax
SyntacticRule (構文的ルール)
構文解析とは、文法に従って、文字列に対して行う次のような操作のこと
- 文字のまとまり(字句/token)ごとに分割(Tokenize)する
- このtokenは、ソースコード上の位置を覚えている
- 字句の列(tokens)から、木構造(抽象構文木 / Abstract Syntax Tree)を作る
SyntacticRule (構文的ルール)
構文解析とは、文法に従って、文字列に対して行う次のような操作のこと
-
文字のまとまり(字句/token)ごとに分割(Tokenize)する
- このtokenは、ソースコード上の位置を覚えている
- 字句の列(tokens)から、木構造(抽象構文木 / Abstract Syntax Tree)を作る
scala> import scala.meta._
scala> val stat = """def run(){println("run")}""".parse[Stat].get
stat: scala.meta.Stat = def run(){println("run")}
scala> val tokens = stat.tokens
tokens: scala.meta.tokens.Tokens = Tokens(, def, , run, (, ), {, println, (, "run", ), }, )
SyntacticRule (構文的ルール)
構文解析とは、文法に従って、文字列に対して行う次のような操作のこと
- 文字のまとまり(字句/token)ごとに分割(Tokenize)する
- このtokenは、ソースコード上の位置を覚えている
- 字句の列(tokens)から、木構造(抽象構文木 / Abstract Syntax Tree)を作る
scala> val astString = stat.structure
astString: String = Defn.Def(Nil, Term.Name("run"), Nil, List(List()), Some(Type.Name("Unit")), Term.Block(List(Term.Apply(Term.Name("println"), List(Lit.String("run"))))))
SyntacticRule (構文的ルール)
木構造(抽象構文木 / Abstract Syntax Tree)を作る
Defn.Def( //メソッド定義
Nil, //修飾子
Term.Name("run"), //メソッド名
Nil, //型パラメータ
List(List()), //引数リスト
Some(Type.Name("Unit")), //戻り値の型
Term.Block( //メソッドボディ
List( //文のリスト
Term.Apply(
Term.Name("println"), //printlnという項に対し
List(Lit.String("run")) //"run" というStringリテラルを適用している
)
)
)
)
SyntacticRule (構文的ルール)
戻り値(`decltpe`)の字句(token)を調べると.....
戻り値の型宣言があり、かつ型宣言のtokenは空、という扱い
scala> val returnTypeTokens = stat.asInstanceOf[Defn.Def].decltpe.map(_.tokens)
returnTypeTokens: Option[scala.meta.tokens.Tokens] = Some()
余談: 細かい仕様を覚える必要はないですが......
- `def run(): Unit` だと、`decltpe.map(_.tokens)`は `Some(Unit)`
- `def run() = {}` だと `decltpe.map(_.tokens)`は `None`
すなわち、ある文が「ProcedureSyntaxを使ったメソッド定義」であることは、構文だけ見れば確定できる
Syntactic APIではできないこと
今回のケースでいえば、以下のことは分からない。
- `Unit` 型が、 `scala.Unit` を参照していること
- `println("run")` が、`scala.Predef.println(x: Any): Unit`のメソッド呼び出しであること
- メソッドボディのブロックが、どの型に推論されるかということ※
たとえば以下のメソッドの戻り値を補うRewriteは、Syntactic APIだけでは実現不可能
def run() = println("run")
よって戻り値の型を補うExplicitResultTypesは、SemanticRuleを継承して定義されています。
SemanticRule (意味論的ルール)
- 構文解析および後述のSemanticDBの両方を利用
- ものによっては、さらにCompilerのAPIを利用していることもある
- SemanticRuleの例
- RemoveUnused
- ExplicitResultTypes
- NoAutoTupling
- など
SemanticRule (意味論的ルール)
SemanticDB
- コンパイル時に生成される、意味論的な情報を保存したファイル
- 大雑把には以下の情報を含むバイナリ。(仕様)
- Language。現在はScalaとJavaをサポート。
- SymbolInformation。Symbolとそのメタデータ(型、修飾子、アノテーションなど)。
- SymbolOccurence。Symbolの参照の解決結果。
- Diagnostic。コンパイラやLinterなどが生成した診断情報。
- Synthetic。コンパイラが生成したTree。(Scalaでいえば、for式の展開結果など)。
- Language Server Protocolに対応する意味論情報の共通フォーマットとして設計された
- Semantic database · Issue \#605 · scalameta/scalameta
SemanticRule (意味論的ルール)
SemanticDBの生成
- Scala 2.x
- semanticdb-scalacコンパイラプラグインによって生成される
- Scala 3.x
- dotcの `-Ysemanticdb` フラグによって生成される
SemanticDBの利用
- ScalametaのSemantic API
- Metals
- Scalafix SemanticType
など
Scalafixの導入の仕方
Scalafixの導入の仕方
ポイントは2つ
- 一部scalacOptionsとの共存
- semanticdbEnabledする範囲
1. 常に有効
//project/plugins.sbt
addSbtPlugin("ch.epfl.scala" % "sbt-scalafix" % "0.9.17")
//build.sbt
ThisBuild / semanticdbEnabled := true
ThisBuild / semanticdbVersion := scalafixSemanticdb.revision
1. 常に有効
// .sbtrc
// scalafixAll は次versionで追加予定なので、それまではこんな感じ
alias execScalafix=;scalafix;Test/scalafix
alias checkScalafix=;scalafix --check;Test/scalafix --check
// もし-Xfatal-warningsをどうしても使いたければ...
alias strictCompile=;set Compile/compile/scalacOptions += "-Xfatal-warnings";compile;session clear
alias strictRecompile=;set Compile/compile/scalacOptions += "-Xfatal-warnings";~compile
1. 常に有効
Good
- scalafixコマンド実行の所要時間が最小
- 各compileでsemanticDBが生成されているため
- compileの合間に、気軽にscalafixコマンドを叩ける
- 各compileでsemanticDBが生成されているため
Bad
- semanticdb生成用の設定が常に有効なため、compile時間が伸びるかも?
- プロジェクトのコードベースによる
- -Xfatal-warningsを利用するには、sbt sessionで一時的に有効にするしかない
- set ~ でsbtビルド定義の読み込みが走る分、余分に時間がかかる
- 他のscalacOptionsやlinterの設定も切り替えるとなると、管理が厳しい
2. 専用のサブプロジェクトで実行
// project/plugins.sbt
addSbtPlugin("ch.epfl.scala" % "sbt-scalafix" % "0.9.17")
// project/ScalafixSettings
object ScalafixSettings {
private val unused = "-Ywarn-unused"
lazy val permanent: Seq[Setting[_]] = Seq(
scalacOptions += unused,
scalafixDependencies in ThisBuild ++= Seq(
"com.github.vovapolu" %% "scaluzzi" % "0.1.8",
"com.github.liancheng" %% "organize-imports" % "0.3.1-RC2"
),
semanticdbEnabled := true,
semanticdbVersion := scalafixSemanticdb.revision
) ++ Seq(Compile, Test).map(_ / console / scalacOptions -= unused)
}
2. 専用のサブプロジェクトで実行
// build.sbt
lazy val myProject = (project in file("."))
.settings(/* ... */)
lazy val refactor = project
.shadow(myProject)
.modify(RemoveXFatalWarnings)
.settings(ScalafixSettings.permanent: _*)
.light
2. 専用のサブプロジェクトで実行(余談)
あるプロジェクトの設定を 微妙に 変えたコピーsub projectを簡単に作れるsbt-plugin。以下の欲求から作りました。
- ソース、リソース、jarはそのまま
- scalacOptionsを切り変えたい
- その他の設定 (scalafix用など)を追加したい
- dependsOnで依存しているプロジェクトにも、同じ設定を適用してコンパイルしたい
// project/plugins.sbt
addSbtPlugin("com.taisukeoe" % "sbt-shadowyproject" % "0.1.0")
2. 専用のサブプロジェクトで実行
Good
- semanticdb-scalac pluginのoverheadなしでcompile実行可能
- sbt session書き換えなしで`-Xfatal-warnings` scalacOptionsも利用可能
- -Ywarn-unused など、Scalafix実行時のみ欲しいものをメインから除外可能
Bad
- scalafixタスク実行前に、clean compileがしばしば必要
- sbt-shadowyproject pluginがまだこなれてない(と思う)
どちらを選ぶべき?
1. 常に有効 が、一番落とし穴がないのでオススメ
-Xfatal-warningsを使いたい、ある程度凝ったsbt設定の使い分けをしたい、メインのコンパイル時間に影響が出た場合などは、2.でやってます。
(余談) scalafixEnable sbtコマンドで一時的にscalafixを有効にする方法もある
設定切り替えの所要時間や面倒臭さで、scalafixを使う頻度が結局下がりやすいので、scalafixを日々の開発に積極的に取り組む際はオススメしません。
Scalafixの使い方Tips
その他のScalafix TIPS
- scalafixの後に必ずscalafmtをかける
- rewriteでフォーマットが崩れる可能性がある
-
各scalaVersionの、最新minor versionを使う
- 2.13.2
- 2.12.11
- 2.11.12
- (特に初めてのRuleで)rewriteする前には:
- scalafix --checkして差分を確認してから、scalafixする
- WIPでもcommitしておくなど、rewrite前にすぐ戻れるようにしておく
異色Scalafixプロジェクト
- simulacrumのscalafixポート
- macroアノテーションでtypeclassのコード生成をするライブラリだった
- Scala 3でmacroアノテーションが使えなくなることを受けての実験
Scalafixまとめ
- 日々の開発を少し楽にするリファクタリングツール
- 警告を1コマンドでただちに直せる
- マイグレーション用のruleで非互換の変更を反映する
- 新旧の対応を一意に決定できる場合はカバーできる
- こなれてない部分は今後に期待
- ポンテンシャルあるツールなので活用していきましょう
Scalafixを使いこなして楽をしよう
By Taisuke Oe
Scalafixを使いこなして楽をしよう
- 1,473