なんか色々がちゃがちゃやっていて面白いので、メモしておく。
rbs_rails とは
Rails で RBS (Rubyの型定義ファイル)を扱うためのライブラリ。
「rbs_rails に Rails の型を取り込む」とは
1つは、ユーザーの書いたRailsアプリケーションのコードから、型定義ファイルを生成すること。
例えば、users
テーブルにaccount
というString
型のカラムがあったら、User
クラスの型にaccount
というString
を返すインスタンスメソッドを定義する。
こちらの機能は今回の記事には関係ない。
もう1つの機能は、Rails 自体の型定義を提供すること。
例えば Rails のコードを読み込むとActiveRecord::Base
クラスが定義されるが、それを型として使うにはActiveRecord::Base
を RBS でも定義する必要がある。
そのため、rbs_rails は Rails 自体の型定義を提供している。
この Rails 自体の型定義を提供するにあたって、これを手書きしていたら時間がいくらあっても足りない。 そのため、Rails のソースコードから型定義を自動生成した上で、それを手直しして提供している。
この記事ではその Rails 自体の型定義をどう自動生成しているかについて書く。
生成手順
以下の手順で生成している。
rbs prototype
コマンドで RBS ファイルを生成- 型パラメータを埋めるコマンドを実行
- 1で生成した RBS ファイルに Syntax Error などがあるとこの処理がコケるので、適宜手で直す
ActiveSupport::Concern
によるClassMethods
モジュールをextend
する処理を実行rbs validate
を実行して、エラーになったところを直す。
以下に各 step を解説する。
rbs prototype
コマンドで RBS ファイルを生成
rbs
コマンドが提供するrbs prototype
サブコマンドを使うと、RBS ファイルのプロトタイプが生成できる。
Ruby プログラムから RBS を生成するにはrbs prototype runtime
とrbs prototype rb
の2種類があり、rbs_rails では後者を採用している。
runtime
の方は対象のプログラムを読み込み動的に解析するためメタプログラミングによって生成されたメソッドなどにも対応できるのが利点だが、動的に動かすのは面倒なので諦めた。
なお、rbs_rails ではrbs prototype rb
をそのまま使うのではなく、手を加えたものを使っている。
これはActiveSupport::Concern
のためである。
extend ActiveSupport::Concern
したモジュールではclass_methods
メソッドが有効になり、このメソッドに渡したブロックはClassMethods
クラスの本文として実行される。
つまり以下の2つのコードは等価である。
module M extend ActiveSupport::Concern class_methods do def foo end end end
module M extend ActiveSupport::Concern class ClassMethods def foo end end end
ただしrbs prototype rb
はclass_methods
を解釈しないため、このケースではfoo
は単なるM
のインスタンスメソッドとして定義されてしまう。
これを防ぐため、rbs prototype rb
を拡張して使用している。
これは次のように使う
# Active Record の型を生成する例 $ bin/rbs-prototype-rb.rb prototype rb /path/to/rails/activerecord/lib/**/*.rb > assets/sig/generated/activerecord.rbs
ここまでで対象のライブラリの型が生成される。 ただし、このままだと読み込んで使える RBS になっていないので、手を入れる必要がある。
型パラメータを埋めるコマンドを実行
まずは、型パラメータを埋める必要がある。
Array
のように型パラメータを持つクラスを継承したクラスがある場合、rbs prototype rb
では次のような RBS が生成される。
# .rb ファイル class A < Array end
# .rbs ファイル class A < Array end
これはうまく動かない。なぜならばArary
は要素の型を型パラメータとして受け取る必要があるためである。
これを解決するために、bin/add-type-params.rb
を書いた。
このプログラムは必要な型パラメータを挿入する。
たとえば上記の .rbs
ファイルは次のようになる。
class A[T] < Array[T] end
なおこのコマンドは生成された.rbs
ファイルの構文が正しいことを前提としている。
rbs prototype rb
はしばしばsyntax errorのコードを生成する(これはバグなので直すべきだと思う)ので、このコマンドがうまく動かない場合、まずsyntax errorを直す必要があるだろう。
syntax errorを修正するコミットの例
- https://github.com/pocke/rbs_rails/pull/37/commits/c873094b8f3231bf618ea0143a22519e749369d1
- https://github.com/pocke/rbs_rails/pull/37/commits/69cc1eb1f3c5e7df9e5c1c39dfa3c7ecbe39bff3
ActiveSupport::Concern
によるClassMethods
モジュールをextend
する処理を実行
ActiveSupport::Concern
をextend
したモジュールをinclude
すると、そのinclude
先のモジュール/クラスにClassMethods
モジュールがextend
される。
module M extend ActiveSupport::Concern class ClassMethods def foo end end end class C # これは同時に extend M::ClassMethods も行う include M end
ところがこのメタプログラミングは RBS の世界では行われないため、別途手動でextend
する必要がある。
そのために、該当のモジュールを自動でextend
するスクリプトを作成した。
$ ruby bin/postprocess.rb -rlogger -rmutex_m -I assets/sig -I sig assets/
上記コードのように実行すると、各種ライブラリを読み込んだ上でassets/
下のファイルについて必要があればextend ClassMethods
する。
rbs validate
を実行して、エラーになったところを直す。
これが一番地味で、めんどくて、時間がかかるところ。
今までの作業で生成したファイルをrbs validate
コマンドで検証して、怒られたところを直していく。
主に必要な作業は次の通り。
- 足りない型を足す
- 例えば
class Foo < Logger
と書かれていたら、Logger
クラスの型が必要になるので足す。 - rbs gem が型を提供しているライブラリであれば、
-rlogger
のように require すれば良い。提供されていなければ、仮の型を書く。
- 例えば
- 継承元で定義されたクラスの参照を修正する
- メタプログラミングで定義されたクラスが必要になる場合、それを足す。
など、他にもいくらかやることがある。
ここまでやってrbs validate
が通れば作業は完了で、Steep から生成した RBS ファイルを読み込んで使えるようになる。
ただし使えるようになると言っても多くの型はuntyped
になっているため、より実用的にするには手動で型を埋めていく必要がある。
これはまだやっていない。
こんな感じで rbs_rails で提供する Rails の型を増やしていっている。 今の所、activesupport, activemodel, activerecord, railties の型を生成した。