付録 — Steepから来た場合
SteepはRubyの確立した静的型チェッカーであり、RBS駆動の解析のデファクト標準実装である。Steepを使ったことがある場合、最も重要なことはRigorが同じ.rbsファイルを読むということ — 既存のシグネチャはそのまま移植できる。ふたつのツールは排他的ではなく補完的。
この付録はSteepの語彙で考えており、Steepのどの概念がRigorのどの概念に対応するかを知りたいユーザー向け。
この付録の内容 5秒ピッチ · 両者ともRBSを消費する · 型語彙マッピング ·
.rbアノテーション · Steepfile vs.rigor.yml· 深刻度モデル · 診断語彙 · 抑制 · 「アノテーション不要」 · SteepにあってRigorにないもの · RigorにあってSteepにないもの · 共存パターン · マイグレーションvignette
| 問い | Steep | Rigor |
|---|---|---|
| 型のソース | .rbsファイル(境界では必須) | .rbsファイル(オプション — 推論がギャップを埋める) |
.rb内のアノテーション | # @typeコメント、型アサーション | ほぼなし — assert_type/dump_typeは内省ヘルパー |
| カバレッジ要件 | Steepfileのcheck/signatureディレクティブがアノテート済みターゲットを要求 | なし — rigor check libはゼロ.rbsでも動作する |
| アノテートされていないコードのデフォルト | Steepに確認させるとエラー | 精密に推論、不明ならDynamic[Top] |
| ツールのフォーカス | オプトインサーフェス(surface)への強い型付け | すべてのファイルへのベストエフォートな精度 |
| 診断の哲学 | すべての型シェイプ(shape)の不一致を示す | バグが証明可能な場合のみ沈黙を破る |
Steepのスローガンが「オプションなマニフェスト型を持つRuby」なら、Rigorのは「証明された事実を持つRuby」。ふたつは重なりながらも異なるワークフローのために設計されている。
両者ともRBSを消費する — それが共通の基盤
Section titled “両者ともRBSを消費する — それが共通の基盤”これが見出し。RBSはRubyの標準シグネチャ言語。SteepとRigorの両方が標準的な型ソースとしてこれを読む。Steep向けに書いた.rbsファイルはRigorでも変更なしに動作する:
class Slug def normalise: (String) -> String def self.default_length: () -> IntegerendSteepはSlug#normaliseのボディをこのsigに対してチェックし、戻り型がずれるとエラーを出す。Rigorはdef.return-type-mismatchルール(第8章)の下で同じことをチェックする。両ツールとも契約(contract)で合意する。
ツールはその上に何をレイヤーリングするかで分岐する:
- Steepはメソッドボディの型チェックと「チェック対象のすべてのメソッドがsigを持たなければならない」という厳格な期待(設定可能だがデフォルト)を追加する。
- Rigorは推論をどこにでも追加し(sigがないと
Dynamic[Top]になるがエラーにならない)、リファインメント(refinement、篩型とも)キャリア(carrier)、定数folding、プラグインサイドのナローイング(narrowing)を追加する。
型語彙 — RBSレベルのマッピングは恒等関数
Section titled “型語彙 — RBSレベルのマッピングは恒等関数”両ツールともRBSを話すので、宣言レベルの型語彙は同じ:
| RBSの形式 | Steep | Rigor |
|---|---|---|
String | String | Nominal[String](表示: String) |
Integer? | `Integer | nil` |
Array[Integer] | Array[Integer] | Array[Integer] |
[Integer, String](タプル) | タプル | Tuple[Integer, String] |
{name: String, age: Integer}(レコード) | レコード | HashShape{name: String, age: Integer} |
_Comparable(インターフェース) | 構造的 | 構造的ファセット |
untyped | untyped | Dynamic[Top](表示: untyped) |
bot | bot | Bot |
top | top | Top |
bool | bool | `Constant |
void | void | void |
Rigorの内部型キャリア(Type::Constant、Type::IntegerRange、Type::Refined、Type::Tuple、Type::HashShape)はSteepのサーフェスには存在しない。境界でRBS等価にエラーされるため、RBSで-> Stringと宣言されたメソッドはRigorが内部的にnon-empty-lowercase-stringと知っていても、呼び出し元の期待を満たす。
この消去契約はdocs/type-specification/rbs-erasure.mdに文書化されている。
.rbソース内のアノテーション
Section titled “.rbソース内のアノテーション”Steepはソース内の型アノテーションの小さなセットを認識する:
Steepの.rbアノテーション | Rigorの対応物 |
|---|---|
# @type var x: Integer | (コアに対応なし) |
# @type self: Foo | rigor-sorbetプラグイン経由のT.bind(self, Foo) |
# @type method foo: () -> String | RBSファイル宣言 |
_ = x(型キャスト) | rigor-sorbetプラグイン経由のT.cast(x, T) |
Rigorはコアにソース内アノテーションコメントを意図的に提供しない。根拠(ADR-0、ADR-5、堅牢性の原則):
.rbファイルはランタイム開発者のためにきれいに保つ。型を気にしない作者は型コメントを見ない。- アノテーションは境界に属する。Rigorのスタンスはパブリック契約がすべての変数代入ではなく
.rbsに存在するということ。 - 推論がほとんどの変数をカバーする。
x = some_callの場合、Rigorはsome_callの戻り型を知っている — アノテートするものはない。
ソース内アサーションが本当に必要なとき(SteepやSorbetから移行中、またはエンジンが追えない複雑なナローイングがある場合)は、rigor-sorbetプラグインがサポートされたパス — 第10章参照。
Steepfile vs .rigor.yml
Section titled “Steepfile vs .rigor.yml”SteepのSteepfile | Rigorの.rigor.yml |
|---|---|
target :lib do ... end | paths: [lib] |
check "lib" | paths:でカバー |
signature "sig" | signature_paths: [sig](省略した場合は自動検出) |
library "set", "json" | rbs_collection.lock.yaml(RBSのgemコレクション) — Steepが使う同じ仕組み |
configure_code_diagnostics | severity_overrides:、severity_profile: |
| Steepfileの複数ターゲット | 複数のpaths:エントリー(プロジェクトごとに単一プロファイル) |
最大の設定上の違い: Steepのターゲットごとの構造により同じプロジェクトでlib/を厳格に、app/を緩く確認できる。Rigorのプロファイルはプロジェクト全体で、粒度のためのルールごととファイルごとのオーバーライドを持つ。
深刻度モデル
Section titled “深刻度モデル”両ツールとも深刻度コントロールを持つ。形状はわずかに異なる。
| Steep | Rigor |
|---|---|
configure_code_diagnostics(D::Ruby.strict)(ターゲットごと) | severity_profile: strict(プロジェクト全体) |
D::Ruby.lenient / default / strict / all_error | lenient / balanced / strict |
| Steepfileの診断ごとの深刻度 | .rigor.ymlのseverity_overrides: |
D::Ruby::UnknownConstant = :error | severity_overrides: { call.undefined-method: error } |
ルール識別子は1:1では一致しない — Steepのものはクラス名、Rigorのものはドット区切りのファミリー。概念的なモデルは同じ: デフォルトレベルと、ルールごとの昇格/降格。
Steepの診断カタログとRigorのものは同じ根本的な条件について重なるが、名前が異なる。
| Steep | Rigor |
|---|---|
Ruby::NoMethod | call.undefined-method |
Ruby::ArgumentTypeMismatch | call.argument-type-mismatch |
Ruby::IncompatibleAssignment | (インスタンス変数にはdef.ivar-write-mismatchでカバー。ローカル変数はフラグを立てない) |
Ruby::MethodBodyTypeMismatch | def.return-type-mismatch |
Ruby::UnknownConstant | (レシーバークラスへのcall.undefined-methodでカバー) |
Ruby::UnexpectedKeywordArgument | call.argument-type-mismatch(キーワードバインドは同じルールを流れる) |
Ruby::IncompatibleTypeCase | (現時点で直接対応なし) |
実践的な含意: SteepとRigorの両方を実行するプロジェクトは、シェイプエラーでは重複した診断を見て、各ツールが相手が捕捉しないものについては補完的な診断を見る。docs/notes/20260503-steep-cross-check-triage.mdノートは作業例 — SteepとRigorを同じプロジェクトで実行し診断ストリームをカテゴリー分けした。
| Steep | Rigor |
|---|---|
# steep:ignore | # rigor:disable all |
# steep:ignore Ruby::NoMethod | # rigor:disable call.undefined-method |
| (ファイルスコープ構文なし) | # rigor:disable-file <rule> |
Steepfile: ターゲットごとのignore_paths:(パススコープ) | .rigor.yml: exclude:(パススコープ);disable:がルールスコープの軸 |
Rigorの抑制語彙はSteepのものよりPHPStanとRuboCopのものに近いが、意図は同じ。
「アノテーション不要」— 最大の実践的な違い
Section titled “「アノテーション不要」— 最大の実践的な違い”Steepは、デフォルトでチェック対象のすべてのメソッドにRBS sigを持つことを期待する(# @typeアノテーションでオプトアウトするか)。sig/ディレクトリのないプロジェクトでsteep checkを実行すると「sigがない」レポートが大量に出る。
Rigorは、デフォルトで推論できるものを推論し、できないときには沈黙する。sig/ディレクトリのないプロジェクトでrigor check libを実行すると少数の高信頼診断が出る — Rigorがボディだけから不健全さを証明できたメソッド。
これは設計通り(ADR-0)。ふたつのツールは異なる採用段階に対応する:
- グリーンフィールド、初日から型規律のプロジェクト。Steepが優秀。まずRBSを書き、ボディをそれに対してチェックする。
- 既存のコードベース、段階的な強化。Rigorが優秀。ゼロ
.rbsから始め、最悪のバグに対する診断をすぐに得て、推論が及ばない箇所にのみ.rbsを追加する。 - 両方同時に。並行して実行する。同じRBSを共有する。Steepの診断ストリームとRigorの診断ストリームは互いを補完する。
SteepにあってRigorにないもの
Section titled “SteepにあってRigorにないもの”- ソース内の
@typeコメント。ソース内アノテーションへのスタンスはともかく、Steepはそれらに対してより豊かなサーフェスを提供する。# @type var x: Integer、# @type self: Foo、_ = xキャスト演算子にRigorコアの対応物はない。rigor-sorbetプラグインがそのギャップを埋める(第10章)。 - 宣言パラメータに対するメソッドボディの型チェック。Steepはボディ内の
xへのすべての参照が宣言されたx: Integerと一致することを強制する。Rigorの類似チェックはdef.return-type-mismatch。パラメータ側のチェックは同等だが保守的(RBS消去ビュー)。 - より厳密なジェネリクス推論。チェーンした呼び出しでのSteepのジェネリクスインスタンス化は現時点でのRigorより積極的。
- 診断分類体系の成熟度。Steepの診断カタログは定着するまでに長い年月を経た。Rigorのものは小さく成長中。
RigorにあってSteepにないもの
Section titled “RigorにあってSteepにないもの”- RBSなしの推論。
.rbsファイルがゼロのlib/ディレクトリはRigorから有用な出力を生む。Steepはsigが必要。 - 自動ナローイングを持つリファインメントキャリア。
unless s.empty?からのnon-empty-string、n > 0からのpositive-int等。 - メソッド呼び出しを通じた定数folding。
"foo".upcaseはStringではなくConstant<"FOO">に解決される。Steepのリテラル型はRigorのものより狭い。 - プラグインサイドの戻り型提供。Steepには
flow_contribution_forに対応するものがない — ドメインDSLの戻り型がリテラルの最初の引数に依存する場合、Rigorはそれをモデル化するが、Steepはしない。 - Sorbet入力アダプタ。
rigor-sorbetの移行はSorbet中間のプロジェクトにとってコストゼロ(sig { ... }ブロックとRBIファイルがRigorのカタログへの入力になる)。SteepはSorbetのsigを読まない。 - キャッシュ駆動のインクリメンタル解析。Rigorのファイルごとのキャッシュは実行をまたいでマシン境界をまたいで生存する(ADR-6)。Steepのインクリメンタルストーリーは改善中だがまだ同等ではない。
共存パターン
Section titled “共存パターン”両チェッカーを望むプロジェクトの一般的な低摩擦なセットアップ:
paths: [lib]severity_profile: balanced# signature_pathsは自動検出。sig/はSteepと共有される# Steepfiletarget :lib do check "lib" signature "sig" configure_code_diagnostics D::Ruby.defaultend両ツールとも同じsig/を読む。CIはsteep checkとrigor check libを独立したステップとして実行する。各ツールの出力は独自のアノテーションチャンネルに行く。同じ行について両者が意見が分かれるとき、立場上のルール: Steepがフラグを立ててRigorが立てない場合は調査する。Steepは通常、Rigorのリファインメントが意識的に吸収するsigのドリフトを示す傾向があり、Rigorは通常Steepが確認しないボディレベルの事実を示す傾向がある。
マイグレーションvignette
Section titled “マイグレーションvignette”2年間Steepを使っているプロジェクトを保守しているとする。sig/ツリーは充実しており、推論が不十分だったいくつかのファイルに# @typeアノテーションが現れる。何も壊さずにRigorを追加したい。
手順:
- Rigorを開発依存関係として追加する。
sig/への変更なし。 rigor check libを一度実行する。いくつかの新しい診断が出る — 通常はSteepが出さないナローイング対応の発見(flow.always-truthy-condition、RBS::Extendedで締め付けられた戻りに対するdef.return-type-mismatch)。バグかノイズかをトリアージする。# @typeアノテーションへの対応を決める。Rigorはそれらを無視する(パーサへのコメント)。ふたつの選択肢: a. そのままにする — Steepがそれを使い続け、Rigorは無視する。何もしない共存。 b. Rigorにもそのアサーションを尊重させたい場合はrigor-sorbetプラグインのT.let/T.castに変換する。- RigorをCIに追加する。両チェッカーが実行され、mergeの前に両方のゲートを通過しなければならない。
- オプションで
RBS::Extendedで既存のsigを締め付ける。Steepは%a{rigor:v1:...}を通常のRBSコメントとして扱い、Rigorはリファインメントディレクティブとして扱う。同じ.rbsファイルがより厳格なRigor出力と変更なしのSteep出力を生む。
基盤的な前提(契約言語としてのRBS)が共有されているため、移行は本当に低摩擦。
次のステップ
Section titled “次のステップ”この付録セクションの残りを順番に読む必要はおそらくない。3つの有用なポインタ:
- 第7章 — RBSと
RBS::Extended— 既にRBSを書いていて、ディレクティブ文法がその上にどう重なるかを知りたい場合。 - 第8章 — エラーの読み方 — ルールカタログ、深刻度プロファイル、ベースライン(baseline)diff — Steepの診断設定の対応物。
docs/notes/20260503-steep-cross-check-triage.md— 同じプロジェクトでSteepとRigorを並行実行した作業例(このプロジェクト自体)。
他のツールと比較したい場合は、兄弟付録ページがTypeScript、PHPStan、mypy、そしてRubyの推論優先(inference-first)ツールでありRigor自身のsig-genに最も近い親戚であるTypeProfをカバーしている。
© 2026 TypedDuck. Licensed under CC BY-SA 4.0.