unused-codeという未使用コードの検知、削除のsbt-plugin & scalafix rule作ってリリースした

イデアというか初期実装は、1年近く前に遡るのですが、それをしっかり作り直して整理してリリースしました。

https://github.com/xuwei-k/unused-code

まだまだ追加したい機能や改善点などありますが、初期versionでも、結構しっかり作ったつもりなので(?)割と既に実用的なはずなので、試してみてのフィードバックお待ちしています。

主な使い方

  • READMEにあるようにaddSbtPluginの記述追加
  • sbt pluginのtask実行
  • そうすると、中間ファイルとして、未使用なものの候補一覧が吐き出される
  • その後は、以下の2つのどちらか(別に両方でもいいが)のscalafix ruleを実行
    • WarnUnusedCode 警告だけ出す
    • RemoveUnusedCode 実際に削除する

アーキテクチャ解説

特徴

  • あくまでグローバルに名前だけ見るので、publicを含んだ、未使用かもしれないclass, trait, object, def, valを検知できる可能性がある。型は見ない
    • publicも検知可能かもしれない、というのが最大のポイント
  • 例えるなら「このclass使ってるかな?」「git grepしても定義部分しか出てないな?」「じゃあ未使用でしょ?」みたいなことを手動でやる機会が多々あると思うが(?)、それをほぼ自動化した、ということ
    • なおかつ、scalametaでparseするのでgit grepより多少賢い
  • SyntacticRuleなので、コンパイルが必要なく、実行が軽い、はやい
  • SyntacticRuleで名前だけで判断するので、もちろん誤検知はあり得る
    • が、出来るだけ少なくなるよう色々工夫したというか、configで制御可能
  • 普通のOSSライブラリにはあまり適さないかもしれない。あくまで、何十万行もあるような、巨大プロジェクトで仕事で書いてるような、ライブラリとして外側から使われることを想定していないものの方が向いている、はず
  • def main があればそれはデフォルトでは勝手に消さない、などがある
  • 名前の参照がなくても、implicitが付与されているものは検知しない、などといった工夫もある

なぜ普通のscalafix ruleではなくsbt pluginか?

  • あくまで、複数のsub projectがある(巨大な)sbtのprojectにおいて、グローバルな未使用コードを見つけたいから、色々試行錯誤した結果、普通のscalafix ruleでは、作成無理ではないがかなり変になる
  • scalafixは、sbtのsub projectごとに実行されるし、Patchというものしか返せない。
  • scalafixには、実行前と後のフックはあるが、それだとsub project毎に個々に複数の中間ファイルに書き出して、それを集計しないといけなくてとても不便
    • あるいはファイルで手動ロック取るとか、実行時にDBが必要、みたいな・・・
  • ファイルないのトップレベルなclassやtraitがなくなって、package文とimport文だけになったら、ファイル丸ごと削除機能などもあり、そうすると普通のscalafixの仕組みではテスト書きづらいので、sbt pluginにしてscripted testも書きたかった、なども

sbt-pluginの役割

  • 自動生成コードから使われている場合もあり、それも未使用かどうか?を判断するため、(sbt-scalafixはデフォルトではそれをparseしないが)それも無理矢理含むようにする
  • sub projectごとではなく、project全体で1つを集計したいので、scalameta使ったparse処理は行うが、それの起動をsbt plugin側から独自に行う
  • 色々config設定機能(普通のscalafix ruleではないので、 .scalafix.conf に書くのは違うというか、やりづらいので)
  • 警告を出すだけにするか?実際に消すか?の選択肢があり得るが、その2つに両方共通の中間ファイル(中身は未使用の候補一覧が書かれたJSON)を使うので、中間ファイルを吐き出すまでがsbt pluginの役割。その後はある意味普通のscalafix rule

gitのcommitから日付を判断に使う機能

  • 「このclass最近追加されたから、これから使われる予定なんだけど、まだ未使用なんだよな〜〜〜」という誤検知(?)パターンを半自動で除外するために、Scalaから git log -1 --format=%ct 実行して、ファイルの最終変更日時を取る、という変なことしてる
  • 例えば 30日以内に追加されたものは除外 1年以内に追加されたものは除外 みたいなことが可能(言い換えると 1年以上放置されて未使用なら、明らかに消していいでしょ!!! という)
  • gitの情報を有効活用する機能は、もっと充実させたい