46
37

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

Rubyのコードをパースするライブラリまとめ

Last updated at Posted at 2013-10-06

この記事は2013年時点の状況をまとめたものです。現在では内容が古くなっています。

今後はプロと読み解くRuby 3.3 NEWS - STORES Product Blogの「(参考)Ruby パーサの比較」などを参照することをお勧めします。

ripper

Ruby1.9からMRIの標準ライブラリとなっており、今後とも安定してメンテナンスされることが期待できる。MRIのパーサーを拡張ライブラリ化しているため、文法の互換性の点では他の追随を許さない。

ripper_sexp_sample.rb
require 'ripper'

p Ripper.sexp('puts "hello, world."')
# [:program,
#  [[:command,
#    [:@ident, "puts", [1, 0]],
#    [:args_add_block,
#     [[:string_literal,
#       [:string_content, [:@tstring_content, "hello, world.", [1, 6]]]]],
#     false]]]]

パース結果はS式で出力したり、SAX風のイベントドリブン型インターフェイスで取得したりできる。

イベントには、字句解析によって発生するscanner eventと、構文解析によって発生するparser eventの二種類がある。それぞれのイベントの種類についてはRipper::SCANNER_EVENTSRipper::PARSER_EVENTSで確認できる。

ソースコードのエラー処理

compile_error, on_parse_error, warn, warningというメソッドを定義することで、文法エラーや警告が取得できる。

require 'ripper'

class Rippy < Ripper
  %w(compile_error on_parse_error warn warning).each do |name|
    define_method name do |*args|
      puts "#{name}:"
      p args
      p({lineno: lineno, column: column})
    end
  end
end

['[,]', '(', 'p /', 'if(a = 1);end'].each do |code|
  puts "input:"
  puts code
  puts
  Rippy.new(code).parse
  puts
end

このコードを実行すると次の出力が得られる。

input:
[,]

on_parse_error:
["syntax error, unexpected ',', expecting ']'"]
{:lineno=>1, :column=>2}

input:
(

on_parse_error:
["syntax error, unexpected $end"]
{:lineno=>1, :column=>1}

input:
p /

compile_error:
["unterminated regexp meets end of file"]
{:lineno=>0, :column=>3}

input:
if(a = 1);end
  • p /ではlinenoが1ではなく0になる。
  • if(a = 1);endではwarning: found = in conditional, should be ==という警告が出るはずだが、取得できない。

ruby 1.9.3p448, ruby 2.0.0p353, ruby 2.1.0p0, ruby 2.1.1p76, ruby 2.1.2p95でこれらの問題が起きることを確認した。

メソッド呼び出しに対応するparser eventは複数ある

次のように書き方によってノード名が変化する。

Ripper.sexp('a')
#=> [:program, [[:vcall, [:@ident, "a", [1, 0]]]]]
Ripper.sexp('a()')
#=> [:program,
#    [[:method_add_arg, [:fcall, [:@ident, "a", [1, 0]]], [:arg_paren, nil]]]]
Ripper.sexp('self.a')
#=> [:program,
#    [[:call, [:var_ref, [:@kw, "self", [1, 0]]], :".", [:@ident, "a", [1, 5]]]]]

# Parsing Ruby - whitespace
# http://whitequark.org/blog/2012/10/02/parsing-ruby/
# から引用

scanner eventは不正確なことがある

# 通常のシンボルリテラルでは本体が`ident`になるが…
Ripper.lex(":abc")
 [[[1, 0], :on_symbeg, ":"], [[1, 1], :on_ident, "abc"]]

# 演算子やキーワードになる文字列は`ident`にならない。
Ripper.lex(":+")
 [[[1, 0], :on_symbeg, ":"], [[1, 1], :on_op, "+"]]
Ripper.lex(":end")
 [[[1, 0], :on_symbeg, ":"], [[1, 1], :on_kw, "end"]]

# `end`というメソッドを定義した場合
Ripper.lex("def end;end")
 [[[1, 0], :on_kw, "def"],
  [[1, 3], :on_sp, " "],
  [[1, 4], :on_kw, "end"], # 通常ならメソッド名はon_identになる
  [[1, 7], :on_semicolon, ";"],
  [[1, 8], :on_kw, "end"]]

# ブロックの仮引数リストの`|`がon_opになる
Ripper.lex("map{|i| i}")
 [[[1, 0], :on_ident, "map"],
  [[1, 3], :on_lbrace, "{"],
  [[1, 4], :on_op, "|"],
  [[1, 5], :on_ident, "i"],
  [[1, 6], :on_op, "|"],
  [[1, 7], :on_sp, " "],
  [[1, 8], :on_ident, "i"],
  [[1, 9], :on_rbrace, "}"]]

後方互換性は保証されない

Ripper is still early-alpha version.
I never assure any kind of backward compatibility.
ruby/ext/ripper/README at trunk · ruby/rubyより

バージョンによってパース結果が変化することがある。

ripper_exbexpr_sample.rb
require 'ripper'
Ripper.lex('"#{}"')

# 2.0.0-p353, 2.1.0
# [[[1, 0], :on_tstring_beg, "\""], [[1, 1], :on_embexpr_beg, "\#{"], [[1, 3], :on_embexpr_end, "}"], [[1, 4], :on_tstring_end, "\""]]

# 1.9.3-p448
# [[[1, 0], :on_tstring_beg, "\""], [[1, 1], :on_embexpr_beg, "\#{"], [[1, 3], :on_rbrace, "}"], [[1, 4], :on_tstring_end, "\""]]

# 文字列内の式展開を閉じる`}`が、1.9.3では`on_rbrace`とされているのに対して、2.0以降では`on_embexpr_end`とされている。

関連ライブラリ

関連リンク

使用例

parser

pure ruby。lexerはRagelで生成しており、parserはMRIのparse.yをベースにした文法ファイルからraccを利用して生成している。旧来のパーサー以上の機能や性能を謳う。

コードを変換するためのツールが付属している(=の位置を揃えるフィルターと、whiledoifthenを削除するフィルターの作例あり)。

ややこしいコードを渡すとパースが微妙に狂うことがある。

require 'parser/ruby25'

p Parser::Ruby25.parse(<<'CODE')
puts(<<HERE)
#{<<THERE}
THERE
HERE
CODE
# s(:send, nil, :puts,
#  s(:dstr,
#    s(:begin,
#      s(:dstr)),
#    s(:str, "\n"),
#    s(:str, "THERE\n"))) ヒアドキュメントを閉じる部分が正しく認識されていない

関連リンク

  • Parsing Ruby - whitespace - 開発に至った経緯。既存のRubyをパースするライブラリの簡単な紹介と問題点の説明など。

関連ライブラリ

  • mbj/unparser · GitHub - parserによって出力されたASTをソースコードに戻す。出力されるコードはASTに変換される前のコードと等価(equivalent)だが、同一(identical)であることは保証されない。たとえば%w(foo bar)をパースしてコードに戻すと["foo", "bar"]となる。

ruby_parser

raccを利用してpure rubyで書かれたパーサー。パースの精度は高い(READMEにrubygemsを対象にしたテストの結果が記載されている)。

たまにトークンの行番号がずれることがある。

jruby-parser

JRubyのパーサーをIDEなどで使えるようにしたもの。NetBeansやEclipseで採用されている。コードはJavaで書かれており、JRubyから使えるラッパーもgemとして提供されている。

そのほか

  • seattlerb/parsetree - Ruby 1.8系までをサポートしている。2014年に開発終了。
  • coatl/redparse - pure RubyなRubyパーサー。2012年にコミットが途絶えている。

パーサーに関連するライブラリ

ripper_ruby_parser

ripperを利用してruby_parser互換のS式を出力する。

ripper2ruby

svenfuchs/ripper2ruby

Ripperを使用。ソースコードをパースして加工したあとソースに戻すのが主な用途。

forkされたkristianmandrup/ripper2rubyでは、クラスの定義部分やメソッドの呼び出し部分を取得するといった高水準の操作もできる。

しかし、

require 'ripper/ruby_builder'
require 'ruby_api'
require 'ripper/event_log'

src = %q{
gets
}
code = Ripper::RubyBuilder.build(src)
p block_node = code.find_call('gets')

このように引数のないメソッド呼び出しが含まれたコードを呼ぶと例外を出して落ちてしまう。

開発は止まっている。

参考

46
37
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
46
37

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?