Rubyのコードをパースしたい。 自前で正規表現をつくったり、strscanでゴリゴリ解析するのは工数不足そう。

ということで、Rubyのコードを字句解析するのためのツールをしらべた。

Ruby標準ライブラリのなかの以下の2つが利用できそうだ。

それぞれ、つかってみる。

やりたいこと

とりあえず、こんなことができればOK.

  • クラスの定数を抜きだし
  • メソッドに含まれるメソッドとその引数の抜きだし

解析対象

今回の解析対象Rubyコードは以下。

TEST  = "test"
TEST2 = "test2"

def method1
  foo(1, 2,"Hello")
  bar(1, 2,"Hello")
end

def method2
  bar(5, 6,"Hi")
  foo(3, 4,"Hi")
end

def foo(val1, val2, str)
end

def bar(val1, val2, str)
end

rdoc

rdocはRubyのドキュメント生成のためのツールだけれども、Rubyコード解析用のライブラリもあるみたい。

そういえば、このまえ記事にしたrspec-kickstarterも rdocでrspecを解析しているっぽい。

コマンドラインから以下を実行すると、カレントディレクトリのコードを解析してHTMLを生成してくれる。

rdoc .

こんな感じ。ちゃんとメソッドと定数が抜き出せている。

どうも、引数は抜き出せなさそう。調査不足かもしれないが、採用は却下。

ripper

Rubyのコード解析をするための標準ライブラリ。

トークン指向型解析(tokenize)

文字列を単語に分解してくれる。

require 'ripper'

File.open("./sample.rb") do |io|
  io.each_line do |line|
    p Ripper.tokenize(line)
  end
end
["TEST", "  ", "=", " ", "\"", "test", "\"", "\n"]
["TEST2", " ", "=", " ", "\"", "test2", "\"", "\n"]
["\n"]
["def", " ", "method1", "\n"]
["  ", "foo", "(", "1", ",", " ", "2", ",", "\"", "Hello", "\"", ")", "\n"]
["  ", "bar", "(", "1", ",", " ", "2", ",", "\"", "Hello", "\"", ")", "\n"]
["end", "\n"]
["\n"]
["def", " ", "method2", "\n"]
["  ", "bar", "(", "5", ",", " ", "6", ",", "\"", "Hi", "\"", ")", "\n"]
["  ", "foo", "(", "3", ",", " ", "4", ",", "\"", "Hi", "\"", ")", "\n"]
["end", "\n"]
["\n"]
["def", " ", "foo", "(", "val1", ",", " ", "val2", ",", " ", "str", ")", "\n"]
["end", "\n"]
["\n"]
["def", " ", "bar", "(", "val1", ",", " ", "val2", ",", " ", "str", ")", "\n"]
["end", "\n"]

tokenizeの他には、sexp(S式)、lexer(位置情報つき)がある。

イベントドリブン型解析

特定の構文に出会うたびに、イベントハンドラがコールされる。 on_XXXで定義する。XXXの部分には、Ripper:EVENTSでとれる値が入る。

pp Ripper::EVENTS

ripper-tagsのソースコードとかも、使い方の勉強になる。

ripper つかってみる

S式配列を取得する

イベントドリブン型でsample.rbをパースしてみる。 まずは、以下のようなコードでS式の配列を出力する。

require 'ripper'
require 'pp'

File.open("./sample.rb") do |io|
  pp Ripper.sexp(io)
end

ずらずらとS式の配列が現れる。

[:program,
 [[:assign,
   [:var_field, [:@const, "TEST", [1, 0]]],
   [:string_literal, [:string_content, [:@tstring_content, "test", [1, 9]]]]],
  [:assign,
   [:var_field, [:@const, "TEST2", [2, 0]]],
   [:string_literal, [:string_content, [:@tstring_content, "test2", [2, 9]]]]],
  [:def,
   [:@ident, "method1", [4, 4]],
   [:params, nil, nil, nil, nil, nil, nil, nil],
   [:bodystmt,
    [[:method_add_arg,
      [:fcall, [:@ident, "foo", [5, 2]]],
      [:arg_paren,
       [:args_add_block,
        [[:@int, "1", [5, 6]],
         [:@int, "2", [5, 9]],
         [:string_literal,
          [:string_content, [:@tstring_content, "Hello", [5, 12]]]]],
        false]]],
     [:method_add_arg,
      [:fcall, [:@ident, "bar", [6, 2]]],
      [:arg_paren,
       [:args_add_block,
        [[:@int, "1", [6, 6]],
         [:@int, "2", [6, 9]],
         [:string_literal,
          [:string_content, [:@tstring_content, "Hello", [6, 12]]]]],
        false]]]],
    nil,
    nil,
    nil]],

@という記号の後にイベント名っぽいものがある。 パースしたいキーワードの近くにあるイベント名っぽいものをXXXとして処理を書いていく。

以下のようなことをする。

  • 定数を抽出
  • 定義されているメソッドをインデックスとする配列を作成
  • 配列の要素にcallされているメソッドをキー、引数をvalとするハッシュを作成。

まとめ

はじめに使い方を覚えるのに苦労した。

一度分かってしまえば正規表現でパースするよりも簡単そうだ。