コンテンツにスキップ

ADR-28 — パススコープのメソッドプロトコル契約

ステータス: Accepted、2026-05-23;同一コミットクラスタ(コミット481d810、a54cd2d)で実装済み

プラグインがふるまいのプロトコル — 「このディレクトリのすべてのクラスがこの形のメソッドを定義しなければならない」— を、クラス側のオプトインなしに静的に強制できるプラグイン拡張ポイントを追加する決定を記録する。メカニズムは新しいManifestフィールド(protocol_contracts:)で、Rigor::Plugin::ProtocolContract値オブジェクトを運ぶ。2つのエンジンサイトで消費される: Inference::MethodParameterBinderでのパラメータ型提供(provide)と、貢献プラグインの#diagnostics_for_fileフックでのメソッド存在 + 戻り型チェック(check)。2つの動作消費者が同梱される: examples/rigor-web/(RigWebフレームワークチュートリアル)とplugins/rigor-hanami/(本番Hanamiプラグイン)。

Rubyフレームワークはクラス宣言に記録されないふるまいのプロトコルを日常的に課す。Rack形のWebフレームワークはコントローラーアクションがRack::Requestを受け取りRack::Responseを返すことを期待する;ジョブフレームワークは#performを期待する;シリアライザは#callを期待する。この規約は実在するが、準拠するクラスのソースではなくフレームワークの文書に存在する — そのため何もチェックしておらず、違反は実行時に発見される。

このADR以前のRigorのプラグイン契約(contract)は、プラグインがプロジェクトに反応する3つの方法を提供していた:

  • flow_contribution_for(call_node:, scope:)呼び出しサイトごと。
  • diagnostics_for_file(path:, scope:, root:) — 推論ファイルごと;プラグインがAST自身を辿る。
  • マクロ基板マニフェスト宣言(ADR-16) — Tier A(block_as_methods)はクラスレベルのDSL呼び出しに渡されるブロックselfを絞り込む;Tier B〜Dはメソッドを合成する。

これらのどれも「パスglobGの下で定義されたクラスがプロトコルPを暗黙的に持つ」を表現しない。2つの機能が欠けている:

  1. ディレクトリ → プロトコルのバインド。マニフェストのメカニズムはクラスをそれを定義するファイルのパスでターゲットにしない。
  2. プレーンなdefへのパラメータ型提供。これが本質的なギャップだ。コントローラーのdef get(request)はRBSを持たないため、エンジンはrequestDynamic[Top]にバインドする — そしてDynamic[Top]レシーバーはすべてのメソッドに応答するため、ボディ内の誤用も不正確な戻り型も表面化しない。パラメータに型を与える既存の方法はすべて不十分だ: %a{rigor:v1:param:}(RBS::Extended)はメソッドがRBS宣言済みであることを要求する;マクロTier Aはブロックselfを絞り込み、defのパラメータは絞り込まない;RBSインターフェース(_Controller)はクラスに暗黙的にバインドされない — RBSには「このディレクトリ下のすべてのクラスがこのインターフェースを実装する」という形式がない。

プラグインはdiagnostics_for_fileで戻り型を手動でチェック(defを辿りScope#type_ofでボディを調べる)することはすでにできる。しかし(2)なしには、そのチェックはほぼ無意味だ: requestDynamic[Top]として型付けされると、requestから構築される戻り式はそれ自体がDynamic[Top]となり、何にでも適合する。「提供」が荷重を担う半分だ。

プラグインのManifestprotocol_contracts:を追加する。各エントリーは以下を名指しするfrozenなRigor::Plugin::ProtocolContract値オブジェクトだ:

  • path_glob — 契約が適用されるファイルを選択するFile.fnmatch glob(プロジェクトルートからの相対パス、例: lib/controller/**/*.rb);
  • method_name + singleton — それらのファイルのすべてのクラスが定義しなければならないメソッド;
  • param_types — 位置ごとのindex → 型名の提供;
  • return_type_name — メソッドのボディが適合しなければならない型;
  • severity — 違反診断の深刻度。

契約は2つのサイトで消費される — provide-and-check:

  • provide(エンジン側)。Inference::MethodParameterBinderがティアを1つ獲得し、(RBSの有無にかかわらず)最後(最も権威ある)に適用される: バインド対象のdefがそのファイルの契約に一致する場合、契約のparam_typesがバインドされたパラメータ型を置き換える。メソッドボディはパラメータがプロトコル型を持つかのように解析され — request.no_such_methodのような誤用が通常のコア診断として表面化し、ボディの推論された戻り型が正確になる。
  • check(プラグイン側)。貢献プラグインの#diagnostics_for_fileが、一致するファイルの各クラスがメソッドを定義していることを確認し(そうでなければmissing-protocol-method)、その推論された戻り型がreturn_type_nameに適合することを確認する(そうでなければprotocol-return-mismatch)。

WD1 — .rigor.yml設定キーではなくManifestフィールド。プロトコル契約はフレームワークを知っているプラグインの属性であり、解析対象のプロジェクトの属性ではない。既存の宣言型マニフェストフィールド(owns_receiversopen_receiversblock_as_methods、…)に加わる。プロジェクトごとのレバーは契約の著作ではなく規約パスの設定オーバーライド(WD5)だ。

WD2 — check-onlyではなくprovide-and-check。提供こそが機能を出荷する価値あるものにする半分だ(コンテキスト(2)参照)。推論に先行するためエンジン側でなければならない;check-onlyではコントローラーボディをDynamic[Top]に対して型付けしたまま戻り型チェックが空虚になる。

WD3 — 契約はバインダーの最高優先ティア。順序: Dynamic[Top]フォールバック → RBSオーバーロード → RBS::Extended param:オーバーライド → プロトコル契約。契約対象メソッドのRBSシグネチャも持つクラスは契約によってパラメータ型が上書きされる。これは意図的 — 契約はフレームワーク著者が強制する要件 — であり、フィールドの文書化されたふるまい。

WD4 — エンジンが提供し、プラグインがチェックする。パラメータ提供は唯一のエンジン側変更;推論より前でなければならないためプラグインフックには置けない。メソッド存在チェックと戻り型チェックは推論後に実行され、プラグインの#diagnostics_for_fileに委ねられる — Analysis::CheckRulesはエンジンの固定ルールカタログであり、プラグインの概念から解放されたまま。プラグインは契約のパラメータ型をクエリスコープに再バインドし、Scope#type_ofがエンジン自身の推論と同一にボディを型付けする。

WD5 — マニフェスト宣言のデフォルトパス、プロジェクトごとのオーバーライド。マニフェストは規約のpath_globlib/controller/**/*.rb)を運ぶ。プラグインはPlugin::Base#protocol_contractsをオーバーライドできる — #signature_pathsが使う同じ間接参照 — プロジェクトごとのconfig値を契約セットに折り込む(例: app/controllers/へのリターゲット)。マニフェストはconfig非関与のまま;インスタンスメソッドがconfigの入口。

WD6 — Dynamic[Top]の場合はサイレント。エンジンが契約対象メソッドの戻り型を固定できない場合、チェックはサイレントのまま。プロジェクトの偽陽性規律に従い、不確実な戻り値はフラグを立てるのではなく実行時に先送りされる。

WD7 — 解決不能な型名ではフェイルソフトparam_types / return_type_nameは文字列として運ばれ、解析対象プロジェクトの環境に対して遅延的に解決される。解決不能な名前(プロトコルのRBSが未ロード)は提供を停止 / チェックをスキップし、raiseしない — バインダーの既存のフェイルソフト姿勢と一致。signature_paths:(ADR-25)でプロトコル型の自前RBSを同梱するプラグインは解決を確実にする。

WD8 — pre-1.0プラグイン契約への加法的変更。新しいオプションのマニフェストフィールドと、新しいオプションのMethodParameterBinderコンストラクタキーワード(source_path:、デフォルトはnil)。既存のプラグインも呼び出し元も壊れない;v0.1.x内で安全。

  • lib/rigor/plugin/protocol_contract.rbProtocolContract値オブジェクト(+ ネストされたParamType)。
  • lib/rigor/plugin/manifest.rbprotocol_contracts:フィールド。
  • lib/rigor/plugin/base.rb#protocol_contractsインスタンスメソッド(マニフェストバック、WD5に従いオーバーライド可)。
  • lib/rigor/plugin/registry.rb#protocol_contractsアグリゲーター + #contracts_for_path globルックアップ。
  • lib/rigor/inference/method_parameter_binder.rbapply_protocol_contract提供ティア;コンストラクタがsource_path:を獲得。
  • lib/rigor/inference/statement_evaluator.rbscope.source_pathをバインダーにスレッド通す;build_fresh_body_scopesource_pathをボディスコープ導出を通じて運ぶ(以前はドロップされており、ネストされたスコープのflow_contribution_forのファイル解決も飢えさせていた)。
  • フレームワークプラグインは準拠クラス側のオプトインなしに、コントローラー / ジョブ / シリアライザのプロトコルを強制できる。
  • コントローラー形のボディが実推論を獲得する: 提供されたパラメータ型がボディ全体をチェック可能なコードに変える。
  • 2つの消費者が同梱される: examples/rigor-web/(RigWebフレームワークチュートリアル — 最小参照消費者)とplugins/rigor-hanami/(本番利用: app/actions/下のHanami 2アクションクラスに#handle(request) -> responseプロトコルを強制)。
  • ディレクトリで暗黙的にバインドされたRBSインターフェース。RBSには「パスGの下のすべてのクラスがインターフェースIを実装する」形式がなく、それを発明するとバインドを型言語に押し込むことになる(ADR-0 / ADR-1はコアをRBS正準に保つ)。契約はプラグイン側の宣言のまま。
  • check-only(エンジン変更なし)。WD2に従い却下 — 提供なしには空虚。
  • 一致するクラスごとに合成RBSを注入することによる提供。既存のdef.return-type-mismatchルールがcheck半分を無料で行えるが、共有環境へのファイルごとの合成RBS注入はバインダーティアよりも重く予測不能な変更。2番目の消費者が望む場合に先送り。
  • 汎用的な「クラスがインターフェースを実装する」チェックルール。より大きな機能(ファーストクラスのRigor::Type::Interfaceキャリア(carrier)、適合性診断) — ここではスコープ外;契約が具体的なニーズをカバーする。

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