LoginSignup
27
12

More than 5 years have passed since last update.

RuboCop の Cop の実装について

Last updated at Posted at 2016-12-24

こんにちは、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_senddef_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_sendsendの正体です。
RuboCopは、上記のASTのNodeをひとつづつ辿っていき、sendNodeに到達したらそのNodeを引数にon_sendを呼び出すようになっています。
また、例えばon_lvasgnメソッドが定義されていれば、lvasgnNodeに到達した時にメソッドが呼ばれます。

では、sendNodeの中身をどのように検査したら良いでしょうか? それは次に紹介する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

27
12
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
27
12