`rigor sig-gen`でRBSを生成する
rigor checkがコードに満足しているのにsig/がまだほぼ空のとき、解析器は自分以外には届かない有用な推論を行っています。rigor sig-genはその仲間のコマンドで、推論されたシグネチャをRBSとして発行し、ツールチェーンの残り — Steepのクロスチェック、IDEのツールチップ、あなたのgemのsig/を読む下流の消費者 — がRigorが見ているものを見えるようにします。
この章では、コマンドのUX、分類モデル、3つの出力モード、そしてADR-5の非対称な「戻り値には厳格、パラメータには寛容」ルールから直接出てきた--paramsポリシーのトレードオフを順に見ていきます。
この章の内容 使いたくなる場面 · 初回の実行 · 出力モード · 分類モデル · 対応するメソッド形状 ·
--paramsポリシーとADR-5 · RSpecを意識した観察 ·--writeが行うこと · まとめ · 今日の制限
使いたくなる場面
Section titled “使いたくなる場面”- RBSカバレッジがゼロのRubyプロジェクトを引き継ぎ、
rbs prototype rbの構文的なスケルトンよりも誠実な出発点が欲しい。 - メソッドを追加し、
rigor checkがそれを認識した。今度はシグネチャを手で打ち直すことなく対応するsigファイルを更新したい。 - 既存のRBSは
() -> Numericと宣言しているが、Rigorは() -> Integerを証明している。レビューの後、より厳密な綴りをsig/に適用したい。
そうでないもの: ソースコードに表れていない意図を捉える手書きのRBSの置き換え。公開メソッドが「to_sに応答するものなら何でも」という契約(contract)のために_ToStrを受け付けるべきでも、現在の呼び出し元がたまたまStringしか渡していない場合、sig-genは_ToStrをあなたのために発明しません — 後述の--paramsポリシーセクションとADR-5がその理由を説明します。
lib/calc.rbが次のように与えられたとします:
class Calc def add(a, b) "sum" end
def greet(name) "hi" endendそして空のsig/の下でrigor sig-genはRBSスケルトンを表示します:
$ rigor sig-genclass Calc # [new] def add: (untyped, untyped) -> String # [new] def greet: (untyped) -> Stringendデフォルトではコマンドは何も書きません — レビューできるよう提案を表示するだけです。--writeを渡すと提案をsig/に適用します。
3つの出力モード
Section titled “3つの出力モード”| モード | 動作 |
|---|---|
--print(デフォルト) | RBSをstdoutに表示。ソースファイル+クラス宣言でグループ化される。 |
--diff | 既存の宣言された綴り(あれば)と推論された綴りを比較する統一スタイルのdiffを表示。読み取り専用。 |
--write | 提案をsig/<path>.rbsに適用。ファイルを作成し、既存のクラス宣言に新しいメソッドを挿入し、クラスがまだ宣言されていないファイルには新しいクラスブロックを追加。 |
--writeはファイルシステムに触れる唯一のモードです。configuration.signature_paths(デフォルトsig/)の内側でのみ動作します;そのツリーの外側にあるものは書き込まれずにskipped_outside_sig_rootとして報告されます。
rigor sig-genが考慮するすべてのメソッドは、5つの状態のいずれかに着地します:
| 分類 | 意味 |
|---|---|
new-file | レシーバークラスを宣言するRBSファイルが一切ない。 |
new-method | RBSファイルがクラスを宣言しているが、このメソッドは宣言していない。 |
tighter-return | RBSファイルがメソッドを宣言しているが、推論された戻り値が宣言された戻り値の真の部分型(subtype)。 |
equivalent | 推論された戻り値が宣言された戻り値の真の部分型ではない — 同一・より広い・無関係のいずれか — ので、締めるものがない。サイレントにスキップ。 |
skipped | 以下のいずれかの理由で対象外。 |
3つのsig.skipped.*理由は:
sig.skipped.complex-shape— メソッドが任意・rest・キーワード・ブロック・転送パラメータを持つ。MVPの本体型付けパスは必須位置パラメータしか扱えない;複雑な形状は将来のスライス(slice)を必要とする。sig.skipped.untyped-return— メソッド本体の最終式がDynamic[top]として型付けされる。untypedを絞り込みとして発行することは助けではなくノイズになる。sig.skipped.user-authored—--overwriteが設定されておらず、メソッドの既存のRBS宣言を置き換える必要がある。
3つのsig.generated.*識別子(sig.generated.new-file / new-method / tighter-return)は--format=jsonの下でJSONフィールドとして発行されるため、CIゲートの消費者がこれらをルーティングできます。
ジェネレータが対応するメソッド形状
Section titled “ジェネレータが対応するメソッド形状”スライスごとに(各スライスはCHANGELOGエントリーを通じて出荷された — このリストは現在の状態です):
- 必須位置パラメータを持つ素のインスタンス
def foo。new-methodとtighter-returnの両パスが適用される。 - シングルトン側
def self.fooとclass << self; def foo; end。def self.foo: ...としてレンダリングされ、既存のRBSに対してはReflection.singleton_method_definitionと照合される。 - リテラルSymbol引数を持つ
attr_reader/attr_writer/attr_accessor。戻り値型はScope#class_ivars_forから累積されたivar型。ジェネレータは長形式のdef name: () -> Tスペルを発行し、ライターのマージパスが変更なしに適用されるようにします;既存の短形式のattr_reader name: T宣言はユーザー著作として認識され、重複するdef挿入を生成しません。
ジェネレータがまだ対応しない(サイレントにスキップする)メソッド形状:
- 任意 / rest / キーワード / ブロック / 転送パラメータ。
define_method(:name) { ... }。- 本体が
Dynamic[top]として型付けされるメソッド(本体推論が有用な戻り値型を証明できない)。
これらはADR-14のフォローアップとして追跡されています。
--paramsポリシーとADR-5
Section titled “--paramsポリシーとADR-5”--params=POLICYフラグは、発行されるRBSでパラメータ位置がどう綴られるかを制御します。3つのポリシーがあります;2つは今日接続されており、1つは予約済みです。
| ポリシー | 動作 |
|---|---|
untyped(デフォルト) | すべてのパラメータがuntypedとして綴られる。推論由来のパラメータ契約は将来の呼び出し元に課されない。ユーザーがパラメータ型付けの著作権を完全に保持する。 |
observed | --observe=PATH...(存在する場合spec/がデフォルト)の下のすべての呼び出しサイトから引数型を収集し、パラメータ位置ごとにユニオン(union、合併型とも)し、RBSに消去し、ユニオンを発行。 |
observed-strict | 予約済み。ロールカタログが出荷されるとケイパビリティ(capability)ロール(_ToStr、_ToS、…)へさらに広げる。現在は使用エラーで拒否される。 |
デフォルトが意図的にuntypedを優先するのは、ADR-5の節2のためです: メソッドのパラメータ契約は、現在の呼び出し元がたまたま使う最も具体的な形ではなく、本体のロジックが正当化する最も寛容な形であるべきです。observedをロックすると、「既存のスペックがたまたま渡すもの」が契約として静かに凍結されてしまいます — これが章の冒頭で示唆した精度 / 採用のトレードオフです。
--params=observedは意図的なオプトインです: あなたは「私の呼び出し元が今日渡すものの和集合こそが、私の望むパラメータ契約だ」と言っているのです。これは正しさを保つ拡幅です — 既存のすべての呼び出し元は依然として通過する — が、untypedに比べて契約を狭めます。
RSpecを意識した観察
Section titled “RSpecを意識した観察”--observeをspec/ディレクトリに向けると、ジェネレータは3つのRSpec形のバインディングパターンを認識し、それを使ってDynamic[top]に縮退するレシーバーを型付けします:
RSpec.describe Calc do subject { Calc.new } # :subject → Nominal[Calc]をバインド let(:other) { Calc.new } # :other → Nominal[Calc]をバインド
it "..." do subject.greet("Alice") # 観察: Calc#greetがStringを受け取る other.greet("Bob") # 観察: 同上 described_class.new.add(1, 2) # 観察: Calc#addがInteger、Integerを受け取る endend認識器はRSpec.describe Foo、素のdescribe Foo(RSpec.レシーバーなし)、subject { … }、subject(:name) { … }、let(:name) { … }、let!(:name)、described_class.new(...)を扱います。ネストされたスコープを越えた同名のletバインディングは「後勝ち」;認識器はRSpecの完全なスコープルールを再実装しません — 典型的な1スペックファイルの形状がターゲットです。
認識器はジェネレータ自身の一部です;その恩恵を受けるためにrigor-rspecをインストールする必要はありません。すでにrigor-rspecを診断のために使っている場合、両者は協調なしに並行して動作します。
安全性: --writeが行うことと行わないこと
Section titled “安全性: --writeが行うことと行わないこと”- 行う:
lib/<path>.rbのレイアウトをミラーする新しい*.rbsファイルを作成(configuration.paths.firstのベース名を取り除き、configuration.signature_paths.firstの下に配置)。 - 行う: クラス宣言の閉じる
endキーワードのすぐ前に新しいメソッド宣言を挿入。ファイルの他のすべてのバイトをそのまま保持。 - 行う: ターゲットファイルがまだクラスを宣言していない場合、新しい
class Foo … endブロックを追加。 - 行わない: 設定されたシグネチャツリーの外側のファイルには触れない。
- 行わない:
--overwriteが設定されておりAND候補がtighter-returnでない限り、既存のメソッド宣言を置き換えない。--overwriteなしでは、既存の宣言はユーザー著作とみなされ、新しいメソッドはサイレントにスキップされる。 - 行わない: 既存のRBSにある
attr_reader/attr_writer/attr_accessor宣言には触れない — それらは常にユーザー著作として扱われる。
推奨されるワークフローは、まず--diff、レビュー、それから--write(または絞り込みが意図的だと判断した場合は--write --overwrite)です。
絞り込みがおそらく不完全な推論であるとき
Section titled “絞り込みがおそらく不完全な推論であるとき”真の部分型チェックはtighter-returnを発行するための必要条件です — 既存のRBSが間違っていることを示す十分なシグナルではありません。スライス1の本体型付けパスは暗黙の戻り値式しか検査しないため、次のようなメソッド:
def find(key) return nil unless @table.key?(key) @table[key]endは@table[key]単独の戻り値として型付けされます。既存のRBSが(K) -> V | nilと宣言している場合、推論されたVは厳密に締まって見えます — しかしそれは間違った理由で締まっています(nil分岐は本体型付け器の目には到達不可能ですが、ランタイムの目には到達可能です)。これを適用するとnilの腕をサイレントに削除してしまいます。
ヒューリスティック: 絞り込みが既存のRBSが宣言するユニオンメンバーを落とすとき — T | nil → T、false | true → true、Float | Integer → Float、Array[T] → [T] — それを精度の勝利ではなく矛盾シグナルとして扱い、既存のRBSはそのまま残します。ジェネレータはこれらを自動的に分類しません;--diffレビューステップが人間のゲートが座っている場所です。
rigor自身のsig/ツリーに対しては、これが荷重を支えるポリシーです: 既存の宣言と矛盾するすべてのtighter-returnは、別途証明されるまで不完全な推論として疑われます。
新しいファイルに対する典型的なイテレーション:
# 1. Rigorが何を提案するか見る。rigor sig-gen lib/calc.rb
# 2. spec/をパラメーター型シグナルとして使うため# observed-paramsポリシーで実行する。rigor sig-gen --params=observed lib/calc.rb
# 3. 現在のsig/ツリーと比較する。rigor sig-gen --params=observed --diff lib/calc.rb
# 4. 適用する。rigor sig-gen --params=observed --write lib/calc.rb
# 5. rigor checkを再実行して回帰がないことを確認する。rigor check5つのステップは、コマンドが構築されている5つのADR-14スライスに対応します。期待しなかった結果がいずれかのステップで表示された場合、同じコードに対して解析器が発行するであろう診断が真実のソースです — sig-genは推論の下流の消費者であり、別個の解析ではありません。
- 任意 / rest / キーワード / ブロック / 転送パラメータを持つメソッドはサイレントにスキップ(
sig.skipped.complex-shape)。 define_methodとData.define固有の発行は先送りのフォローアップ(Data.define由来のリーダーはメソッド本体が存在すれば通過)。- 真の部分型チェックは今日漸進的(gradual)モードの受理を使用;
Inference::Acceptanceに予約された:strictモードはフォローアップで到着。 RBS::Writerを介したラウンドトリップは使用されない(上流の設計でコメントを落とす);ジェネレータのバイト範囲挿入は触れない宣言をそのまま保持しますが、触れた宣言の範囲内に散りばめられたコメントは保持できません。
これらはADR-14の先送り項目です;設計の根拠はdocs/adr/14-rbs-sig-generation.mdにあります。
© 2026 TypedDuck. Licensed under CC BY-SA 4.0.