RailsアプリをRBS + Steepで型チェックするまでの手順

rails new してから steep check が通るまでにやってみた作業をまとめておく。

GitHub

コードはGitHubで公開しているので、詳細な手順を知りたい方は参照してください。

github.com

手順

gemを入れる

steepとrbs_railsをGemfileに追加する。

# Gemfile
group :development do
  gem 'rbs_rails', require: false
  gem 'steep', require: false
end

bundle install を実行する。

$ bundle install

rbs_rails のREADMEの手順に従って lib/tasks/rbs.rake を作成する。

# lib/tasks/rbs.rake
require 'rbs_rails/rake_task'

RbsRails::RakeTask.new

gemのrbsを取得する

いくつかのgemの型定義はgem_rbs_collectionから取得して利用できる。

$ bundle exec rbs collection init
$ bundle exec rbs collection install

取得したrbsファイルはGitで管理する必要はないので .gitignore に追加しておく。

/.gem_rbs_collection

rbs_railsでrbsを生成する

Railsの提供するメソッド定義をいくつか自動生成してくれる。

bin/rails rbs_rails:all

自動生成されるファイルなので .gitattributes に追加しておくと良い。*1

sig/rbs_rails/** linguist-generated

Steepfileを作る

bundle exec steep init で雛形が作成されます。最小の設定だと以下の通り。

target :app do
  signature "sig"

  check "app"
end

既存ファイルのrbsを用意する

ディレクトリ構造にあわせてrbsのプロトタイプを生成するシェル - アジャイルSEの憂鬱 で紹介したワンライナーを使う。

$ find app/ -name '*.rb' | xargs -I{} bash -c 'mkdir -p sig/$(dirname {}); bundle exec rbs prototype rb {} > sig/{}s;'

app/models/application_record.rb のように最初から存在するファイルに対して、rbsのプロトタイプを生成する。

不足してるrbsを用意する

gem_rbs_collectionにないgemのrbsは自分で書く必要があるので書く。

rails new した直後の状態で不足していた型定義は以下の通り。(オプションによって変わるかもしれない)

# sig/patch.rbs
module ActiveRecord
  class Base
    def self.primary_abstract_class: () -> void
  end
end

module ActionMailer
  class Base
    def self.default: (untyped) -> void
    def self.layout: (untyped) -> void
  end
end

module ActionCable
  module Channel
    class Base
    end
  end

  module Connection
    class Base
    end
  end
end

型チェックを実行する

ここまで設定がおわれば、 steep check を実行できる。

$ bundle exec steep check
# Type checking files:

.....................................................................................................................................................

No type error detected. 🫖

ジェネレーター後にrbsを自動生成する

ジェネレーターを使ったときにrbsのプロトタイプを作成したり、 rbs_rails:all を自動で実行するようにしておく。 少し楽になる。

# config/environments/development.rb
Rails.application.configure do
  config.generators.after_generate do |files|
    files.each do |f|
      next unless f.match?(%r{^app/.+\.rb$})

      rb_path = Rails.root.join(f)
      rbs_path = Rails.root.join('sig', f.sub(/\.rb$/, '.rbs'))
      rbs_path.dirname.mkpath unless rbs_path.dirname.exist?
      system("bundle exec rbs prototype rb #{rb_path} > #{rbs_path}", exception: true)
    end

    if files.any? { |f| f.match?(%r{^app/.+\.rb$}) }
      system("bin/rails rbs_rails:all", exception: true)
    end
  end
end

scaffold を試す

ここまでの設定を済ませた状態で、scaffoldを試してみる。

$ bin/rails g scaffold book title author

型チェックを実行する。

$ bundle exec steep check
# Type checking files:

......................................................................................................................................F......................

app/controllers/books_controller.rb:28:34: [error] Type `::BooksController` does not have method `book_url`
│ Diagnostic ID: Ruby::NoMethod
│
â””         format.html { redirect_to book_url(@book), notice: "Book was successfully created." }
                                    ~~~~~~~~

app/controllers/books_controller.rb:41:34: [error] Type `::BooksController` does not have method `book_url`
│ Diagnostic ID: Ruby::NoMethod
│
â””         format.html { redirect_to book_url(@book), notice: "Book was successfully updated." }
                                    ~~~~~~~~

app/controllers/books_controller.rb:55:32: [error] Type `::BooksController` does not have method `books_url`
│ Diagnostic ID: Ruby::NoMethod
│
â””       format.html { redirect_to books_url, notice: "Book was successfully destroyed." }
                                  ~~~~~~~~~

Detected 3 problems from 1 file

コントローラで呼び出しているメソッドで、エラーが出る。

エラーについて

web上の記事だと check "app/models" だけ型チェックしてる記事が多いので、現状だとコントローラの型チェックは難しいのかも?

直し方が分かったら、またブログに書くかもしれない。

おまけ

GitHub Actionsで型チェックを動かす

# .github/workflows/check.yml
name: check

on:
  push:

concurrency:
  group: ${{ github.workflow }}-${{ github.ref }}
  cancel-in-progress: true

jobs:
  run:
    runs-on: ubuntu-latest
    timeout-minutes: 10

    steps:
    - uses: actions/checkout@v3

    - uses: ruby/setup-ruby@v1
      with:
        bundler-cache: true

    - run: bundle exec rbs collection install
    - run: bundle exec steep check

Steep VSCode Integration

steep-vscode を使うと、VSCode上で型チェックの結果を見ることができる。

f:id:sinsoku:20220309232348p:plain
VSCode

注意: devcontainerだとrbsの変更が反映されない?

devcontainerでsteep-vscodeを使ってもrbsのファイル変更が検知されない問題が起きた。 Command + Shift + P => Steep: Restart all で再起動すれば直る。

原因はよく分からないですが、ファイルIO関連なのでDocker for Mac限定の問題かもしれません。

*1:プルリクのdiffが閉じた状態になる