この記事は2013年時点の状況をまとめたものです。現在では内容が古くなっています。
今後はプロと読み解くRuby 3.3 NEWS - STORES Product Blogの「(参考)Ruby パーサの比較」などを参照することをお勧めします。
ripper
Ruby1.9からMRIの標準ライブラリとなっており、今後とも安定してメンテナンスされることが期待できる。MRIのパーサーを拡張ライブラリ化しているため、文法の互換性の点では他の追随を許
さない。
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_EVENTS
とRipper::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より
バージョンによってパース結果が変化することがある。
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`とされている。
関連ライブラリ
- jimweirich/sorcerer · GitHub - RipperのS式をRubyのコードに変換する
関連リンク
- LoveRubyNet Wiki: Ripper (リンク切れ - Internet Archive)- 作者による解説など。
- Rubyリファレンスマニュアル: class Ripper
- Index of Classes & Methods in ripper: Ruby Standard Library Documentation (Ruby 2.5.0)
- [ruby-list:49823] Re: ripperのcompile_error呼び出しの挙動について
使用例
- todesking/ruby_hl_lvar.vim - ローカル変数をハイライトするVimプラグイン
- zenspider/enhanced-ruby-mode - EmacsのRuby用メジャーモード
- k-tsj/power_assert
- ruby-formatter/rufo - コード整形ツール
parser
pure ruby。lexerはRagelで生成しており、parserはMRIのparse.yをベースにした文法ファイルからraccを利用して生成している。旧来のパーサー以上の機能や性能を謳う。
コードを変換するためのツールが付属している(=
の位置を揃えるフィルターと、while
のdo
・if
のthen
を削除するフィルターの作例あり)。
ややこしいコードを渡すとパースが微妙に狂うことがある。
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
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')
このように引数のないメソッド呼び出しが含まれたコードを呼ぶと例外を出して落ちてしまう。
開発は止まっている。
参考
- Rubyのコードを解析する…? - Qiita
- How many s-expression formats are there for Ruby? | Toxic Elephant - RubyのソースコードのS式表現のバリアントが多すぎるという話。