wartremoverでScalaコードを整える

Scalaの静的解析ツールwartremoverを使ってコードを整えます。
2019.08.22

この記事は公開されてから1年以上経過しています。情報が古い可能性がありますので、ご注意ください。

はじめに

前回に引き続きScalaコードを整えていこうと思います。

wartremoverとは

wartremoverはScala用のlintツール(静的解析ツール)です。 公式サイトには以下のようにあります。

WartRemover takes the pain out of writing scala by removing some of the language’s nastier features.

Scalaの言語仕様には色々な機能が含まれているのでこうしたツールでチェックできるのは、あまりScalaに詳しくない自分としては助かります。

チェックの内容

wartremoverで行われる全てのチェックはこちらで説明されています。 下記でいくつか例を紹介します。

チェック例: ArrayEquals

下記の例のようにArrayどうしを == で比較するコードを検出します。 (Arrayの==での比較は内容ではなく参照が一致するかを判定するので意図した比較が行われない可能性があるため)

List(1) == List(1) //true
Array(1) == Array(1) //false, won't compile: == is disabled, use sameElements instead

チェック例: DefaultArguments

メソッドのデフォルト引数を検出します。 (デフォルト引数はメソッドを使うのを難しくするため)

// Won't compile: Function has default arguments
def x(y: Int = 0)

チェック例: Equals

型安全ではないAny== およびequals, eq, ne の使用を検出します。 ドキュメントにあるようにImplicit class 内に === を定義するかCatsのEqを使いましょう。

"1" == 1 //コンパイルできるがチェックは失敗

// === を定義する
@SuppressWarnings(Array("org.wartremover.warts.Equals"))
implicit final class AnyOps[A](self: A) {
   def ===(other: A): Boolean = self == other
}

//またはCatsのEqを使う
import cats.implicits._
"1"  === "1" //ok

wartremoverの導入

project/plugins.sbt に次の1行を追加します。

addSbtPlugin("org.wartremover" % "sbt-wartremover" % "2.4.2")

次にbuild.sbt にチェックの詳細を定義します。 どのチェックをワーニング、エラーとするかをwartremoverWarnings, wartremoverErrors に指定することで設定します。

すべてのチェックをエラーにする場合

wartremoverErrors ++= Warts.unsafe

すべてのチェックをワーニングにする場合

wartremoverWarnings ++= Warts.unsafe

一部のチェックをワーニングにする場合

wartremoverErrors ++= Warts.allBut(Wart.Any, Wart.Nothing, Wart.Serializable)
wartremoverWarnings += Wart.Nothing
wartremoverWarnings ++= Seq(Wart.Any, Wart.Serializable)

コード中で明示的にチェックを抑制する

実際に開発していると、原則的にはチェックしたい項目だが「このクラスのこのメソッドだけは見逃して・・・」という箇所が出てきます。 そんな時には下記のようにアノテーションを付与することでチェックを抑制できます。

下記の例では VarNull を抑制しています。

@SuppressWarnings(Array("org.wartremover.warts.Var", "org.wartremover.warts.Null"))
var foo = null

使ってみる

上記の設定をした上でsbtからcompile タスクを実行するとチェックが行われます。

以下のコードをコンパイルしてみます。

package object model {
  case class Name(name: String)
  case class Message(message: String)
}

次のようにwartremover:FinalCaseClass に違反していることが検出できました。

> sbt compile
[info] Loading settings for project global-plugins from idea.sbt ...
[info] Loading global plugins from /Users/sasaki.kazuhiro/.sbt/1.0/plugins
[info] Loading settings for project scalafmt-example-build from plugins.sbt ...
[info] Loading project definition from /Users/sasaki.kazuhiro/src/github.com/cm-kazup0n/scalafmt-example/project
[info] Loading settings for project scalafmt-example from build.sbt ...
[info] Set current project to scalafmt-example (in build file:/Users/sasaki.kazuhiro/src/github.com/cm-kazup0n/scalafmt-example/)
[info] Executing in batch mode. For better performance use sbt's shell
[info] Compiling 1 Scala source to /Users/sasaki.kazuhiro/src/github.com/cm-kazup0n/scalafmt-example/target/scala-2.12/classes ...
[error] /Users/sasaki.kazuhiro/src/github.com/cm-kazup0n/scalafmt-example/src/main/scala/model/package.scala:2:14: [wartremover:FinalCaseClass] case classes must be final
[error]   case class Name(name: String)
[error]              ^
[error] /Users/sasaki.kazuhiro/src/github.com/cm-kazup0n/scalafmt-example/src/main/scala/model/package.scala:3:14: [wartremover:FinalCaseClass] case classes must be final
[error]   case class Message(message: String)
[error]              ^
[error] two errors found
[error] (Compile / compileIncremental) Compilation failed
[error] Total time: 6 s, completed 2019/08/21 16:59:35

case classes must be final と怒られたので、次のように修正してみるとコンパイルが成功します。

package object model {
  final case class Name(name: String)
  final case class Message(message: String)
}
> sbt compile
[info] Loading settings for project global-plugins from idea.sbt ...
[info] Loading global plugins from /Users/sasaki.kazuhiro/.sbt/1.0/plugins
[info] Loading settings for project scalafmt-example-build from plugins.sbt ...
[info] Loading project definition from /Users/sasaki.kazuhiro/src/github.com/cm-kazup0n/scalafmt-example/project
[info] Loading settings for project scalafmt-example from build.sbt ...
[info] Set current project to scalafmt-example (in build file:/Users/sasaki.kazuhiro/src/github.com/cm-kazup0n/scalafmt-example/)
[info] Executing in batch mode. For better performance use sbt's shell
[success] Total time: 1 s, completed 2019/08/21 17:04:26

CircleCI上でチェックする

ここまで設定しているなら追加の設定は必要ありません。sbt compileが実行された時にチェックが行われます。

まとめ

今回も整いました。

おまけ wartremover-contrib

ビルトインのチェックに加えてwartremover-contrib で提供されているチェックを使用することもできます。