OSSのコードを読むときは基本的にgithub.devを使っていますが、最近、限界を感じています。
というのも巨大なコードベースを持つOSSの場合は処理を追うだけで大変です。その上、この変数には〇〇クラスのインスタンスが...。この関数の引数には△△が指定されて...と自分の脳(揮発性メモリ)では非常に辛い作業です。
最近はrubocopのコードを読んでいるのですが、何度も「ローカル環境で実行できればなぁ...」と感じていました。
そんな中、技術書典で「Rails のコードを読む」という書籍を購入して読み進めていると、ローカルでRailsにbinding.irbを仕込んでデバッグする方法が紹介されていました。
まさに自分が求めていたものです...。
今回は、書籍を参考にRailsではなくrubocopにbinding.irbを仕込んでデバッグしてみます。
実行環境の準備
まずはrubocopの実行環境を用意します。
他のgemの場合でも何らかの形でgem内のコードが実行されることが確認できれば問題ありません。
今回はrubocopの実行結果がターミナルに表示された所で確認完了とします。
$ mkdir rubocop-debug $ cd rubocop-debug $ bundle init Writing new Gemfile to ../rubocop-debug/Gemfile
生成されたGemfileに以下を追加。
gem 'rubocop', require: false
gemをインストール。
$ bundle i Fetching gem metadata from https://rubygems.org/......... : Using rubocop 1.52.1 Bundle complete! 1 Gemfile dependency, 13 gems now installed. Use `bundle info [gemname]` to see where a bundled gem is installed.
rubocopにパースしてもらうために適当なファイルを用意します。コードは適当です。
main.rb
puts "hello world!"
bundle経由でインストールしたrubocopを実行します。
この時点で、rubocopの実行結果がターミナルに出力されていれば確認完了です。
bundle exec rubocop main.rb Inspecting 1 file C : 1 file inspected, 2 offenses detected, 2 offenses autocorrectable
gemのクローンと切り替え
次はbundle install
のインストール先をローカル環境のファイルに変更します。
といってもパスを指定するだけなので、gemのソースコードをローカル環境に用意すればOKです。
githubからcloneしてrubocopのソースコードを用意します。gemによってはファイルサイズが大きく少し時間がかかるかもしれません。
$ pwd ../rubocop-debug $ git clone https://github.com/rubocop/rubocop.git Cloning into 'rubocop'... : Resolving deltas: 100% (103266/103266), done. $ ls Gemfile Gemfile.lock main.rb rubocop
クローンしたrubocopのバージョンを切り替えます。
今回は特にバージョンのこだわりはないので、最新版のrubocopに切り替えます。master
ブランチのままでも良いですが、リリース後に何かしらの変更が入っていたりするので、タグを使ってリリースされたバージョンのコードに切り替えることをオススメします。
$ cd rubocop/ $ git checkout refs/tags/v1.52.1 Note: switching to 'refs/tags/v1.52.1'. : HEAD is now at 276a53801 Cut 1.52.1
あとはGemfileを変更して、クローンしたrubocopを参照するようにします。
# gem 'rubocop', require: false gem 'rubocop', path: './rubocop'
インストールを再実行します。
rubocopのインストール元を見てみるとローカル環境のrubocop
を指していることが分かります。
$ bundle i bundle i Resolving dependencies... Fetching gem metadata from https://rubygems.org/......... : Using rubocop 1.52.1 from source at `rubocop` and installing its executables # ローカルからインストールしている Bundle complete! 1 Gemfile dependency, 13 gems now installed. Use `bundle info [gemname]` to see where a bundled gem is installed.
ようやく準備が完了しました。再度、rubocopを実行してみます。
$ bundle exec rubocop main.rb Inspecting 1 file C : 1 file inspected, 2 offenses detected, 2 offenses autocorrectable
binding.irbを仕込む
あとはデバッグしたい箇所にbinding.irb
を仕込むだけです。
gemの場合、lib直下に配置されたgem名に対応するファイル(例: rubocop.rb)が最初に実行されるため、とりあえず最初にbinding.irb
を仕込むにはもってこいの場所です。
rubocop/lib/rubocop.rb
unless File.exist?("#{__dir__}/../rubocop.gemspec") # Check if we are a gem RuboCop::ResultCache.rubocop_required_features = $LOADED_FEATURES - before_us end binding.irb RuboCop::AST.rubocop_loaded if RuboCop::AST.respond_to?(:rubocop_loaded)
rubocopを実行してみると、先ほど仕込んだbinding.irb
によって処理が停止します。
$ bundle exec rubocop main.rb From: ../rubocop/lib/rubocop.rb @ line 758 : 753: 754: unless File.exist?("#{__dir__}/../rubocop.gemspec") # Check if we are a gem 755: RuboCop::ResultCache.rubocop_required_features = $LOADED_FEATURES - before_us 756: end 757: => 758: binding.irb 759: 760: RuboCop::AST.rubocop_loaded if RuboCop::AST.respond_to?(:rubocop_loaded)
あとはやりたい放題です。
試しに、関数の実行結果を見てみましょう。
irb(main):001:0> File.exist?("#{__dir__}/../rubocop.gemspec") => true irb(main):002:0> RuboCop::AST.rubocop_loaded => nil
より詳細にデバッグがしたければdebug
コマンドを使用します。
以前の記事にも書いたのですが、irbのバージョンが古いとdebug
コマンドが使えないのでご注意ください。
debugモードに移行してnextを使って、次にどの処理が呼び出されるのか追ってみます。
rubocop.rbが実行された後、rubocop/exe/rubocopが実行されているようです。
irb(main):003:0> debug Loaded debug-1.8.0 (rdbg) next # command [12, 21] in ~/workspace/ruby/rubocop-reading/rubocop/exe/rubocop 12| exit_status = RuboCop::Server::ClientCommand::Exec.new.run 13| else 14| require 'benchmark' 15| require 'rubocop' 16| => 17| cli = RuboCop::CLI.new 18| 19| time = Benchmark.realtime { exit_status = cli.run } 20| 21| puts "Finished in #{time} seconds" if cli.options[:debug] || cli.options[:display_time]
デバッグ画面上でcli.run
を実行してみた所、ターミナルに出力された実行結果が表示されました。
(ruby) cli.run Inspecting 1 file C Offenses: main.rb:1:1: C: [Correctable] Style/FrozenStringLiteralComment: Missing frozen string literal comment. puts "hello world!" ^ main.rb:1:6: C: [Correctable] Style/StringLiterals: Prefer single-quoted strings when you don't need string interpolation or special symbols. puts "hello world!" ^^^^^^^^^^^^^^ main.rb:1:20: C: [Correctable] Layout/TrailingEmptyLines: Final newline missing. puts "hello world!" 1 file inspected, 3 offenses detected, 3 offenses autocorrectable 1
どうやら次はRuboCop::CLIクラスの実装を追っていけば良さそうですね。
続きが非常に気になりますが、今回はコードリーディングがメインではないのでここで止めておきます。
最後に
無事にローカル環境でgemにbinding.irb
を仕込んでデバッグすることができました。
別のgemでも同様の手順で再現ができると思います。ただ、ローカルにクローンしたgemが内部で使用しているgemは別途、クローンする必要があります。場合によっては大量の依存ファイルを手元に用意する必要があります。
これでOSSのコードリーディングがはかどりそうです。
きっかけを頂いた「Rails のコードを読む」の著者 hachiさんには感謝しかありません。書籍の内容もとても素晴らしく勉強になりました。
少しでも「ええな〜」と思ったらイイネ!・シェア!・はてなブックマークを頂けると励みになります。