こんにちは、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
実装を眺める
では、実際に実装を眺めてみましょう。
Copの実態はlib/rubocop/cop/lint/eval.rb
にあります(なお、次期リリースではs/lint/security/
される可能性が濃厚です Move Lint/Eval to Security/Eval by cyberdelia · Pull Request #3820 · bbatsov/rubocop )。
短いので、コメントを除く全文を掲載します。
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を使用して調べてみましょう。
手元で動かしたい場合、gem install rpr
とすることでrpr
コマンドが使用可能になります。
以下のコマンドで、RuboCopの使用するASTを表示します。
$ 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
は以下のファイルで定義されています。詳細な使用方法に関する説明もあるため、使用する際は目を通してみると良いでしょう。
先程のファイルのマッチャ定義をもう一度見てみましょう。
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