basyura's blog

あしたになったらほんきだす。

Ripper → S 式 → XML

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 を利用すればいいのかと思うなど。ただ、あそこに書くと完全に埋もれてしまう感(誰が書いてるかはどうでもいい的な)があるんだよなぁ。書いたこと無いけど。