こんにちは、Findy Freelanceの開発をしているエンジニアの@2boです。
先日、愛媛県で開催されたRubyKaigi 2025に参加してきました。ファインディのブースにお立ち寄りいただいた方、Rubyクイズに答えてくださった方、Drinkupに参加していただいた方、運営やSpeakerの皆様、ありがとうございました! おかげさまでとても楽しく過ごすことができ、興味深いセッションもたくさんありました。
本記事では、その中の1つである@sinsoku_listyさんの「Automatically generating types by running tests」で発表されていた「RBS::Trace」を早速、Findy FreelanceのRailsプロジェクトで試してみた結果と所感を紹介します。
RBS::Traceとは
RBS::Traceは、コード実行時にメソッドの引数と戻り値の型情報を収集し、自動的にInline RBSとしてコメントを挿入したり、RBSファイルを作成してくれるGemです。
実行手順
Findy FreelanceのRailsプロジェクトのテストでRBS::Traceを実行してみました。
執筆時点のバージョンは0.5.1
です。
なお、今回は大枠の動作と結果を確認することが目的のため、対象はapp/models/
配下に絞っています。
次の手順で実行しました。
1. Gemfileへの追加
gem "rbs-trace"
Gemfile追記後にbundle installを実行します。
$ bundle install
2. RSpecの設定
次に、RSpec用の設定ファイルを作成します。
RBS::Trace.new
の引数paths
で対象のファイルを指定しています。
RBSファイルの格納先として、sig/trace/
を指定しています。なお、この設定は任意です。
# spec/support/rbs_trace.rb RSpec.configure do |config| # RBSの出力対象とするファイルを指定 trace = RBS::Trace.new(paths: Dir.glob("#{Dir.pwd}/app/models/**/*")) config.before(:suite) { trace.enable } config.after(:suite) do trace.disable trace.save_comments # RBSファイルの格納先を指定 trace.save_files(out_dir: "sig/trace/") end end
3. テストの実行
テストを実行します
$ bundle exec rspec spec/models/
結果の確認
テストを実行すると、Inline RBSが対象のファイルのメソッド定義の上に追記され、RBSファイルが生成されます。 それぞれの結果を次に示します。 なお、掲載しているのは例示用のコードで、実際のFindy Freelanceのコードや処理とはまったく関係ありません。
Inline RBS
app/models/user.rb
に次のようなInline RBSが生成されました。
class User < ApplicationRecord has_many :projects # @rbs () -> String def full_name format_name end # @rbs () -> Project? def main_project projects.find_by(active: true) end # @rbs (Date) -> Project::ActiveRecord_AssociationRelation def projects_after_date(date) projects.where('start_date >= ?', date) end private # @rbs () -> String def format_name "#{last_name} #{first_name}" end end
RBSファイル
sig/trace/app/models/user.rbs
に次のようなRBSファイルが生成されました。
class User def full_name: () -> String def main_project: () -> nil | () -> Project def projects_after_date: (Date) -> Project::ActiveRecord_AssociationRelation def format_name: () -> String end
参考: テストコード
RBS::TraceによるRBSの自動生成は、テストで実行された内容に依存するため、参考としてテストコードの例を掲載します。
RSpec.describe User do describe '#full_name' do subject { user.full_name } let(:user) { create(:user, first_name: '太郎', last_name: '山田') } it 'returns a string combining last_name and first_name' do expect(subject).to eq('山田 太郎') end end describe '#main_project' do subject { user.main_project } let(:user) { create(:user) } context 'when there is an active project' do let!(:active_project) { create(:project, user: user, active: true) } let!(:inactive_project) { create(:project, user: user, active: false) } it 'returns the active project' do expect(subject).to eq(active_project) end end context 'when there is no active project' do let!(:inactive_project) { create(:project, user: user, active: false) } it 'returns nil' do expect(subject).to be_nil end end end describe '#projects_after_date' do subject { user.projects_after_date(target_date) } let(:user) { create(:user) } let(:target_date) { Date.new(2025, 4, 1) } let!(:before_project) { create(:project, user: user, start_date: Date.new(2025, 3, 31)) } let!(:on_date_project) { create(:project, user: user, start_date: Date.new(2025, 4, 1)) } let!(:after_project) { create(:project, user: user, start_date: Date.new(2025, 4, 2)) } it 'returns only projects starting on or after the specified date' do expect(subject).to include(on_date_project, after_project) expect(subject).not_to include(before_project) end end end
結果からわかったこと
実行した結果、次のようなことがわかりました。
- 直接テストしていないが、テスト中に実行されるprivateメソッドの型情報も生成されている
- ApplicationRecordのサブクラスを返すメソッドは、具体的なクラス名で型情報が生成されている
- Railsのassociationやscopeメソッドには、型情報が生成されない
- これらはメソッド定義ではないため当然の結果である
- ActiveRecord::AssociationRelationのサブクラスのインスタンスを返すメソッドは、[具体クラス名]::ActiveRecord_AssociationRelationのように型情報が記載される
- これはRailsの仕様によるもので、そのようなクラスを動的生成しているためである
- 既に記載されているInline RBSは上書きされない
- RBSファイルの内容はテストを実行するたびに更新される
Steepの導入
せっかくRBSファイルが生成されたので、型チェッカーであるSteepもセットアップして型のあるRailsプロジェクトを疑似体感してみました。 ただし、RBS::TraceだけでRailsプロジェクトの型チェックすべてパスさせることはできないため、VSCodeで型情報を確認できるようにすることを目的としています。
次の手順でセットアップしました。
1. Gemfileへの追加
# Gemfile gem 'steep'
2. Bundle install
Gemfile追記後にbundle installを実行します。
$ bundle install
3. Steepの設定
設定ファイルとなるSteepfile
作成します。
D = Steep::Diagnostic target :app do # RBSファイルの格納先を指定 signature "sig/trace" # Steepで型チェックする対象のファイルを指定 check "app/models" # 型チェック結果を全て抑制(無音)する configure_code_diagnostics(D::Ruby.silent) end
4. VSCodeのSteep拡張のインストール
steep-vscodeをVSCodeにインストールします。
5. RBSファイルのエラーをコメントアウト
RBSファイル内にエラーがあると、VSCodeのSteep拡張が動作しないため、エラーしている箇所をコメントアウトしました。本来は型定義を追加、修正するなどしてエラーを解消する必要がありますが、今回はVSCodeで型情報を確認できることが目的のため、このような対応をしました。
先のRBSファイルの例で言うと、RBS::Traceの実行だけではProject::ActiveRecord_AssociationRelation
クラスの型情報が生成されないため、RBS::UnknownTypeName
エラーになります。これをコメントアウトすることでエラーを握りつぶしています。
class User def full_name: () -> String def main_project: () -> nil | () -> Project # RBS::UnknownTypeName エラーになるためコメントアウト # def projects_after_date: (Date) -> Project::ActiveRecord_AssociationRelation def format_name: () -> String end
VSCodeでの型情報の表示結果
メソッドの呼び出し箇所をホバーすると、型情報が表示されます。
また、入力の補完時にも型情報が表示されます。
所感
RBS::Trace導入のメリットと可能性
RBS::Traceは、RBSが全くないRailsプロジェクトにとって、型情報導入の最初の一歩として非常に有効だと感じました。テスト実行だけで型情報が自動生成されるため、手動で記載する手間が大幅に省けます。
Inline RBSだけの生成も可能なので、ドキュメント生成ツールとしても活用できそうです。これは人間にとって読みやすいだけでなく、GitHub Copilotなどの生成AIツールにも型情報を提供できるメリットがあります。生成AIがInline RBSから型情報を読み取れば、より正確なコード提案が得られるのではないかと期待しています。
将来的には、RBS::InlineがRBS本体に組み込まれる計画もあるようで、Inline RBSだけで型チェックができる日も来るかもしれません。
活用における注意点
型情報の出力結果はテストの実行内容に依存します。例えばStringとnilを返すメソッドがあっても、nilを返すケースのテストがなければ、型情報はStringだけになってしまいます。つまり、RBS::Traceを活用するには、テストの品質確保が前提となります。
また、Railsプロジェクト全体にRBSやSteepを導入した場合、相応のメンテナンス工数がかかると感じました。RBS::Traceだけでは、Rails自体が生成するクラスやメソッドの型情報は提供されないため、rbs_railsなどの併用や、手動での型情報メンテナンスも必要になるでしょう。
RBSのプロジェクトへの導入について
今回の試行から、Findy FreelanceプロジェクトへのRBSやSteep導入を決定したわけではありませんが、これらのツールとエコシステムの現状を実感できました。RBS::Traceのおかげで試すハードルが下がったのは大きな収穫です。開発者の@sinsoku_listyさんには感謝しています。
最後に
RubyKaigi 2025では、Rubyの型に関するセッションがいくつかありました。 すべてを聞けてはいませんが、総じてRubyの型に関するエコシステムは今後もまだまだ進化していきそうだと感じました。 正直、私は今までちゃんとキャッチアップができていなかったのですが、RubyKaigiへの参加をきっかけに興味が強くなり、理解を深めるきっかけとなりました。 今後もRBS, Steep, Sorbetなどの型に関するエコシステムの進化をキャッチアップしつつ、プロジェクトに導入するかどうかを検討していきたいと思います。
5/13(火)に、「After RubyKaigi 2025〜ZOZO、ファインディ、ピクシブ〜」として、ピクシブ株式会社、株式会社ZOZO、ファインディ株式会社の3社でRubyKaigi 2025の振り返りを行います。 オンライン・オフラインどちらもありLTやパネルディスカッションなどコンテンツが盛りだくさんなのでぜひご参加ください!!
ファインディでは、一緒にRubyやRailsの開発をしてくれる仲間を募集しています。 興味のある方は、ぜひこちらからチェックしてみてください! herp.careers