Railsで型チェックを導入してみる

この記事は [Akatsuki Advent Calendar 2020](https://adventar.org/calendars/5174) 6日目の記事です.

ruby-3.0.0が[12/25日](https://bugs.ruby-lang.org/projects/ruby-master/roadmap)にリリースされる予定なので、新しく追加される型検査機能について簡単なアプリケーションを作成しながら触れていこうと思います。

## Ruby3.0.0-preview1のインストール

まず型検査機能を使うために機能が含まれているRuby3.0.0のpreview版をインストールしていきます。

```bash
> brew upgrade ruby-build
> rbenv install 3.0.0-preview1
> rbenv global 3.0.0-preview1
```

インストールされたrubyにはデフォルトでrbsと言うgemが含まれています。

```bash
> gem list | grep rbs
> rbs(0.12.2)
```

これがruby3.0.0で導入される予定であるgemです。

## RBSとは？

RBSとはRubyのプログラム構造を記述するための言語です。

クラスやモジュール、定義されたクラスのメソッドや変数の型の定義を書く事が出来ます。

[RBS](https://github.com/ruby/rbs)

## その他必要なツールのインストール

Railsのアプリケーションを作成するので、その他必要なgemのインストールをしていきます。

Railsのインストールの他に、RailsのRBSファイル作成ツールであるrbs_rails、RBSファイルを元に型チェックを行ってくれる型チェッカーが必要になります。

今回は型チェッカーとして[steep](https://github.com/soutaro/steep)を導入したいと思います。

まずはRailsのアプリケーションを作成します。

使用したバージョンは6.0.3.4です。

```bash
> gem install rails
> rails new rbs_training
```

次にGemfileに追記を行ってrbs_railsとsteepのインストールを行います。

```ruby

source 'https://rubygems.org'
git_source(:github) { |repo| "https://github.com/#{repo}.git" }

ruby '3.0.0'

gem 'rails', '~> 6.0.3', '>= 6.0.3.4'
gem 'mysql2', '>= 0.4.4'
gem 'puma', '~> 4.1'
gem 'sass-rails', '>= 6'
gem 'webpacker', '~> 4.0'
gem 'turbolinks', '~> 5'
gem 'jbuilder', '~> 2.7'
gem 'bootsnap', '>= 1.4.2', require: false

group :development, :test do
  gem 'byebug', platforms: [:mri, :mingw, :x64_mingw]
  gem 'steep'
  gem 'rbs_rails'
end

group :development do
  gem 'web-console', '>= 3.3.0'
  gem 'listen', '~> 3.2'
  gem 'spring'
  gem 'spring-watcher-listen', '~> 2.0.0'
end

group :test do
  gem 'capybara', '>= 2.15'
  gem 'selenium-webdriver'
  gem 'webdrivers'
end

# Windows does not include zoneinfo files, so bundle the tzinfo-data gem
gem 'tzinfo-data', platforms: [:mingw, :mswin, :x64_mingw, :jruby]
```

これで準備は完了したので、実際のアプリケーションを書いていきます。

## rbs_railsによる型情報の追加

### Railsの型情報の追加

簡単な掲示板を作成して、モデルに対して型情報を追加して行きます。

今回作成するのはトピックと投稿がある簡単な掲示板です。

```shell
> rails g model Topic title:string
> rails g model Post body:string  posts:references
> rails db:migrate
```

```ruby 
class Topic < ApplicationRecord
  validates :title, presence: true
end
```

```ruby
class Post < ApplicationRecord
  belongs_to :topic

  validates :body, presence: true
end
```

アプリケーションの型情報を追加するには、Railsの型情報を定義する必要があります。

rbs_railsのドキュメントに書いてあるように型情報を追加してくれるrakeタスクを作成します。

```ruby
task copy_signature_files: :environment do
  require 'rbs_rails'

  to = Rails.root.join('sig/rbs_rails/')
  to.mkpath unless to.exist?
  RbsRails.copy_signatures(to: to)
end
```

このrakeタスクを実行するとアプリケーションのルートディレクトリにsig/rbs_railsというディレクトリが追加され、Railsの型情報が定義されたrbsファイルが自動で生成されます。

### モデルに対する型情報の追加

次に作成したモデルに対して型情報を追加していきます。

こちらもrbs_railsを利用して自動で生成する事が出来ます。

次のようなrakeタスクを作成して、実行します。

```ruby
task generate_rbs_for_model: :environment do
  require 'rbs_rails'

  out_dir = Rails.root / 'sig'
  out_dir.mkdir unless out_dir.exist?

  Rails.application.eager_load!

  ActiveRecord::Base.descendants.each do |klass|
    next if klass.abstract_class?

    path = out_dir / "app/models/#{klass.name.underscore}.rbs"
    FileUtils.mkdir_p(path.dirname)

    sig = RbsRails::ActiveRecord.class_to_rbs(klass)
    path.write sig
  end
end
task generate_rbs_for_path_helpers: :environment do
  require 'rbs_rails'
  out_path = Rails.root.join 'sig/path_helpers.rbs'
  rbs = RbsRails::PathHelpers.generate
  out_path.write rbs
end
```

```bash

> bundle exec rake generate_rbs_for_model
```

すると'sig/app'以下に'rails generate'コマンドで作成されたモデルの型情報が自動で追加されます。

Rails6でコマンドを実行するとActionText関連のrbsファイルを生成しようとしてしまい、エラーとなってしまいますがモデル自体のrbsファイルは生成されるようです。

また、自分で独自に定義したメソッドに対しては自分で型情報を追加していく必要があります。

```ruby
class Post < ApplicationRecord
  def hoge
    "hogehoge"
  end
end
```

```ruby
class Post < ApplicationRecord
  def hoge: () -> String
end
```

## steepによる型チェックの実行

型定義の追加は終わったので、次にsteepを利用して型チェックをしていきたいと思います。

'steep init'コマンドを叩いてsteepの設定ファイルを作成し、以下を追加します。

```ruby
target :app do
  signature "sig"

  check "app"
  check 'rbs_rails'

  library "pathname"
  library "logger"
  library "mutex_m"
  library "date"
end
```

次に'steep check'コマンドを叩いて型チェックを実行します。

```bash
> steep check
W, [2020-12-07T07:39:16.274442 #11364]  WARN -- rbs: `overload def` syntax is deprecated. Use `...` syntax instead.
sig/app/models/post.rbs:1:0...70:3      UnknownTypeNameError: name=ApplicationRecord
sig/app/models/topic.rbs:1:0...54:3     UnknownTypeNameError: name=ApplicationRecord
```

どうやら、ApplicationRecordの定義がうまく生成されていないようなので、追加します。

```ruby
class ApplicationRecord < ActiveRecord::Base
end
```

```bash
> steep check
W, [2020-12-07T07:33:24.351191 #11004]  WARN -- rbs: `overload def` syntax is deprecated. Use `...` syntax instead.
```

型定義に問題が無ければ上記のようにエラーが出力されないで終了します。
以上で導入は一通り完了です！

今回は簡単に導入をしただけだったのですが、ruby 3.0.0がリリースされたら自分のプロダクトに導入してみて使い方やチップスを共有出来たらなと思います。


## まとめ

- ruby3.0.0からrbsが追加される
- Rails本体の型情報を追加するのにはrbs_railsを使う
- rbs_railsでモデルの型情報を自動生成する事が出来る
- steepを使って型チェックをする