はじめに
RubyKaigiとの連動企画ということで、先日行ったRubyKaigi 2023でのLTのセルフ解説をしたいと思います。
題材
rspec-current.vimという、カーソルの位置に応じたsubjectやcontextの内容を返すRSpec用VimプラグインをRubyで書きました。LTではこのプラグインの解説をメインに行いました。
実装の解説
プラグインの名前こそ.vimですが、中身はほぼ全てがRubyで書かれています。特にRubyVM::AbstractSyntaxTreeクラスを使うところが肝で、ASTを取得することでそこから構文要素(例えばsubject { described_class.call }における"described_class.call"に当たる部分)を取り出すことができます。この際、Ruby3.2で追加されたkeep_tokens: trueオプションを利用する必要がありました。
トーク中で流したnode.children.last.children.last.tokens.map { _1[2] }.joinの部分ですが、最初のnodeがsubjectを包むITERノード、そのchildren.lastでSCOPEノード、そのさらにchildren.lastでCALLノード、そこにtokensメソッドを呼ぶことで各要素が配列の配列として返ってきます。各要素はその3番目の要素がトークンの文字列であるような配列であるので、_1[2]でそれを取得してjoinすると元のソースコードに近い文字列を得ることができます。このへんの詳細は金子さんの記事を参照するとわかりやすいです。
簡単なの?
発表の中でこのプラグインのようなものを作ることは"Easy"だと言いましたが、実際はどうなのでしょうか。たしかに、実装としてはASTをいじる以外に難しいことはやってはいません。
ただし、実装している最中はASTを手探りするしかありませんでした。各ノードの構造についてはドキュメントが一切なく(意図的なものだと思います)、そのためまずはRubyだけのVimとは無関係なファイルを用意してそこで色々実験して、得られた知見をVimプラグインに適用する形で実装を進めました。
rubyevalを用いる方法も、一度気づいてしまえばなんてことはないのですが、最初はRubyからVimに値を返す方法がわかっていませんでした。このへんはVimプラグインを書いたことがないということが大きいですね。