独自scalafixのruleを同じsbt project内部に置く際の設定方法

個人的に、すごい細かい使い捨て含めるとおそらくもう1000個くらいはrule書いたことあるので、おそらく現状では日本一scalafix rule書いていると思うのですが、 慣れるとそのくらい気軽に書けてすぐ役に立って便利なので、既存の他人が書いたruleを使うだけではなく、独自に書くことを強くすすめていきたいです。

しかし、それにあたって、sbtのproject構成が思ったより面倒なので、それの解説をします。

タイトルの通りあくまで「同じsbt project内部に置く」場合の話をします。 gitやsbtのprojectそのものを完全に分けてしまえば、もちろん考えることが減って楽になる点もありますが、分けることによるデメリットもあるので、個人的には(scalafixに限らないですが)わりとmonorepoというか、同じprojectで頑張ることを推奨したいです。

(OSSにはしないような社内共通ruleを複数repoで参照したいんだ〜、という需要があるなら話が別ですが)

前提としてややこしいのが

  • これを書いてる2024年6月のscalafix version 0.12.1時点で、scalafixのruleは必ずScala 2.12か2.13で書く必要がある
    • もちろん将来的にはScala 3対応されるはず(詳細時期未定?)
  • しかし、ruleを書くときのversionと、それを使って解析や書き換えする対象のscalaコードのversionは異なっていて良い
    • 余談ですが、同じlinterでもwartremoverは必ずscala versionが一致している必要があるため、そこは明らかにscalafixとwartremoverで異なる箇所です

表に示すとこう

2.10以前 2.11 2.12 2.13 3
2.11以前
2.12 🔺
2.13 🔺
3

改めて表の説明

  • 2.12で書くか、2.13で書くか?に関係なく、解析や書き換えする対象のコードに対しての対応versionは全く同じです
    • 自分が知らないだけで、実はすごく細かい違いがある場合は教えてください
  • 繰り返しになりますが、2.12か2.13以外のversionでruleを書くのは今は不可能です
    • すごく無理やり頑張れば3で書くのは不可能ではない可能性がありますが、全くおすすめできません(自分もやったことない)
    • 結構古いscalafixを使えば、Scala 2.11でruleを書くことは出来ますが、今更古いscalafix使うのは色々罠がありそうで厳しいし、2.11で書く必要がある状況ならscalafix書く前にまずscalaのversion上げましょう
  • Scala 2.10以前が🔺なのは、semanticdbがpublishされていないので、SemanticRuleを書くのがおそらく不可能です
  • Scala 2.10以前でも、parseさえ可能なら、実質2.8や2.9の太古のversionだろうとも、原理上はSyntacticRuleなら適用可能です。よって🔺としています
    • そもそもSyntacticRuleはScala compilerに直接一切かかわらず、独自にソースコードを直接parseするものなので

さて、ここまである程度前置きで、ここからが本題なのですが、皆さんは(scalafix導入したいprojectにおいて)どのscala versionで書いていますか? それによって同じproject内でscalafixを導入する際に、sbtのproject構成が大きく変わります。 さらに言い方を変えるというか具体的な話としては、特定のversionの組み合わせの場合は、以下のsbt plugin使うのが実質必須というか、個人的には強く推奨することになります

github.com

sbt-projectmatrixを使わなくても済む場合の例 (使わなくて済むが、とはいえ、どうせなら使った方がいい場合もある)

  • Scala 2.12あるいは2.13の範囲でcross build、あるいはそれのどちらか単体でbuild
    • 言い換えると、Scala 2.11以前や、Scala 3ではビルドしてない(しばらくする予定がない)
    • ruleも同じversionで書けばいいだけなので
    • この場合は無理にsbt-projectmatrix導入すぐしなくてもいいかなぁ
    • ただし、割とすぐScala 3でのcross buildか移行する予定あるなら最初からsbt-projectmatrix導入しておいた方がいいかも
  • Scala 3あるいはScala 2.11だが、一切複数のscala versionでcross buildする予定がない
    • ただし、結構ややこしいので、この場合でもsbt-projectmatrix導入は割と推奨ではある
  • Scala 2.13と3でcross buildしてるが、Scala 2.13の場合だけscalafix実行できればいいや、という場合
    • 絶妙な状態ではあるが、まぁそれでいいなら、ひとまずsbt-projectmatrix回避可能だが、それでもScala 3の場合にscalafix関連を無理やり無効化する特別な設定が必要だから、結局sbtの知識必要
    • 自分が関わっているある程度のprojectでこれをやってる場合があるが、最初からsbt-projectmatrix導入すればよかったか?今からでも導入するか?と悩んでる

sbt-projectmatrixがほぼ必須な場合の例

  • Scala 2.13と3でcross buildしていて(あるいはそれに追加で2.12もあって)、なおかつ全てのscala versionで必ずしっかりscalafix実行したい
  • Scala 2.12と2.13でcross buildしているだけだが、ruleは2.13のみで記述して、2.12では記述したくない

そもそもsbt-projectmatrixの説明をしていませんが、その前に現在のsbt 1.xの標準のcross buildの方法から簡単に振り返りましょう。

sbt 1.xというかそれ以前の0.x時代からですが、 ++ 2.13.14 という ++ に続けてScala versionを入力すると、そのproject全体のscala versionを切り替える機能があります。

場合によっては

「簡単に切り替えれて便利〜〜〜」

と思うかもしれませんが、これは状態を変えることになり、単純には複数のscala versionのbuildを並列実行できない、必要ない場所まで変わったりする、などのデメリットがあり、特定の用途でかなり不便です。 例えば、sbt pluginは2.12でのみbuildしたいというかする必要があるけれど、他の部分は2.12、2.13、3全部でcross buildしたい、といった場合ですね。

独自のrootを作ってそこに特定のprojectだけaggregateして〜、などを例えば現状のscalikejdbcなどでは実施していますが、

https://github.com/scalikejdbc/scalikejdbc/blob/7cefbe48fc119ad113940668817909314652eda3/build.sbt#L129-L140

結局不便なことも多く、 そういう面倒な場面に遭遇したら、ある意味ではさっさとsbt-projectmatrixを導入した方が結果的には楽になるかもしれません。

sbt標準の ++ には、その他細かい機能というか言及するべきことがあるのですが、自分の経験上、どう頑張っても不便なときは不便なので ++ などのsbt標準の機能だけで無理やり?頑張るならば、さっさとsbt-projectmatrixを導入した方がいいと思います。

sbt-projectmatrixは、状態を持たせることをやめて、半自動でscalaのversionごとにsub projectを大量に作成します。 すると

「このprojectは2.12と2.13、このprojectは2.12だけ、このprojectは2.12と2.13と3、このprojectは3だけ」

といった、cross buildするscala versionが、sub projectごとにどんなに複雑になっていても、 とにかく、自動生成される全てをaggregateするrootでcompileやtestすれば、全部一気にbuildその他が可能なので、色々と楽になります。

このplugin機能相当がsbt 2.xからは標準搭載される話があるらしいです。

さて、ある程度前提知識のある人は?頭のいい人は?ここまでの説明で、多少scalafix導入時にsbt-projectmatrixが必要になることに察しがついたかもしれません。

つまり

Scala 3でscalafix ruleは書けないのに、 ++ 3.3.3 で一気に全部をScala 3に切り替えたら、rule記述部分のsub projectがbuildエラーになりますよね???」

といった事態に遭遇します。もちろん、すごく工夫すれば、scalafixのruleのprojectだけ2.13にしたまま他を3に上げるのは原理上は可能ですが、それをやるのはとてもややこしく辛いので、繰り返しになりますが全くお勧めしません。

ここで

Scala 3あるいはScala 2.11だが、一切複数のscala versionでcross buildする予定がない」

という場合にsbt-projectmatrixを使わなくて済むかもしれない件の説明ですが

  • rule記述のprojectでは scalaVersion := "2.13.14" を設定
  • それ以外の普通のprojectでは scalaVersion := "3.3.3" を設定
  • ++ などを使って一切scala version切り替えを行わない。build.sbtで指定したversionでずっと固定である

という状態なら、おそらく可能だと思います。

さて、大体書きたいことは書いたというか、原理や理屈を説明して、具体的なコード例を示していないですが、そもそもscalafix公式のgiter8 templateがsbt-projectmatrix使うようになっていますし、

https://github.com/scalacenter/scalafix.g8/blob/5808515aa73eef27e3ee39652e195f6e668d11c9/src/main/g8/scalafix/project/plugins.sbt#L3

一旦今回の説明はこれで終わりにします。

(同じproject内部で)scalafixを導入するにあたって、sbt-projectmatrix以外にも細かいbuild設定のテクニックはあるのですが、それはまた気が向いたら別の時に説明します。