Scalafixを

使いこなして楽しよう

2020.6.16 Taisuke Oe

あなた誰?

Taisuke Oe

 

  • フリーランスエンジニア
  • ScalaMatsuri座長
  • (株)セプテーニ・オリジナル技術アドバイザー

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など
      • コメントにはマッチしない
    • 正規表現による検出も可能
  • procedure syntaxの戻り値を明記するようrewriteするRule
  • `scalafix --check` だと、期待される書き換えをエラーメッセージとして表示する
  • 使われていないimport、private変数、ローカル変数を削除するようrewriteするrule
  • scalacOptionsに以下の制約がある
    • `-Ywarn-unused` や、`-Wunused` (2.13以上)を使う
      • 使われていないシンボルの検出に、エラーメッセージを利用している
    • `-Xfatal-warnings` を使わない。
      • scalafixの実行前にコンパイルする必要があるため、警告有りでもコンパイルできる必要がある
[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に頼らなくていい
    • 新規で導入するときも、警告の修正で圧倒的に楽ができる

Scalafixのメリット

 

  • 設計上、Scala 3もサポート可能
    • dottyは既にSemanticDBを生成可能。
    • あとはScala 3のソースコードを構文解析できれば……
  • 極一部の、コンパイラ内部に密結合しているRuleは対応が大変?

Scalafixが苦手なところ

  • (単純に比較はできないが)実行に時間がかかる
    • コンパイル時間のoverheadがある可能性もある
  • (その他のLinterに比べて) Ruleは少なめ
  • 一部のscalac optionsへの依存や、共存が難しい問題がある
  • Ruleごとに設定フォーマットが違う
    • 大量のRuleを入れると、設定ファイルが爆発しそう?

個人的なScalafixの使い方

  • scalac optionやlinterによる警告を、完全に代替しない
    • 修正方法を決定できないものを、Scalafixで警告するメリットはあまりない
      • lintのみのScalafix ruleがほとんど無い理由
    • コンパイラとは別途、構文解析している分、時間がかかる
      • コンパイル毎に実行させるのは厳しい
  • scalacOptionsやlinterと併用するのが便利
    • Scalafixで修正可能なものは、可能な限りScalafixに任せる
    • 修正が難しいもの、コンパイル毎に警告したいものはscalac optionやlinterを使う

便利な常用Ruleの例

  • RemoveUnused
    • 前述
  • ExplicitResultTypes
    • public、もしくはprotectedなメンバに型注釈をつける
      • Literalなど自明な場合は型注釈をつけない
    • 現在の実装はScala compilerに依存しており、2.12.11でしか動かない

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から一部取り込み)
    • もちろん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


library

マイグレーション用のRuleの例

マイグレーション用のRule

  • 基本的に、一度使えば終わり

  • build.sbtに追記せず使うことが多い

publishされているartifactを利用

  • repositoryにpublishされているもの
  • local repositoryにpublishLocalされているもの
sbt:myProject> scalafix dependency:RULE@GROUP:ARTIFACT:VERSION

Ruleのソースコードを利用

  • ローカルFile
  • GitHub
  • Http

それぞれで、Ruleのソースファイルを指定して利用可能

詳細: Run the Rule from source code

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の利用

など

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コマンドを叩ける

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. 専用のサブプロジェクトで実行(余談)

taisukeoe/sbt-ShadowyProject

 

あるプロジェクトの設定を 微妙に 変えたコピー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

  •  simulacrumのscalafixポート
    • macroアノテーションでtypeclassのコード生成をするライブラリだった
    •  Scala 3でmacroアノテーションが使えなくなることを受けての実験

Scalafixまとめ

  • 日々の開発を少し楽にするリファクタリングツール
    • 警告を1コマンドでただちに直せる
  • マイグレーション用のruleで非互換の変更を反映する
    • 新旧の対応を一意に決定できる場合はカバーできる
  • こなれてない部分は今後に期待
    • ポンテンシャルあるツールなので活用していきましょう

Scalafixを使いこなして楽をしよう

By Taisuke Oe

Scalafixを使いこなして楽をしよう

  • 1,267