entry-header-eye-catch.html
entry-title-container.html

entry-header-author-info.html
Article by

Scala Linter (Scalafix) 入門

はじめましての方ははじめまして! Scala エンジニアの Javakky です。

本日は Scala Advent Calendar 2022 5日目ということで、僕が普段利用している Linter である、 Scalafix についてお話ししていこうと思います。

qiita.com

Linter とは?

Linter とは、コードチェックや解析をしてくれるツールのことです。

コンパイラは言語仕様に則った実行可能な形式になっているかをチェックしてくれますが、 Linter ではより詳細に、例えば、コーディング規約などに反していないかも確認してくれます。 逆に、字句レベルでの規約 (インデントなど) をチェックしたい場合にはフォーマッタを利用するといいでしょう。

Scalafix とは?

改めて、 Scalafix とは、 Scala の Linter として有名なツールの一つです。

一つ、と書いたように、 Scala には WartRemoverScalaStyle など様々な Linter が存在します。

scalacenter.github.io

では、なぜ僕のプロジェクトでは Scalafix を優先して採用しているのでしょうか?それは、 Scalafix が自動書き換えを行なってくれるからです!

通常 Linter に読ませるような規約は複雑なことが多いため、警告のみしか提供していないものが多いです。しかし、 Scalafix には警告 / 自動書き換えの両方からルール実装者が選択できるようになっているため、多くのルールで書き換えの手間を必要とせずに対応を行うことができます。

Scalafix の使い方

あなたが sbt を利用してプロジェクトを管理しているなら、 Scalafix の導入は非常に簡単です。 project/plugins.sbt に一行依存関係を追加するだけです。

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

この状態で以下コマンドを実行することで、 Scalafix は動き出します。

sbt scalafixEnable scalafixAll

CI などで利用する場合、 sbt scalafix --check を利用すれば、すべてのルール違反を警告のみに留めてくれます。また、 詳細な説明 は割愛しますが、シェルセッション内で scalafix を使えるようにするためのコマンドが scalafixEnable なので、2回目からは省略することができます。

もし、あなたが SemanticDB を利用するような複雑なルールを使おうとしている場合、コンパイラプラグインを有効化する必要があります。 build.sbt に以下のような設定を加えてください。

inThisBuild(
   List(
    scalaVersion := "2.12.17", // 既に書かれているはず
    semanticdbEnabled := true,
    semanticdbVersion := scalafixSemanticdb.revision // Scala 2.x 系なら必要
   )
 )

また、 The Scala compiler option "-〇〇" is required to ****というエラーが返される場合、該当するコンパイラオプションをプロジェクトに適用する必要があります。 (build.sbt)

lazy val myproject = project.settings(
   scalacOptions += "-Ywarn-unused-import" // コンパイラオプションを追加
 )

ここまで設定して、さあ、実行しよう!と思ったはいいのですが、まだ解析ルールが設定されていませんでした。プロジェクトルートに .scalafix.conf を作成し、以下のように記述すると、 DisableSyntax ルールが追加されます。

rules = [
  DisableSyntax
]

scalacenter.github.io

これによって、 Scalafix の組み込みルールを利用することができるようになります。

コミュニティのルール

組み込みルールを導入し、あなたのプロジェクトには平穏が訪れました。しかし、なんだか少し物足りない気持ちがあります。

Scalafix の組み込みルールは確かに便利ですが、他の Linter と比べるとルールの種類がイマイチ多くありません。

しかし大丈夫です! Scalafix ではコミュニティの開発者がそれぞれルールを作成・公開・利用できる機能があります。例えば、 build.sbt に以下のように依存関係を追加することで scalafix-pixiv-rule を利用することができます。

ThisBuild / scalafixDependencies += "net.pixiv" %% "scalafix-pixiv-rule" % "2.4.0"

Scalafix 公式ページでもいくつかルールセットが紹介されているので、ぜひ使ってみてください!

scalacenter.github.io

  • ついでに僕が普段利用しているルール設定も記載しておきます。
rules = [
    MockitoThenToDo
    SingleConditionMatch
    RemoveUnused
    NoAutoTupling
    NoValInForComprehension
    ProcedureSyntax
    fix.scala213.FinalObject
    fix.scala213.Any2StringAdd
    fix.scala213.Varargs
    fix.scala213.ExplicitNullaryEtaExpansion
    ExplicitResultTypes
    ZeroIndexToHead
    CheckIsEmpty
    UnnecessarySemicolon
    NonCaseException
    UnifyEmptyList
    OrganizeImports
]
    
RemoveUnused {
    imports = true
    privates = false
    locals = true
    patternvars = true
    params = false
}
    
OrganizeImports {
    coalesceToWildcardImportThreshold = 6
    groupedImports = Merge
    groups = [
        "re:javax?\\."
        "scala.",
        "*",
        "com.sun."
    ]
}

scalafix-pixiv-rule

ここまで読んでくださったそこのあなた!少しだけ宣伝をさせてください。

先述した Scalafix コミュニティルールの一つとして、僕たちが提供している scalafix-pixiv-rule があります。

github.com

提供しているルールをいくつか抜粋して紹介します。他にもいくつかルールを提供しているので、よければ GitHub Repository の README も確認してみてください。

UnnecessarySemicolon

不要な行末セミコロンを削除します。

val x = 3; // rewrite to: `val x = 3`
val a = 1; val b = 2

ZeroIndexToHead

Seq のインデックスによる最初の要素へのアクセスを head 呼び出しに置換します。

Seq(1, 2, 3)(0) // rewrite to: `Seq(1, 2, 3).head

CheckIsEmpty

Option や Seq の空チェックで isEmptynonEmptyisDefined を利用するように置き換えます。

Some(1) == None // rewrite to: Some(1).isEmpty
Some(1).nonEmpty // if `CheckIsEmpty.alignIsDefined = true` then rewrite to Some(1).isDefined

UnifyEmptyList

型変数指定のない List() や List.empty を Nil に置き換えます。 これは、 Nil が List[Nothing] として定義されているためです。 また、型変数指定のある List[Any]() は List.empty[Any] へと置換されます。

val empty = List.empty // rewrite to: val empty = Nil
val list = List[String]() // rewrite to: val list = List.empty[String]

SingleConditionMatch

単一の case しか持たないパターンマッチを分解します。

また、 Some.unapply のみが利用されているパターンを foreach 呼び出しに置換します。

Some(1) match {
  case result => println(result)
}
// rewrite to: println(Some(1))

おわりに

今回は、 Scalafix とはどんなツールか?どうやって使うのか?を紹介してきました。

こちら の記事では Scalafix ルールを自作する方法についても解説していますので、「こんな規約がほしいけど、誰も公開していないんだよな〜」という悩みを抱えている方は、ぜひ挑戦してみてください!

inside.pixiv.blog

icon
javakky
決済周りの改善を中心に働いている2021年入社エンジニア。その名の通りJavaが好きなことで有名(?)で、最近はScalaを使える部署へ入ったらしい。