RuboCop の Cop の実装について



こんにちは、pockeです。今回はLintツールのひとつ、RuboCopのCopの実装方法に関して述べようと思います。



対象読者
-----


- RuboCopの内部実装を知りたい人
- Lintの実装をしたい人





Cop とは?
-------

Cop とは、RuboCopにおけるひとつのルールの単位です。
例えば、

- `eval`メソッドの使用を検出する
- インデントの不整合を検出する
- `Gemfile`内のgemがアルファベット順になっているか検査する

などがひとつのCopの単位となります。



今回述べるscope
-------

RuboCopには現在(2016/12/23現在のmaster)、321個のCopがあります。
数多くのCopがある中で、Copが問題とする領域によってCopが必要とする情報が変わるため、Copの実装方法も変化します。
そのためRuboCopにはCopの実装にいくつかの方法があるのですが、今回はその中から1つを選んで紹介したいと思います。

今回紹介するものは、AST Visitorパターンの上に実装されており、かつASTのマッチングによって実装されているCopとなります。
上記のCopの例ですと、`eval`メソッドの使用を検出するCopはこのパターンを用いて実装されています。

なお、AST Visitorパターンについては以下の記事がよくまとまっていますので是非ご覧ください。

[Vim script の Lint 作者による誰得 Lint デザインパターン - Qiita](http://qiita.com/Kuniwak/items/d6a2d22711e4d7856edd)




実装を眺める
------

では、実際に実装を眺めてみましょう。

Copの実態は`lib/rubocop/cop/lint/eval.rb`にあります(なお、次期リリースでは`s/lint/security/`される可能性が濃厚です [Move Lint/Eval to Security/Eval by cyberdelia · Pull Request #3820 · bbatsov/rubocop](https://github.com/bbatsov/rubocop/pull/3820) )。

https://github.com/bbatsov/rubocop/blob/7053c715c3100911a8fd34109520a786a6408c39/lib/rubocop/cop/lint/eval.rb

短いので、コメントを除く全文を掲載します。

```ruby
module RuboCop
  module Cop
    module Lint
      class Eval < Cop
        MSG = 'The use of `eval` is a serious security risk.'.freeze

        def_node_matcher :eval?, '(send nil :eval $!str ...)'

        def on_send(node)
          eval?(node) { add_offense(node, :selector) }
        end
      end
    end
  end
end
```

ここで重要なポイントは2つ、`on_send`と`def_node_matcher`です。順に解説します。

### `on_send`

このメソッドは、Visitorパターンを使用するCopのエントリーポイントとなります。
名前の通り、`send`の時に`on_send`メソッドが呼ばれ、以降の解析が実行されます。


では、`send`とはなんでしょうか? これにはRubyのASTが関係しています。
拙作の[pocke/rpr](https://github.com/pocke/rpr)を使用して調べてみましょう。
手元で動かしたい場合、`gem install rpr`とすることで`rpr`コマンドが使用可能になります。

以下のコマンドで、RuboCopの使用するASTを表示します。

```sh
$ rpr -e 'code = "dangerous code!"; eval(code)' -p rubocop
s(:begin,
  s(:lvasgn, :code,
    s(:str, "dangerous code!")),
  s(:send, nil, :eval,
    s(:lvar, :code)))
```

上記のような出力がなされたと思います。

出力の4行目に`s(:send, ...`という記述があることに気がつくと思います。これが`on_send`の`send`の正体です。
RuboCopは、上記のASTのNodeをひとつづつ辿っていき、`send`Nodeに到達したらそのNodeを引数に`on_send`を呼び出すようになっています。
また、例えば`on_lvasgn`メソッドが定義されていれば、`lvasgn`Nodeに到達した時にメソッドが呼ばれます。

では、`send`Nodeの中身をどのように検査したら良いでしょうか? それは次に紹介する`def_node_matcher`によって行うことが出来ます。

### `def_node_matcher`

このメソッドは、Nodeにマッチするようなメソッドを動的に定義します。

`def_node_matcher`は以下のファイルで定義されています。詳細な使用方法に関する説明もあるため、使用する際は目を通してみると良いでしょう。

https://github.com/bbatsov/rubocop/blob/7053c715c3100911a8fd34109520a786a6408c39/lib/rubocop/node_pattern.rb



先程のファイルのマッチャ定義をもう一度見てみましょう。

`def_node_matcher :eval?, '(send nil :eval $!str ...)'`

`def_node_matcher`メソッドの第一引数は、定義するメソッド名になります。この例では、`eval?`メソッドが定義されることになります。

第二引数は、`node_pattern.rb`内で定義されているDSLのコードです。
このDSLは先程見たASTの出力と殆ど同じ形をしているため、理解が容易だと思います。

この例では、`send`タイプのNodeで、レシーバが`nil`, `:eval`という名前のメソッド呼び出しであり、第一引数が`str`でないもの、にマッチする`eval?`メソッドが定義されます。

そして、そのメソッドを先述した`on_send`メソッドで使用することで、対象の問題にマッチするメソッド呼び出しかを判定しています。



では、最後に`def_node_matcher`で定義した`eval?`メソッドを呼び出している箇所を見てみましょう。

`eval?(node) { add_offense(node, :selector) }`となっています。
`def_node_matcher`で定義したメソッドは、対象のNodeがパターンにマッチした場合、渡されたブロックを実行します。
この例では、パターンにマッチした時に`add_offense(node, :selector)`というコードが呼び出されています。
`add_offense`メソッドは、解析対象のNodeに問題があることをCopに登録します。また、この際のメッセージは`MSG`定数に定義したものが自動的に使われます。



まとめ
--------


以上で`Lint/Eval` Cop の実装を通して、Copの実装をさらっと眺めました。
RuboCop に手を加える場合、もしくは新たなLintツールの設計をする際の手助けになれば幸いです。


この他にも様々なCopの実装があったり、`on_send`などのメソッドを実際に呼び出しているところなど、今回の記事で述べられなかったコードも多々あります。気になる方は、一度RuboCopの実装を眺めてみては以下かでしょうか。(もしくはそのうちその部分に関する記事も書こうと思いますので、ご期待下さい)


追記(2016/12/28)
-------

より実践的な記事も書いたので、よければご覧ください。 [Performance/RegexpMatch Cop の概要と実装 - pockestrap](http://pocke.hatenablog.com/entry/2016/12/27/174436)