Steep by Steep(Day2)

Posted on December 23, 2020 23:00
Share on:

この記事は、ケーシーエスキャロット Advent Calendar 2020 23日目の記事です。

昨日は、naokishi の aws cli と jq コマンド でした。
今、AWS使ってるんですねー。もうWindowsアプリは開発していないのかな…?

さて、前回はエラーが増えたところで終わりました。

一度、リポジトリ にあるサンプルコードを実行してみようか…ということで、コピペで実行してみると…

1
2
3
4
$ bundle exec steep check --log-level=fatal
lib/phone.rb:9:2: MethodBodyTypeMismatch: method===, expected=bool, actual=(bool | nil) (def ==(other))
  (bool | nil) <: bool="" false="" nil="" true="="> nil 
<: bool="" false="" nil="" true="=">

エラーが出ました… が、一昨日出ていたエラーとは違うし、
このメソッド以外のチェックは通っているようなので、
このエラーは置いておいて、メソッドの定義について確認してみます。

うーん…(コードを見比べている)

!!!

メソッドのパラメータだ!!!!

で、以下のようにコードを修正して実行してみると…

1
2
3
4
5
6
7
8
9
10
11
12
class User
  def initialize(no:, name:)                                                                                                
    @no = no
    @name = name
    @scores = Array.new
  end
  attr_reader :no, :name, :scores

  def add(score:)
    @scores << score
  end
end
1
2
3
4
5
6
$ bundle exec steep check --log-level=fatal
bin/main.rb:20:2: NoMethodError: type=singleton(::File), method=open (File.open(fpath, "w") {|fd| fd.write data })
bin/main.rb:22:24: ArgumentTypeMismatch: receiver=singleton(::Analyzer), expected={ :path => untyped }, actual=::String (fpath)
lib/analyzer.rb:6:2: MethodArityMismatch: method=(self) (def self.load(path))
lib/analyzer.rb:15:2: MethodArityMismatch: method=(self) (def self.parse_row(row))
lib/analyzer.rb:16:8: IncompatibleArguments: receiver=singleton(::User), method_type=(name: untyped, no: untyped) -> ::User (User.new(row["No"], row["Name"]))

メソッドの定義がSteepで解析されて、Userのエラーが消えました!!!

同じように、Analyzerも修正すると、

1
2
3
bin/main.rb:20:2: NoMethodError: type=singleton(::File), method=open (File.open(fpath, "w") {|fd| fd.write data })
bin/main.rb:22:24: ArgumentTypeMismatch: receiver=singleton(::Analyzer), expected={ :path => untyped }, actual=::String (fpath)
lib/analyzer.rb:16:8: IncompatibleArguments: receiver=singleton(::User), method_type=(name: untyped, no: untyped) -> ::User (User.new(row["No"], row["Name"]))

エラーが更に減った〜

出ているエラーが全部違うので、ここからは1つずつ確認していこう…

NoMethodError

これはRubyを使っているとよく見るエラーですが、RubyのNoMethodErrorではなく、TypeInference::MethodCall::NoMethodError です。

エラーとして検出されている場所は、File.open() のRubyのクラス。
Rubyのクラス検知して、エラーにされている???

うーん…  steep の --verbose オプション付けて、ログ出力してみるか…

1
$ bundle exec steep check --log-level=fatal --verbose --log-output="./check.log"

出力されたログを確認してみると、、、
あぁ、なるほど、ここのファイル読み込んでる。
で、File.rbs を見てみると…

なるほど、確かに File.open() が定義されていない…。
initialize が定義されているので、 File.new() に変えてみて、実行してみる。

1
2
3
bin/main.rb:22:2: UnexpectedBlockGiven: method_type=((::string | ::_ToPath | ::int), ?(::string | ::int), ?::int) -> ::File (File.new(fpath, "w") {|fd| fd.write data })
bin/main.rb:25:24: ArgumentTypeMismatch: receiver=singleton(::Analyzer), expected={ :path => untyped }, actual=::String (fpath)
lib/analyzer.rb:16:8: IncompatibleArguments: receiver=singleton(::User), method_type=(name: untyped, no: untyped) -> ::User (User.new(row["No"], row["Name"]))

お、エラーが変わった。(行数が変わっているのは、元のコードをコメントアウトして残しているため)

UnexpectedBlockGiven は、File.new() の定義は、Fileクラスを返す定義になっていて、blockを渡す定義がされていないからだな。なるほど、なるほど。

File.open() の行を以下のように結局書き換える。

1
2
3
4
5
 # rbs に File.open()が定義されていない
 # File.open(fpath, "w") {|fd| fd.write data }
 fd = File.new(fpath, "w")
 fd.write data
 fd.close
1
2
3
$ bundle exec steep check --log-level=fatal
bin/main.rb:26:24: ArgumentTypeMismatch: receiver=singleton(::Analyzer), expected={ :path => untyped }, actual=::String (fpath)
lib/analyzer.rb:16:8: IncompatibleArguments: receiver=singleton(::User), method_type=(name: untyped, no: untyped) -> ::User (User.new(row["No"], row["Name"]))

やった! main.rb で出ていた、Fileクラスのエラーが消えました!

ArgumentTypeMismatch, IncompatibleArgument

この2つのエラーですが、キーワード引数に変更した時の呼び出し側の修正をしていなかったことで出ていたエラーでした。

bin/main.rb 実行したらエラーになったので、修正したら、上記エラーはでなくなりました。(テスト書いてないから、こういうことになるんだよ…テスト大事)

エラーが出なくなったから、終わり!!!にはなりません。

なぜなら、rbsファイルには、untyped の宣言しかしていないのだから…

本番はこれからです。

rbsファイルに型を定義する

rbs ファイルを以下のように修正しました。
(Structを使ったクラスはとりあえず、untypedのまま)

1
2
3
4
5
6
7
8
9
10
11
12
class User
  @no: Integer
  @name: String
  @scores: Array[untyped]
  def initialize: (no: Integer, name: String) -> untyped
  def add: (score: untyped) -> untyped                                                                                      
end

class Analyzer
  def self.load: (path: String) -> Array[User]
  def self.parse_row: (row: CSV::Row) -> User
end

これで実行すると、、、

1
2
$ bundle exec steep check --log-level=fatal
sig/sample.rbs:11:28...11:36	UnknownTypeNameError: name=::CSV::Row

CSVクラスは、標準添付ライブラリのため、requireして使っているので、Steepfile のlibrary で追加します。

1
2
3
4
5
6
7
8
target :app do
  signature "sig"

  check "bin"
  check "lib"

  library "csv"                                                                                                             
end

そして実行すると、、、

1
2
3
$ bundle exec steep check --log-level=fatal
lib/analyzer.rb:9:30: IncompatibleAssignment: lhs_type=::CSV::Row, rhs_type=::Array[(::String | nil)] (row)
  ::Array[(::String | nil)] <: ::basicobject="" ::csv::row="=" ::object=""> ::BasicObject <: ::csv::row="" code="" does="" hold="" not="">

やっとそれっぽいエラーが出てきました。

エラーメッセージに、lhs_type, rhs_typeが出ていますが、lhs_type がrbsで定義している型、rhs_typeが想定している型が表示されています。

CSV.foreach() のブロック引数のrow は、実行すると、CSV::Rowのインスタンスとなり、parse_row()のパラメータに設定されるのですが、rbsのCSV.foreach() では、Array[String?] で定義されているようです。

rbsに合わせて、Analyzer.parse_row() の引数をArray[String?] とすると、今度はrow.headers でエラーになってしまう…

悩ましい…

そこで、CSVは、rbsを参考に使用しているメソッドの以下の定義を sig/sample.rbsに追加することにしました。(Steepfileのlibrary定義はコメントアウトしました)

1
2
3
4
5
6
7
class CSV < Object
  def self.foreach: [U] (String, ?::Hash[Symbol, U] options) { (CSV::Row arg0) -> void } -> void
end

class CSV::Row < Object
  alias [] field                                                                                                                                                               
end

実行してみると、、、
エラーがなくなり、Steepでの型解析は正常となったようです。

使ってみての感想

Rubyのクラスについては、rbsの定義を参照しているので、rbsに定義されていない型だとエラーになってしまうようです。
そのため、普段書き慣れている記述でエラーになるケースもあり、この小さなプログラムでも確認したりするのに、結構時間を使いました。

使ってみての感想は、「やっぱり型書くのは大変だな…」という印象が強いのですが、
IDEを使ってコードを書く時に型情報までSuggest表示されれば、
Rubyの経験があまりない人でも書きやすかったり、大人数のプロジェクトなどでは、プログラムの記述が揃うし、確かにバグは少なくなるような気がします。

でも、やっぱりRubyは自由に書きたい…。