コンテンツにスキップ

ADR-34 — トップレベルのunresolved implicit-self呼び出しはデフォルトで警告する

ステータス: Accepted, 2026-05-29; v0.1.13で実装

可視のメソッド貢献者に対して解決に失敗するトップレベルのimplicit-self呼び出しサイトで、現在の無音なDynamic[top]の挙動を反転させ、代わりに専用のcall.unresolved-toplevel診断をemitするという決定を記録する。エスケープハッチ——monkey-patchingやメタプログラミングを通じてトップレベルメソッドを導入するプロジェクト向け——はADR-17pre_eval:設定軸であり、同じリリースでランディングした。call.unresolved-toplevelルールとScope#toplevel?述語が出荷され、重大度はseverity_profile:を通じてマップされ、クロスファイルのトップレベルdefインデックスも整っている;ADR-29のPlaygroundデフォルト重大度の配線が残りのスライス(slice)である。

このADRは意図的に狭い範囲に限定している: トップレベルスライス(slice)のみデフォルトを反転する。class / moduleボディ内のimplicit-self呼び出しはADR-24 WD3の下で寛容なままとなる;それらを診断に格上げすることはADR-24 WD4の別途ゲートされた決定であり、このADRによって開かれるものではない

.rbファイルの先頭に書かれたfoo 1(存在しないメソッドを参照)は、今日のRigorで診断を生成しない。実証的に確認済み:

$ echo 'foo 1' > main.rb
$ rigor check main.rb
No diagnostics
$ rigor type-of main.rb:1:1
type: Dynamic[top]

この挙動はADR-24 WD3——「unresolvedなself呼び出しはDynamic[top]のまま」——をすべてのコンテキストに均一に適用した直接の結果である。WD3がその判断を下した理由は、Railsモデルのクラスボディに対する偽陽性の規律だった: すべてのattr_accessor / has_many / scope / validatesは、静的ウォーカーが解決できないselfコンテキスト呼び出しであり、unresolvedなケースをエラーにすると、そのドメインがノイズで溢れる。

しかし同じデフォルトは他の2つのコンテキストで積極的に誤っている:

  1. スタンドアロンスクリプト。ユーザーがbin/processlib/scripts/import.rb、または一時的な分析を駆動する一回限りの.rbファイルを書く。メソッド名のタイポ(processの代わりにprocss)は型チェッカーが検出すべきバグそのものだ——DSLも、メタプログラミングも、クラスボディコンテキストもない。タイポを無音にすることは、スクリプトの単純さに比例したUX上の失敗だ。

  2. ADR-29プレイグラウンド。貼り付けたスニペットにはプロジェクト設定もGemfileもpre_eval:もない。プレイグラウンドの存在意義はまさに「Rigorが何を見ているかを表面化する」ことだ。ユーザーがどの診断が発火するかを学ぼうとしているまさにその瞬間にfoo 1Dynamic[top]と静かに型付けすることは、プレイグラウンドの価値提案と正反対だ。

非対称性が問題だ: クラスボディコンテキストにはメタプログラミングが多いRailsパターンから高いFPリスクがある;トップレベルコンテキストにはない。両者を一つのデフォルトにまとめると情報を失う。

トップレベルスコープ(囲むdefなし、囲むclass / moduleボディなし)でのimplicit-self呼び出しにおいて、呼び出し名が以下のいずれにも解決しない場合:

  1. 同じファイルまたは解析されたpaths:のいずれかのファイル内のトップレベルdef
  2. (Object, name, instance)の下のADR-17Inference::ProjectPatchedMethodsレジストリ内のエントリー、
  3. ロードされたRBS環境から引いた標準的なKernel / Objectプライベートメソッド表面(putsprequireloopraise、…)、

エンジンは新しいcall.unresolved-toplevel診断をemitする。ヒットした場合、解決されたメソッドの戻り型とパラメータ契約(contract)はADR-24スライス1〜3と同様に伝播する。

新しいルールのデフォルト重大度はseverity_profile:に連動する:

プロファイルcall.unresolved-toplevelデフォルト
strict:error
balanced:warning
lenient抑制

プロジェクトは既存のseverity_overrides:設定キーを通じてルールごとのマッピングをオーバーライドでき、個別の呼び出しサイトを# rigor:disable call.unresolved-toplevelで無音にできる。

クラスボディ / defボディのケース(ADR-24 WD4)は閉じたままとする。このADRはdef内部やクラスボディには一般化しない——それらはWD4がすでに検討して先送りにしたRails-DSLのFPコストを抱えている。

WD1 — トップレベルのデフォルト: 無音ではなく警告

Section titled “WD1 — トップレベルのデフォルト: 無音ではなく警告”

無音なDynamic[top]デフォルトはクラスボディコンテキストには正しい(ADR-24 WD3によるRails-DSL寛容性)が、トップレベルコンテキストには誤っている(スクリプト / プレイグラウンドUX)。

Why: FPプロファイルが異なる。クラスボディのunresolved-self呼び出しは静的ウォーカーが追えないメタプログラミングが支配的;トップレベルのunresolved-self呼び出しはタイポ / 忘れたrequireが支配的——まさに型チェッカーが目的とするもの。

How to apply:診断のemitポイントはADR-24スライス1〜2がすでに使っている同じimplicit-selfディスパッチャーパスと同じ;変わるのは、囲むコンテキストがトップレベルのときにミスで何が起きるかだ。WD4がその境界を正確に記録している。

WD2 — エスケープハッチはADR-17のpre_eval:

Section titled “WD2 — エスケープハッチはADR-17のpre_eval:”

monkey-patchingを通じてトップレベルメソッドを正当に導入するプロジェクト(ブート時にロードされるString-on-Objectシェイプ(shape)のパッチ、lib/core_ext/*.rbヘルパー、フレームワークトップレベルヘルパー)は、.rigor.ymlpre_eval:配列(ADR-17に従う)にそれらのファイルを宣言する。事前評価パスがProjectPatchedMethodsを投入し、WD1ディスパッチャーがレジストリを参照し、解決されたエントリーに対して診断が発火しない。

Why:ユーザーはまさにこの形を提案した——「基本的には警告するようにして、monkeypatchやメタプログラミングの供給源は設定で明示的に先行評価させる」——そしてそれは変更なしにADR-17の既存の契約に一致する。メカニズムはすでに存在する;このADRはそれを「機会的な精度向上」から「WD1デフォルトフリップの正規オプトアウト」へと格上げするだけだ。

How to apply:このADRによって新しい設定キーは導入しない。pre_eval:が唯一のノブ。ADR-17のフェイルソフト契約(WD3)は、不正形式のpre_eval:ファイルが解析の継続を妨げないことを意味する——WD1診断はそのファイルが登録したはずのメソッドに対して発火するだけ。

WD3 — call.undefined-methodに折り込まず、新ルールcall.unresolved-toplevel

Section titled “WD3 — call.undefined-methodに折り込まず、新ルールcall.unresolved-toplevel”

別途保持する3つの理由:

  1. 外科的な無効化# rigor:disable call.unresolved-toplevelはこの診断だけを無音にし、明示的レシーバー呼び出しに対してはるかに重要なcall.undefined-methodには影響しない。
  2. 独立した重大度プロファイルマッピング。「決定」の表はcall.unresolved-toplevelをプロファイル変調ルールとして扱っている;call.undefined-methodに折り込むと、両方の呼び出し形状で一つの重大度を強制することになる。
  3. 診断メッセージの特異性call.unresolved-toplevelはADR-17を指すヒントを運べる(「このメソッドを定義するファイルをpre_eval:に追加することを検討してください」);汎用のcall.undefined-methodメッセージは汎用のまま。

Why:プロジェクトの既存ルールファミリー分類法(docs/type-specification/diagnostic-policy.md)に一致し、ルールのアイデンティティは呼び出しサイト形状を追跡し、重大度を追跡しない。

How to apply:診断ポリシーカタログにルールを追加する;囲むスコープテストがトップレベルと報告するときのみ発火するRigor::Analysis::CheckRules側チェックを登録する。

WD4 — 境界: トップレベルのimplicit-self、それだけ

Section titled “WD4 — 境界: トップレベルのimplicit-self、それだけ”

3つの正確な境界条件、すべて連言:

  • 呼び出しに明示的なレシーバーがない(foo 1obj.foo 1ではない)。
  • 呼び出しがdefの中にない(トップレベルスクリプトコンテキスト)。
  • 呼び出しがclass / moduleボディの中にない(つまり、囲む静的スコープがObjectのシングルトン——Rubyが「main」と呼ぶもの)。

いずれかの条件を失敗する呼び出しは既存のディスパッチャーパスにフォールバックする:

  • 明示的レシーバー呼び出し → 標準のcall.undefined-methodパイプライン(既存、挙動変更なし)。
  • def内のimplicit-self → ADR-24スライス1〜3の解決;ミスでADR-24 WD3に従いDynamic[top]のまま(挙動変更なし)。
  • class / moduleボディ内のimplicit-self → ADR-24 WD4の領域;閉じたまま(挙動変更なし)。

Why:このADRの要点はトップレベルとクラスボディのFPプロファイルの非対称性だ。さらに一般化するとADR-24 WD4を再開してRailsモデルボディでのFPリスクを継承することになる。スライスを狭く保つ。

How to apply:囲むスコープテストはInference::Scopeに属する;implicit-selfディスパッチャーブランチは、emitとフォールバックのどちらにするかを決定する前にscope.toplevel?(新しいプレディケート)を確認する。

WD5 — ADR-17の実装は硬い前提条件

Section titled “WD5 — ADR-17の実装は硬い前提条件”

WD1デフォルトフリップは、ADR-17のスライス1〜2がランドする前にリリースしてはならない。レジストリなしでは、トップレベルmonkey-patchを持つプロジェクトにはオプトアウトがなく、新しいデフォルトは好ましくないリグレッションになる。

Why:偽陽性の規律はプロジェクトの最優先価値(feedback_false_positive_disciplineの下でメモリに記録済み)。エスケープハッチなしに診断を出荷すると、動作するコードが怯えてしまい、診断が価値を加えるより速く信頼を損なう。

How to apply:実装順序は固定——

  1. ADR-17スライス1(設定の配管)。
  2. ADR-17スライス2(事前評価ウォーカー + レジストリ + ディスパッチャーティア)。
  3. ADR-34スライス1(このADRの診断emit、今存在するレジストリを参照)。

ADR-17スライス3(キャッシュディスクリプタ)は独立しており、ADR-34スライス1の前後どちらでもランドできる。

WD6 — ハードコードされた重大度ではなく、重大度プロファイルマッピング

Section titled “WD6 — ハードコードされた重大度ではなく、重大度プロファイルマッピング”

「決定」の表はルールの重大度をseverity_profile:を通じてマッピングし、単一の重大度をハードコードしない。

Why:このADRを動機づける3つのコンテキストはリスク許容度が異なる。プレイグラウンドのデモは:error(大声)が欲しい;lenientで新規オンボードされたRailsアプリはチームが移行する間はルールを邪魔にならないようにしたい;balancedの成熟したスクリプト多用lib/:warningでノイズが監査可能な状態に保ちたい。

How to apply:ルールをdocs/adr/8-steep-inspired-improvements.mdの重大度プロファイル表に接続する;新しいメカニズムは不要。

WD7 — プレイグラウンド(ADR-29)はこのルールでstrictをデフォルトにする

Section titled “WD7 — プレイグラウンド(ADR-29)はこのルールでstrictをデフォルトにする”

ADR-29ブラウザプレイグラウンドのリクエストごとのサンドボックスはseverity_profile: strict(またはルールごとのオーバーライドで新しいルールを:errorに)を設定し、貼り付けられたfoo 1スニペットが期待される診断を生成するようにすべきだ。

Why:プレイグラウンドの価値提案は「Rigorが何を見ているかを見せる」こと。balancedデフォルトを継承してこのルールを:warningにマッピングすると、ツールとのユーザーの最初のインタラクションになる可能性が最も高い例をまさに隠すことになる。

How to apply: ADR-29スライス1はすでに同様の理由でADR-32 WD10に従いrequire_magic_comment: falserigor-rbs-inlineをロードするよう修正されていた。このADRが並行の修正を加える: リクエストごとのサンドボックス設定で重大度プロファイル(またはルールオーバーライド)を設定し、新しいルールが発火するようにする。

WD8 — paths:内のファイルが解決スコープ、ファイルシステム全体ではない

Section titled “WD8 — paths:内のファイルが解決スコープ、ファイルシステム全体ではない”

WD1のステップ1ルックアップ(「同じファイルまたは解析されたpaths:のいずれかのファイル内のトップレベルdef」)は明示的に設定済みのpaths:セットに基づき、任意のrequireされたファイルに基づかない。

Why:ユーザーはpaths:にリストすることでファイルをオプトインできる(通常のインターフェース)。paths:の外のファイルは定義上解析の対象外であり、それらに対して解決すると非決定性が導入される(結果は宣言された設定ではなくランタイムのロード順序に依存する)。

How to apply:クロスファイルのトップレベルdefインデックスは既存のプロジェクトクラス発見事前パスと並んで存在し;アナライザー起動時に一度投入されてディスパッチャーに参照される。

推奨順序;各スライスは独立してリリース可能。スライス1〜2はWD5前提条件の背後にあるMVP機能を提供;スライス3はプレイグラウンド統合。

  1. ルール + emit。診断ポリシーカタログにcall.unresolved-toplevelを登録する。Inference::Scope#toplevel?(新しいプレディケート)を追加する。implicit-selfディスパッチャーのミスパスを、scope.toplevel?が成立するときのミス時にProjectPatchedMethodsを参照してemitするよう接続する。severity_profile:を通じて重大度をマッピングする。
  2. クロスファイルのトップレベルdefインデックス。既存のクラス発見事前パスを、すべてのpaths:ファイル全体にわたるトップレベルdef宣言のインデックスも作成するよう拡張する;ProjectPatchedMethodsの前の最初のプローブとして参照する。
  3. プレイグラウンドのデフォルト。WD7に従い、プレイグラウンドサンドボックスの重大度プロファイル(またはルールごとのオーバーライド)を設定して、貼り付けたスニペットで新しいルールが発火するようにする。ADR-29自体の実装にゲートされている。
  • すべてに対してADR-24スライス4を有効にするだけ。WD4に従い却下: スライス4のRailsモデルクラスボディでのFPリスクはまさにWD4が検討して先送りにしたもの。トップレベルは先にリリースできる;クラスボディのケースはゲートされたままとなる。
  • call.undefined-methodに折り込む。WD3に従い却下: 外科的にすべきものを統合する。
  • 無音をデフォルトにしてオプトインを要求する。却下: ユーザーのフレーミング(「基本的には警告するように」)は明確であり、プレイグラウンドのユースケース(WD7)は逆のデフォルトを要求する。
  • 新しいルールなし;ただしトップレベルのミスに対してcall.undefined-methodとして:warningをemitする。WD3のポイント2に従い却下——重大度プロファイルマッピングには重大度を独立して変化させるためにルールのアイデンティティが必要。
  • pre_eval:より発見しやすいメカニズムを使う(例: 専用のtoplevel_methods:配列)。却下: ADR-17はすでに存在し、より広いmonkey-patchケースをカバーする。専用配列は機能を追加せずに発見パイプラインを複製する。
  • Rakeタスクファイル(Rakefilelib/tasks/*.rake。これらはトップレベルだがDSLだ: task :foo => :bar do ... endはunresolved-toplevel呼び出し(taskdescnamespace、…)として読まれる(プロジェクトがRakeスタブを事前評価しない限り)。2つのパス: (a) paths:にRakeタスクを持つプロジェクトに推奨のpre_eval:スニペットをドキュメント化する、(b)ProjectPatchedMethodsにADR-9のflow_contribution_forを通じてトップレベルRake DSLを登録するrigor-rakeプラグインを提供する。スライス1のドッグフードに先送り。
  • bin/*のシバンスクリプト。通常は短く、require_relativeが多く、paths:にリストされるより実行可能ファイルであることが多い。このルールでそれらをトップレベルとして扱うかどうかは、bin/ヒューリスティックまたはファイルごとのスコーピングが必要かもしれない。スライス1のドッグフードに先送り。
  • 診断メッセージはpre_eval:をヒントとして示すべきか? おそらくbalanced / strictではyes;議論の余地はあるが:infoではno。ヒントを書くのに費用はかからないが、ADR-17のインターフェースが進化すれば腐る可能性がある。スライス1の実装に先送り。
  • ブロックボディとclass << selfのインタラクション。トップレベルのclass << self; foo; endはWD4の意味ではトップレベルスコープではない(シングルトンクラスボディはクラスボディ)が、トップレベルに現れるclass << selfはユーザーには「まだトップレベル」と直感的に読めるかもしれない。v1はWD4の厳密な読みに従う;報告が出てきたら再検討する。

今日の挙動がトップレベルのunresolved呼び出しで無音であること——rigor type-of main.rb:1:1Dynamic[top]を返し、rigor checkが「No diagnostics」を返す——が、このADRを促した2026-05-29の会話中に実証的に確認された。ADR-24のクラスボディ寛容デフォルトはそのターゲットドメインには正しい判断だ;このADRは元のWD3のフレーミングが暗黙のままにしていたコンテキストごとの分割を記録する。

  • 2026-05-29 — 初回提案。トップレベルの無音な挙動に関するユーザーの問いと、プレイグラウンド / スタンドアロンスクリプトのユースケースが逆のデフォルトを望むという認識によって引き起こされた。ADR-17の事前評価メカニズムが自然なエスケープハッチだ——このADR自体によって新しい設定インターフェースは導入されない。

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