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

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

今後は[プロと読み解くRuby 3.3 NEWS - STORES Product Blog](https://product.st.inc/entry/2023/12/25/160504#:~:text=%EF%BC%88%E5%8F%82%E8%80%83%EF%BC%89Ruby%20%E3%83%91%E3%83%BC%E3%82%B5%E3%81%AE%E6%AF%94%E8%BC%83)の「（参考）Ruby パーサの比較」などを参照することをお勧めします。
:::

## ripper

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

```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_EVENTS`と`Ripper::PARSER_EVENTS`で確認できる。

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

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

```rb
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は複数ある

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

```rb
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は不正確なことがある

```rb
# 通常のシンボルリテラルでは本体が`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](https://github.com/ruby/ruby/blob/trunk/ext/ripper/README)より

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

```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`とされている。
```

### 関連ライブラリ
* [jimweirich/sorcerer · GitHub](https://github.com/jimweirich/sorcerer) - RipperのS式をRubyのコードに変換する

### 関連リンク
* [LoveRubyNet Wiki: Ripper](http://i.loveruby.net/w/Ripper.html) （リンク切れ - [Internet Archive](http://web.archive.org/web/20170310052904/http://i.loveruby.net/w/Ripper.html)）- 作者による解説など。
* [Rubyリファレンスマニュアル: class Ripper](https://docs.ruby-lang.org/ja/latest/class/Ripper.html)
* [Index of Classes & Methods in ripper: Ruby Standard Library Documentation (Ruby 2.5.0)](http://ruby-doc.org/stdlib-2.5.0/libdoc/ripper/rdoc/index.html)
* [[ruby-list:49823] Re: ripperのcompile_error呼び出しの挙動について](http://blade.nagaokaut.ac.jp/cgi-bin/scat.rb/ruby/ruby-list/49823)

### 使用例
* [todesking/ruby_hl_lvar.vim](https://github.com/todesking/ruby_hl_lvar.vim) - ローカル変数をハイライトするVimプラグイン
* [zenspider/enhanced-ruby-mode](https://github.com/zenspider/enhanced-ruby-mode) - EmacsのRuby用メジャーモード
* [k-tsj/power_assert](https://github.com/k-tsj/power_assert)
* [ruby-formatter/rufo](https://github.com/ruby-formatter/rufo) - コード整形ツール

## parser
* [whitequark/parser · GitHub](https://github.com/whitequark/parser)
* [parser | RubyGems.org | your community gem host](http://rubygems.org/gems/parser)

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

コードを変換するためのツールが付属している（`=`の位置を揃えるフィルターと、`while`の`do`・`if`の`then`を削除するフィルターの[作例](http://whitequark.org/blog/2013/04/26/lets-play-with-ruby-code/)あり）。

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

```rb
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](http://whitequark.org/blog/2012/10/02/parsing-ruby/) - 開発に至った経緯。既存のRubyをパースするライブラリの簡単な紹介と問題点の説明など。

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

## ruby_parser
* [seattlerb/ruby_parser](https://github.com/seattlerb/ruby_parser)
* [ruby_parser | RubyGems.org | your community gem host](http://rubygems.org/gems/ruby_parser)

raccを利用してpure rubyで書かれたパーサー。パースの精度は高い（[README](https://github.com/seattlerb/ruby_parser/blob/master/README.txt)にrubygemsを対象にしたテストの結果が記載されている）。

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

## jruby-parser
* [jruby/jruby-parser · GitHub](https://github.com/jruby/jruby-parser)
* [jruby-parser | RubyGems.org | your community gem host](http://rubygems.org/gems/jruby-parser)

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

## そのほか

* [seattlerb/parsetree](https://github.com/seattlerb/parsetree) - Ruby 1.8系までをサポートしている。2014年に開発終了。
* [coatl/redparse](https://github.com/coatl/redparse) - pure RubyなRubyパーサー。2012年にコミットが途絶えている。

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

### ripper_ruby_parser
* [ripper_ruby_parser | RubyGems.org | your community gem host](http://rubygems.org/gems/ripper_ruby_parser)
* [mvz/ripper_ruby_parser · GitHub](https://github.com/mvz/ripper_ruby_parser)

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

### ripper2ruby
[svenfuchs/ripper2ruby](https://github.com/svenfuchs/ripper2ruby)

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

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

しかし、

```rb
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](http://qiita.com/tadsan/items/7afce87fe2cae3580f61)
* [How many s-expression formats are there for Ruby? | Toxic Elephant](http://www.matijs.net/blog/2012/11/04/how-many-s-expression-formats-are-there-for-ruby) - RubyのソースコードのS式表現のバリアントが多すぎるという話。