wartremover-contribをどうやって依存させるかという問題

以前こういうのを書きましたが

xuwei-k.hatenablog.com

wartremover、ここ2年くらい、(権限ない人からpull reqやissueはたまにくるが)実質権限ある人は自分くらいしか活動してないです。

表題の件ですが、一部結論から言うと、つい最近wartremoverやwartremover-contribに対して、とある改善をしたのですが、その改善方法が最善だったのか自信がないというか、選択肢がいくつかありえる気がするので、何をやったのか?の説明を書こうとしています。

まず前提などを簡単に説明すると

  • wartremoverやwartremover-contribは、Scalaのcompiler pluginの仕組みを使って、独自のルール(wart)を書いて、特定のコードに対してcompile時にwarning出したりerrorにするものである
  • wartremoverは、wartremover本体とwartremover-contribという2種類がある
  • 最初はwartremover本体だけだったが「wartremover本体に入れるほどではないけど、どんどんマイナーな(つまり多くの人が有効にするにはかなり議論の余地がある?)ものでも、気軽にルールを追加できるものあったほうがいいよね」みたいな話により、wartremover-contribという別リポジトリが出来た*1
  • wartremover-contribはwartremoverに依存している
  • wartremover自体は、内部的にはcompiler pluginとsbt-plugin
  • wartermover-contribにも同じような目的のsbt-pluginあり
  • sbt-pluginは必ずしも必要ないが、それがないとsbtのビルドの設定が面倒なため、それを補助するような目的のもの
  • wartremover-contribの本体のほうの立ち位置がややこしいというか、ここが今回の問題の発端なわけだが「wartermover-contribそのものは単純にはcompiler pluginではなく、wartremoverというcompiler pluginを拡張するlibrary」というもの


さて、問題の本質というか、今回言いたいことを一言で言ってみると

「compiler pluginを拡張するlibrary、というものの依存の追加の仕方のベストプラクティスが定まっていない」

ということです。 以前似たような状況でeed3si9nさんに相談して、こういう会話もありました。

事実、sbt 1.3.10の現在でも

といった状況です。

では今回の改善前までwartremover-contribがどのように依存していたか?というと、wartremover-contrib側のsbt-pluginで、以下のようになっていました

https://github.com/wartremover/wartremover-contrib/blob/f1e939848e2ab9ebde21346a0e8df22f5f2e4a34/sbt-plugin/src/main/scala/wartremover/contrib/ContribWarts.scala#L20-L26

libraryDependencies += "org.wartremover" %% "wartremover-contrib" % ContribWart.ContribVersion$ % Provided,
wartremoverClasspaths ++= {
  (dependencyClasspath in Compile).value.files
    .find(_.name.contains("wartremover-contrib"))
    .map(_.toURI.toString)
    .toList
}
  • libraryDependenciesにProvidedで依存追加
  • wartremoverClasspathsというStringで指定するKey(wartremover本体側のsbt pluginのkey)には、dependencyClasspathというところから、若干無理やり名前で引っ張ってきて追加

という方法です。 このコードは自分が書いたわけではなく、おそらくwartremover-contribを作った当初からのコードのようです https://github.com/wartremover/wartremover-contrib/commit/60d87120aa76226b05ae539cb4dbfac20916c23a

さて「ibraryDependenciesにProvidedで依存追加」というのは、よく考えると問題がありますね?つまり

  • Providedなので、必要ない依存がcompile時に入る
  • 具体的には
    • wartremover-contrib
    • wartremover-contirbが依存するwartremover
    • wartremoverが依存するscala-compiler
    • scala-compilerが依存するscala-reflect
  • それらはcompile時には意味なく参照出来てしまうが、実際間違って参照したら(他で追加されてない限り)実行時にClassNotFoundExceptionなどが発生してしまう危険がある

などです。 さて、これの解決策を考えた結果、結論から言うと以下の変更をしました

簡単に説明すると

  • wartremover側には、新たに wartremoverDependencies: Seq[ModuleID] というKeyを追加
  • そのKeyの値から、独自に手動でjarをdownloadして、そのpathをwartremoverClasspathsという従来からのkeyに渡すようにする
  • wartremover-contrib側はProvidedなlibraryDependenciesに追加するのをやめて、wartremoverDependenciesというkeyに追加だけにする
  • jarをdownloadする仕組みなどはwartremover-contrib側にだけあればいいか?と一瞬思ったが、同じように独自wartを書いてライブラリとしてpublishした場合は共通で必要になるはずなので、contrib側ではなくwartremover本体側に追加
  • これにより、上記のProvidedなことで余計な依存が入るデメリットはひとまず解決したはず

さて、今回の修正で一応目的は達成できているわけですが、悩んだ点というか、納得してない点というか、他に考えたことを説明すると

  • そもそも、マイナーだが全くありえないシチュエーションではない(?)ので、独自にdownloadしなくて済むようにしたい(独自にdownloadも、数十行必要としている)
  • 言い換えると、依存libraryのdownloadの詳細は、本来sbt側に良い意味で隠蔽されているはずだが、直接書いているのが気持ち悪い
  • 独自にdownloadのコードも、とりあえず思いついたlightbend/mimaからコピペしたが、あまり意味理解せずコピペしたので、細かい部分がこれで良いのか微妙
  • (sbt内部の?独自に依存追加した普通の?)coursierでdownloadしようか?も悩んだが、sbt内部だとそもそもアクセス可能かわからんしsbt 1.2.xとの互換壊れるし、独自に追加もしたくなかったので、ivyのほうでdownloadした*2
  • extraProjectsという、自動生成projectをsbt plugin側で作成する仕組みがあり、それを利用すれば直接downloadする必要ないはずなので、それを利用しようかどうか迷ったが、そのためだけにextraProjects利用しても、extraProjectsで生成したprojectがsbt pluginのユーザー側に見える(隠す手段がない?)ので、微妙だと思ってやめた
  • 完全に関係ない新たなScopeというかConfigurationを作って、それに紐付けてダウンロード、という手法はあり得るか・・・?(未検証)

これで大体説明し終わったので、これ以上の結論はなにもないのですが、誰かもし何かいい案思いついたら教えてください。

compiler pluginとは違いますが、そういえばsbt-scalafixあたりはどうしてるんだろう? (普通にlibraryDependenciesに追加したら余計な依存がついてしまうという点に関しては同じ、という意味で) とおもって、かるくコード読んだら、(多少改変した?)coursierに依存して直接ダウンロードしているようですね。やはりそうなるか・・・

https://github.com/scalacenter/sbt-scalafix/blob/ae173f44cf4875e9b1d4a172d9ce7d76e2e7076e/project/Dependencies.scala#L9-L13

あと、前述のkind-projectorとmacro-paradiseの場合は、両方compiler pluginだから、結局両方addCompilerPluginで追加すればいいだけでしたが、今回の場合は(一部繰り返しの説明になりますが)

  • もう片方(wartremover-contrib)自体はcompiler pluginではない
  • いずれにせよ "-P:wartremover:cp: で指定する必要がある(kind-projectorのパターンとは違い、wartremover-contribは単にwartremoverを使うのではなく、wartremover-contribは wartremoverから認識されて使われるという状態になる 必要がある)

というのが、もっと面倒になっている要因ですかね。

また、この方法でやったときにlibaryDependenciesを適当なTaskなどのscopeに紐付けて再利用しようかと思ったけど、適当なTaskがwartremoverのsbt pluginになかったのと、あまり好きじゃないというかあまり完璧に理解してない(?)ので、新しいKeyを作ってしまいました。

あと、wartremoverやwartremover-contribに関しては

「CrossVersion.fullとCrossVersion.binaryどちらにするか問題」

というのがあり、そこも最近色々試行錯誤して変更を入れてみたのですが、それは別の話なので、もしまた別途説明を書く時間がとれたら書くかもしれません。

大抵の場合そこまで深刻ではないのですが、Scalaのcompiler pluginはこういう細かいことまで考えると難しいですね。

*1:ちゃんと歴史追ってないので詳細な議論は違うかもしれないが

*2:正確にはmimaのやつがivyだったのでそのまま使っただけ、というのもある