✍️

rbs-inlineのコードを読む

2024/05/22に公開

イネーブリングチームの東です。

先日、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::ParserRBS::Inline::Writerを呼び出していますが、その他にもOptionParserを使ってオプションをパースしています。
処理全体を指揮していますが、このクラス自体は比較的薄いので、RBS::Inline::ParserRBS::Inline::Writerを読んでいくことにします。

RBS::Inline::Parser

RBS::Inline::ParserPrismのパース結果からrbs-inlineのASTにパースするクラスです。
前述の通り、周辺のコードを読むと、Prismを使って実現されていることがわかります。(使ったことはない...)
Prismは、RubyのソースコードをパースしてASTを生成するライブラリです。RBS::Inline::Parserへの入力もPrism.parse_fileを通してRubyのコードを読み込んでいます。
また、RBS::Inline::Parser自身もPrism::Visitorを継承しており、visit_xxxメソッドをオーバーライドして各ノードに対して処理しています。このオーバーライド自体も@rbs overriderbsアノテーションされています。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配下にAnnotationDeclarationsなどが配置されているので、rbs-inlineもそれに倣っているのかなと思います。コードの中身をみてもRBS::ASTを使っている部分が多いので、ここがrbsとの結合部分なのかもしれません。

RBS::Inline::Writer

RBS::Inline::WriterRBS::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では共に働く仲間を募集しています。
一緒に開発をハックしましょう。

株式会社TOKIUM テックブログ

Discussion