Rubykaigi で何回か登場した Ruby プログラムのパーサである Ripper。
web を徘徊してみたけど使い方がよくわからないのでゴニョゴニョしてみた。
基本的なクラスをパース
クラス定義
contents =<<-EOF class Hoge def say end end EOF
Ripper で s 式に変える
Ripper でパース
require 'ripper' require 'pp' require 'rexml/document' require 'stringio' source = Ripper.sexp(contents) pp source
出力結果
[:program, [[:class, [:const_ref, [:@const, "Hoge", [1, 8]]], nil, [:bodystmt, [[:def, [:@ident, "say", [2, 8]], [:params, nil, nil, nil, nil, nil, nil, nil], [:bodystmt, [[:void_stmt]], nil, nil, nil]]], nil, nil, nil]]]]
s 式を xml に変換する
ごりごりとノードを作って xml に変換していく。@
のイベントは属性にしたかったけど、複数登場する場合があるので子ノードとして追加するようにした。
def analyze(source, node = nil) node ||= REXML::Document.new return node unless source if source.first.kind_of?(Symbol) && source.first.to_s =~ /^@/ node.add_element(source.first.to_s.sub('@','')).text = source[1] elsif source.first.kind_of? Symbol new_node = node.add_element(source.first.to_s) for i in 1...source.length analyze(source[i], new_node) end elsif source.kind_of? Array source.each {|v| analyze(v, node) } end node end doc = analyze(source) output = StringIO.new formatter = REXML::Formatters::Pretty.new formatter.compact = true formatter.write(doc, output) puts output.string
出力結果
<program> <class> <const_ref> <const>Hoge</const> </const_ref> <bodystmt> <def> <ident>say</ident> <params/> <bodystmt> <void_stmt/> </bodystmt> </def> </bodystmt> </class> </program>
xml に変換できたので、xpath を使って定義を抜き出す
xpath って途中省略とかできた気がするんだけど、思うように取り出せないのでパスをゴリ書き。
ndoc = REXML::Document.new(output.string) classes = ndoc.get_elements('//class') classes.each do |clazz| print clazz.elements['const_ref/const'].text parent = clazz.elements['var_ref/const'] puts parent ? ' < ' + parent.text : '' include = clazz.elements['bodystmt/command/ident'] if include && include.text == 'include' print "\t include - " puts include.parent.get_elements('args_add_block/var_ref/const').map {|v| v.text }.join(', ') end clazz.get_elements('bodystmt/def').each do |m| print "\t " + m.elements['ident'].text params = m.get_elements('paren/params/ident') puts params.length > 0 ? ' - ' + params.map {|v| v.text }.join(', ') : ' - void' end end
出力結果
Hoge say - void
もうちょっと複雑なクラスをパースしてみる
複数のクラスで継承しててメソッドも複数。
contents =<<-EOF class Hoge < Fuga include HogeModule, FugaModule def initialize(name, age) name end def say end end class Fuga def foo end end EOF
S 式にパースする
[:program, [[:class, [:const_ref, [:@const, "Hoge", [1, 8]]], [:var_ref, [:@const, "Fuga", [1, 15]]], [:bodystmt, [[:command, [:@ident, "include", [2, 4]], [:args_add_block, [[:var_ref, [:@const, "HogeModule", [2, 12]]], [:var_ref, [:@const, "FugaModule", [2, 24]]]], false]], [:def, [:@ident, "initialize", [3, 8]], [:paren, [:params, [[:@ident, "name", [3, 19]], [:@ident, "age", [3, 25]]], nil, nil, nil, nil, nil, nil]], [:bodystmt, [[:var_ref, [:@ident, "name", [4, 6]]]], nil, nil, nil]], [:def, [:@ident, "say", [6, 8]], [:params, nil, nil, nil, nil, nil, nil, nil], [:bodystmt, [[:void_stmt]], nil, nil, nil]]], nil, nil, nil]], [:class, [:const_ref, [:@const, "Fuga", [9, 8]]], nil, [:bodystmt, [[:def, [:@ident, "foo", [10, 8]], [:params, nil, nil, nil, nil, nil, nil, nil], [:bodystmt, [[:void_stmt]], nil, nil, nil]]], nil, nil, nil]]]]
xml に変換する
<program> <class> <const_ref> <const>Hoge</const> </const_ref> <var_ref> <const>Fuga</const> </var_ref> <bodystmt> <command> <ident>include</ident> <args_add_block> <var_ref> <const>HogeModule</const> </var_ref> <var_ref> <const>FugaModule</const> </var_ref> </args_add_block> </command> <def> <ident>initialize</ident> <paren> <params> <ident>name</ident> <ident>age</ident> </params> </paren> <bodystmt> <var_ref> <ident>name</ident> </var_ref> </bodystmt> </def> <def> <ident>say</ident> <params/> <bodystmt> <void_stmt/> </bodystmt> </def> </bodystmt> </class> <class> <const_ref> <const>Fuga</const> </const_ref> <bodystmt> <def> <ident>foo</ident> <params/> <bodystmt> <void_stmt/> </bodystmt> </def> </bodystmt> </class> </program>
xpath で定義を抜き出す
Hoge < Fuga include - HogeModule, FugaModule initialize - name, age say - void Fuga foo - void
vim での補完に利用したい
と、思ったけど対象の変数の型が分からんことにはやっぱり・・・ということで Ruby 3.0 に期待。
久しぶりに Ruby 書いたけど、リファレンスをあちこち見ながらなじゃないと書けなくなってる。「こんなメソッド無かったっけ?どう使えばいいんだっけ?」 → 「ひたすらググる」の繰り返しだったのだけど、こういう時に qiita を利用すればいいのかと思うなど。ただ、あそこに書くと完全に埋もれてしまう感(誰が書いてるかはどうでもいい的な)があるんだよなぁ。書いたこと無いけど。