Steep by Steep

Posted on December 18, 2020 00:58
Share on:

この記事は、ケーシーエスキャロット Advent Calendar 2020 17 日目の記事です。(ちょっと過ぎちゃったけど)

Ruby 3の静的解析機能の1つ。steep を使ってみようと思います。

リポジトリは、こちら

タイトルの Steep by Steep は、中学の頃流行った New Kids On The Block の “Step By Step” にかけてみました。

 

インストール

Rubyは、3.0.0-preview2を使っています。
steepのinstallをします。gem でinstallします。

1
2
3
4
5
6
$ gem install steep
Successfully installed steep-0.38.0
Parsing documentation for steep-0.38.0
Installing ri documentation for steep-0.38.0
Done installing documentation for steep after 2 seconds
1 gem installed

サンプルで使うコードです。

色々な型を試してみたかったので、設計面は無視したコードになっています。

lib/user.rb

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

lib/analyzer.rb

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
require 'csv'                                                                                                                                                                          
  
class Analyzer
  @@score = Struct.new("Score", :subject, :score)

  def self.load(path)
    users = Array.new
    CSV.foreach(path, headers: true) do |row|
      users << parse_row(row)
    end

    users
  end

  def self.parse_row(row)
    u = User.new(row["No"], row["Name"])

    row.headers[2..-1].each do |subject|
      u.add @@score.new(subject.downcase, row[subject].to_i)
    end

    u
  end
end

bin/main.rb

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
LOAD_PATH &lt;&lt; File.join(File.dirname(File.expand_path(__FILE__)), "../lib")                                                                                                            
  
require 'user'
require 'analyzer'

# 使用するデータ
data = <<-EOS
No,Name,English,History,Science
1,Akina,85,88,87
2,Bob,78,95,85
3,Candy,83,80,92
4,David,85,83,88
5,Emily,78,93,85
6,Fumiya,80,82,89
7,George,92,88,79 
EOS

それでは、実際に使ってみます

1
$ steep init

と入力すると、Steepfile が作成されるので、ここに定義をしていきます。

Steepfile

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

  check "bin"
  check "lib"
end

これだけ定義して、まず実行してみます。

1
2
3
$ 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 })
lib/user.rb:7:2: NoMethodError: type=self, method=attr_reader (attr_reader :no, :name, :scores)

エラーが出ました。
main.rbはおいておいて、Userクラスで、attr_reader に定義をしていないことを指摘しているようなので、定義を追加してみます。

定義ファイルは、sigフォルダの中に .rbsファイルで書くとあるので、
sig/sample.rbs を作成して以下のように定義してみました。

1
2
3
4
5
class User                                                                                                                                                     
  @no: Integer
  @name: String
  @scores: Array[Score]
end

もう1度実行してみると、

1
sig/user.rbs:4:17...4:22        UnknownTypeNameError: name=::Score

エラーが変わりました。
ScoreというTypeがわからないというエラーが出ました。

これは、Analyzerクラスの中でStructで定義しているからかな…

ひとまず、先に進めるために untyped として、もう1回実行してみると、次のエラーに変わりました。

1
lib/user.rb:2:2: MethodArityMismatch: method=initialize (def initialize(no, name))

initializeの定義をしていないので、次はメソッドの定義をしていきます。

こんな感じかな(とりあえずuntyped)

1
2
3
4
5
6
7
class User
  @no: Integer
  @name: String
  @scores: Array[untyped]
  def initialize: (no: untyped, name: untyped) -&gt; untyped                                                           
  def add: (score: untyped) -&gt; untyped
end

実行してみると、、、

1
2
lib/user.rb:2:2: MethodArityMismatch: method=initialize (def initialize(no, name))
lib/user.rb:9:2: MethodArityMismatch: method=add (def add(score))

エラーになってしまいました…
Analyerクラスが定義されていないことによる影響かな…と頭をよぎったので、Analayzerクラスも定義してみます。

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

class Analyzer
  def self.load: (path: untyped) -&gt; Array[User]
  def self.parse_row: (row: untyped) -&gt; User
end

実行してみると、、、

1
2
3
4
5
6
7
8
$ 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 =&gt; 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) -&gt; ::User (User.new(row["No"], row["Name"]))
lib/user.rb:2:2: MethodArityMismatch: method=initialize (def initialize(no, name))
lib/user.rb:9:2: MethodArityMismatch: method=add (def add(score))

エラー増えた…

リポジトリ内の sig/project.rbi も確認してみたのですが、定義の仕方がおかしいわけではなさそうです… 

もう少し中身を確認した方がよさそうなので、日を改めて取り組んでいきたいと思います。