コンテンツにスキップ

プラグイン

プラグインが存在する理由はひとつ: 一部のメソッドの型が、どんなRBSシグでも表現できない方法でランタイムでの引数のシェイプ(shape)に依存するからです。この章は、それがプラグインに値するのはいつか — そして同じくらい多くの場合、値しないのはいつか — を判断する助けになります。

この章はプラグインの作成は教えません。それはexamples/にあります — 16個のチュートリアルウォークスルーで、それぞれが1つの拡張サーフェスにスポットを当てています — 一方、実際のフレームワーク向けのすぐにインストールできるgemはplugins/にあります。プラグインが必要かどうかを判断するには読み進めてください;作成したくなったらexamples/へ、既存のものをインストールするならplugins/へ進んでください。

この章の内容 プラグインを使うとき · プラグインを書くべきか? — まずこれを読む · プラグインが今日できること · マクロ / DSL展開基板 · 次に読むもの

典型的なケースはドメイン固有の評価器です:

Lisp.eval([:+, 1, 2]) # ランタイムでInteger
Lisp.eval([:<, 1, 2]) # ランタイムでbool
Lisp.eval([:if, true, "a", 0]) # ランタイムでString | Integer

戻り値型は引数配列の先頭のリテラルシンボルに依存します。RBSはここでuntypedしか言えません; Rigorの推論にはどうしようもありません; RBS::Extendedディレクティブは引数のシェイプで変えられません。プラグインならできます

プラグインのニッチに当てはまる他の形状:

  • 単位DSL100.kilometers / 2.hoursSpeedを生成しますが、Rubyのランタイムはユーザークラスを返すIntegerのメソッドとして見ます。
  • ルートヘルパーusers_pathはStringを返しますが、ヘルパーが存在するかどうかは解析器が読む必要があるYAMLファイルに依存します。
  • ステートマシンtransition_to(:foo)は、:fooがどこかで宣言されたstate_machine do ... endブロック内にある場合には有効ですが、そうでなければタイポです。
  • カスタムバリデーターvalidate(:email, value)は解析時に名前付きパターンに一致しないリテラルを捕捉すべきです。

これらのそれぞれにexamples/に実例があります。examples/README.mdページは16の実例をアーキテクチャ軸(設定スキーマ、ファイルI/O、キャッシュプロデューサー、Scope#type_ofを通じたエンジン連携、クロスプラグインファクト(fact)、戻り型コントリビューションなど)で比較し、読む順序を推奨しています。

まだここにいますか? ほとんどの読者はまずプラグインを書くべきか?へ飛ぶべきです — 答えはたいてい「いいえ、RBSとRBS::Extendedで事足ります」です。下記のサーフェスは、本当に「はい」のときのためのものです。

v0.1.0+プラグイン契約(contract) — docs/internal-spec/plugin.mdに固定されており、同ディレクトリのいくつかのスライス(slice)仕様に展開されています — はプラグインに5つの主要サーフェス(surface)を与えます:

  1. #diagnostics_for_file(path:, scope:, root:) — ファイルごとの出力フック。解析されたASTを辿り、Rigor::Analysis::Diagnostic行の配列を返します。ランナーは各行にsource_family: "plugin.<your-id>"をスタンプします。
  2. #flow_contribution_for(call_node:, scope:) — コールサイトごとの戻り型コントリビューションフック(v0.1.1 Track 2スライス7)。プラグインはコールサイトでの推論された戻り型を命名したRigor::FlowContributionバンドルを返します;解析器のディスパッチャーはコントリビューションをマージし、マージされた戻り型をRBS宣言済みかのように使います。
  3. Plugin::IoBoundary#read_file / #open_url — アクティブなTrustPolicyの下でサンドボックス化されたファイルおよび(v0.1.2以降)HTTPSの読み取り。プラグインがプロジェクトファイル(ルートテーブル、スキーマ、ロケールファイル)を読む、または安定したURLをフェッチする必要があるときに使います。
  4. Plugin::Base.producer + #cache_for — プラグイン側キャッシュプロデューサー。クロスランキャッシングが欲しいほど高コストなパース/ルックアップに使います。IoBoundaryが結果を構築している間に読んだすべてのファイルのダイジェスト(およびURLのコンテンツハッシュ)で自動的に無効化されます。
  5. Plugin::FactStore + #prepare(services) — クロスプラグインファクト公開サーフェス(v0.1.1 Track 2、ADR-9)。プラグインはprepareでファクトを公開します;下流のプラグインはservices.fact_storeを通じてそれらを消費するため、プロデューサー側の解析(例: config/routes.rb)をすべてのコンシューマーで再利用できます。

v0.1.2リリースは4つの実例(rigor-lisp-evalrigor-patternrigor-unitsrigor-activerecord)を「診断専用」から「flow_contribution_forを通じてナローイング(narrowing)された戻り型」に移行したため、プラグイン型の値へのチェーンされたコールがRBSレベルのuntypedエンベロープではなく解析器の通常のディスパッチで解決されます。各プラグインのREADMEを参照して、それぞれがどのサーフェスをデモしているか確認してください。

上記の手書きウォーカー契約の上に、2つ目の作成パスが追加されました: マクロ展開基板(ADR-16)。メタプログラミングを多用するDSL — Railsスタイルのhas_one_attached、dry-structのattribute、Deviseのdevise :strategy、Sinatraのget '/foo' do ... end — に対して、基板はプラグイン作者がASTを手で歩く代わりにコール形状を宣言することを可能にします。プラグインの本体は単一のマニフェストエントリーになります;基板がリテラルシンボル抽出、名前補間、レジストリルックアップ、メソッドごとの合成を処理します。

4つのティア形状が認識されます。ライブラリごとのサーベイが、どのライブラリが各ティアに収まり、どのライブラリが基板のスコープ外に該当するかを特定します。

ティア形状マニフェスト宣言動作例
A — ブロック-as-メソッドDSL呼び出しのブロックがレシーバークラス上のインスタンスメソッドとして実行される(Sinatra::Base#generate_methodblock_as_methods: [Macro::BlockAsMethod.new(receiver_constraint:, verbs:)]rigor-sinatra
B — トレイトインライニングレジストリクラスレベルの呼び出しがシンボルを列挙 → バンドルされたレジストリが各々をモジュールにマップ → 基板がモジュールのRBSメソッドを呼び出し元クラスに展開trait_registries: [Macro::TraitRegistry.new(receiver_constraint:, method_name:, modules_by_symbol:, always_included:)]rigor-devise
C — heredocテンプレートクラスレベルの呼び出しがリテラルシンボルをメソッド名テンプレートに補間;基板が合成リーダーを発行heredoc_templates: [Macro::HeredocTemplate.new(receiver_constraint:, method_name:, symbol_arg_position:, emit:)]rigor-dry-struct
D — 外部ファイルインクルージョンglobにマッチするファイルが、宣言されたクラスとして型付けされたselfで実行されるexternal_files: [Macro::ExternalFile.new(glob:, receiver_type:, bound_ivars:)](v0.1.x時点では契約のみ — エンジン統合は需要駆動)

上記の3つのTier A/B/Cプラグインは各々60〜110 LoCの純粋に宣言的なRuby — ウォーカーなし、diagnostics_for_fileなし、プラグイン側の状態なし。基板のプレパス + ディスパッチャー統合が作業を行います。

ActiveSupport::Concern.included do ... end遅延されたclass_eval: ブロック内のDSL呼び出しはconcernモジュール自身ではなく、includeした人に対して発火します。基板のスキャナはこの再ターゲティングを自動的に処理します。次のようなソースに対して:

module Auditable
extend ActiveSupport::Concern
included do
attribute :audited_at, Types::Time
end
end
class Address < Dry::Struct
include Auditable
attribute :city, Types::String
end

Addresscity(直接)AND audited_atAuditableから再ターゲティング)の両方を合成リーダーとして取得します。同じパターンがTier Bトレイト(Concern経由でincludeされるDeviseモジュール)でも動作します。

ADR-16 § WD13に従い、フロアは合成メソッドが名前で発行されることであり、これによってクロスファイルディスパッチが解決されます(call.undefined-methodなし)。一般的なケースでは精密な戻り型も回復されます: Tier Bは由来モジュールが著作したRBSに再ディスパッチし(Deviseのvalid_password?Dynamic[T]ではなくboolに解決される)、Tier Cは素のクラス名の戻り値をそのNominalに解決します。依然Dynamic[T]に縮退するのは、パラメータ化された/ユーティリティ型形のTier Cの戻り値(Array[String]Pick<T, K>)です;それらをADR-13リゾルバチェイン経由でルーティングすることがシーリングで、需要駆動です。基板はADR-5のロバストネスに従い、精度を捏造しません。

基板と手書きウォーカーの選択

Section titled “基板と手書きウォーカーの選択”
DSLが…基板を使う手書きウォーカーを使う
クラスレベル呼び出し + リテラルシンボル引数 + フレームワークclass_eval'dヘレドック✓ Tier C
クラスレベル呼び出し + リテラルシンボル引数 + レジストリ駆動のモジュールinclude✓ Tier B
クラスレベル呼び出し + インスタンスメソッドとして実行されるdo…endブロック✓ Tier A
宣言されたself下でinstance_eval'dされた外部Rubyファイル✓ Tier D(v0.1.x時点では契約のみ)
戻り型が引数のシェイプに依存するドメインDSLflow_contribution_forrigor-lisp-eval
クロスファイル検証(宣言を収集してから使用を検証)2パスウォーカー(rigor-statesman
外部プロジェクトファイル(ルート、スキーマ、ロケール)のパースIoBoundary + キャッシュプロデューサー(rigor-routes
スキーマグラフレコーダー(GraphQL-Rubyスタイル)スキーマ解決パス(プラグインまだ未作成)

基板と手書きウォーカー契約は共存します — プラグインはmanifestで宣言された基板エントリーをdiagnostics_for_fileウォーカーと混在させられます。skills/rigor-plugin-author/SKILL.md SKILLが決定フローを詳細にキャプチャします;docs/notes/20260515-macro-expansion-library-survey.mdのサーベイが、基板がどのRubyライブラリをカバーし、どのライブラリがスコープ外に該当するかを記録します。

おそらくそうではありません — ほとんどのプロジェクトは、プラグインのニッチに達する前にRBSとRBS::Extendedから恩恵を受けます。プラグインに手を伸ばすのは以下の場合のみです:

  • ドメインDSLの型付けが引数のシェイプ、ファイルの内容、またはクロスメソッド宣言に依存している。
  • アプリケーションと共にプラグインgemを保守する意欲がある。
  • チームがプラグインのソースを読める — それは誰も無視できるブラックボックスではありません。

これらが当てはまるなら、examples/README.mdが出発点です。rigor-deprecationsの例は80行未満で、「最初のプラグインを書きたい」のための推奨テンプレートです。

ハンドブックの終わりに到達しました。ここからは:

静的Rubyを信じる小さな、成長中のコミュニティへようこそ。

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