gemをrbs対応してsteepで型チェックできるようにする
先日 ruby 3.0.0 が公開されました。
いろんな機能が追加されていますが、その中でも静的型解析は個人的にも使ってみたかったので、試してみました。
今回は、
- 自分が作っている型情報の無いgemのコードにTypeProfで型情報を推論してrbsファイルを作成し、gemに添付する。
- そのgemを使う側のコードで添付されているrbsファイルを元にsteepで型チェックしてもらう。
の2つを試してみたいと思います。
用語
結構新しい単語があって混乱しがちです。
下記のサイトがわかりやすかったです。
先程のサイトからの引用ですが、
用語 | 意味 |
---|---|
RBS | Rubyの型情報記述用の言語 |
TypeProf | 型情報の無いrubyコードからRBSを出力するツール |
steep | rbsファイルを用いてrubyに静的型付を導入できるツール |
です。
準備
今回は僕が作っている、最近slackに追加されたアルファベットの絵文字を簡単に入力するための
slack_alphabetter
という誰得なgemを使おうと思います。
gemの紹介
基本的にはcliツールで
$ slack_alphabetter 'hello!'
:alphabet-white-h::alphabet-white-e::alphabet-white-l::alphabet-white-l::alphabet-white-o::alphabet-white-exclamation:
と出力されるので、これをコピー(slack_alphabetter 'hello!' | pbcopy
など)してslackに貼り付けると
と変換できて楽しい、というgemです。
これは大まかには内部的には、
require 'slack_alphabetter'
puts SlackAlphabetter.convert(引数の文字列)
という感じで動いています。
現状は何も型情報がないので、任意の型のオブジェクトを渡すことができますので、下記のようなエラーを起こせます。
[2] pry(main)> SlackAlphabetter.convert(123)
NoMethodError: undefined method `downcase' for 123:Integer
from /Users/pocari/.rbenv/versions/3.0.0/lib/ruby/gems/3.0.0/gems/slack_alphabetter-0.1.4/lib/slack_alphabetter.rb:24:in `convert'
こういうエラーを回避できるようにslack_alphabetterに型情報を追加してsteepで型チェックすることで、こういった誤った引数の渡している部分をチェックできるようにしよう!というのが今回試す内容です。
gem側の対応
slack_alphabetter側で、typeprofを使い、型情報の無いrubyのコードの型情報を推論してもらいます。
% typeprof lib/slack_alphabetter.rb
# Classes
class OptionParser
alias set_banner banner=
alias set_program_name program_name=
alias set_summary_width summary_width=
alias set_summary_indent summary_indent=
alias to_s help
alias def_option define
alias def_head_option define_head
alias def_tail_option define_tail
class ParseError < RuntimeError
alias to_s message
end
end
module SlackAlphabetter
def emoji: (untyped char, ?String color) -> String
def self.emoji: (untyped char, ?String color) -> String
def convert: (untyped str, ?String color) -> untyped
def self.convert: (untyped str, ?String color) -> untyped
class Error < StandardError
end
end
module Shellwords
alias shellwords shellsplit
alias self.shellwords self.shellsplit
alias self.split self.shellsplit
alias self.escape self.shellescape
alias self.join self.shelljoin
end
なにやら SlackAlphabetter.convert
の引数が untyped(=なんでもいい型) になっています。また、 slack_alphabetter内でrequireしている optparse
の情報も出力されています。
前者のuntypedについては、typeprofの仕組みが実際にrubyのコードを型レベルで 実行し、実際にメソッドに渡されている引数の情報を元に推論しているためで、このファイルのようにメソッド定義だけあっても推論はされません。
後者のoptparseの定義に関しては、ちょっとよくわかりませんが、そういうものなんですかね??
とりあえず、ここでは、SlackAlphabetterの定義だけ使わせてもらって、手動で引数の情報を書くことにします。また、SlackAlphabetter内部のErrorクラスも全然つかってないので、削除します。
ということで、下記のようなrbsファイルを作成します。
module SlackAlphabetter
def emoji: (String char, ?String color) -> String
def self.emoji: (String char, ?String color) -> String
def convert: (String str, ?String color) -> String
def self.convert: (String str, ?String color) -> String
end
rbsを置く場所
rbsができましたが、これをgemのどこにおけばいいんでしょう?
調べてみると、steepでチェックする場合(他のツールがどうかはしらないです) gemのインストールディレクトリ/sig
のフォルダ以下にあるrbsファイルを読んでもらえるようなので、sig/slack_alphabetter.rbs
として保存します。
gemファイルのルート
+ bin/
+ exe/
+ lib/
+ pkg/
- sig/
slack_alphabetter.rbs
+ spec/
Gemfile
Gemfile.lock
LICENSE.txt
Rakefile
README.md
slack_alphabetter.gemspec
この状態で、gemリリースしたものが、 slack_alphabetterの バージョン 0.1.4
になります。
gemを使う側の対応
次にgemを使う側で、slack_alphabetterとsteepをいれて、型チェックしてみます。
一式は https://github.com/pocari/steep_test にpushしています。
まず、今回の対象のコードです。
require 'slack_alphabetter'
puts SlackAlphabetter.convert('hoge')
puts SlackAlphabetter.convert(123)
1つ目のconvert呼び出しはStringを渡しているので正常、2つ目はIntegerを渡しているので型エラーになってほしい呼び出し方です。
次にsteepの設定です。
steepは実行のためにSteepfileというものが必要でそこで使うライブラリの設定などをしていきます。
target :lib do
check "lib"
library "slack_alphabetter"
end
check
でチェック対象rubyコードがあるディレクトリを指定し、library
で型チェックの対象にしたいライブラリを指定します。これをすることで、先ほどgem側で追加したsigディレクトリの中のrbsファイルを使って型チェックをしてくれるそうです。
これで準備できたので、steepで型チェックしてみましょう。
$ bundle exec steep check
lib/main.rb:4:30: ArgumentTypeMismatch: receiver=singleton(::SlackAlphabetter), expected=::String, actual=::Integer (123)
無事、エラーになってほしい Integerを渡している行のみArgumentTypeMismatchになってくれました。
まとめ
- gemに型情報を添付してみた。
- そのgemを使う側のコードを型チェックしてみた。
その他、steepはlspも実装しているらしく、念願のエディタでのrubyコードの自動補完などもできるようになるとますます書きやすくなりそうで楽しみですね。
Discussion