この記事は CAMPHOR- Advent Calendar 2016 7日目の記事です。
WartRemover は Scala のASTレベルの静的解析ツールで、WartRemover に組み込まれているパターンに加えて、自分で定義したパターンをビルド時に検出することができます。
これを使えばscalacはエラーや警告を出さないけど検出してほしいコーディング規約などをビルド時に検出することができるようになって便利。
もとから組み込まれてるパターンはGitHubのREADMEに詳しく書かれています。
使ってみる
project/plugins.sbt に以下を追記
addSbtPlugin("org.wartremover" % "sbt-wartremover" % wartremoverVersion)
build.sbt に以下を追記
wartremoverErrors ++= Warts.all
これだけでビルド時にビルド対象のscalaプログラムを見て、すべてのパターンにマッチするASTを探索し、マッチしたパターンをエラーとして出力してくれるようになります、簡単ですね。
上の例だとすべてのプログラムに対して、すべてのパターンを検出しますが
- 特定ファイルは検出対象から外す
- 一部のパターンのみ/一部のパターンを除いてすべてのパターンを検出
- あるパターンはエラー、あるパターンは警告を出すようにする
などのことが build.sbt に記述するだけでシュッとできるようになります。
詳しい設定方法はREADMEを(ry
カスタム Wart を作ってみる
ここ を見ると wart rule を自分で追加できるようです。
今回は HOGE_HOGE みたいな全部大文字とアンスコのみの変数名を見つけてみます。
Scala の定数は 公式のscala code style で UpperCamel だよって言われているのですが、僕はよくミスって LIMIT みたいな定数名を作ってしまうし、scalac も scalastyle も warning を出さないからです。(もしかして僕の設定が足りてないだけ?)(まあそこは本題じゃないし)
完成したものはこちら
WartRemoverはscalaのASTとのマッチングによって検出を行うので、まずは val HOGE = 1 みたいなプログラムがどういう AST になるのか確認しましょう。
scala> import scala.reflect.runtime.universe import scala.reflect.runtime.universe scala> universe.showRaw(universe.reify { val HOGE = 1 }.tree) res1: String = Block( List(ValDef(Modifiers(), TermName("HOGE"), TypeTree(), Literal(Constant(1)))), Literal(Constant(())))
なんとなく ValDef(Modifiers(), TermName(), TypeTree(), Litenarl()) にマッチさせてやれば良いことがわかりますね。
(Modifiers には final private みたいな修飾子 が入ります。TypeTree はよく分からない...)
ちなみにScalaのリフレクションを使った構文木へのアクセスは公式ドキュメントが詳しい
概要
よっしゃ、それでは ValDef(Modifiers(), TermName(), TypeTree(), Litenarl()) のうち TermName が全部大文字かアンスコだけな構文木にマッチしてくれるwartを書いていきましょう!
ディレクトリ構成はこんな感じ、LargeValueName.scala は wartルールを記述するプログラム、wartsは別プロジェクトとして管理しています。
.
├── build.sbt
├── project
│ ├── build.properties
│ └── plugins.sbt
├── src/main/scala/Main.scala
└── warts
└── src/main/scala/LargeValueName.scala
LargeValueName.scala
自分で wart を作るときはだいたい以下のような必要があります。
- wart object は
WartTraverserを継承 def apply(u: WartUniverse): u.Traverserのみをメソッドとして持ちWartUniverseはreflect.api.Universeを内部にもつ
applyが返すTraverserはtraverse(tree: Tree): Unitを override- 引数として与えられたASTと、検出したいパターンとのマッチングさせる
package warts import org.wartremover.{WartTraverser, WartUniverse} object LargeValueName extends WartTraverser { def apply(u: WartUniverse): u.Traverser = { import u.universe._ new Traverser { override def traverse(tree: Tree) { tree match { case t @ ValDef(_, TermName(s), _, _) if s.trim.matches("[A-Z_]+") => u.error(tree.pos, s"Value name $s should be upper/lower camel case") // 他のwartもチェックするかもしれないので super.traverse を呼ぶ super.traverse(tree) case _ => super.traverse(tree) } } } } }
build.sbt
それじゃあこれを build.sbt に登録してみます、今回は LargeValueName.scala は sbt のマルチプロジェクトを使ってメインプロジェクトとは別で管理します。
wartremoverClasspathsにカスタム wart の classpath を追加wartremoverWarnings += Wart.custom("your.own.wart")
build.sbt はこんな感じ、
val wartremoverVersion = "1.2.1" val customWartProjectName = "MyWarts" lazy val commonSettings = Seq( scalaVersion := "2.11.8" ) lazy val root = (project in file(".")). settings(commonSettings: _*). dependsOn(warts). settings( name := "wartremoverPlayground", version := "1.0", // warts project の jar ファイルが欲しい // wartremoverClasspaths += // "file://" + baseDirectory.value + "/warts/target/scala-2.11/mywarts_2.11-1.0.jar", wartremoverClasspaths ++= { // lazy val warts = ... settings( exportJars := true) と // dependsOn(warts) により dependencyClasspath in Compile に上記のjarが追加されてるはず (dependencyClasspath in Compile).value.files .find(_.name.contains(customWartProjectName.toLowerCase())) .map(_.toURI.toString) .toList }, wartremoverWarnings += Wart.custom("warts.LargeValueName") ) lazy val warts = (project in file("warts")). settings(commonSettings: _*). settings( name := customWartProjectName, version := "1.0", exportJars := true, libraryDependencies ++= Seq( "org.wartremover" %% "wartremover" % wartremoverVersion ) )
warts project に移動して jar ファイルをいちいち生成して、頑張ってjarへのpathを渡しても動くのですが大変、自動化したすぎます。
wartremoverClasspaths += "file://" + baseDirectory.value + "/warts/target/scala-2.11/mywarts_2.11-1.0.jar"
この部分で、wartsプロジェクトの生成したjarファイルを見つけています。
wartremoverClasspaths ++= {
(dependencyClasspath in Compile).value.files
.find(_.name.contains(customWartProjectName.toLowerCase()))
.map(_.toURI.toString)
.toList
}
動かしてみる
Main.scala
package playground object Main { val LIMIT = 1 }
これで sbt compile すると
[warn] /path/to/Main.scala:4: Value name LIMIT should be upper/lower camel case [warn] val LIMIT = 1
おお、警告出してくれた!
これを使って、簡単にパターンとして記述できるコーディング規約などは静的解析で検出してくれると良いですね!
明日は id:ryota-ka の「 Vim script でジェネレータを作ったり、遅延評価してみる」です。
参考
- GitHub - wartremover/wartremover: Flexible Scala code linting tool
- GitHub - danielnixon/extrawarts: Extra WartRemover warts.
- http://docs.scala-lang.org/ja/overviews/reflection/overview
- ScalaのReflectionについて、まとめてみる - 導入編 - CLOVER
- A deep dive into scalac - ScalaMatsuri 2016 by Chris Birchall
- Naming Conventions | Scala Documentation