Prismとは
Prismとは最近できたRubyのソースコードパーサです。ソースコードを渡すとAST(抽象構文木)を作ります。そして、それぞれのノードには便利なメソッドが備わってあります。
require "prism"
code = <<~CODE
  "foo"
CODE
prism_result = Prism.parse(code)
p prism_result.value
#=> @ ProgramNode (location: (1,0)-(1,5))
#=> ├── locals: []
#=> └── statements:
#=>     @ StatementsNode (location: (1,0)-(1,5))
#=>     └── body: (length: 1)
#=>         └── @ StringNode (location: (1,0)-(1,5))
#=>             ├── flags: ∅
#=>             ├── opening_loc: (1,0)-(1,1) = "\""
#=>             ├── content_loc: (1,1)-(1,4) = "foo"
#=>             ├── closing_loc: (1,4)-(1,5) = "\""
#=>             └── unescaped: "foo"
対象のノードの位置を簡単に取得できます。
# "foo"文字列を取得する
string_node = prism_result.value.child_nodes.first.child_nodes.first
# 位置の取得
string_node.location
#=> (1,0)-(1,5)
string_node.location.start_line
#=> 1
string_node.location.start_column
#=> 0
string_node.location.end_column
#=> 5
特にこの位置の取得が結構すごいだと思って、なぜかというと、普通のRipperの出力を見ると(1,0)の情報しか含まれていません(下記では[1, 1]という風に表現されています)。
require "ripper"
p Ripper.sexp(code)
#=> [:program,
#=>   [
#=>     [:string_literal,
#=>       [:string_content,
#=>         [:@tstring_content, "foo", [1, 1]]
#=>       ]
#=>     ]
#=>   ]
#=> ]
Prismではそのノードがどこで始まるかだけじゃなくて、どこで終わるかも取得できます。ifやwhileなどのブロックの場合、そのブロックがどこで終わっているかを探し出せるということです!かなりすごい。
Masamune
筆者はなぜその位置情報がそんなにほしいかと言ったら、Bullet TrainでMasamuneというツールを使っているからです。Bullet Trainのconfig/routes.rbではnamespaceやresourceのブロックをたくさん修正することがあります。Masamuneを使うと、そのブロックの始まりと終わりがどこなのか分かります。
ただし、v2.0.0までは生のRipperを使っていました。かなりハードな作業でした...
それで、生のRipperのままだけで上手く行っていたけど、その実装自体がちょっと複雑でした。
Prismを使ったらいいんじゃない?と上司からのアドバイスをいただきました。
それでPrismにアップデートしましたが、Prismの作者であるKevinさんもその実装についてアドバイスを与えてくれたので、興味があればプルリクエストの方を読んでみてください。
内容としては下記の通りです。
Visitorクラスを使おう
Masamuneで実現したいこと:
- 例えば文字列がほしいならstringsというメソッドを呼ぶだけで、適切なノードを見つけたい
- 見つけたノードを配列に格納して返したい
Prismを使う前は、筆者は再帰的にRipperの出力を分割してそれぞれのノードを解析していました。ただし、Prismでは、Visitorというクラスがあります。これは何をするクラスかというと、作られた抽象構文木に対して探したいノードを簡単なメソッドでアクセスできるようにするクラスです。
module Masamune
  class AbstractSyntaxTree
    class StringsVisitor < Prism::Visitor
      attr_reader :token_value, :results
      def initialize(token_value)
        @token_value = token_value
        @results = []
      end
      def visit_string_node(node)
        results << node if token_value.nil? || token_value == node.content
        super
      end
    end
  end
end
ここで注目してほしいのはvisit_string_nodeです。もしStringNodeの中身をみたいならvisit_string_node、DefNode(関数宣言)のノードならvisit_def_nodeという具合に、こういったメソッドをオーバーライドするだけで中身を見ることができ、自由自在に扱うことができます。そして、superを呼ぶと、ASTの残りのノードを探してくれます。
筆者の場合は、resultsという配列に文字列のノードを格納して返しています。
感想
以上!とまあ、Ripperを直接使うより遥かに楽ですね。本当にいろんなメソッドがPrismのノードに準備してありますが、これは結構役に立つし、もっと使いたいなと思います。もし読者も使ったことがあるなら、是非下でコメントを残してください!
お読みいただきありがとうございます。