💎

RBS CollectionをRailsアプリで試してみよう

9 min read

Leaner 開発チームの黒曜(@kokuyouwind)です。

先週行われたRubyKaigi Takeout 2021に参加しました。今回も興味深いセッションが多かったですが、なかでもThe newsletter of RBS
updates
では型システムの進化が感じられて面白い内容でした。

https://docs.google.com/presentation/d/e/2PACX-1vREU6ZguqLxGk_k1l3zvKbRo_TbMTKN3yEgfzrjA85foVXrmeYvWnOTefsaBycsb9m6H924VsZw_YKt/pub?start=false&loop=false&delayms=3000&slide=id.p

発表内で紹介されていた rbs collection を使って既存 Rails アプリケーションへの型検査導入を試してみたので、本記事で手順などをまとめて紹介します。

なお今回の Rubykaigi には自分の他にころちゃん(@corocn)も参加していました。
ころちゃんは debug.gem の話を記事にまとめているので、そちらもぜひご覧ください。

https://zenn.dev/leaner_tech/articles/20210915-rubykaigi-2021-debug-gem

rbs collection について

(この節は The newsletter of RBS updates の発表内容からの抜粋・要約です)

RBS とは

RBS は Ruby のための型記述言語です。 Ruby 3.0 で導入されました。
クラスやモジュールの構造を RBS に書き下すことができ、型検査などの用途に利用できます。

RBS の仕様自体は Ruby に含まれており、標準ライブラリの RBS ファイルが提供されている他、サードパーティーの gem の RBS ファイルを提供する gem_rbs_collection リポジトリも運用されています。

この型定義を利用するツールとして、型検査ツールである Steep や型解析器の TypeProf などがあります。[1]

既存の RBS 依存解決の問題点

RBS 関連のツールを利用する際、これまでは型定義リポジトリである gem_rbs_collection から依存 Gem ごとの RBS を手動で解決する必要がありました。
しかしこの作業には以下のような問題が存在します。

  • Gem のバージョンによっては複数リビジョンの gem_rbs_collection を使い分ける必要があり、依存関係解決が大変
  • ツールごとに依存解決を記述する設定ファイルやオプションの渡し方が異なるため、それぞれを記述しないといけない
    • rbs コマンドでは rbs -rlogger -r pathname --repo=gem_rbs_collection validate のようにコマンドラインオプションを渡す
    • TypeProf も同様に type -rlogger -r pathname --repo=gem_rbs_collection target.rb といったコマンドラインオプションを渡す
    • Steep では設定ファイルである Steepfile に repo_pathlibrary を記述する

これらの問題を解決するためのツールが rbs collection です。

rbs collection の機能

rbs collection は RBS ファイルの依存関係を解決するバンドルツールです。

このツールでは Gemfile.lock を用いて依存ライブラリとそのバージョンを解析し、そのバージョンに合わせた RBS ファイルをダウンロードします。
また Steep などのツールで個別に依存解決を設定する代わりに rbs collection の情報を利用することで、依存関係設定を集約できます。
手動での RBS ファイル運用から解放される、非常に便利なツールですね。

rbs collection は Ruby 3.1 にバンドルされる RBS v2 で正式リリースされる予定ですが、最新の RBS を導入することで実験的な機能として利用できます。

Rails アプリの型検査に rbs collection を利用してみる

この節では、既存の Rails アプリケーションに rbs collection を利用して型検査を導入する手順と所感をまとめていきます。
なお前節に書いたとおり rbs collection はまだ実験的な段階なので、実アプリでの利用は Ruby 3.1 での GA を待ったほうが良いでしょう。

型検査関連 Gem の導入

Gemfile に以下を記述し、rbs collection が含まれたRBSと型検査ツールのSteep、さらに Rails のモデルなどの型情報を生成するためのrbs_railsをインストールします。

Gemfile
group :development, :test do
  gem 'rbs', '1.7.0.beta.2'
  gem 'rbs_rails', '~> 0.8.2', require: false
  gem 'steep', github: 'soutaro/steep'
end

RBS のバージョンは以下のバグ修正が含まれていないと Active Record の RBS ファイルが正しく解決できないため、 1.7.0.beta.2 以上が必要でした。

https://github.com/ruby/rbs/pull/785

また steep は以下の Pull Request で rbs collection に対応していますが、こちらは記事執筆時点では rubygems にリリースされていないため、 GitHub の最新を取得するようにしています。

https://github.com/soutaro/steep/pull/420

rbs collection の設定

まずは rbs collection を設定していきます。
rbs リポジトリの docs/collection.mdに手順が書かれているため、これに従って設定します。

rbs collection init コマンドで設定ファイルの雛形を生成します。
また RBS ファイルが置かれるディレクトリは .gitignore に追加しておきます。

$ rbs collection init
created: rbs_collection.yaml
$ echo /.gem_rbs_collection/ >> .gitignore

次に、生成された rbs_collection.yaml を編集して、 Gemfile.lock だけでは解決できない依存 gem の設定を記述します。
ここでは Rails が依存している標準ライブラリを gems に列挙します。

rbs_collection.yaml

# Download sources
sources:
  - name: ruby/gem_rbs_collection
    remote: https://github.com/ruby/gem_rbs_collection.git
    revision: main
    repo_dir: gems

# A directory to install the downloaded RBSs
path: .gem_rbs_collection

gems:
  - name: rbs
    ignore: true
  - name: pathname
  - name: logger
  - name: mutex_m
  - name: date
  - name: monitor
  - name: singleton
  - name: tsort
  - name: time
  - name: set

各 gem から依存している標準ライブラリがわからないため、現在はユーザ側でこのように列挙する必要があります。今後はこうした内容を gem 側からマニフェストのような形で提供できないか検討しているそうです。

これで準備ができたので、 rbs collection install コマンドを実行して RBS ファイルをダウンロードできます。

$ rbs collection install
warning: rbs collection is experimental, and the behavior may change until RBS v2.0
Using pathname:0 (/usr/local/bundle/gems/rbs-1.7.0.beta.2/stdlib/pathname/0)
...
It's done! 26 gems' RBSs now installed.

以下のように、 .gem_rbs_collection 以下に RBS ファイルをダウンロードできたことが確認できます。

$ ls .gem_rbs_collection/
actionpack/    actionview/    activejob/     activemodel/   activerecord/  activesupport/ ast/           httpclient/    listen/        nokogiri/      parallel/      rack/          railties/      rainbow/

$ tree .gem_rbs_collection/activerecord/6.1/
.gem_rbs_collection/activerecord/6.1/
├── activerecord-6.1.rbs
├── activerecord-generated.rbs
├── activerecord.rbs
└── patch.rbs

RBS Rails を用いた型情報の生成

続けて、 Rails のモデルなどに RBS ファイルを生成してくれる RBS Rails を設定し、型情報を生成していきます。
rbs_rails リポジトリのREADMEに従って設定しましょう。

rake タスクを定義して rake rbs_rails:all とすれば、モデルとパスヘルパーの RBS ファイルが生成されます。

lib/tasks/rbs.rake

require 'rbs_rails/rake_task'

RbsRails::RakeTask.new
$ rake rbs_rails:all
$ tree sig
sig
└── rbs_rails
    ├── app
    │   └── models
    │       ├── ...
    │       └── user.rbs
    ├── model_dependencies.rbs
    └── path_helpers.rbs

5 directories, 9 files

Steep の設定

rbs collection と RBS Rails の型情報を組み合わせて型検査を行えるよう Steep を設定します。
SteepのREADMEに設定方法が書かれているため、こちらに従っていきます。

まずは steep init で設定ファイルの雛形を生成します。

$ steep init
Writing Steepfile...

生成された Steepfile を編集し、 sig 以下の型情報を利用して app 以下の型を検査するよう指定します。

Steepfile

target :app do
  signature "sig"

  check "app"
end

rbs_railsのREADMEには libraryrepo_path を記述するよう指示がありますが、今回は rbs collection がこの辺りを解決してくれるため記述する必要はありません。

型検査の実行

それでは、 steep check で型を検査しましょう。
rbs collection が実験的機能だという警告がたくさん出ますが無視します。

$ steep check
# Type checking files:

warning: rbs collection is experimental, and the behavior may change until RBS v2.0
warning: rbs collection is experimental, and the behavior may change until RBS v2.0
warning: rbs collection is experimental, and the behavior may change until RBS v2.0
warning: rbs collection is experimental, and the behavior may change until RBS v2.0
warning: rbs collection is experimental, and the behavior may change until RBS v2.0
..............................................................................................F...F...............F.F.F...........F.........FFFF....F......

app/controllers/health_controller.rb:4:2: [error] Type `singleton(::Object)` does not have method `skip_before_action`
│ Diagnostic ID: Ruby::NoMethod
│
└   skip_before_action :require_login
    ~~~~~~~~~~~~~~~~~~

# ...

型エラーがいくつか検出されました。

ここでは skip_before_actionsingleton(::Object) に定義されていないと怒られていますね。
skip_before_action は Action Controller の機能ですが、 Action Controller は gem_rbs_collection に RBS ファイルが無いため型情報を取れていないようです。
他にも Action Mailer などライブラリで型情報がないもの、 current_user など独自に生やしたメソッドなどで型エラーが検出されます。

逆に、 rbs collection で型情報が取れている Active Support などや、 RBS Rails で型情報が生成されているモデルなどでは型エラーが検出されませんでした。
symlink 問題が直っていない古いバージョンの RBS などでは ActiveModel の型エラーが多く出ていたため、これらの機能はうまく動いたとみて良いでしょう。

とりあえず、 Rails の型を検査して型エラーを検出できるところまでは確認できましたね。
これらの型エラーを元に、足りない型情報を自分で RBS に記述していくことで正しく型検査が行えるようになるはずです。

感想

rbs collection を実際に試してみて、導入作業や依存性解決の面倒事がほとんどなくなったと感じました。
これが正式リリースされれば、型検査を CI に組み込んでいくのがかなり簡単になるのではないでしょうか。
実際、 RBS v2.0 がリリースされたら今回の作業の続きを行い、型エラーを解消して CI に組み込んでいきたいと画策しています。

参考文献

宣伝

Leaner Technologies では Ruby でも型検査したいエンジニアを募集しています!

https://careers.leaner.co.jp/
脚注
  1. 型検査ツールのSorbetも有名ですが、こちらは RBS ではなく独自形式の rbi ファイルを用いて型情報を記述します。 ↩︎

Discussion

ログインするとコメントできます