rbs-inlineのコードを読む
イネーブリングチームの東です。
先日、RubyKaigi2024に参加してきました。家庭の都合もあり2日目のみの参加でしたが、興味深いセッションをいくつか聞くことができました。
Embedding it into Ruby codeもその一つで、soutaroさんが開発したrbs-inlineについてのセッションです。これは、Rubyの型情報をコメントに埋め込むことで、rbsファイルを別途作成せずに型情報を記述できるツールです。
Rubyの型チェックはTOKIUMではまだ扱えていないのですが、静的型付けを扱った身としてはやはりrbsファイルのように実装と型定義が分かれてしまうのは厳しいなと思っています。rbs-inlineが発展していくと、Rubyの型チェックがいよいよ実用的になっていくかもしれません。
rbs-inline自体の使い方はシンプルでgithubのREADMEやwikiをみれば理解できると思うので、興味がある方はぜひ試してみてください。
参加レポは他の会社の方もたくさん上げてくれると思うので、今回はせっかくなのでrbs-inlineのコードを少し読んでみることにします。
(2024/05/17時点での最新のコードはv0.3.0です)
rbs-inlineの主要な流れ
bundle exec rbs-inline
を実行したとき、exe/rbs-inline
が呼ばれます。このファイルはエントリーポイントで、その中でRBS::Inline::CLI#run
を呼び出しています。
RBS::Inline::CLI#run
はオプションをパースしたのち、RBS::Inline::Parser.parse
を通してrubyファイル内のコメントをパースし, RBS::Inline::Writer.write
を通してrbsファイルを出力します。
RBS::Inline::CLI
RBS::Inline::CLI
はrbs-inlineのCLIを実装するクラスです。
後述のRBS::Inline::Parser
やRBS::Inline::Writer
を呼び出していますが、その他にもOptionParser
を使ってオプションをパースしています。
処理全体を指揮していますが、このクラス自体は比較的薄いので、RBS::Inline::Parser
やRBS::Inline::Writer
を読んでいくことにします。
RBS::Inline::Parser
RBS::Inline::Parser
はPrism
のパース結果からrbs-inlineのASTにパースするクラスです。
前述の通り、周辺のコードを読むと、Prism
を使って実現されていることがわかります。(使ったことはない...)
Prism
は、RubyのソースコードをパースしてASTを生成するライブラリです。RBS::Inline::Parser
への入力もPrism.parse_file
を通してRubyのコードを読み込んでいます。
また、RBS::Inline::Parser
自身もPrism::Visitor
を継承しており、visit_xxx
メソッドをオーバーライドして各ノードに対して処理しています。このオーバーライド自体も@rbs override
でrbsアノテーションされています。Rubyのメソッドがオーバーライドしているのか・していないのかわかりやすくなっていますね。
module RBS
module Inline
class Parser < Prism::Visitor
# ↓ overrideしてるのわかりやすい
# @rbs override
def visit_class_node(node)
return if ignored_node?(node)
visit node.constant_path
visit node.superclass
rbs-inlineは# @rbs
や#:
といった特定の形式のコメントを対象に処理します。
その部分はRBS::Inline::AnnotationParser
で実装されているようです。
RBS::Inline::AnnotationParser
RBS::Inline::AnnotationParser
はrbs-inlineでサポートされているアノテーションをパースするクラスです。
内部でRBS::Inline::AnnotationParser::Tokenizer
クラスが定義されており、サポートされているアノテーションはKEYWORDS等の定数で定義されています。
module RBS
module Inline
class AnnotationParser
class Tokenizer
...
# ↓ useとかin, outはまだwikiにないっぽい
KEYWORDS = {
"returns" => :kRETURNS,
"inherits" => :kINHERITS,
"as" => :kAS,
"override" => :kOVERRIDE,
"use" => :kUSE,
"module-self" => :kMODULESELF,
"generic" => :kGENERIC,
"in" => :kIN,
"out" => :kOUT,
"unchecked" => :kUNCHECKED,
"self" => :kSELF,
"skip" => :kSKIP,
"yields" => :kYIELDS,
} #:: Hash[String, Symbol]
よく見てみるとuse
など、まだwiki等に明記されていないアノテーションもあったりしますね。
アノテーションに対応するASTのクラスはRBS::Inline::AST::Annotations
モジュール下に配置されています。
また、RBS::Inline::AnnotationParser::ParsingResult
クラスが定義されており、パースした結果はこのクラスで返されます。
RBS::Inline::AST
モジュールについてですが、本家のrbsのモジュール構造もRBS::AST
配下にAnnotation
やDeclarations
などが配置されているので、rbs-inlineもそれに倣っているのかなと思います。コードの中身をみてもRBS::AST
を使っている部分が多いので、ここがrbsとの結合部分なのかもしれません。
RBS::Inline::Writer
RBS::Inline::Writer
はRBS::Inline::Parser
でパースしたASTをrbsファイルに書き出すクラスです。
こちらも内部で本家のrbsと結合しており、rbsファイルへの書き出し自体はRBS::Writer
に任せて、RBS::Inline::AST
モジュール内のクラスをRBS::AST
モジュールにマッピングしているようです。
module RBS
module Inline
class Writer
...
def translate_member(member)
case member
when AST::Members::RubyDef
if member.comments
comment = RBS::AST::Comment.new(string: member.comments.content, location: nil) # このあたりとかがRBS::Inline::AST -> RBS::AST変換
end
rbs自体がwriterの仕組みを提供することによって、rbsファイルの入出力を簡単に実装できているのは美しいなと感じました。社内でライブラリを作るときは、このような設計を参考にしたいですね。
まとめ
僕自身もコードコメント上にアノテーションを仕込んで開発プロセスに役立てるという仕組みを@tokiumjp/dictで実現したことがあったので、同じくコメントに情報を埋め込むrbs-inlineは非常に興味をそそられました。
dictのほうはPrismなど言語標準のものは使わなかったのですが、コメントのパース以外でも結構苦労して読みにくいコードになった記憶があり...rbs-inlineはきれいに設計されているなあと感じました。soutaroさんすごい。
また、小さいプロジェクトだったので、全容を理解するのが難しくなく、久しぶりにRubyのコードを読むのにもってこいのプロジェクトでした。
この規模なら自分にも何かできるかも、と思えてきますね。もうちょっと時間ができたらしっかり読んでみたいです。
TOKIUMでは共に働く仲間を募集しています。
一緒に開発をハックしましょう。
Discussion