こんにちは、ソフトウェアエンジニアの千葉です。RubyKaigi も最終日の3日目、会場は大いに盛り上がっています。次々やってくる発表がわくわくの連続で、居ても立っても居られない状況です…!
今回、日本最大の Ruby に関するカンファレンスである RubyKaigi に Wantedly がスポンサードし、いくつかの講演を聴講させていただいています。
RubyKaigi の3日目、Kevin Newton さんによる「Syntax Tree」の発表があり、ここで syntax_tree gem が紹介されました。この記事では、今回紹介された syntax_tree gem について、および Ruby の parser についての状況をまとめます。
Ripper を扱いやすく wrap する syntax_tree gem
今回発表された syntax_tree は CRuby 本体の parser である、Ripper をベースとした parser 及び formatter ライブラリです。
syntax_tree は Ripper が parse した結果の AST を 木構造のオブジェクトとして返してくれます。
[1] pry(main)> require 'syntax_tree'
=> true
[2] pry(main)> SyntaxTree.parse("f(a, 1)")
=> (program (statements ((fcall (ident "f") (arg_paren (args ((vcall (ident "a")), (int "1"))))))))
木構造は Node という以下のような実装で書かれたオブジェクトから構成され、位置情報やコメントなどの取得が行えたり、パターンマッチが行えるなどの豊富なインターフェイスを備えています。
# VCall represent any plain named object with Ruby that could be either a
# local variable or a method call.
#
# variable
#
class VCall < Node
# [Ident] the value of this expression
attr_reader :value
# [Array[ Comment | EmbDoc ]] the comments attached to this node
attr_reader :comments
def initialize(value:, location:, comments: [])
@value = value
@location = location
@comments = comments
end
def accept(visitor)
visitor.visit_vcall(self)
end
def child_nodes
[value]
end
alias deconstruct child_nodes
def deconstruct_keys(_keys)
{ value: value, location: location, comments: comments }
end
def format(q)
q.format(value)
end
end
(https://github.com/ruby-syntax-tree/syntax_tree/blob/75cc00a769c28a49ec42e35b9a09bfbe175d8264/lib/syntax_tree/node.rb#L9607-L9642 より)
また、Visitor という、AST を走査するための機構を備えており、特定の種類の Node を走査するような実装を簡単に書くことが出来ます。
class ArithmeticVisitor < SyntaxTree::Visitor
def visit_binary(node)
if node in { left: SyntaxTree::Int, operator: :+ | :- | :* | :/, right: SyntaxTree::Int }
puts "The result is: #{node.left.value.to_i.public_send(node.operator, node.right.value.to_i)}"
end
end
end
visitor = ArithmeticVisitor.new
visitor.visit(SyntaxTree.parse("1 + 1"))
# The result is: 2
(https://github.com/ruby-syntax-tree/syntax_tree#visitor より)
また、 formatter の機構を備えており、format も行ごとの文字数に応じた折返しなどを行ったりしています。
このうち、柔軟な折返しの機構などは prettier_print という gem に切り出さされています。
総合して、様々な情報を扱いやすいインターフェイスで取得でき、豊富な機能を備え、これを使った静的解析は非常に行いやすそうです。
補足: Ruby コードの Parse に何使うか問題
syntax_tree の良さの背景として、Ruby の静的解析を行うときなどに Ruby コードから AST への Parse に何を使うかの問題があります。
Ruby 本体には Ripper というパーサー実装が存在しますが、ドキュメントの少なさや、独特のデータ形式やインターフェイス、メタデータの取りにくさ、などにより parser gem などのサードパーティ実装が代わりに使われることが多い、という状況でした。
例えば、 RuboCop は内部で使用する Ripper から parser gem に乗り換えを行っており、その際の機能の比較を以下の記事でまとめています。
ただ、サードパーティ実装には、その開発体制や実装が異なることによる非互換などの不安要素もあります。例えば parser gem は、Ruby 本体の parser とは完全に別の実装であることから、CRuby とは異なる Parse をしてしまうことも (まれに) あります。
今回紹介された syntax_tree では、扱いやすいインターフェイス、豊富なメタデータを含んでいること、ドキュメントなどもあることから、 これを用いることで Ripper の問題の克服ができそうです。
また、 syntax_tree は他のライブラリ (rubocop-ast, parser, ruby_parser) 用のデータへの変換も行うことが出来るようになっていて、置き換えなども行いやすそうです。
以下のようなコードで変換を行うことが出来ます。
buffer = Parser::Source::Buffer.new("(string)")
buffer.source = source
visitor = SyntaxTree::Translator::Parser.new(buffer)
node = visitor.visit(program)
(https://github.com/ruby-syntax-tree/syntax_tree-translator より)
実際に Syntax Tree を試すには
syntax_tree は Gem として公開されていて、https://github.com/ruby-syntax-tree/syntax_tree から使い方を知ることが出来ます。CLI などを組み込んでいて、 AST の表示や format は CLI から行うことが出来ます。
また、これを組み込んだ Language Server として https://github.com/Shopify/ruby-lsp が公開されています。 VSCode extension としても利用可能です。
補足: Web 上でのデモもある
また、Syntax Tree は https://ruby-syntax-tree.github.io/ で Web 上で実際に使ってみることが出来ます。
このウェブサイトの設計については、以下の記事で解説がされています。(なんとデモには WebAssembly を利用して構築されているようです 👀)
Ruby 本体の parser の今後にも期待できそう
今回 Syntax Tree の発表を行った Kevin Newton さんは、CRuby のコミッターで、CRuby の parser の再実装を今後進めていくそうです。(Day2 の Ruby Committers vs The World でその宣言が行われました)
Ruby 本体の parser の課題はなにか、今後どうしていくのか、ということについては、以下のツイートにまとまっています。
parser の改善は、 syntax_tree が終わりではなく、今後 Ruby 本体の parser の改善を行っていくとのことで、今後が楽しみです!