最近、Ruby の型チェッカーである Sorbet とその周辺ツールの Tapioca を色々試してみています。
Tapioca は型定義 (RBI) に関することを色々やってくれるツールで、RBI 版 DefinitelyTyped から RBI を取得してくれる他、 Rails や他の DSL に対応した RBI を生成してくれたり、 gem から RBI を生成してくれるなど、 RBI に関することを本当に色々やってくれます。(他にも色々機能があります)
…ここで気になるのが、 Tapioca が DSL から RBI を生成する部分です。この記事では、 Tapioca のコードなどに目を通しながら、どのようにして RBI を生成しているのかを整理してみました。
Tapioca の DSL compiler のしくみ
Tapioca が DSL から 型定義を生成する機構は、 DSL compiler といいます。
Tapioca 自身が ビルドインとして Rails などの DSL に対応した Compiler を持っているほか、カスタムの DSL Compiler を書いて、独自の DSL に対応した型定義を生成することができます。(※ DSL 自体は Rails 以外のも対応可能ですが、 DSL Compiler は今のところ Rails アプリケーションでのみ使用可能です。)
例として ActionMailer の DSL Compiler を見てみます。 ActionMailer ではインスタンスメソッドに対応したクラスメソッドが自動的に定義されます。以下は、そのクラスメソッドの型定義を生成します。
module Tapioca
module Dsl
module Compilers
class ActionMailer < Compiler
extend T::Sig
ConstantType = type_member { { fixed: T.class_of(::ActionMailer::Base) } }
sig { override.returns(T::Enumerable[Module]) }
def self.gather_constants
descendants_of(::ActionMailer::Base).reject(&:abstract?)
end
sig { override.void }
def decorate
root.create_path(constant) do |mailer|
action_methods_for_constant.each do |mailer_method|
method_def = constant.instance_method(mailer_method)
parameters = compile_method_parameters_to_rbi(method_def)
mailer.create_method(
mailer_method,
parameters: parameters,
return_type: "::ActionMailer::MessageDelivery",
class_method: true,
)
end
end
end
# ...
end
end
end
end
参考: https://github.com/Shopify/tapioca/blob/v0.11.8/lib/tapioca/dsl/compilers/action_mailer.rb
Tapioca DSL Compiler はどのような環境で実行されるか
それではこれらの DSL Compiler はどのような環境で実行されているのでしょうか。簡潔に言うと、DSL Compiler は Rails アプリケーションを実際に読み込んだ上で、 DSL Compiler が Ruby オブジェクトから情報を収集し、RBI ファイルとして書き出します。
つまり静的解析ではなく Ruby のリフレクション機構を利用した解析、parser を使った DSL の構文解析をするのではなく、 DSL の評価済みのオブジェクトに対して解析を行う、ということになります。
以下の箇条書きの流れで、実行されます。
-
tapioca dsl
またはtapioca init
コマンドを実行- Rails アプリケーションのコード、 DSL Compiler のコードを順に
- 各 DSL Compiler が処理の対象としたい定数をリストアップ
- 定数ごと、DSL Compiler ごとに、RBI ファイルの内容を生成する処理を実行
- 定数ごとに、生成した RBI ファイルの内容を書き出し
DSL Compiler を読む
それでは DSL Compiler の構成について見てみましょう。先程の ActionMailer 用の DSL Compiler の実装を参考にしながら見てみます。
module Tapioca
module Dsl
module Compilers
class ActionMailer < Compiler
extend T::Sig
ConstantType = type_member { { fixed: T.class_of(::ActionMailer::Base) } } # 対象としたい定数の型を定義する型変数
sig { override.returns(T::Enumerable[Module]) }
def self.gather_constants # `gather_constants` クラスメソッドで、対象としたい定数をリストアップ
descendants_of(::ActionMailer::Base).reject(&:abstract?)
end
sig { override.void }
def decorate # `decorate` インスタンスメソッドで、定数 (constant) ごとに RBI ファイルの内容を生成する
root.create_path(constant) do |mailer| # root は書き込む対象となる RBI ファイルのルートを指すオブジェクト。 `create_path` で定数 (constant) に対応した名前空間を RBI ファイルに追加する。
action_methods_for_constant.each do |mailer_method|
method_def = constant.instance_method(mailer_method)
parameters = compile_method_parameters_to_rbi(method_def)
mailer.create_method( # メソッドの signature を RBI ファイルに追加する。
mailer_method,
parameters: parameters,
return_type: "::ActionMailer::MessageDelivery",
class_method: true,
)
end
end
end
private
sig { returns(T::Array[String]) }
def action_methods_for_constant
constant.action_methods.to_a
end
end
end
end
end
参考: https://github.com/Shopify/tapioca/blob/v0.11.8/lib/tapioca/dsl/compilers/action_mailer.rb
DSL Compiler は Tapioca::Dsl::Compiler クラスを継承し、以下の2つの abstract method を実装します。
- gather_constants: 処理の対象としたい定数をリストアップするクラスメソッド
- decorate: 定数ごとに、RBI ファイルの内容を生成するインスタンスメソッド
実装したファイルは sorbet/tapioca/compilers
ディレクトリ以下に配置します。このディレクトリに配置した DSL Compiler は、 tapioca dsl
または tapioca init
コマンドを実行した際に、自動的に読み込まれます。
(また、各 Gem 内の tapioca/dsl/compilers ディレクトリ以下のファイルも読み込んで、、その中の DSL Compiler も自動的に読み込まれるようです。 Ref: https://github.com/Shopify/tapioca/blob/v0.11.8/lib/tapioca/loaders/dsl.rb#L63-L65)
Compiler による RBI ファイルへの書き込み方法
Tapioca::Dsl::Compiler#decorate
での RBI ファイルの内容の生成は、 root
オブジェクト (RBI::Tree クラスのインスタンス) を通して行います。
このオブジェクトの実装は rbi gem で行われています。 Tapioca gem 内にも、独自の拡張 が行われていて、これらのメソッドを通して RBI ファイルの内容を生成します。
Compiler クラスのメソッドに関して
RBI ファイルへの型定義の追加を簡潔に記述できるよう、定数の列挙を簡単にするために、以下のクラスやモジュールのモジュールのメソッドが提供されています。
より詳しく知るには
- 公式のチュートリアル: https://github.com/Shopify/tapioca#writing-custom-dsl-compilers
- 既存の DSL Compiler の実装: https://github.com/Shopify/tapioca/tree/v0.11.8/lib/tapioca/dsl/compilers