コンテンツにスキップ

付録 — Steepから来た場合

SteepはRubyの確立した静的型チェッカーであり、RBS駆動の解析のデファクト標準実装である。Steepを使ったことがある場合、最も重要なことはRigorが同じ.rbsファイルを読むということ — 既存のシグネチャはそのまま移植できる。ふたつのツールは排他的ではなく補完的。

この付録はSteepの語彙で考えており、Steepのどの概念がRigorのどの概念に対応するかを知りたいユーザー向け。

この付録の内容 5秒ピッチ · 両者ともRBSを消費する · 型語彙マッピング · .rbアノテーション · Steepfile vs .rigor.yml · 深刻度モデル · 診断語彙 · 抑制 · 「アノテーション不要」 · SteepにあってRigorにないもの · RigorにあってSteepにないもの · 共存パターン · マイグレーションvignette

問いSteepRigor
型のソース.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でも変更なしに動作する:

sig/slug.rbs
class Slug
def normalise: (String) -> String
def self.default_length: () -> Integer
end

Steepは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の形式SteepRigor
StringStringNominal[String](表示: String
Integer?`Integernil`
Array[Integer]Array[Integer]Array[Integer]
[Integer, String](タプル)タプルTuple[Integer, String]
{name: String, age: Integer}(レコード)レコードHashShape{name: String, age: Integer}
_Comparable(インターフェース)構造的構造的ファセット
untypeduntypedDynamic[Top](表示: untyped
botbotBot
toptopTop
boolbool`Constant
voidvoidvoid

Rigorの内部型キャリア(Type::ConstantType::IntegerRangeType::RefinedType::TupleType::HashShape)はSteepのサーフェスには存在しない。境界でRBS等価にエラーされるため、RBSで-> Stringと宣言されたメソッドはRigorが内部的にnon-empty-lowercase-stringと知っていても、呼び出し元の期待を満たす。

この消去契約はdocs/type-specification/rbs-erasure.mdに文書化されている。

Steepはソース内の型アノテーションの小さなセットを認識する:

Steepの.rbアノテーションRigorの対応物
# @type var x: Integer(コアに対応なし)
# @type self: Foorigor-sorbetプラグイン経由のT.bind(self, Foo)
# @type method foo: () -> StringRBSファイル宣言
_ = x(型キャスト)rigor-sorbetプラグイン経由のT.cast(x, T)

Rigorはコアにソース内アノテーションコメントを意図的に提供しない。根拠(ADR-0、ADR-5、堅牢性の原則):

  1. .rbファイルはランタイム開発者のためにきれいに保つ。型を気にしない作者は型コメントを見ない。
  2. アノテーションは境界に属する。Rigorのスタンスはパブリック契約がすべての変数代入ではなく.rbsに存在するということ。
  3. 推論がほとんどの変数をカバーするx = some_callの場合、Rigorはsome_callの戻り型を知っている — アノテートするものはない。

ソース内アサーションが本当に必要なとき(SteepやSorbetから移行中、またはエンジンが追えない複雑なナローイングがある場合)は、rigor-sorbetプラグインがサポートされたパス — 第10章参照。

SteepのSteepfileRigorの.rigor.yml
target :lib do ... endpaths: [lib]
check "lib"paths:でカバー
signature "sig"signature_paths: [sig](省略した場合は自動検出)
library "set", "json"rbs_collection.lock.yaml(RBSのgemコレクション) — Steepが使う同じ仕組み
configure_code_diagnosticsseverity_overrides:severity_profile:
Steepfileの複数ターゲット複数のpaths:エントリー(プロジェクトごとに単一プロファイル)

最大の設定上の違い: Steepのターゲットごとの構造により同じプロジェクトでlib/を厳格に、app/を緩く確認できる。Rigorのプロファイルはプロジェクト全体で、粒度のためのルールごととファイルごとのオーバーライドを持つ。

両ツールとも深刻度コントロールを持つ。形状はわずかに異なる。

SteepRigor
configure_code_diagnostics(D::Ruby.strict)(ターゲットごと)severity_profile: strict(プロジェクト全体)
D::Ruby.lenient / default / strict / all_errorlenient / balanced / strict
Steepfileの診断ごとの深刻度.rigor.ymlseverity_overrides:
D::Ruby::UnknownConstant = :errorseverity_overrides: { call.undefined-method: error }

ルール識別子は1:1では一致しない — Steepのものはクラス名、Rigorのものはドット区切りのファミリー。概念的なモデルは同じ: デフォルトレベルと、ルールごとの昇格/降格。

Steepの診断カタログとRigorのものは同じ根本的な条件について重なるが、名前が異なる。

SteepRigor
Ruby::NoMethodcall.undefined-method
Ruby::ArgumentTypeMismatchcall.argument-type-mismatch
Ruby::IncompatibleAssignment(インスタンス変数にはdef.ivar-write-mismatchでカバー。ローカル変数はフラグを立てない)
Ruby::MethodBodyTypeMismatchdef.return-type-mismatch
Ruby::UnknownConstant(レシーバークラスへのcall.undefined-methodでカバー)
Ruby::UnexpectedKeywordArgumentcall.argument-type-mismatch(キーワードバインドは同じルールを流れる)
Ruby::IncompatibleTypeCase(現時点で直接対応なし)

実践的な含意: SteepとRigorの両方を実行するプロジェクトは、シェイプエラーでは重複した診断を見て、各ツールが相手が捕捉しないものについては補完的な診断を見る。docs/notes/20260503-steep-cross-check-triage.mdノートは作業例 — SteepとRigorを同じプロジェクトで実行し診断ストリームをカテゴリー分けした。

SteepRigor
# 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)。ふたつのツールは異なる採用段階に対応する:

  • グリーンフィールド、初日から型規律disciplineのプロジェクト。Steepが優秀。まずRBSを書き、ボディをそれに対してチェックする。
  • 既存のコードベース、段階的な強化。Rigorが優秀。ゼロ.rbsから始め、最悪のバグに対する診断をすぐに得て、推論が及ばない箇所にのみ.rbsを追加する。
  • 両方同時に。並行して実行する。同じRBSを共有する。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のものは小さく成長中。
  • RBSなしの推論.rbsファイルがゼロのlib/ディレクトリはRigorから有用な出力を生む。Steepはsigが必要。
  • 自動ナローイングを持つリファインメントキャリアunless s.empty?からのnon-empty-stringn > 0からのpositive-int等。
  • メソッド呼び出しを通じた定数folding"foo".upcaseStringではなく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のインクリメンタルストーリーは改善中だがまだ同等ではない。

両チェッカーを望むプロジェクトの一般的な低摩擦なセットアップ:

.rigor.yml
paths: [lib]
severity_profile: balanced
# signature_pathsは自動検出。sig/はSteepと共有される
# Steepfile
target :lib do
check "lib"
signature "sig"
configure_code_diagnostics D::Ruby.default
end

両ツールとも同じsig/を読む。CIはsteep checkrigor check libを独立したステップとして実行する。各ツールの出力は独自のアノテーションチャンネルに行く。同じ行について両者が意見が分かれるとき、立場上のルール: Steepがフラグを立ててRigorが立てない場合は調査する。Steepは通常、Rigorのリファインメントが意識的に吸収するsigのドリフトを示す傾向があり、Rigorは通常Steepが確認しないボディレベルの事実を示す傾向がある。

2年間Steepを使っているプロジェクトを保守しているとする。sig/ツリーは充実しており、推論が不十分だったいくつかのファイルに# @typeアノテーションが現れる。何も壊さずにRigorを追加したい。

手順:

  1. Rigorを開発依存関係として追加するsig/への変更なし。
  2. rigor check libを一度実行する。いくつかの新しい診断が出る — 通常はSteepが出さないナローイング対応の発見(flow.always-truthy-conditionRBS::Extendedで締め付けられた戻りに対するdef.return-type-mismatch)。バグかノイズかをトリアージする。
  3. # @typeアノテーションへの対応を決める。Rigorはそれらを無視する(パーサへのコメント)。ふたつの選択肢: a. そのままにする — Steepがそれを使い続け、Rigorは無視する。何もしない共存。 b. Rigorにもそのアサーションを尊重させたい場合はrigor-sorbetプラグインのT.let/T.castに変換する。
  4. RigorをCIに追加する。両チェッカーが実行され、mergeの前に両方のゲートを通過しなければならない。
  5. オプションでRBS::Extendedで既存のsigを締め付ける。Steepは%a{rigor:v1:...}を通常のRBSコメントとして扱い、Rigorはリファインメントディレクティブとして扱う。同じ.rbsファイルがより厳格なRigor出力と変更なしのSteep出力を生む。

基盤的な前提(契約言語としてのRBS)が共有されているため、移行は本当に低摩擦。

この付録セクションの残りを順番に読む必要はおそらくない。3つの有用なポインタ:

他のツールと比較したい場合は、兄弟付録ページがTypeScriptPHPStanmypy、そしてRubyの推論優先(inference-first)ツールでありRigor自身のsig-genに最も近い親戚であるTypeProfをカバーしている。

© 2026 TypedDuck. Licensed under CC BY-SA 4.0.