本記事は Rakuten Rakuma Advent Calendar 2022 20日目の記事です。
Rustを勉強しながらrubocop:disableで無効化されているRuboCopの規約ごとに無効化が記述されているファイル数を集計するツール rdgrep を作成しました。(ちょっとややこしい)

背景

勤務先では下記のRuboCop configを使っているのですが、開発メンバーが入れ替わったりしてメンテされなくなり手元のソースコードにrubocop:disableが増えてきてしまってきたので今一度設定を整理する必要が出てきました。

https://github.com/Fablic/fablicop

重要ではないルールで警告が出続けると本当に守るべきルールまで軽視されてlintが無視されたり、とりあえずでrubocop:disableで無効化するようになってしまうので、不要なルールはこのconfig内でどんどん無効化してしまおうと考えています。

とはいえ効果的にルールの改善を行いたいため、差し当たってまずは設定されている規約と実際のコードの乖離がどの程度あるのかを知ろうと思い、以下のことを行ってConfigと実際のコードの差異を測ろうと考えました。

  • RuboCop出力を規約でグループ化して集計
  • rubocop:disableで無効化されている規約を集計

find . -type f -name "*.rb" -print0 | xargs -0 sed -i -e "s/rubocop:disable//g"等で無効化のコメントを消してからRubocop出力を規約でグループ化して集計すれば良いのではと思いましたが、古いプロダクトだと古いバージョンのRubyの書き方が残っていて規約違反として検出されるなどノイズが多く、人の手で恣意的に無効化している規約の一覧が欲しいのであえて分けました。
前者は既につくられている方がいましたが、後者ができるツールがなかったので自分で書くことにしました。

なぜRustで書くのか

Rustは以前から流行っていて興味はあったものの、プログラマ歴が短いのでまずは今使っているRubyの造詣を深めるのに集中するのが先だと思い触れてきませんでした。しかし、

  • 2022年はRubyGems 3.3.11でRust拡張の対応が実験的に入ったり、Rust版YJITがマージされたりだったりとRubyの周りにRustが進出してきた年だった
  • 取り組む内容がそんなに難しい内容ではないので、数週間でどこまで新しい言語を勉強してコードを書けるかを試してみたかった

ということでRubyistにも身近な存在になってきた&チャレンジしてみたいという理由でRustで書きました。
時間があまりなかったのでThe Rust Programming Language 日本語版 を途中までざっくりやって、その後は公式ドキュメントを参照しながらツール作成しました。

やったこと

集計にあたってrubocop:disableで無効化されている規約の数をそのまま測るのではなく、規約が無効化されているファイル数を計測することにしました。
理由としては範囲指定で無効化していたり、特定の行で無効化していたりと無効化の仕方が違うためです。 例えば以下のコードではStyle/NumericPredicate, Style/NumericLiterals共に無効化のコメントが書かれているのは1箇所ですが、実際にはこの規約に違反している箇所はStyle/NumericPredicate1箇所、Style/NumericLiterals2箇所あります。

def a
  foo == 0 # rubocop:disable Style/NumericPredicate
end

# rubocop:disable Style/NumericLiterals
def b
  10000
end

def c
  100000
end
# rubocop:enable Style/NumericLiterals

ファイル数で確認する事ができればこの差異を避ける事ができると考えこの実装にしました。

つくったもの

無効化されたRuboCopの規約をファイル数で集計するrdgrepを作成しました。

https://github.com/craftscat/rdgrep

まだまだ全然未完成で粗いですが、ある程度やりたいことはできるところまではつくりました。

$ rdgrep ./testdata/ok
("Style/AccessModifierDeclarations", 3)
("Style/Alias", 2)
("Style/AccessorGrouping", 1)

指定したディレクトリ配下の**.rbを捜査して規約ごとに無効化されているファイル数を表示します。暫定対応でvendor/bundle配下のファイルは除外するようになっています。(今後設定ファイルで変更できるようにする予定です。)

ざっくりとした図にするとこのような処理流れで動いています。


flowchart LR A[input] --> B[finds **.rb] B --> C1[grep] B --> C2[grep] B --> C3[grep] C1 --> D1[extracts rules] C2 --> D2[extracts rules] C3 --> D3[extracts rules] D1 --> E[aggregate] D2 --> E D3 --> E E --> F[prints]

  1. 引数で指定されたディレクトリ以下のrbファイルを検索してリスト化
  2. 並列でrubocop:disablegrepして無効化されているルールを抽出
  3. 無効化された各ルールごとにファイル数を集計
  4. 集計内容を標準出力

Rustの並行処理は難しいと思っていましたがGoで並行処理を書いたことがあったので思ったよりもすんなり書けました。

let mut result: HashMap<String, i32> = HashMap::new();
let len: usize = paths.len();
let (tx, rx) = mpsc::channel::<Vec<String>>();

for path in paths {
    let tx: mpsc::Sender<Vec<String>> = tx.clone();

    thread::spawn(move || match fs::read_to_string(path) {
        Ok(content) => {
            let copss = find_disabled_copss(&content);
            tx.send(copss).unwrap();
        }
        Err(err) => {
            eprintln!("{}", err);
            tx.send(Vec::new()).unwrap();
        }
    });
}

let mut n = 0;
while n < len {
    n += 1;

    let copss: Vec<String> = rx.recv().unwrap();
    for c in copss {
        *result.entry(c).or_insert(0) += 1;
    }
}

result

https://github.com/craftscat/rdgrep/blob/e5eadc4e343a1350203cee58e273f3b355e904cc/src/main.rs#L57-L86

Rustの並行処理、並列処理についてはRustのスレッドとチャネルと共有メモリの話(Zenn)という記事がすごく分かりやすく書かれていて参考になったのでオススメです。

普段Rubyでゆるふわっとコーディングしているので、難しいとよく言われる所有権に最初は苦しみましたが、並行処理などで処理を行う際に異なるスレッド間でデータが共有されないように制限されるため安全に複数スレッド間の処理を実装することができるのが個人的に良い開発体験でした。
共有するとしてもスレッドセーフでない変数をスレッドに渡そうとするとコンパイラエラーになったりとしっかりとチェックしてくれるので安心して並行処理が書けました。マルチスレッドプログラミング初心者に優しい言語ですね。

まだ私はRustで書くメリットを完全には理解できてはいないのとは思うのですが、コンパイラがとても優秀で個人的には良い開発体験でした。 その反面、ある程度雑に書いてコンパイルの指示に従ってコードを書けてしまうのでRustを書く力は身についた気がしませんが。。。精進します。

おわりに

とりあえずやりたいことは達成したのでこれを使ってRuboCop configをバリバリ直していこうと思います。 今回つくったrdgrepにも以下のような問題点、改善点があるので今後もRustを勉強しながら改良していきたいと思っています。

  • Gemfile, Rakefile, gemspecは調査対象外になってしまっている
  • unwrap()で雑に書いているところが多いのでエラー処理を充実させたい
  • 検索の除外対象などを設定ファイルで変更できるようにしたい

もし「もっと良い書き方がある」とか、「ここに改善の余地がある」などご意見ございましたらぜひ教えて頂けますと幸いです。