LoginSignup
2
2

Prismを使ってみた感想

Posted at

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)の情報しか含まれていません。

require "ripper"

p Ripper.sexp(code)
#=> [:program,
#=>   [
#=>     [:string_literal,
#=>       [:string_content,
#=>         [:@tstring_content, "foo", [1, 1]]
#=>       ]
#=>     ]
#=>   ]
#=> ]

けれども、Prismではそのノードがどこで始まるかだけじゃなくて、どこで終わるかも取得できます。ifやwhileなどのブロックでは、そのブロックがどこで終わっているかを探し出せるということです!かなりすごい。

Masamune

筆者はなぜその位置情報がそんなにほしいかと言ったら、Bullet TrainMasamuneというツールを使っているからです。Bullet Trainのconfig/routes.rbではnamespaceresourceのブロックをたくさん修正することがあります。それでMasamuneを使うと、ブロックの始まりがどこなのか分かることができます。

ただし、v2.0.0までは生のRipperを使っていました。かなりハードな作業でした...

それでまあ、生のRipperのままだけで上手く行っていたけど、その実装自体がちょっと複雑でした。
Prismを使ったらいいんじゃない?と上司からのアドバイスをいただきました。

それでPrismにアップデートしましたが、Prismの作者であるKevinさんもその実装についてアドバイスを与えてくれたので、興味があればプルリクエストの方を読んでみてください。

内容としては下記の通りです。

Visitorクラスを使おう

Masamuneで実現したいこと:

  1. 例えば文字列がほしいならstringsというメソッドを呼ぶだけで、適切なノードを見つけたい
  2. 見つけたノードを配列に格納して返したい

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_nodeDefNode(関数宣言)のノードならvisit_def_nodeという具合に、こういったメソッドをオーバーライドするだけで中身を見ることができ、自由自在に扱うことができます。そして、superを呼ぶと、ASTの残りのノードを探してくれます。

筆者の場合は、resultsという配列に文字列のノードを格納して返しています。

感想

以上!とまあ、Ripperを直接使うより遥かに楽ですね。本当にいろんなメソッドがPrismのノードに準備してありますが、これは結構役に立つし、もっと使いたいなと思います。もし読者も使ったことがあるなら、是非下でコメントを残してください!

お読みいただきありがとうございます。

2
2
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
2
2