RubyKaigi 2025 レポート: 早速「RBS::Trace」でRailsプロジェクトの型情報を自動生成してみた!

こんにちは、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での型情報の表示結果

メソッドの呼び出し箇所をホバーすると、型情報が表示されます。

VSCodeでメソッドをホバーすると型情報が表示される

また、入力の補完時にも型情報が表示されます。

VSCodeで補完時に型情報が表示される

所感

RBS::Trace導入のメリットと可能性

RBS::Traceは、RBSが全くないRailsプロジェクトにとって、型情報導入の最初の一歩として非常に有効だと感じました。テスト実行だけで型情報が自動生成されるため、手動で記載する手間が大幅に省けます。

Inline RBSだけの生成も可能なので、ドキュメント生成ツールとしても活用できそうです。これは人間にとって読みやすいだけでなく、GitHub Copilotなどの生成AIツールにも型情報を提供できるメリットがあります。生成AIがInline RBSから型情報を読み取れば、より正確なコード提案が得られるのではないかと期待しています。

将来的には、RBS::InlineRBS本体に組み込まれる計画もあるようで、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やパネルディスカッションなどコンテンツが盛りだくさんなのでぜひご参加ください!!

pixiv.connpass.com

ファインディでは、一緒にRubyやRailsの開発をしてくれる仲間を募集しています。 興味のある方は、ぜひこちらからチェックしてみてください! herp.careers

参考