sorbet は少し前に話題になっていた Ruby の type annotaion gem。
sorbet.org
以下のようなコードを書くと静的型チェック、動的型チェックの両方をやってくれる。
require 'sorbet-runtime'
class A
extend T::Sig
sig {params(x: Integer).returns(String)}
def bar(x)
x.to_s
end
end
def main
A.new.barr(91)
A.new.bar("91")
end
上記コードのsrb ts
実行結果
editor.rb:12: Method barr does not exist on A https://srb.help/7003
12 | A.new.barr(91) # error: Typo!
^^^^^^^^^^^^^^
Autocorrect: Use `-a` to autocorrect
editor.rb:12: Replace with bar
12 | A.new.barr(91) # error: Typo!
^^^^
editor.rb:13: Expected Integer but found String("91") for argument x https://srb.help/7002
13 | A.new.bar("91") # error: Type mismatch!
^^^^^^^^^^^^^^^
editor.rb:5: Method A#bar has specified x as Integer
5 | sig {params(x: Integer).returns(String)}
^
Got String("91") originating from:
editor.rb:13:
13 | A.new.bar("91") # error: Type mismatch!
^^^^
Errors: 2
sorbet は前から気になっていたけど社内で運用できる状態になっているのかどうか気になったので土日で調べてみた。
気になっていたこと
- sorbet-typed に更新があったら各リポジトリに更新を反映した PR を自動で作ることは可能なのか
- 社内 gem 用の sorbet-typed は作れるのか
- CI に組み込んで型チェックにコケたら PR 上で自動で指摘させることは可能か
- 型の coverage のようなものを計測することはできるか
- メタプロで生成されたメソッドの型定義はどうなるのか
- 周辺環境はどのくらい整備されているのか
- エディタとの integrate, sorbet の型定義を何かから自動生成する何か
sorbet-typed に更新があったら自身のリポジトリに更新を反映した PR を自動で作ることは可能なのか
sorbet には TypeScript の DefinitelyTyped のような型定義の中央管理システムがあって、gem に型定義を追加する場合はここに PR を投げる形になっている。
GitHub - sorbet/sorbet-typed: A central repository for sharing type definitions for Ruby gems
また、sorbet は型定義ファイルを commit する方針を取っている1ので利用している gem の型定義が更新されると自身のリポジトリでsrb rbi update
を実行し、明示的に更新する必要がある。
型定義をいい感じに更新する PR を投げてくれる script がどこかに転がっていればいいなと思ったけど、パッと調べたところ見当たらなかった。作る必要がありそう。
travis cronjob とか使えばいい感じにできるんじゃないかと思っている。しらんけど
docs.travis-ci.com
結論:自作すれば可能
社内 gem 用の sorbet-typed は作れるのか
上に書いたように sorbet は sorbet-typed を使って型定義を共有する仕組みを作っている。
ただ、これは public なので private gem の型定義をここに書いちゃうと全世界に公開することになってしまう。
それは困る。
社内用の sorbet-typed を作り、それを差し込めるような仕組みがあれば private gem でも社内で利用することはできるはず。
sorbet, sorbet-typed の issue や sorbet Slack を眺めたりしたけど private gem に対する言及はどこにもされていなかった。
その辺りのソースコードも眺めてみたけど sorbet-typed の URL がハードコードされていた。
サポートされていない可能性が高い?
じゃあ Stripe はどうしているんだという話はあるし、他で問題になっていないわけがないので何を根本的に勘違いしている可能性がある
結論:本家に PR 投げれば可能?
CI に組み込んで型チェックにコケたら PR 上で自動で指摘させることは可能か
PR 上でコメントをつけてくれるサービスはいくつがあるが、社内では reviewdog を使うことが多い。
reivewdog は sorbet 未対応だったので PR を出した。数日中に使えるようになるはず。
追記:出していた PR が merge された!
github.com
他のサービスが sorbet に対応しているかどうかはちゃんと調べてない
結論:reviewdog を使えば可能
型の coverage のようなものを計測することはできるか
ここのサポートは手厚く、かなり細かい情報まで取得することができる。
Tracking Adoption with Metrics · Sorbet
しかも、導入フェーズに合わせてどの metrics に着目すべきかまで書いてくれているのでとても分かりやすい。
https://sorbet.org/docs/metrics#suggestions-for-driving-adoption
取得できる metrics 一覧
% srb tc --metrics-file=metrics.json
% cat metrics.json | jq ".metrics[].name" | sort
"ruby_typer.unknown..error.max"
"ruby_typer.unknown..error.min"
"ruby_typer.unknown..error.p25"
"ruby_typer.unknown..error.p50"
"ruby_typer.unknown..error.p75"
"ruby_typer.unknown..error.p90"
"ruby_typer.unknown..error.total"
"ruby_typer.unknown..release.build_scm_commit_count"
"ruby_typer.unknown..release.build_timestamp"
"ruby_typer.unknown..run.utilization.context_switch.involuntary"
"ruby_typer.unknown..run.utilization.context_switch.voluntary"
"ruby_typer.unknown..run.utilization.inblock"
"ruby_typer.unknown..run.utilization.major_faults"
"ruby_typer.unknown..run.utilization.max_rss"
"ruby_typer.unknown..run.utilization.minor_faults"
"ruby_typer.unknown..run.utilization.oublock"
"ruby_typer.unknown..run.utilization.system_time.us"
"ruby_typer.unknown..run.utilization.user_time.us"
"ruby_typer.unknown..types.input.bytes"
"ruby_typer.unknown..types.input.classes.total"
"ruby_typer.unknown..types.input.files"
"ruby_typer.unknown..types.input.files.sigil.autogenerated"
"ruby_typer.unknown..types.input.files.sigil.false"
"ruby_typer.unknown..types.input.files.sigil.ignore"
"ruby_typer.unknown..types.input.files.sigil.strict"
"ruby_typer.unknown..types.input.files.sigil.strong"
"ruby_typer.unknown..types.input.files.sigil.true"
"ruby_typer.unknown..types.input.methods.total"
"ruby_typer.unknown..types.input.methods.typechecked"
"ruby_typer.unknown..types.input.sends.total"
"ruby_typer.unknown..types.input.sends.typed"
"ruby_typer.unknown..types.sig.count"
metrics.json を人間が見やすい形式にしてくれる gem も見つけた。これを各 PR に comment する bot 作るととても良さそう。
github.com
bundle exec srb tc --metrics-file /tmp/sorbet_metrics.json
# No errors! Great job.
bundle exec sorbet_progress /tmp/sorbet_metrics.json
# Sorbet Progress
# Progress for sig coverage
# total_signatures 7528
# total_methods 183447
# total_classes 112433
# Progress for file coverage
# sigil_ignore 12 0.20 %
# sigil_false 5466 91.60 %
# sigil_true 460 7.71 %
# sigil_strict 12 0.20 %
# sigil_strong 17 0.28 %
# ---------------------------------------
# Total: 5967 100%
# Keep up the good work 👍
結論:可能
メタプロで生成されたメソッドの型定義はどうなるのか
experimental な機能として、メタプロで定義したメソッドに対して型定義を行うための plugin を書くことができる。
Metaprogramming plugins · Sorbet
class Metaprogramming
def self.macro(name)
define_method("#{name}_time") { name }
end
macro(:bed)
macro(:fun)
end
hello = Metaprogramming.new
hello.bed_time
hello.fun_time
上記のような macro を定義する DSL があった場合、以下のような plugin を書くことで sorbet にメソッドが定義されていたかのような挙動をさせることができる。
source = ARGV[5]
/macro\(:(.*)\)/.match(source) do |match_data|
puts "def #{match_data[1]}_time; end"
end
Caveats
Sorbet decides which plugin to call using method names only. This might be a problem if different metaprogramming methods use the same name, have similar usages, but behave differently. To illustrate, using the configuration above, the following calls macro_plugin.rb twice but results in an error.
https://sorbet.org/docs/metaprogramming-plugins#caveats
気をつけないといけないこととして、同じ interface の DSL を作ると見分けるのが難しくなることが挙げられていた。
plugin には string のクラス名が渡ってくるので klass_str.constantize.method(method_name).owner
で定義元を確かめることはできるが、クラスを load する必要があるのでsrb tc
がかなり遅くなるはず。悩ましい。。。
※ Rails はメタプロの宝庫なのでどうなるんだろうと思っていたら、ちゃんとプロジェクトが進んでいた。
GitHub - chanzuckerberg/sorbet-rails: A set of tools to make the Sorbet typechecker work with Ruby on Rails seamlessly.
結論:基本的には可能
周辺環境はどのくらい整備されているのか
エディタとの integrate
Language Server Protocolに準拠しているので様々なエディタと互換性はあるらしいが、vscode plugin のようなものは見当たらなかった。
自分で設定して Language Server を立ち上げれば使えるのかも。
結論:可能(と言っている)
sorbet の型定義を何かから自動生成する何か
色々あった。
ちょっと性質は違うけど sorbet 用の rubocop も見つけた。
GitHub - Shopify/rubocop-sorbet
まとめ
今からすぐに使える感じではなかったけど、社内 gem の sorbet-typed 問題さえ解決すれば手の届く範囲にあるなと思った。
前向きに検討したい。