これは Ruby Advent Calendar 2022 の7日目の記事です。
TL; DR
ABC size を可視化する abc_size_visualizer という gem を作りました。 こんな感じで、メソッドの各行がどれだけ ABC size の増加に貢献しているかを可視化する CLI ツールです。
赤が代入、黄色がメソッド呼び出し、青が条件式の数を表しています。
また、各行の末尾にある # <0, 2, 1>
のようなコメントも abc_size_visualizer が付加したもので、それぞれ代入、メソッド呼び出し、条件式の大きさを表しています。
使い方
以下のコマンドでインストールできます。
$ gem install abc_size_visualizer
そして、以下のコマンドで ABC size の可視化を実行します
$ visualize_abc_size some_code.rb ... # ABC size の情報が付与された some_code.rb の内容が出力される
背景
abc_size_visualizer を実装するに至った背景を説明します。
ABC size はどのように理解されているか
多くの Rubyist は、以下のような警告によって ABC size を知ります。
$ bundle exec rubocop (略) Metrics/AbcSize: Assignment Branch Condition size for auth is too high. [20.5/15]
このような警告に直面した人々は、インターネットを検索して回った後、おもむろに .rubocop.yml
を編集し、ABC size に関する警告を「解決」します 1。
Metrics/AbcSize: Max: 100
このように、一般的に ABC size は、
- なんかよくわからないけど rubocop が怒ってくるもの
- (ホントは良くないと分かっているが... という disclaimer 付きで)無視するもの
と理解されています。
ABC size とは何であるか
では、そもそも ABC size とは何でしょうか?
ABC size は、代入(assignment)、メソッド呼び出し(branch)、条件式(condition)の出現回数をそれぞれ2乗して和をとり、その平方根を計算したもの2であり、コードの「サイズ」を表す指標です3。
定義を述べてもよくわからないと思うので、例を挙げます。以下のメソッドを考えると、
def some_method if [true, false].sample # if による条件式(C)、sample メソッドの呼び出し(B) puts "#{100 + 200}" # puts メソッドの呼び出し(B)、 `+` メソッドの呼び出し(B) end end
ABC size は、
となります。
ABC size の問題点
ABC size の問題点は分かりづらいことにあります。
例えば、以下のコードには「メソッド呼び出し(branch)」が何回出現するでしょうか?
puts user_name
正解は..................「わからない」です!
なぜなら、 user_name
がローカル変数なのかメソッド呼び出しなのか、この一行だけでは判定できないからです。
ABC size は、メソッド全体の文脈を加味しないと算出することができず、見た目が全く同じコード断片であっても、文脈によって ABC size は全く異なる可能性があります。
def some_method1 puts user_name # user_name というローカル変数がないので user_name はメソッド呼び出しである end def some_method2(user_name:) puts user_name # user_name というローカル変数が定義されているので user_name はメソッド呼び出しではない end
ほかにも、ABC size には非自明なケースがいくつも存在します。
- メソッドの引数は assignment としてカウントされるか?
&.some_method
は condition としてカウントされるか? branch としてカウントされるか?rescue SomeError => e
は condition としてカウントされるか? assignment としてカウントされるか?else
は condition としてカウントされるか?||=
は condition としてカウントされるか? assignment としてカウントされるか?>
は condition としてカウントされるか? branch としてカウントされるか?- デフォルト値付きのキーワード引数は assignment としてカウントされるか? condition としてカウントされるか?
また、これらの非自明なケースはドキュメント等にも特に記載がありません。 そもそも、Rubocop の Metrics/AbcSize では ABC の内訳は表示されません。
そのため、ABC size 削減チャレンジは、
- 当てずっぽうで色々書き換える
- rubocop に怒られないことを祈る
- 怒られたら 1 に戻る
という方法で行われる4ことになり、不毛度が高いです。
abc_size_visualizer で ABC size の闇を払う
abc_size_visualizer の存在する世界では、ABC size 削減チャレンジは
- ABC size を可視化し、ABC size が大きい原因を理解する
- 問題となっている行を書き換える
- rubocop に怒られないことを確認する
というステップで実施することができます。
また、rubocop に鍛えられて Ruby のベストプラクティスを学ぶように、人間側が ABC size をより深く理解するようになり、abc_size_visualizer がなくても適切に ABC size を削減できるようになっていくことでしょう。
実装方法とその問題点
abc_size_visualizer は、rubocop の内部で利用されている AbcSizeCalculator クラスを流用して実装しています。
- abc_size_visuzalizer の実装 https://github.com/genya0407/abc_size_visualizer/blob/trunk/lib/abc_size_visualizer/abc_size_calculator.rb#L1
- 元になったクラス https://github.com/rubocop/rubocop/blob/master/lib/rubocop/cop/metrics/utils/abc_size_calculator.rb
これは、rubocop の ABC size 計算と実装をなるべく一致させることを意図したものですが、今後 rubocop 側の実装が変更されたときに追従できない可能性が高いため、なんらかのうまいやり方を考える必要があるナアと思っています...
まとめ
ABC size を可視化する CLI ツールを実装しました。 これにより、rubocop の Metrics/AbcSize の闇を払い、Rubyist の皆さんをサポートすることができれば嬉しいです。
また、この CLI ツール自体の実装には闇が多いため、どうにかマトモにしていきたいなと考えています。
-
# rubocop:disable Metrics/AbcSize
によって「解決」することもある。↩ - https://nacl-ltd.github.io/2016/02/23/ruby-abcmetrics.html から。↩
- https://en.wikipedia.org/wiki/ABC_Software_Metric から。ソースコードの「複雑さ」や「計算量」を表す値ではないことに注意。↩
- ABC size を完全に理解している人は別ですが↩