RBS Railsを使ってRailsアプリケーションにSteepを導入する
RBS Railsを使ってRailsアプリケーションにSteepを導入するまでの解説します。
ただしこの「導入できる」というのは、解析が完走するという意味です。 型エラーを全てつぶすことや、Steepを実際の開発フローに乗っけることは私もまだできていないため、この記事ではサポートしません。
登場人物
RailsアプリケーションにSteepを導入するには、複数のプロジェクトが関連してきます。 まずはそれらをざっと紹介します。
- steep gem
- Rubyの静的型検査器です。
- rbs_rails gem
- rbs gem
- ビルトインのライブラリ、及びスタンダードライブラリの型定義を提供します。
- また、型を書いていく上で便利なツールを
rbs
コマンドとして提供します。
- ruby/gem_rbs repository
- gemの型を提供します。
- activesupportなど、rails関連のgemの型はここから入手できます。
次の記事を読んでおくと、更に理解が深まるでしょう。
手順
では、RailsアプリケーションにSteepを導入する手順を確認していきましょう。
1. 使うGemをGemfileに追加する
まずは使うGemをGemfileに追加します。
RailsアプリケーションにSteepを導入するには、steep
, rbs
そしてrbs_rails
の1つのgemが必要です。
この記事では、それぞれsteep v0.39.0, rbs v1.0.0, rbs_rails v0.6.0 を使って検証しました。また、Rubyのバージョンは2.7.1を使っています(RBSやSteepはRuby 3でなくても問題なく動作します)。
# Gemfile group :development do gem 'steep', '>= 0.39.0', require: false gem 'rbs', '>= 1', require: false gem 'rbs_rails', '>= 0.6.0', require: false end
どのgemもRailsアプリケーションがrequireする必要はないため、require: false
を指定して構いません。
Gemfileを書いたらbundle install
しましょう。
$ bundle install
2. RBS Railsが定義するRake Taskを用意する
次に、RBS Railsが定義するRake Taskを用意しましょう。
次のコードをlib/tasks/rbs.rake
に置いてください。
require 'rbs_rails/rake_task' RbsRails::RakeTask.new
これで色々rake taskが定義されます。
とりあえずrbs_rails:all
taskだけを使えば良いでしょう。
3. RBS Railsが定義するRake Taskを実行する
rake taskを定義したら、それを実行しましょう。 これによって次の3つの処理が行われます。
- RBS Railsから、解析に必要なRBSファイルをコピーしてくる
- データベースのスキーマ定義から、対応するモデルのRBSファイルを生成する
- routes定義から、パスヘルパーのRBSファイルを生成する
なお、この2番目と3番目の処理は実際にRailsのアプリケーションを読み込んだ上で実行されます。 そのため多少不安定なことが予想されます。うまくいかなかったらRBS RailsのGitHub Issueや私のTwitterアカウントまで教えてください。
$ bin/rake rbs_rails:all
これによってsig
ディレクトリ下に型定義が生成されます。
RBS Railsからコピーされてきたファイルがsig/rbs_rails/sig
下に1、モデルの型定義がsig/app/models/
下に、パスヘルパーの定義がsig/path_helpers.rbs
に出力されます。
(2020-12-28追記: RBS Rails v0.7.0では、この辺のパスが多少変わりました。)
なおRBS Railsが生成するモデルの型定義は、Railsが自動的に定義するメソッドのみを定義します。
つまり、カラムに対応するメソッドや、has_many
によって定義されるメソッドは型が出力されます。ですが、ユーザーがモデルのファイル内でdef
を使って定義したメソッドの型は出力されません。
それらのメソッドは後述する方法で別途自動生成すると良いでしょう。
またパスヘルパーの型定義は、現状_RbsRailsPathHelpers
インターフェイスを定義するだけです。
このインターフェイスはデフォルトではどこからも参照されていません。そのため、パスヘルパーの定義を使うには、ユーザーの手でApplicationController
などにinclude _RbsRailsPathHelpers
などを書く必要があります。
この辺は将来的になんとかしたいですが、まだ手がつけられていないのが現状です。2
4. gem_rbsをダウンロードする
次に、gem_rbsリポジトリの内容をダウンロードします。
どこにどのような方法でダウンロードしても良いのですが、この記事ではgit submodule
を使います。
他の場所にダウンロードしたい場合には、適宜パスを読み替えてください。
# Rails.root 下に、gem_rbsをgit submoduleとしてダウンロード $ git submodule add https://github.com/ruby/gem_rbs.git gem_rbs $ ls gem_rbs/ bin Gemfile Gemfile.lock gems LICENSE README.md
5. RBSのvalidateをして、出たエラーを直す
では、ここまでの結果で手に入れたRBSをrbs validate
コマンドで検証してみましょう。
このコマンドによって、RBSが正しく書かれていることが検証できます。
次のようにrbs validate
コマンドを実行します。--repo
引数には、先程git submodule
でcloneしてきたディレクトリの中のgems
ディレクトリを指定します。
-r
引数には、必要なライブラリ、gemの名前を指定します。
-I
引数には、その他に書いたRBSが入っているディレクトリを指定します。3
$ bundle exec rbs -rlogger -rpathname -rmutex_m -rdate --repo=gem_rbs/gems -ractivesupport -ractionpack -ractivejob -ractivemodel -ractionview -ractiverecord -rrailties -I sig validate
これを実行すると、おそらく次のようなエラーが表示されたのではないでしょうか。
(2020-12-28追記: RBS Rails v0.7.0ではこの辺のエラーが出なくなりました。)
RBS::NoSuperclassFoundError: sig/app/models/something.rbs:1:0...154:3: Could not find super class: ApplicationRecord
これは、Something
モデルのスーパークラスに指定されているApplicationRecord
の型定義がない、というエラーです。
RBS Railsはモデルの型定義を生成しますが、ApplicationRecord
はデータベースのスキーマとしては存在しないため、RBS Railsの守備範囲外です。
そのためApplicationRecord
の型の定義はユーザーが行う必要があります。
まずは、適当に空のApplicationRecord
の型定義を書いてしまいましょう。
rbs validate
を通すため(== steep check
を走らせるため)には、とりあえずクラスの定義のみがあれば充分です。
より良い解析を行うためにはApplicationRecord
で定義されているメソッドの型を書く必要がありますが、ひとまずrbs validate
を通すことを優先します。
次のコードをsig/main.rbs
として保存します(名前は何でもいいです)。
# sig/main.rbs class ApplicationRecord < ActiveRecord::Base end
これでもう一度rbs validate
コマンドを実行すると、rbs validate
が成功すると思います。
もしくは別のエラーが出るかもしれませんが、その場合も(この段階では)おそらく同様にクラス/モジュール定義を書き足せば動くと思います。
6. Steepのセットアップをする
rbs validate
が無事通ると、最低限のRBSの準備ができました。
ですのでSteepの設定をして、Steepを実際に走らせてみましょう。
まず、次のコードをSteepfile
として置きます。4
(2021-01-01追記: RBS Rails v0.8.0ではSteepfileに書く必要があるライブラリが増えました。詳しくはRBS RailsのREADMEを見てください。)
このコードでは、先程rbs validate
を実行したときと同じ環境で動くよう、Steepも設定しています。
# Steepfile target :app do # 生成・手書きしたRBSファイルがあるディレクトリを指定する。 signature 'sig' # 解析したいRubyコードがあるディレクトリを指定する check 'app' # 先程ダウンロードした gem_rbs 内の gems ディレクトリへのパスを指定する repo_path "gem_rbs/gems" # rbs gem, gem_rbs からrequireしたいライブラリ名を指定する。 library 'pathname' library 'logger' library 'mutex_m' library 'date' library 'activesupport' library 'actionpack' library 'activejob' library 'activemodel' library 'actionview' library 'activerecord' library 'railties' end
これでapp
ディレクトリ下を解析する設定ができました。
7. Steepを実行する
では、steep check
コマンドで実際に型チェックをしてみましょう。
$ bundle exec steep check
おそらく、次のような型エラーが大量に出たのではないでしょうか。(こうならなかったら何かがおかしいので、頑張ってください。)
app/models/user.rb:21:46: NoMethodError: type=::Something, method=do_something (do_something)
おめでとうございます。これで「大量の型エラーは出るけどとりあえずSteepで型検査できる」状態になりました。
型定義を実用的にしていく
ここまででSteepを実行することはできました。ですがまだ大量の型エラーが残っていますし、型定義もほんの少ししかありません。 そのため、ここからは型定義を更に実用的にしていく方法を解説します。5
型定義の改善には、型定義を自動生成するのが簡単でしょう。 型定義を自動生成する方法は次の記事を参考にしてください。
ここではrbs prototype rb
を使った方法を紹介します。
rbs prototype runtime
でも、おそらく大きな困難はなく生成が行えると思います。6
TypeProfはうまく行くかわからない…。いい感じだったら教えてください。
基本的には型定義を生成した後、rbs validate
で型定義を検証し、エラーが出たら直してまたrbs validate
し、の繰り返しです。
rbs prototype rb
を使ってRBSを生成する
RBSを生成していく際にはまず依存の少ないところから生成していくのが良いでしょう。 ここで言う依存はクラスの継承ツリーです。つまり、継承ツリーのよりルートに近いものから生成していきます。
たとえば、concernは依存が少なくて楽なのでは…と予想しています。そこで、モデルのconcernの型だけを生成するコマンドを次に示します。7
$ bundle exec rbs prototype rb app/models/concerns/**/*.rb > sig/concerns.rbs
このコマンドを実行すると、モデルのconcernの型定義をsig/concerns.rbs
に吐き出します。
そうそう、先程手書きしたApplicationRecord
も、rbs prototype
で生成してしまっても良いでしょう。
なお、依存関係を考えずにとりあえずアプリケーションのコード全てに対してRBSを生成してしまう手もあります。つまり、bundle exec rbs prototype rb app/**/*.rb lib/**/*.rb
のように実行します。
この方法ならばアプリケーション内の型定義を一気に生成できます。ただし、次に解説するrbs validate
を実行してエラーを直す作業にかかる時間が長くなるので、細かく分割したほうが楽かなあ、と予想しています。8
rbs validateする
型定義を生成したら、rbs validate
コマンドで生成した型定義を検証しましょう。
# 再掲 $ bundle exec 1rbs -rlogger -rpathname -rmutex_m -rdate --repo=gem_rbs/gems -ractivesupport -ractionpack -ractivejob -ractivemodel -ractionview -ractiverecord -rrailties -I sig validate
おそらくApplicationRecord
が足りなかったときと同様に、何かしらのエラーが出ると思います。
これらのエラーを全て修正することで、steep check
が動くようになります。
次によく見ることになりそうなエラーを2つと、その対処法を解説します。
モジュールやクラスが足りていない場合
モジュールやクラスが足りていなさそうなエラーが出た場合、ApplicationRecord
と同様にそれを適当に足してください。
具体的には次のエラーが該当します。(ほかにもあるかも)
RBS::NoSuperclassFoundError
- superclassの型がない
RBS::NoMixinFoundError
- include/extendしているモジュールの型がない
RBS::NoTypeFoundError
class A::B
のようにB
クラスを定義しているときに、A
の型がない- エラーの名前的に他の状況でも出そうだけど、自動生成の文脈ではこのケースが多いかな
アプリケーション内で定義しているモジュールやクラスの型定義が足りなければ、先にそれをrbs prototype
で自動生成すると良いでしょう。
gemの型定義が足りていなければ、ApplicationRecord
でやったようにとりあえず空の型定義を手書きしてしまうのが楽でしょう。9
もしくは型定義がgem_rbsに存在するgemの場合、Steepfile
とrbs validate
でそのgemを指定することもできます。
ただし2020-12-25現在では、まだほとんどgemの型定義はありません(Rails系を除くとlisten, rainbow, redis, retryableの4つのみ)。
gemの型定義を書く気力があれば、トライしてみるのも良いと思います。
https://github.com/ruby/gem_rbs/tree/main/gems
RBS::DuplicatedMethodDefinitionError
RBSでは1つのクラスに同名のメソッドが2つ定義されていると、RBS::DuplicatedMethodDefinitionError
クラスのエラーになります。
# test.rbs class C def foo: () -> String end class C # 別の箇所でもう一度 foo を定義 def foo: () -> Integer end
$ rbs -I . validate --silent /path/to/lib/rbs/definition_builder/method_builder.rb:30:in `block in validate!': test.rbs:4:2...4:24: ::C#foo has duplicated definitions in test.rbs:9:2...9:24 (RBS::DuplicatedMethodDefinitionError) (snip)
そのため、どちらかのメソッド定義を削除するか、どちらかのメソッド定義に| ...
を付け足す必要があります。
# test.rbs class C def foo: () -> String end class C # これは () -> Integer | () -> Stringという意味になる。 def foo: () -> Integer | ... end
Steepで解析する範囲を狭くする
ここまでは型定義を充実させる方法について解説してきました。一方、型解析を実用的にしていくためには、ひとまずSteepで解析する範囲を狭くしてしまうのも1つの方法です。
先に挙げたSteepfile
では、app
ディレクトリ下全てを解析対象としていました。
これだと当然ながら、解析範囲がとても大きくなってしまって、エラーに全て目を通すことはむずかしいです。
そのため最初のうちは解析しやすいファイルを個別に見ていくのが良いと思います。
そのためには、Steepfile
のcheck
に指定するパスを変更します。
たとえば私が関わっているRailsアプリケーションではapp/services/
下にPOROのコードが集まっていました。
POROは変なことをしていないため解析がしやすいです。また、モデルを触ることが多いので、RBS Railsによって自動生成された型が動いていることも確認しやすいです。
次のようなSteepfile
を書くとapp/services
のみを解析対象にできます。
# Steepfile target :app do signature 'sig' # check 'app' check 'app/services' # ← app/services のみをチェックする repo_path "gem_rbs/gems" # rbs gem, gem_rbs からrequireしたいライブラリ名を指定する。 library 'pathname' library 'logger' library 'mutex_m' library 'date' library 'activesupport' library 'actionpack' library 'activejob' library 'activemodel' library 'actionview' library 'activerecord' library 'railties' end
まとめ
RailsアプリケーションにSteepを導入する方法を紹介しました。 Ruby 3もリリースされたことですし、冬休みにはRailsアプリケーションにSteepを導入してみてはいかがでしょうか。