コンテンツにスキップ

ADR-8: Steepに着想を得た改善

ステータス: 承認(作業上の決定)

Rigor自己解析レポート(非公式)とdocs/notes/20260503-steep-cross-check-triage.mdにあるv0.0.5 Steepクロスチェックトリアージの付属物。診断IDファミリー階層、重大度プロファイル、return-type-mismatchルールファミリーという3つのSteepに触発された改善の実装選択を記録する。

lib/に対してSteep 2.0を実行する(make steep-checkに従って)と、RigorがSteepと比較して持つ3つの構造的なギャップが表面化した。

  1. Steepの診断IDは2セグメント(Ruby::MethodParameterMismatchRBS::DuplicatedMethodDefinition)。Rigorは単一セグメント(undefined-methodwrong-arity)。フラットな名前空間では、# rigor:disableや設定で関連する診断のファミリー(例:「すべての呼び出しサイトルール」)をターゲットにしにくい。
  2. Steepには組み込みの重大度プロファイル(Steep::Diagnostic::Ruby.lenient.strict)が付属する。Rigorは.rigor.ymldisable:リストによるルールごとのオン/オフのみをサポートする。その結果、CIと開発での重大度チューニングが扱いにくい。
  3. Steepはメソッドボディの推論された返り値型が宣言された返り値型を満たせない場合にRuby::MethodBodyTypeMismatchを出力する。Rigorには基盤(スライス(slice)4のFlowContribution::Merger、B1のメソッドごとのReflectionキャッシュ)はあるがルールはまだない——返り値側における既存のargument-type-mismatchの対称的な存在。

このADRは実装がサーフェス(surface)設計を再検討せずにランドできるよう、各改善の選択された方向を記録する。

決定: ルール識別子をfamily.rule-name形式に正規化するfamily[a-z][a-z0-9_]*セグメントの小さな固定セットの1つだ。

ファミリープレフィックス:

familyルール
callcall.undefined-methodcall.wrong-aritycall.argument-type-mismatchcall.possible-nil-receiver
assertassert.type-mismatch(テストハーネスアサーション)、dump.type(デバッグ)
flowflow.always-raises(フローパスがraiseで終わることを証明)、flow.unreachable-branch(リテラル述語による到達不能ブランチ)、flow.dead-assignment(書き込み後に読み込まれないローカル変数)、flow.always-truthy-condition(推論された定数述語、ループ / ブロック / 防衛的な形式の外部)
defdef.return-type-mismatch(以下のスライス#1)、def.method-visibility-mismatch(privateメソッドレシーバーチェック)、def.ivar-write-mismatch(クラスごとのivar具体クラスドリフト)

dump.typeassert.dump-typeではなく独自のdumpファミリーに置かれる。ランタイムセマンティクスが異なるからだ(アサーションは実行を失敗させ、ダンプは診断副作用で常に成功する)。

後方互換性。既存の# rigor:disable undefined-methoddisable: ["undefined-method"]はv0.1.xで動作し続ける。設定/抑制レイヤーは両方を受け入れる。

  • <rule>(プレフィックスなし、レガシー形式)。
  • <family>.<rule>(新しい正規形式)。
  • <family>(ワイルドカード——<family>.で始まる識別子を持つすべてのルールを無効化)。

プレフィックスなし形式はAnalysis::CheckRulesの固定エイリアステーブルを通じて解決される。ユーザーコードが移行した後でエイリアステーブルを削除することは将来のADRだ。

診断サーフェスDiagnostic#ruleは正規(family.rule-name)形式を公開する。Diagnostic#qualified_ruleはデフォルト以外のsource_familyの場合にすでにsource_familyプレフィックスを付ける。組み合わせた形式はsource_family ∉ {:builtin}の場合に<source_family>.<family>.<rule>だ。Diagnostic#to_sは既存の[<qualified-rule>]レンダリングを保つ。

決定: 3つの名前付きプロファイルを導入するlenientbalanced(デフォルト)、strict。各プロファイルはfamily.rule-name:error/:warning/:info/:offにマッピングする固定テーブルだ。

プロファイル挙動
lenient:noクラスの診断のみがエラー。:maybeクラスの診断は:warning。レガシーコードでの段階的な採用に有用。
balancedデフォルト現在のRigorのスタンス: ほとんどのルールは:errordump.type:info。不確かなルールは:warning
strictすべてのルール(flow.*の証明失敗を含む)が:error。CI向け。

プロファイルは最終フィルタだ。ルールは作成された重大度でDiagnostic行を出力する。Analysis::Runnerは結果に追加する前に各診断の重大度をプロファイルから再スタンプする。ルールはプロファイルを直接参照しない。

.rigor.ymlに2つのキーが追加される。

severity_profile: balanced # one of lenient | balanced | strict
severity_overrides:
call.argument-type-mismatch: warning

severity_overridesはルールごとのエスケープハッチ——テーブルは正規ルールid(またはファミリーワイルドカード)でマッチする。severity_overridesの未知のルールidはサイレントにスキップされる。実行ごとのドリフトはパブリックAPIドリフトspecが代わりに検出する。

決定: メソッドボディの推論された返り値型が宣言されたRBSの返り値型を満たせない場合に診断を出力する

スコープ(v0.1.x最初のカット):

  • メソッドにRigor::Reflectionを通じて到達可能な明示的なRBSシグ(インスタンスまたはシングルトン)がある。
  • メソッドボディの最後に評価された式の型がInference::ExpressionTyperから計算可能(Dynamic[top]フォールバックなし)。
  • 比較はdeclared.accepts(inferred):
    • :yes — サイレント。
    • :no:errorとルールdef.return-type-mismatchで診断を出力する。
    • :maybe — v0.1.x最初のカットではサイレント。実装上の規律: ドッグフーディングでRigor自身のlib/に16の警告が見つかり、すべてが同じ解析器精度ギャップのセット({}が宣言された要素型を回復しない、Set.newSet[Symbol]ではなく裸のSetを返す、…)から来ており、ボディの推論型がまだ十分に正確にピン留めできていない。:maybe:warning(とseverity_profile: strict下での:error)に引き上げることは、それらのケースが必要とするナローイング(narrowing)精度改善と一緒にランドするフォローアップにキューイングされている。

最初のカットのスコープ外:

  • RBSシグのないメソッド(比較する宣言された契約(contract)がない)。
  • 複数返り値パス解析。最初のカットはボディの最後の式を推論された返り値のプロキシとして取る。ボディ途中の明示的なreturn、分岐する返り値、raise終了、next/breakパスは今は素通りする。
  • ブロック返り値型。IteratorDispatch/BlockFoldingの上の将来の作業。
  • メソッドオーバーロード——ルールはメソッドのmethod_types配列を参照し、宣言されたすべての返り値型のユニオン(union、合併型とも)を比較ターゲットとして考慮する。

根拠: これはSteepのRuby::MethodBodyTypeMismatchスコープと一致する。ADR-5(ロバストネス原則)は「返り値では厳格に」を要求する。このルールはそのポリシーの最初の具体的なコンシューマーだ。

4. スコープ外の項目(記録のために)

Section titled “4. スコープ外の項目(記録のために)”

Steepに触発されたリストはまた次のものもフラグ立てした。

  • LSP/langserverモード。v0.1.x以降に延期。キャッシュレイヤーは準備できている(B1のメソッドごとのキャッシュ+Steepが誘発したrescueの厳格化)が、モード自体には別の設計パスが必要だ。
  • 詳細テキストフォーマッタ。ソーススニペットレンダリングを持つオプションの--format=detailed。延期。デフォルトのテキストフォーマットはgrep/カウント互換性のためにシングルラインレイアウトを保つ。
  • Data.defineオーバーライド対応イニシャライザーディスパッチ。このADRのスコープ外。CURRENT_WORKはすでにそれを並行安全なエントリーポイントとして追跡している。
  • 診断ファミリーワイルドカードは# rigor:disable callとファミリーごとのCIゲートを明確に表現可能にする。
  • 重大度プロファイルはSteepユーザーが日常的に採用するstrict-CI/lenient-developmentパターンのブロックを解除する。
  • def.return-type-mismatchは既存のargument-type-mismatch(パラメータ)と返り値側の間の対称的なギャップを閉じ、ADR-5の「返り値では厳格に」という約束を果たす。
  • ユーザーコードの既存の# rigor:disable undefined-methodコメントとdisable:設定エントリーはプレフィックスなし形式を使用する。エイリアステーブルが移行を吸収するが、2つの綴りの共存はプラグイン作成者とフォーマッタが理解しなければならないサーフェスを増加させる。計画は正規形式が広く採用された後、将来のADRでエイリアステーブルを削除することだ。
  • 重大度プロファイルの再スタンプは下流のコンシューマー(フォーマッタ、JSON出力)が観察するDiagnostic#severityを変更する。特定の重大度に依存するCIパーサはプロファイルをピン留めすべきだ。
  • def.return-type-mismatchの最初のカットは保守的だ。Dynamic[top]ボディをスキップすることで偽陽性は最小化されるが、分岐する返り値を持つ実世界のコードはv0.1.xカットが処理しないケースを表面化させる可能性がある。計画: それらをフォローアップチケットとして収集する。

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