この記事は、ケーシーエスキャロット 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
サンプルで使うコードです。
色々な型を試してみたかったので、設計面は無視したコードになっています。
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
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
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
LOAD_PATH << 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 が作成されるので、ここに定義をしていきます。
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) -> untyped
def add: (score: untyped) -> 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) -> untyped
def add: (score: untyped) -> untyped
end
class Analyzer
def self.load: (path: untyped) -> Array[User]
def self.parse_row: (row: untyped) -> 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 => 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"]))
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 も確認してみたのですが、定義の仕方がおかしいわけではなさそうです…
もう少し中身を確認した方がよさそうなので、日を改めて取り組んでいきたいと思います。