見出し画像

Ruby の 静的な型解析に関連する RBS や TypeProf 、 Steep を試してみた

はじめに

最近は業務で Ruby (Ruby on Rails)を書いています。
たまに型解析があると嬉しいなと思っています。

Ruby 3 から標準 Gem となった RBS や TypeProf はこれまで触れることがなかったので色々試してみました。

利用した各バージョンは以下の通りです。

$ ruby -v
ruby 3.4.1 (2024-12-25 revision 48d4efcb85) +PRISM [x86_64-darwin24]
$ rbs -v
rbs 3.8.0
$ typeprof --version
typeprof 0.30.1
$ steep --version
1.9.3

RBS

Ruby プログラムの型情報を記述できるようになります。
これはあくまで型情報の記述ができるだけで、これらを用いて各種の解析ツールがチェックをしてくれます。

README にあるサンプルコードの通り Person クラスとその型情報である rbs ファイルを作成しました。
私は普段 RubyMine を利用しているのですが、これだけで認識をしてくれました。
これがあると助かる場面が多そうですね。

RubyMine のエディター上で表示

VS Code や Vim のプラグインもドキュメントで紹介をされています。

rbs collection

RBS のバンドラーのような仕組みで、以下のコマンドを実行すると rbs_collection.yaml が生成されます。

$ rbs collection init

生成されたファイルは rbs のドキュメントにも記載されている通りです。
Gemfile のように取得先を指定して、そこからライブラリに紐づく rbs ファイルをインストールしてくれます。
以下のサイトに代表的な Gem は載っていました。

Gemfile を作成した上で、以下のコマンドを実行するとインストール先にインストールされます。
特定の Gem だけ除外することも可能です。

$ rbs collection install

TypeProf

Ruby のコードを静的に解析し、型情報を推測するツールです。
RubyKaigi 2024 の発表資料もありました。
Ruby 3.4 でデフォルトパーサーになった Prism も利用されているようです。

例えば以下のようなクラスを書いて、 typeprof コマンドを実行すると RBS の型情報が生成されました。

class Item
  TAX = 1.1
  SALE_DESCRIPTION = "Just for Today!"

  attr_reader :name
  attr_reader :price

  def initialize(name:, price:)
    @name = name
    @price = price
  end

  def price_with_tax
    price * TAX
  end

  def sale_description
    SALE_DESCRIPTION
  end
end
$ typeprof item.rb -o item.rbs
# TypeProf 0.30.1

# item.rb
class Item
  Item::TAX: Float
  Item::SALE_DESCRIPTION: String
  def name: -> untyped
  def price: -> untyped
  def initialize: (name: untyped, price: untyped) -> untyped
  def price_with_tax: -> untyped
  def sale_description: -> String
end

定数から型情報もちゃんと推測できています。

また以下のようなサンプルコードを渡すと引数などの情報から推測してくれるようです。
上記の型だと初期化時の name や price パラメータが untyped でしたが、String や Integer で返るようになります。

item = Item.new(name: 'banana', price: 100)
item.price_with_tax
class Item
  Item::TAX: Float
  Item::SALE_DESCRIPTION: String
  def name: -> String
  def price: -> Integer
  def initialize: (name: String, price: Integer) -> Integer
  def price_with_tax: -> Float
  def sale_description: -> String
end

ただ、先ほどの RBS で他のライブラリの型情報をローカルにインストールすると、以下のエラーになることがありそうです(私の場合は faraday でした)
ちゃんと調べていないのですが、まだ typeprof 側で対応してないこともあるんでしょうか。
この場合は対象のライブラリの RBS データを一度削除する必要があります。

unsupported: RBS::AST::Members::InstanceVariable (RuntimeError)

余談ですが、rbs collection 経由でインストールした場合は、lock ファイルに記載が残っているとローカルから手動削除するだけではダメなので、ちゃんと rbs_collection.yaml 側も修正する必要があります。

typeprof をざっとプロジェクトクラスに適用して、RBS の初期情報を生成もできそうですが、基本的には私のような一般開発者はそこまで利用せずに後述する静的解析ツールや各種エディターの拡張を通して利用することになりそうです。

Steep

Steep は、RBS を使用して Ruby コードの型検査を行うツールです。Steep は、RBS で定義された型情報に基づいてコードを解析し、型エラーを検出することができます。

ちなみに Sorbet というツールもあるようですが、こちらは RBI と呼ばれる独自の型アノテーション形式を使用するようです。  

steep init した後に生成される Steepfile をみるといくつかオプションがあるようです。
モジュールごとに分けて検査することも可能そうです。

ガイドがあったのでそちらの通りにやってみました。
String 型の引数に対して、わざと nil を入れてみるとちゃんと検出されました。

phone = Phone.new(country: nil, number: nil)
pp phone.country
[error] Cannot pass a value of type `nil` as an argument of type `::String`
│   nil <: ::String
│
│ Diagnostic ID: Ruby::ArgumentTypeMismatch
│
└ phone = Phone.new(country: nil, number: nil)

rbs_rails

じゃあ Rails に実際に導入するにはどうしたら良いかなと思ったら、Rails 関連のファイルの RBS を生成してくれるライブラリがありました。

とりあえず簡単な Rails プロジェクトを作って入れてみることに。
余談ですが、 rails new して 8 系がインストールされてワクワクしますねw
8 系はまだあんまり調べてないので、今度改めて調査したいと思います。
(個人的には Solid 系のアダプターが面白そうだなとは思いました)

rbs_rails:install タスクを実行すると、lib/tasks/rbs.rake ファイルができます。
主な実態は lib/rbs_rails/rbs_task.rb にあるようで、モデルやヘルパーなどの定義を生成するコマンドを提供してくれています。

モデルの方の自動生成で対応しているのは ActiveRecord のクラスのようです。
基本的にはデータベースを参照し、カラムに応じた ActiveRecord が生やしたメソッドの定義を作ってくれます。

その後、 rbs collection や steep を利用していきます。

感想

静的な型解析とはいえ、テストの補助として利用することで抜け漏れの防止にもつながりそうだなと思います。
個人的には、やはりエディターや IDE の補完が強くなるのが開発体験の向上にもつながるのではないかと考えます。

一方で、 rbs_rails などを利用したとしても、自分(チーム)で RBS ファイルを整備しないといけない手間は依然として残ります。
できる範囲から始めるためにも Steep の設定で、例えば app/models から始める、とかが良さそうですかね。

参考


いいなと思ったら応援しよう!