パパエンジニアのアウトプット帳

30歳に突入した1児のパパエンジニアのブログ

Railsアプリを型チェックできるようにSteepに入門してみた

今年も残り少なくなってきて、そういえばRailsアプリに型チェックを導入してみたいなーと思っていたのを思い出したのでSteepに入門してみた。


RailsアプリへのSteep導入は神速さんのブログがすごく丁寧だったのでそれを参考にしてやった sinsoku.hatenablog.com


差分としては

  • lib/tasks/rbs.rake を作成する
    • 現在はbin/rails g rbs_rails:installするとlib/tasks/rbs.rakeが生成される
  • 不足してるrbsを用意する
    • 自分は ActionCable::Channel::BaseActionCable::Connection::Base だけでよかった
# sig/patch.rbs
module ActionCable
  module Channel
    class Base
    end
  end

  module Connection
    class Base
    end
  end
end


あと、神速さんのブログのscaffold を試すのところにあるようにコントローラーのエラーも確かに発生していて、自分は scaffold userをしたんだけどそのエラーが下記。

# Type checking files:

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

app/controllers/users_controller.rb:7:4: [error] Cannot find the declaration of instance variable: `@users`
│ Diagnostic ID: Ruby::UnknownInstanceVariable
│
└     @users = User.all
      ~~~~~~

app/controllers/users_controller.rb:17:4: [error] Cannot find the declaration of instance variable: `@user`
│ Diagnostic ID: Ruby::UnknownInstanceVariable
│
└     @user = User.new
      ~~~~~

app/controllers/users_controller.rb:27:4: [error] Cannot find the declaration of instance variable: `@user`
│ Diagnostic ID: Ruby::UnknownInstanceVariable
│
└     @user = User.new(user_params)
      ~~~~~

app/controllers/users_controller.rb:31:34: [error] Type `::UsersController` does not have method `user_url`
│ Diagnostic ID: Ruby::NoMethod
│
└         format.html { redirect_to user_url(@user), notice: "User was successfully created." }
                                    ~~~~~~~~

app/controllers/users_controller.rb:45:34: [error] Type `::UsersController` does not have method `user_url`
│ Diagnostic ID: Ruby::NoMethod
│
└         format.html { redirect_to user_url(@user), notice: "User was successfully updated." }
                                    ~~~~~~~~

app/controllers/users_controller.rb:60:32: [error] Type `::UsersController` does not have method `users_url`
│ Diagnostic ID: Ruby::NoMethod
│
└       format.html { redirect_to users_url, notice: "User was successfully destroyed." }
                                  ~~~~~~~~~

app/controllers/users_controller.rb:69:4: [error] Cannot find the declaration of instance variable: `@user`
│ Diagnostic ID: Ruby::UnknownInstanceVariable
│
└     @user = User.find(params[:id])
      ~~~~~

Detected 7 problems from 1 file

path helperのやつとインスタンス変数の定義がないと怒られたので解決してみた。

path helper関連

これはrbs_rails:allをしたときにsig/rbs_rails/path_helpers.rbsが生成されているのになんでそれ見てくれないのかなと疑問だったんだけど、

# sig/rbs_rails/path_helpers.rbs

interface _RbsRailsPathHelpers
・・・
  def users_url: (*untyped) -> String
  def new_user_url: (*untyped) -> String
  def edit_user_url: (*untyped) -> String
  def user_url: (*untyped) -> String
・・・
end

このようにinterfaceで定義されているだけなので、これをコントローラーのrbsファイルでincludeしてやればよさそう。
(まだRBSが全く分かってないんだけど、RBS基礎文法最速マスター - pockestrap見つつこう使うのかなーみたいな感じでやってみたらエラーはでなくなった)

# sig/app/controllers/users_controller.rbs

class UsersController < ApplicationController
  include _RbsRailsPathHelpers

  # GET /users or /users.json
  def index: () -> untyped

  # GET /users/1 or /users/1.json
  def show: () -> nil

  # GET /users/new
  def new: () -> untyped
・・・
end

インスタンス変数

Cannot find the declaration of instance variable: `@user`
Cannot find the declaration of instance variable: `@users`

@userと@usersのRBS定義がないのでこれは手動で定義した。

# sig/app/controllers/users_controller.rbs

class UsersController < ApplicationController
  @user: User
  @users: User::ActiveRecord_Relation

  include _RbsRailsPathHelpers

  # GET /users or /users.json
  def index: () -> untyped

  # GET /users/1 or /users/1.json
  def show: () -> nil

  # GET /users/new
  def new: () -> untyped
・・・
end

あと注意点としては rbs_rails:allしたときにmodelの定義も生成してくれるけど、それはdb:migrateを実行しておかないと生成されない。

rbs_rails/rake_task.rb at 227285bd04d526525331a9b05fe9ce5e78038946 · pocke/rbs_rails · GitHub

        ::ActiveRecord::Base.descendants.each do |klass|
          next unless RbsRails::ActiveRecord.generatable?(klass)

ここの RbsRails::ActiveRecord.generatable?(klass)

rbs_rails/active_record.rb at 227285bd04d526525331a9b05fe9ce5e78038946 · pocke/rbs_rails · GitHub

    def self.generatable?(klass)
      return false if klass.abstract_class?

      klass.connection.table_exists?(klass.table_name)
    end

とあるようにDBにテーブルが存在しているかを見てるので。


これでとりあえずはsteep checkが通るようになったので、なんとか入門までは行けた感じがする。

$ bundle exec steep check
# Type checking files:

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

No type error detected. 🫖