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::NoTypeFoundErrorclass 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を導入してみてはいかがでしょうか。