Rubyのコードをripperでパースする方法

    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)

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

    sample code

    
    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,
    [:varfield, [:@const, “TEST”, [1, 0]]],
    [:stringliteral, [:stringcontent, [:@tstringcontent, “test”, [1, 9]]]]],
    [:assign,
    [:varfield, [:@const, “TEST2”, [2, 0]]],
    [:stringliteral, [:stringcontent, [:@tstringcontent, “test2”, [2, 9]]]]],
    [:def,
    [:@ident, “method1”, [4, 4]],
    [:params, nil, nil, nil, nil, nil, nil, nil],
    [:bodystmt,
    [[:methodaddarg,
    [:fcall, [:@ident, “foo”, [5, 2]]],
    [:argparen,
    [:argsaddblock,
    [[:@int, “1”, [5, 6]],
    [:@int, “2”, [5, 9]],
    [:stringliteral,
    [:stringcontent, [:@tstringcontent, “Hello”, [5, 12]]]]],
    false]]],
    [:methodaddarg,
    [:fcall, [:@ident, “bar”, [6, 2]]],
    [:argparen,
    [:argsaddblock,
    [[:@int, “1”, [6, 6]],
    [:@int, “2”, [6, 9]],
    [:stringliteral,
    [:stringcontent, [:@tstringcontent, “Hello”, [6, 12]]]]],
    false]]]],
    nil,
    nil,
    nil]],
    

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


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


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

    結果

    まとめ

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

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