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とするハッシュを作成。
まとめ
はじめに使い方を覚えるのに苦労した。
一度分かってしまえば正規表現でパースするよりも簡単そうだ。