selmertsxの素振り日記

ひたすら日々の素振り内容を書き続けるだけの日記

Rubyで型チェックをしてくれるsteepを試してみた

TL;DR

使い方

class Person
  # @dynamic name, contacts
  attr_reader :name, :contacts

  def initialize(name:)
    @name = name
    @contacts = []
  end

  def guess_country()
    contacts.map do |contact|
      case contact
      when Phone
        contact.country
      end
    end.compact.first
  end
end

class Phone
  # @dynamic country, number
  attr_reader :country, :number

  def initialize(country:, number:)
    @country = country
    @number = number
  end

  def ==(other)
    if other.is_a?(Phone)
      other.country == country && other.number == number
    end
  end

  def hash
    self.class.hash ^ country.hash ^ number.hash
  end
end
# 型定義ファイル
class Person
  @name: String
  @contacts: Array<Phone>
  def initialize: (name: String) -> any
  def name: -> String
  def contacts: -> Array<Phone>
  def guess_country: -> (String | nil)
end

class Phone
  @country: String
  @number: String
  def initialize: (country: String, number: String) -> any
  def country: -> String
  def number: -> String
end
bundle exec steep check -I test.rbi test.rb
# 何も問題がなければ何も出ない

問題がある場合

phone = Phone.new(country: 'Japan', number: 1)

こんな感じでstring型が期待される場所に、integerを渡してみた.

➜  steep git:(master) ✗ bundle exec steep check -I test.rbi test.rb
test.rb:40:44: ArgumentTypeMismatch: receiver=::Phone.class constructor, expected=::String, actual=::Integer (1)

という訳で、string型が求められてる場所にintegerを渡すと怒ってくれる。

忘れがちな設定

@dynamic コメント

class Person
  # @dynamic name, contacts
  attr_reader :name, :contacts

attr_readerなど、methodとして定義しないattributesを扱う場合、 # @dynamic attribute_name で指定しなければならない。 これを忘れると、下記のようにmethod missingのエラーが出る。

➜  steep git:(master) ✗ bundle exec steep check -I test.rbi test.rb
test.rb:1:0: MethodDefinitionMissing: module=::Person, method=name (class Person)
test.rb:1:0: MethodDefinitionMissing: module=::Person, method=contacts (class Person)

所感

  • 普通に自分の個人プロダクトでは採用して良いのではなかろうかというレベルに来てる。
  • Railsくらいの規模のコードで問題なく使えるか検証したい
  • 大きなライブラリも導入してくれると、コードを読むときに助かる気持ち