rubygem開発でSteepを使って型を書く時の現状のオススメ設定 (2023年3月版)

Rails(というかActiveRecord)に型を付けるのは大変だが、Railsが絡まないrubygemにはそんなに苦労なく型が書けるので、これからgemを書く時には型を書きたいという人向けに今のところオススメの設定を紹介します。 というか自分が忘れるのでまとめておきます。

現状とはsteep-1.3.1, rbs-2.8.4を指します。

rbsは既に3系が出ていますが、一般利用者が型検査に利用する場合はsteepを使うはずで、steepはまだrbsの3系に対応していません。また、rbs-3.0で多少変わっているところもあるので、割と寿命が短い話かもしれません。

設定例

とりあえず結論から。Steepfileとrbs_collection.yamlを修正します。

Steepfile:

D = Steep::Diagnostic

target :lib do
  signature "sig"

  check "lib"                       # Directory name
  
  # configure_code_diagnostics(D::Ruby.strict)       # `strict` diagnostics setting
  # configure_code_diagnostics(D::Ruby.lenient)      # `lenient` diagnostics setting
  configure_code_diagnostics do |hash|
    hash[D::Ruby::MethodDefinitionMissing] = :warning
    hash[D::Ruby::UnknownConstant] = :information
  end
end

rbs_collection.yaml:

# Download sources
sources:
  - name: ruby/gem_rbs_collection
    remote: https://github.com/ruby/gem_rbs_collection.git
    revision: main
    repo_dir: gems

# A directory to install the downloaded RBSs
path: .gem_rbs_collection

gems:
  - name: steep
    ignore: true
  - name: rbs
    ignore: true
  - name: <developing gem name>
    ignore: true

それぞれsteep init, rbs collection initで雛形を生成できます。

この設定をした上で、自分が開発中のgemの型定義をsig以下に書いていきます。lib以下とファイルパスを揃えてエディタで切り替え易い様に設定しておくと良いでしょう。

解説

Steepfile

Steepfileで型検査ツールであるsteepの挙動を制御できます。signatureとcheckは何を指定しているのか簡単なのですが、configure_code_diagnosticsがちょっと分かりにくいのと、快適な結果を得るために地味に重要なので、ここを設定しておきます。

この設定は、steepが検出した各通知対象をどのエラーレベルに設定するかを指定します。nilにセットすると報告しなくなります。

steepはsteep checkコマンドで型を検査できますが、この時デフォルトのエラー通知レベルはwarningになっています。なので、configureで指定した通知対象のレベルが:error:warningでないと通知しませんしFailedになりません。エラー通知レベルは--severity-levelオプションで実行時に指定できます。

では、各通知対象のデフォルト設定がどうなっているかというと、現時点で明確なドキュメントは無いのでsteepのソースコードを見る必要があります。lib/steep/diagnostic/ruby.rb辺りにあります。

ソースコードを参照すると分かるんですが、デフォルトが一番厳しくて、strict, lenientの準に緩くなります。自分の感覚ではデフォルトは厳し過ぎて、strictでは緩過ぎる所はあって、現状では個別に調整した方が良いと感じています。

その中でも特に設定しておいた方が良い値が、上記の設定例で記載したMethodDefinitionMissingUnknownConstantです。MethodDefinitionMissingは型定義に書いてあるのに実装が存在しない場合の警告で、UnknownConstantは型定義の無い定数・クラス・モジュールに対する参照がコード上に存在する時の警告です。

MethodDefinitionMissingはデフォルトが:informationで、自分はこれは単純に実行した時に警告として出て欲しいのでwarningに変更しました。

UnknownConstantは、現状型定義の無いライブラリを参照するのが普通なので、ノーオプションで実行してこのwarningが出ると非常に結果が煩雑になるので、:informationレベルに設定して、オプション指定して意図的に見たい時にだけ引っかかる様にしました。完全に無効化すると、gem_rbs_collectionにライブラリの型定義を追加したいというモチベーションを奪うので、無効化はしていません。

その他にもコードの状況に応じて、現時点では必要無いなと思う通知内容があれば、:informationか:hintレベルにしておく方が結果を見易くする上では有用だと思います。

rbs_collection.yaml

これはrbs collectionコマンドの設定ファイルです。rbs collectionを使うと、Gemfileやgemspecで依存している各ライブラリの型定義をgem_rbs_collectionリポジトリから取得してきてsteepに認識させることができます。また、gemの中にsigがある場合はそちらを参照する様になります。

しかし、現時点で何も考えずに設定すると結構ハマるというか、rbs collection initやった上で設定を変更せずにsteepを実行すると、めちゃくちゃ大量にエラーが出ると思います。 これは、steepとrbsのgem自体に含まれているsigを検査しようとするので、そこに必要な型定義が足りていないとめちゃくちゃエラーが出るんですね。そしてその型検査は通常自分のgem開発ではほぼ必要が無いものになります。(steepやrbs自体を活用するツールは別)

なので、普通のgem開発でrbs collectionを利用する場合は、steepとrbsを除外する設定を追加しておく必要があります。この設定例についてはsteepのguidesディレクトリに解説があります。 (see. https://github.com/soutaro/steep/blob/master/guides/src/gem-rbs-collection/gem-rbs-collection.md)

もう一つの問題がDuplication declarationエラーが出てしまうことです。

github.com

issueに書かれてるんですが、デフォルトだとrbs collectionが開発中のgem自身を認識してしまうのでSteepfileのsigとrbs collectionを読みにいく挙動で重複してしまい、上記のエラーに繋がります。

この挙動は仕様なのかsteepの管轄なのかrbsの管轄なのか微妙なラインだと思いますが、現状何もガイド無しに二つを同時に使ってgem開発しようとすると結構ハマる問題だと思います。

この問題に対する対策は、Steepfileからsignature指定を削除してrbs collectionに任せるか、rbs_collection.yamlで開発中のgemをignore対象に追加してSteepfileに任せるかのどちらかになります。結果は同じなので好みのやり方を選択してください。上記の設定例では後者を採用しています。

この辺りは、デファクトとなるドキュメントへの導線があれば解決すると思いますが、デフォルトの設定値をどうするかは結構難しい問題で開発体験にも結構影響する所です。今後のアップデートによってこういったことを余り気にする必要がなくなると良いですね。

最近rbs-3.0が出たばかりだし、今後の変化に注目しておきましょう。