Techouse Developers Blog

テックハウス開発者ブログ|マルチプロダクト型スタートアップ|エンジニアによる技術情報を発信|SaaS、求人プラットフォーム、DX推進

RubyKaigi 2025 Automatically generating types by running tests (Day1)

ogp

はじめに

こんにちは、株式会社Techouseでバックエンドエンジニアをしている、新卒1年目のAsagaKoshoです。

2025年4月16日〜18日に愛媛県松山市にて開催されていたRubyKaigi 2025に参加してきました。

この記事では、@sinsokuさんによるDay1の発表 "Automatically generating types by running tests" (発表要旨スライド)をご紹介します。

Rubyにおける型定義の記述について

Rubyは動的型付け言語であり、型宣言に該当する文法が存在しません。

一方で、Rubyで型を扱う手段が一切ないわけではなく、型宣言を記述するための言語であるRBSや、RBSを用いて型検査をするgemであるSteepなどが開発されています。

RBSでは、型宣言をRubyスクリプトとは分けて、.rbsファイルに記述する必要があり、コードと型情報の対応を追うのが大変です。そこで、Rubyスクリプト内にコメントとして型宣言を記述できる、RBS::Inlineも開発されています。

RBS::Traceとは

さて、ここからが本題となる型宣言の自動生成についてです。

RBS::Traceは、この発表のスピーカーである@sinsokuさんによって開発されているgemで、テストを実行することで型宣言を自動で記述してくれるツールです。

テストから型宣言を生成するとは

テストの実行で自動的に型宣言を生成するとはどういうことなのでしょうか。

RBS::Traceでは、テストケースで想定されている入出力の型に応じて、RBSの型コメントが生成されます。

テストケース

RSpec.describe "Calculator" do
  describe ".sum" do
    context "when args are Integer"
    subject { Calculator.sum(1, 2) }

    it { is_expected.to eq 3 }
  end
end

RBSの型コメントが追加されたRubyスクリプト

class Calculator
  # @rbs (Integer, Integer) -> Integer
  def self.sum(x, y)
    x.to_i + y.to_i
  end
end

スライド 14ページより抜粋

これを見て、既存のプロジェクトにRBSを導入する際、多量の型コメントを手で書くことなく自動生成できるという点に大きなメリットを感じました。

テストケースに不備がある場合は

RBS::Traceでは、テストケースをもとにして型コメントを生成するため、テストケースに不備がある場合には適切な型コメントが生成されません。

例えば、以下のような場合に問題となります。

  • 想定している入出力が複数の型のUnionである場合に、テストケースでそのうちの一部のみしか記述していない場合
  • テストケースが誤っている場合
  • そもそもメソッドに対するテストケースが記述されていない場合

いずれも当たり前のことではありますが、RBS::Traceをフル活用するためには、テストを網羅的に・不備なく記述しておく必要があります。

Railsアプリでも利用可能

RBS::TraceはもちろんRuby on Railsアプリでも利用可能です。

この発表では、実際にRedmineやMastodonといったRuby on Rails製のOSSのコードに対して、型コメントの自動生成をした結果が紹介されました。 その結果、いくつかの問題はありましたが(後述)、型コメントを出力できました。 実際に追加された型コメントは、以下で見ることができます。

パフォーマンス面

RBS::Traceはテストの実行時に同時に実行されるため、テストの実行時間に影響が出るのは避けられません。 Redmine、Mastodonでのテスト実行時間への影響は、以下のようだったとのことです。 テスト実行時間への影響 スライド 23ページより抜粋

このように、テスト実行時間は4〜5倍程度にはなりますが、そもそもRBS::Traceを実行して型コメントを付与する必要があるのは1度のみのため、大きな影響はありません。

どのように実装されているのか

RBS::Traceは、Rubyの組み込みクラスである、TracePointを使って実装されています。 TracePointでは、さまざまなイベントをトレースし、そのイベントに関する情報(例えばメソッドの呼び出しであれば、そのメソッドを定義するクラス名や、パラメータ定義など)を取得できます。

class User
  def initialize(first_name, last_name)
    p("initialize")
  end
end

TracePoint.trace(:call) do |tp|
  p(defined_class: tp.defined_class, method_id: tp.method_id, parameters: tp.parameters)
end

User.new("Yukihiko", "Matsumoto")

# 実行結果
# {defined_class: User, method_id: :initialize, parameters: [[:req, :first_name, [:req, last_name]]}
# "initialize"

スライド 30ページより抜粋

RBS::TraceではTracePointを活用することによって、メソッドの引数や返り値の型を取得し、これを型コメントに反映しています。

開発時に起きた課題

RBS::Traceの開発時には、いくつかの問題が発生したそうです。(いずれも解消済み)それらの問題についてご紹介します。

classメソッドを呼び出す際に、NoMethodErrorが発生

Redmineでは、BasicObjectを継承したクラスがあり、BasicObjectclassメソッドを持たないため、TracePointでクラス名が取得できない問題が発生しました。この問題はUnboundMethodを使うことにより解決したそうです。

ActiveRecordを継承したクラスの型が、ActiveRecord::Relationになってしまう

Railsアプリケーションで、ActiveRecordを継承したクラスの型が、上書きされてしまい、ActiveRecord::Relationになってしまう問題もありました。こちらも、UnboundMethodを使うことにより解決したそうです。

void型に非対応

当初、void型に対応できていませんでした。上記でも説明したように、メソッドの引数や返り値の型を取得するという実装のため、返り値がない場合にvoidの型コメントを追加できていませんでした。

この問題は、Prismの解析処理と組み合わせることで、解消したとのことです。

並列テストで動作しない

並列テストで期待通りに動作しない問題もありました。それは、同じメソッドに対して複数のテストがある場合、並列テストのそれぞれのプロセスで型コメントが書き込まれるため、すでに型コメントが書き込まれている箇所に対して再度型コメントを追加するという挙動が起こり得ます。その際には、新たなコメントでの上書きはできないため、出力される型コメントが不十分なものになるという問題がありました。

この問題に対しては、並列実行したテストを一度RBSファイルとして書き出したのちにマージして、その結果を型コメントとして追加することで解決されました。

並列テストへの対応 スライド 60ページより抜粋

まとめ

RBS::Traceは、既存の比較的大きなプロジェクトにRBSを導入する際、大きな力を発揮するツールだと感じました。私が関わっているプロジェクトでもRBSを導入することがあれば、フル活用できればと考えています。


Techouseでは、社会課題の解決に一緒に取り組むエンジニアを募集しております。 ご応募お待ちしております。

jp.techouse.com