rprでRubyの抽象構文木を画像として出力する

はじめに
===


この記事は [Ruby2.6 + Graphvizで抽象構文木をグラフにしてみる](https://qiita.com/Nabetani/items/7904651fae1f925129c7) という記事を大きく参考にさせていただいています。
ありがとうございます。



rprとは
===

rprは私が作成したgemで、コマンドラインからRubyのコードを抽象構文木に変換して表示するためのツールです。

https://github.com/pocke/rpr



Rubyには様々なパーサが存在します。Ruby本体が提供している[Ripper](https://docs.ruby-lang.org/ja/latest/library/ripper.html)やRuby 2.6から提供されているRubyVM::AbstractSyntaxTreeの他にも、[Parser gem](https://github.com/whitequark/parser)や[RubyParser gem](https://github.com/seattlerb/ruby_parser)などがあります。
rprを使うと、これらのパーサを同一のインターフェイスで扱えます。
またコマンドラインから扱うことに特化しているため、手軽に構文解析結果を表示できます。
例を見てみましょう。

```bash
# Ripper.sexp メソッドを使って構文解析をし、ppで表示する
# -pと-fの値はこれがデフォルトなので、 rpr -e 'puts 1 + 2' のように省略しても同様の結果になる。
$ rpr -e 'puts 1 + 2' -p sexp -f pp
[:program,
 [[:command,
   [:@ident, "puts", [1, 0]],
   [:args_add_block,
    [[:binary, [:@int, "1", [1, 5]], :+, [:@int, "2", [1, 9]]]],
    false]]]]

# Parser gemを使って構文解析する
$ rpr -e 'puts 1 + 2' -p parser -f pp
s(:send, nil, :puts,
  s(:send,
    s(:int, 1), :+,
    s(:int, 2)))

# 構文木をpryで扱う
# self に構文木が入っている。
$ rpr -e 'puts 1 + 2' -p parser -f pry
[1] pry(#<Parser::AST::Node>)> self
=> s(:send, nil, :puts,
  s(:send,
    s(:int, 1), :+,
    s(:int, 2)))
[2] pry(#<Parser::AST::Node>)> self.type
=> :send
[3] pry(#<Parser::AST::Node>)> self.children
=> [nil, :puts, s(:send,
  s(:int, 1), :+,
  s(:int, 2))]
```


`-f`オプションはフォーマッタを指定するオプションです。バージョン1.9[^1]からフォーマッタに`dot`を指定できるようにしたので、この記事ではそのオプションについて解説します。

Dotフォーマッタの使用例
===


Dotフォーマッタは構文木をDOT Languageとして出力するためのフォーマッタです。DOT LanguageはGraphvizによって画像に変換できます。
実際に使用例を見てみましょう。次のコマンドを実行すると、`ast.png`が生成されます。


```bash
$ rpr -e 'puts 1 + 2' -p parser -f dot | dot -Tpng -oast.png
$ open ast.png
```

`ast.png`は次のようになります。


![ast.png](https://qiita-image-store.s3.amazonaws.com/0/33922/439a5097-40e3-8f26-9b33-5b55ef49c50c.png)


このようにASTを画像として見ることができます。



実装
===


rprでは複数の構文木の形式に対応する必要があるため、各構文木を共通のインターフェイスで扱えるよう、Refinementsを用いてパッチをあてています。
全てのオブジェクトに`node_value`, `children`, `traversable?`という3つのメソッドを定義しています。[^2]
`node_value`はそのノードのグラフ上での表示内容をStringで返します。
`traversable?`はそのノードが子要素を持っているかどうかを返します。
そして`children`はそのノードの子要素をArrayで返します。

これらのメソッドをフォーマッタから扱うことで、1つのコードで複数のASTの形式をサポートしています。

https://github.com/pocke/rpr/blob/efbf7717a3f83deeb18b171aefd9f2f47709f261/lib/rpr/formatter/dot.rb#L3-L50

```ruby
module UnifiedInterface
  refine Object do
    def node_value() inspect end
    def traversable?() false end
  end

  if defined?(Rpr::Parser::Sexp)
    refine Array do
      def node_value() traversable? ? self[0].to_s : super end
      def children
        res = []
        self[1..-1].each do |child|
          if child.is_a?(Array) &&
             !child.traversable? &&
             !(child.size == 2 && child[0].is_a?(Integer) && child[1].is_a?(Integer))
            res.concat(child)
          else
            res << child
          end
        end
        res
      end
      def traversable?() self.first.is_a?(Symbol) end
    end
  end

  if defined?(Rpr::Parser::Parser) || defined?(Rpr::Parser::Rubocop)
    refine ::Parser::AST::Node do
      def node_value() self.type.to_s end
      def traversable?() true end
    end
  end

  if defined?(Rpr::Parser::Rubyparser)
    refine Sexp do
      def node_value() self.sexp_type.to_s end
      def children() self.each.to_a end
      def traversable?() true end
    end
  end

  if defined?(Rpr::Parser::Rubyvm_ast)
    refine RubyVM::AbstractSyntaxTree::Node do
      def node_value() self.type.to_s end
      def traversable?() true end
    end
  end
end
```

DOT Languageを出力する部分の実装は冒頭にあげた記事のものとほとんど変わりがないので、解説を省略します。


最後に
===


RubyのASTをコマンドラインから扱うrprと、新機能であるdotフォーマットの紹介でした。
RubyのASTを扱う機会が多い方にインストールして使っていただけるとうれしいです。

[^1]: v1.9.0から対応していますが、バグがあったのでv1.9.1をリリースしています。そちらをお使いください。
[^2]: `traversable?`がfalseを返すなら`children`は呼ばれないので、正確にはObjectには`children`は実装されていません。