この記事では、RuboCop の開発をする上で役に立つ情報の内、デバッグ入門にフォーカスした話をお伝えします。
対象読者
- RuboCop の開発者 (not user)
- RuboCop の開発をしてみたいけど敷居が高いと感じている人
RuboCop をソースからインストールする
RuboCop をデバッグするために、まず最新の RuboCop をソースからインストールしましょう。
バージョンが古かったり、最新バージョンでもリリース版を使用していたりすると、「実は master では直っているバグだった」なんてことがよくあります。
また、コードに修正を加えた場合も、修正を加えたコードをインストールする必要があります。
そのため、まずはソースから RuboCop をインストールする方法を知りましょう。
$ git clone https://github.com/bbatsov/rubocop
$ cd rubocop
$ bundle install
$ bundle exec rake install:local
以上で最新の RuboCop をインストールすることが可能です。
尚、ファイルを追加した場合にはそのファイルをgit add
する必要あり、注意が必要です。
デバッグに有用な RuboCop のオプション
RuboCop には多くのオプションがありますが、その中からデバッグをする際に便利なオプションを紹介します。
--debug
--debug
オプションは、その名の通りデバッグ情報を表示するオプションです。
通常、RuboCop はエラーが発生してもスタックトレースを表示しません。
試しに RuboCop にわざとバグを埋め込んで実行してみましょう。
$ rubocop
An error occurred while Lint/MultipleCompare cop was inspecting /tmp/tmp.3IJl8RsGgZ/test.rb:4:7.
To see the complete backtrace run rubocop -d.
1 error occurred:
An error occurred while Lint/MultipleCompare cop was inspecting /tmp/tmp.3IJl8RsGgZ/test.rb:4:7.
Errors are usually caused by RuboCop bugs.
Please, report your problems to RuboCop's issue tracker.
Mention the following information in the issue report:
0.47.1 (using Parser 2.3.3.1, running on ruby 2.4.0 x86_64-linux)
Inspecting 1 file
.
1 file inspected, no offenses detected
test.rb
の4行、7列目を解析している際にエラーが起きているのはわかりますが、RuboCop 側のどこでエラーが発生したかの情報はありません。
そこで--debug
オプションを使用することで、RuboCop でエラーが発生した際のスタックトレースを表示することが出来ます。
$ rubocop --debug
An error occurred while Lint/MultipleCompare cop was inspecting /tmp/tmp.3IJl8RsGgZ/test.rb:4:7.
1 error occurred:
An error occurred while Lint/MultipleCompare cop was inspecting /tmp/tmp.3IJl8RsGgZ/test.rb:4:7.
Errors are usually caused by RuboCop bugs.
Please, report your problems to RuboCop's issue tracker.
Mention the following information in the issue report:
0.47.1 (using Parser 2.3.3.1, running on ruby 2.4.0 x86_64-linux)
For /tmp/tmp.3IJl8RsGgZ: configuration from /home/pocke/.gem/ruby/2.4.0/gems/rubocop-0.47.1/config/default.yml
Inheriting configuration from /home/pocke/.gem/ruby/2.4.0/gems/rubocop-0.47.1/config/enabled.yml
Inheriting configuration from /home/pocke/.gem/ruby/2.4.0/gems/rubocop-0.47.1/config/disabled.yml
Inspecting 1 file
Scanning /tmp/tmp.3IJl8RsGgZ/test.rb
Error!
/home/pocke/.gem/ruby/2.4.0/gems/rubocop-0.47.1/lib/rubocop/cop/lint/multiple_compare.rb:33:in `on_send'
/home/pocke/.gem/ruby/2.4.0/gems/rubocop-0.47.1/lib/rubocop/cop/commissioner.rb:43:in `block (2 levels) in on_send'
/home/pocke/.gem/ruby/2.4.0/gems/rubocop-0.47.1/lib/rubocop/cop/commissioner.rb:102:in `with_cop_error_handling'
/home/pocke/.gem/ruby/2.4.0/gems/rubocop-0.47.1/lib/rubocop/cop/commissioner.rb:42:in `block in on_send'
/home/pocke/.gem/ruby/2.4.0/gems/rubocop-0.47.1/lib/rubocop/cop/commissioner.rb:41:in `each'
/home/pocke/.gem/ruby/2.4.0/gems/rubocop-0.47.1/lib/rubocop/cop/commissioner.rb:41:in `on_send'
/home/pocke/.gem/ruby/2.4.0/gems/rubocop-0.47.1/lib/rubocop/ast/traversal.rb:128:in `on_if'
/home/pocke/.gem/ruby/2.4.0/gems/rubocop-0.47.1/lib/rubocop/cop/commissioner.rb:47:in `on_if'
(eval):2:in `block in on_begin'
(eval):2:in `each'
(eval):2:in `on_begin'
/home/pocke/.gem/ruby/2.4.0/gems/rubocop-0.47.1/lib/rubocop/cop/commissioner.rb:47:in `on_begin'
/home/pocke/.gem/ruby/2.4.0/gems/rubocop-0.47.1/lib/rubocop/ast/traversal.rb:95:in `on_def'
/home/pocke/.gem/ruby/2.4.0/gems/rubocop-0.47.1/lib/rubocop/cop/commissioner.rb:47:in `on_def'
/home/pocke/.gem/ruby/2.4.0/gems/rubocop-0.47.1/lib/rubocop/ast/traversal.rb:88:in `on_class'
/home/pocke/.gem/ruby/2.4.0/gems/rubocop-0.47.1/lib/rubocop/cop/commissioner.rb:47:in `on_class'
/home/pocke/.gem/ruby/2.4.0/gems/rubocop-0.47.1/lib/rubocop/ast/traversal.rb:12:in `walk'
/home/pocke/.gem/ruby/2.4.0/gems/rubocop-0.47.1/lib/rubocop/cop/commissioner.rb:60:in `investigate'
/home/pocke/.gem/ruby/2.4.0/gems/rubocop-0.47.1/lib/rubocop/cop/team.rb:121:in `investigate'
/home/pocke/.gem/ruby/2.4.0/gems/rubocop-0.47.1/lib/rubocop/cop/team.rb:109:in `offenses'
/home/pocke/.gem/ruby/2.4.0/gems/rubocop-0.47.1/lib/rubocop/cop/team.rb:51:in `inspect_file'
/home/pocke/.gem/ruby/2.4.0/gems/rubocop-0.47.1/lib/rubocop/runner.rb:248:in `inspect_file'
/home/pocke/.gem/ruby/2.4.0/gems/rubocop-0.47.1/lib/rubocop/runner.rb:195:in `block in do_inspection_loop'
/home/pocke/.gem/ruby/2.4.0/gems/rubocop-0.47.1/lib/rubocop/runner.rb:227:in `block in iterate_until_no_changes'
/home/pocke/.gem/ruby/2.4.0/gems/rubocop-0.47.1/lib/rubocop/runner.rb:220:in `loop'
/home/pocke/.gem/ruby/2.4.0/gems/rubocop-0.47.1/lib/rubocop/runner.rb:220:in `iterate_until_no_changes'
/home/pocke/.gem/ruby/2.4.0/gems/rubocop-0.47.1/lib/rubocop/runner.rb:191:in `do_inspection_loop'
/home/pocke/.gem/ruby/2.4.0/gems/rubocop-0.47.1/lib/rubocop/runner.rb:101:in `block in file_offenses'
/home/pocke/.gem/ruby/2.4.0/gems/rubocop-0.47.1/lib/rubocop/runner.rb:111:in `file_offense_cache'
/home/pocke/.gem/ruby/2.4.0/gems/rubocop-0.47.1/lib/rubocop/runner.rb:99:in `file_offenses'
/home/pocke/.gem/ruby/2.4.0/gems/rubocop-0.47.1/lib/rubocop/runner.rb:90:in `process_file'
/home/pocke/.gem/ruby/2.4.0/gems/rubocop-0.47.1/lib/rubocop/runner.rb:68:in `block in each_inspected_file'
/home/pocke/.gem/ruby/2.4.0/gems/rubocop-0.47.1/lib/rubocop/runner.rb:65:in `each'
/home/pocke/.gem/ruby/2.4.0/gems/rubocop-0.47.1/lib/rubocop/runner.rb:65:in `reduce'
/home/pocke/.gem/ruby/2.4.0/gems/rubocop-0.47.1/lib/rubocop/runner.rb:65:in `each_inspected_file'
/home/pocke/.gem/ruby/2.4.0/gems/rubocop-0.47.1/lib/rubocop/runner.rb:57:in `inspect_files'
/home/pocke/.gem/ruby/2.4.0/gems/rubocop-0.47.1/lib/rubocop/runner.rb:36:in `run'
/home/pocke/.gem/ruby/2.4.0/gems/rubocop-0.47.1/lib/rubocop/cli.rb:72:in `execute_runner'
/home/pocke/.gem/ruby/2.4.0/gems/rubocop-0.47.1/lib/rubocop/cli.rb:27:in `run'
/home/pocke/.gem/ruby/2.4.0/gems/rubocop-0.47.1/bin/rubocop:13:in `block in <top (required)>'
/usr/lib/ruby/2.4.0/benchmark.rb:308:in `realtime'
/home/pocke/.gem/ruby/2.4.0/gems/rubocop-0.47.1/bin/rubocop:12:in `<top (required)>'
/home/pocke/.gem/ruby/2.4.0/bin//rubocop:22:in `load'
/home/pocke/.gem/ruby/2.4.0/bin//rubocop:22:in `<main>'
.
1 file inspected, no offenses detected
Finished in 0.20457513802102767 seconds
エラーが発生した際はまず--debug
オプションをつけるようにすると良いでしょう。
-C
, --cache
このオプションは、新規 Cop の追加やパフォーマンス計測の際に使用することが多いでしょう。
RuboCop は高速化の為に解析結果をキャッシュします。
ファイルの内容や使用する設定が変化した場合はキャッシュがクリアされるため通常はこの挙動が問題になることはありません。
ですが、RuboCop 自体の開発をしている際はキャッシュが問題になることがあります。
Cop のソースコードを変更してもキャッシュはクリアされないため、コードの変更が解析結果に反映されなくなってしまいます。
また、パフォーマンスを計測したい祭にはキャッシュが邪魔になるでしょう。
rubocop --cache false
の様に、--cache
オプションにfalse
を指定することでキャッシュを無効化することが出来ます。
Cop の開発時には常にこのオプションをつけておいても良いでしょう。
--only
RuboCop には非常に多くの Cop (記事執筆時点で300以上)があります。
ですがデバッグ中など、特定の Cop のみの解析を実行したい場合があるでしょう。
そのような場合は、--only
オプションを使用することが出来ます。
例えば、rubocop --only Lint/Void
のようにすることでLint/Void
Cop のみを実行することが可能です。
-c
, --config
特定の設定での RuboCop の動作を確認したい時は、--config
オプションが有効です。
rubocop --config ~/some/config/path/config.yml
の様に実行することで、特定の設定を有効にして RuboCop を実行することが可能です。
-D
, --display-cop-names
「偽陽性が出ているが、問題のある Cop がどれだかわからない…」と言った場合にこのオプションが有効です。
-D
オプションを付与することで、警告に対応する Cop の名前を表示する様になります。
$ rubocop -D
Inspecting 1 file
W
Offenses:
test.rb:4:8: W: Lint/MultipleCompare: Use && operator to compare multiple value.
if x < 10 < y
^^^^^^^^^^
1 file inspected, 1 offense detected
Lint/MultipleCompare
と表示されているのがおわかりでしょうか?
-D
オプションを使用することで、このように Cop 名を表示することが出来ます。
デバッグに有用な外部ツール
RuboCop の開発をする上で、デバッグに便利な外部ツールがいくつかあります。
この章では、そのようなツールをいくつか紹介します。
ruby-parse
whitequark/parser: A Ruby parser.
RuboCop の開発を行う上で、Ruby の AST は無視できない存在です。
開発中も AST を確認しながらコードを書くことが多いでしょう。
ruby-parse
コマンドを使用すると、AST を表示することができます。
なお、このコマンドは RuboCop が使用している parser gem に付属している為、RuboCop がインストールされていれば既にインストールされているはずです。
# test.rb
def foo(x)
puts x if cond
end
$ ruby-parse test.rb
(def :foo
(args
(arg :x))
(if
(send nil :cond)
(send nil :puts
(lvar :x)) nil))
上記のように Ruby ファイルを引数に渡すことで、そのファイルの AST を標準出力に書き出します。
rpr
pocke/rpr: RPR displays Ruby's AST on command line.
前項でruby-parse
を紹介しました。このコマンドは手軽に AST を確認できて便利ですが、いくつか機能が足りないと私は感じています。
- AST を表示するだけで、インタラクティブな操作を行えない
- AST を表示するには一度ファイルに保存しないといけない
私はこの様な問題を解決するため、ruby-parse
の代わりに本項で解説するrpr
を主に使用しています。
Installation
rpr
は RubyGems で提供されています。
$ gem install rpr
Usage
ruby-parse
と同じようにrpr test.rb
と実行すると、ruby-parse
と同じように動作します。
インタラクティブな操作
先程上げた問題の1つ目「インタラクティブな操作を行えない」という点は-f
オプションによって解決することが出来ます。
rpr -f pry test.rb
のようにしてrpr
を実行することでpry
が立ち上がり、対象の AST に対してインタラクティブな操作を行うことが出来ます。
$ rpr -f pry test.rb
[1] pry(#<RuboCop::AST::Node>)> self
=> s(:def, :foo,
s(:args,
s(:arg, :x)),
s(:if,
s(:send, nil, :cond),
s(:send, nil, :puts,
s(:lvar, :x)), nil))
[2] pry(#<RuboCop::AST::Node>)> self.children.first
=> :foo
[3] pry(#<RuboCop::AST::Node>)> self.children[1]
=> s(:args,
s(:arg, :x))
[4] pry(#<RuboCop::AST::Node>)> self.children[2]
=> s(:if,
s(:send, nil, :cond),
s(:send, nil, :puts,
s(:lvar, :x)), nil)
[5] pry(#<RuboCop::AST::Node>)> self.children[2].children
=> [s(:send, nil, :cond), s(:send, nil, :puts,
s(:lvar, :x)), nil]
[6] pry(#<RuboCop::AST::Node>)> ls self
AST::Node#methods: + << == append children clone concat dup eql? hash inspect to_a to_ast to_s to_sexp type
Parser::AST::Node#methods: assign_properties loc location
RuboCop::AST::Sexp#methods: s
RuboCop::AST::Node#methods:
__FILE___type? chained? ensure_type? lambda? parent_module_name source_range
__LINE___type? child_nodes equals_asgn? lambda_or_proc? postexe_type? special_keyword?
__pry__ class_constructor? erange_type? literal? preexe_type? splat_type?
alias_type? class_type? false_type? lvar_type? proc? str_content
ancestors command? falsey_literal? lvasgn_type? pure? str_type?
and_asgn_type? complete! float_type? masgn_type? rational_type? super_type?
and_type? complete? for_type? match_current_line_type? receiver sym_type?
arg_expr_type? complex_type? guard_clause? match_with_lvasgn_type? recursive_basic_literal? true_type?
arg_type? const_name gvar_type? method_args recursive_literal? truthy_literal?
args_type? const_type? gvasgn_type? method_name redo_type? unary_operation?
argument? csend_type? hash_type? mlhs_type? reference? undef_type?
array_type? cvar_type? if_type? module_definition? regexp_type? until_post_type?
asgn_method_call? cvasgn_type? iflipflop_type? module_type? regopt_type? until_type?
asgn_rhs def_type? immutable_literal? multiline? resbody_type? updated
assignment? defined_module int_type? mutable_literal? rescue_type? value_used?
back_ref_type? defined_module_name irange_type? next_type? restarg_type? variable?
basic_literal? defined_type? ivar_type? nil_type? retry_type? when_type?
begin_type? defs_type? ivasgn_type? not_type? return_type? while_post_type?
binary_operation? descendants keyword? nth_ref_type? sclass_type? while_type?
block_pass_type? dstr_type? keyword_bang? numeric_type? self_type? xstr_type?
block_type? dsym_type? keyword_not? op_asgn_type? send_type? yield_type?
blockarg_type? each_ancestor kwarg_type? optarg_type? shadowarg_type? zsuper_type?
break_type? each_child_node kwbegin_type? or_asgn_type? shorthand_asgn?
case_type? each_descendant kwoptarg_type? or_type? sibling_index
casgn_type? each_node kwrestarg_type? pair_type? single_line?
cbase_type? eflipflop_type? kwsplat_type? parent source
instance variables: @children @hash @location @mutable_attributes @type
このようにself
に対象の AST が格納されており、pry
上でインタラクティブに操作をすることが出来ます。
ファイルに保存せず解析
また、先程上げた問題の2つ目、「一度ファイルに保存しないといけない」も解決されています。
-e
オプションを使用することでファイルにコードを保存することなく、AST を表示することが可能です。
$ rpr -e 'foo || bar.baz'
s(:or,
s(:send, nil, :foo),
s(:send,
s(:send, nil, :bar), :baz))
stackprof-run && stackprof
tmm1/stackprof: a sampling call-stack profiler for ruby 2.1+
pocke/stackprof-run
このツールは、RuboCop のパフォーマンス計測をする際に便利です。
Installation
$ gem install stackprof-run
Usage
$ stackprof-run rubocop --cache false
$ stackprof stackprof-out
==================================
Mode: cpu(1000)
Samples: 1070 (0.09% miss rate)
GC: 57 (5.33%)
==================================
TOTAL (pct) SAMPLES (pct) FRAME
106 (9.9%) 106 (9.9%) Parser::Source::Buffer#slice
241 (22.5%) 96 (9.0%) Parser::Lexer#advance
64 (6.0%) 64 (6.0%) block (2 levels) in <class:Node>
47 (4.4%) 47 (4.4%) RuboCop::Cop::Cop.badge
1707 (159.5%) 37 (3.5%) RuboCop::AST::Node#each_child_node
46 (4.3%) 22 (2.1%) AST::Node#initialize
22 (2.1%) 22 (2.1%) AST::Node#to_a
24 (2.2%) 20 (1.9%) Parser::AST::Node#assign_properties
167 (15.6%) 19 (1.8%) Kernel#require
28 (2.6%) 19 (1.8%) RuboCop::Cop::Badge#to_s
18 (1.7%) 18 (1.7%) Parser::Source::Range#initialize
17 (1.6%) 17 (1.6%) Parser::Source::Buffer#line_begins
37 (3.5%) 16 (1.5%) Kernel#require
28 (2.6%) 15 (1.4%) Kernel#require
69 (6.4%) 15 (1.4%) Kernel#require
26 (2.4%) 13 (1.2%) RuboCop::Cop::Cop#cop_config
491 (45.9%) 12 (1.1%) RuboCop::Cop::Commissioner#on_send
11 (1.0%) 11 (1.0%) Parser::Lexer::Literal#coerce_encoding
10 (0.9%) 10 (0.9%) RuboCop::AST::Node#parent
9 (0.8%) 9 (0.8%) RuboCop::Cop::Badge#qualified?
9 (0.8%) 9 (0.8%) Parser::Source::Map#initialize
70 (6.5%) 8 (0.7%) RuboCop::Config#cop_enabled?
8 (0.7%) 8 (0.7%) RuboCop::AST::Node#parent=
16 (1.5%) 8 (0.7%) RuboCop::Cop::VariableForce#scanned_node?
8 (0.7%) 8 (0.7%) Parser::Builders::Default#value
10 (0.9%) 7 (0.7%) Gem::StubSpecification#data
48 (4.5%) 7 (0.7%) RuboCop::Config#for_cop
262 (24.5%) 7 (0.7%) RuboCop::Cop::VariableForce#dispatch_node
11 (1.0%) 7 (0.7%) AST::Node#==
7 (0.7%) 7 (0.7%) RuboCop::Cop::Cop#initialize
このように、stackprof-out
を前置して RuboCop を実行することで、stackprof-out
に Stackprof を実行した結果を吐き出します。
尚このツールは RuboCop 専用というわけではなく、Ruby で書かれた他のツールに対しても使用することが出来ます。
誰か RuboCop を高速化して下さい
まとめ
この記事では、RuboCop のデバッグを始める際に便利なオプションやツール等を紹介しました。
気が向いたら RuboCop の内部構造の話なども続編として書こうと思います。