1. Qiita
  2. 投稿
  3. Ruby

Happy Hacking RuboCop! #1 デバッグ入門

  • 3
    いいね
  • 0
    コメント

この記事では、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 を高速化して下さい :pray:

まとめ

この記事では、RuboCop のデバッグを始める際に便利なオプションやツール等を紹介しました。
気が向いたら RuboCop の内部構造の話なども続編として書こうと思います。