RBSと`RBS::Extended`
Rigorの推論が型を証明できないとき、次の逃げ道はRBS — Rubyのシグネチャ言語 — です。RBSが求める精密な契約(contract)を表現できないとき、RBS::Extendedがその上に小さなアノテーション表面を追加します。
この章では、通常手を伸ばす順序でその両方を扱います。
この章の内容 RBSが必要なとき · 最初のシグ · RBSシェイプが広すぎるとき · ディレクティブ文法 · リファインメント名 · 実例 — アサーションゲート · 型述語 · パラメータオーバーライド · ランタイムが強制できないオーバーライド · アノテーションの置き場所 · インラインRBS(
rigor-rbs-inline) ·untypedへのフォールバック · PHPStanから来た方へ · プラグインの逃げ道
RBSが必要なとき
Section titled “RBSが必要なとき”以下の場合にRBSファイルを追加する必要があるでしょう:
- メソッド本体の戻り値型が、Rigorのバンドルされたstdlibがカバーしていない外部gemに依存している。
- 引数シェイプ(shape)エラーに対して
call.argument-type-mismatchを発火させたい(インソースdefはパラメータ契約を強制しません; RBS宣言メソッドのみが強制します)。 - 本体の推論された戻り値が宣言された戻り値からずれたときに
def.return-type-mismatchを発火させたい。 - 将来のRBS対応ツール(Steep、ruby-lsp)が同じファイルを読んで、契約から恩恵を受けるでしょう。
以下の場合はRBSが不要でしょう:
- メソッドがプロジェクトのプライベートで、本体が短く、Rigorがすでに正しい戻り値型を推論している。
- メソッドがすでにシグを持つメソッドのラッパーに過ぎない(Rigorは本体を辿って伝播する)。
新しいプロジェクトで:
my-app/├── lib/│ └── slug.rb└── sig/ └── slug.rbs # ← あなたのシグclass Slug def normalise(id) id.downcase.gsub(/\s+/, "-") endendclass Slug def normalise: (String) -> Stringend.rbsファイルをsig/に置けばRigorが自動的に拾います — .rigor.ymlの変更は不要です。デフォルト設定にはsignature_paths: [sig]があります。
その後、このコード:
Slug.new.normalise(42)はcall.argument-type-mismatchを発火させます: 42はIntegerで、パラメータはStringです。
RBSシェイプが広すぎるとき
Section titled “RBSシェイプが広すぎるとき”Slugの例のランタイムは常に非空の小文字文字列を返しますが、RBSシグはStringとしか言っていません。Rigorにより狭い事実を知らせたい場合、RBS::Extendedアノテーションを付けます:
class Slug %a{rigor:v1:return: non-empty-lowercase-string} def normalise: (String) -> Stringendこれで:
s = Slug.new.normalise("Hello World")# s: non-empty-lowercase-strings.empty? # Constant<false> — 証明済みs.size # positive-int — 証明済みs == "hello-world" # bool — 等値ナローイングが適用される.rbsファイルは依然として有効なRBSです — %a{...}はRBSアノテーション構文です。Steep / typeprof / ruby-lspはコメントとして見ます; Rigorは締め付けとして見ます。
ディレクティブ文法
Section titled “ディレクティブ文法”RBS::Extendedはdocs/type-specification/rbs-extended.mdにあります。5つのディレクティブ:
| ディレクティブ | 意味 |
|---|---|
%a{rigor:v1:return: <type>} | メソッドの戻り値型を締め付ける。 |
%a{rigor:v1:param: <name> is <type>} | 呼び出し元でのパラメータの受け入れ型を締め付け、かつ本体内のローカル変数をナローイング(narrowing)する。 |
%a{rigor:v1:assert: <name> is <type>} | このメソッドが返った後、呼び出し元スコープの名前付きローカル変数は<type>である。 |
%a{rigor:v1:predicate-if-true: <name> is <type>} | このメソッドが真値を返したとき、呼び出し元スコープの名前付きローカル変数は<type>である。(対称なpredicate-if-false。) |
%a{rigor:v1:assertion-on: <name>} | メソッドをアサーションゲートとしてマークする — 本体の最後の式の型が<name>に関する事実になる。 |
<type>スロットは以下を受け入れます:
- RBSクラス名 —
String、Integer、::Foo::Bar。 - インポートされたリファインメント(refinement、篩型とも)名 —
non-empty-string、lowercase-string、numeric-string、int<5, 10>、non-empty-array[Integer]、literal-stringなど。 - 否定
~T—~lowercase-stringは「非小文字string」を意味します。
リファインメント名
Section titled “リファインメント名”完全なカタログはdocs/type-specification/imported-built-in-types.mdにあります。短いリファレンス:
| ファミリー | 名前 |
|---|---|
| 空/非空 | non-empty-string、non-empty-array[T]、non-empty-hash[K, V] |
| 整数範囲 | positive-int、non-negative-int、negative-int、non-positive-int、non-zero-int、int<min, max> |
| 文字列述語 | lowercase-string、uppercase-string、numeric-string、decimal-int-string、octal-int-string、hex-int-string、literal-string |
| ペアになった補完 | non-lowercase-string、non-uppercase-string、non-numeric-string |
| 合成 | non-empty-lowercase-string、non-empty-uppercase-string、non-empty-literal-string |
| シェイプ射影 | pick_of[T, K]、omit_of[T, K]、partial_of[T]、required_of[T]、readonly_of[T] — 既存のHashShape/Tupleから新しいキャリア(carrier)を派生させます。第4章 §「新しいシェイプを派生させる」を参照。 |
実例: アサーションゲート
Section titled “実例: アサーションゲート”class Validator %a{rigor:v1:assert: x is non-empty-string} def assert_non_empty: (String x) -> voidenddef configure(host) Validator.new.assert_non_empty(host) # この呼び出し後、host: non-empty-string host.size # positive-int — 証明済みendランタイム側はassert_non_emptyが何をするかです(空のとき例外、ログなど)— Rigorはディレクティブのみを読みます。
実例: 型述語
Section titled “実例: 型述語”class Range %a{rigor:v1:predicate-if-true: value is Integer} def integer?: (untyped value) -> boolenddef double_if_int(value) if (1..10).integer?(value) # 真値ブランチでvalue: Integer value * 2 else value endendこれは、エンジンの組み込みis_a? / nil?ルールが認識できないカスタム型述語メソッドについてRigorに教えるためのサポートされた方法です。
実例: パラメータオーバーライド
Section titled “実例: パラメータオーバーライド”class Slug %a{rigor:v1:param: id is non-empty-string} def normalise: (String id) -> Stringendこれには2つの効果があります:
- 呼び出し元チェック。
Slug.new.normalise("")はConstant<"">がnon-empty-stringを満たさないため、call.argument-type-mismatchになります。 - 本体側ナローイング。
normaliseのメソッド本体内側で、パラメータidはnon-empty-stringです。したがってid.empty?はConstant<false>に還元され、id.sizeはpositive-intに還元されます。
ランタイムが強制できないパラメータオーバーライドが必要なとき
Section titled “ランタイムが強制できないパラメータオーバーライドが必要なとき”ランタイム関数が不正な入力で例外を投げない場合 — nilを返す、デフォルトを返す、またはエラーを飲み込む — があります。Rigorのparam:ディレクティブは依然として呼び出し元の契約を締め付けます:
class FileLoader %a{rigor:v1:param: path is non-empty-string} def load: (String path) -> String?endFileLoader.new.load("")は、ランタイムでloadが穏やかに失敗するにもかかわらず、call.argument-type-mismatchを発火させます。ディレクティブは「本体が何を強制するか」ではなく「呼び出し元が何を渡すべきか」を表現します。
アノテーションの置き場所
Section titled “アノテーションの置き場所”RBS::Extendedアノテーションは、それが絞り込むdefと同じ.rbsファイル内の同じdefの上に置きます。メソッドの上にグループ化します:
class Slug %a{rigor:v1:return: non-empty-string} %a{rigor:v1:param: id is non-empty-string} def normalise: (String id) -> Stringendこれらの%a{rigor:v1:…}ディレクティブを.rbファイルの内側に置くことはできません。ディレクティブはRBSから読まれたときのみ発火します — これは設計上の選択です(ADR-5、堅牢性の原則: 戻り値に対して厳密、パラメータに対して寛大を参照)。
RubyソースへのインラインRBS — rigor-rbs-inlineプラグイン
Section titled “RubyソースへのインラインRBS — rigor-rbs-inlineプラグイン”オプトイン型の別プラグインを使うと、Rubyファイル内のdefの直上にメソッド型を直接書けます。上流のrbs-inlineが定義するコメント語彙を使います:
# rbs_inline: enabled
class AscDesc # @rbs asc_or_desc: :asc | :desc def ascdesc(asc_or_desc) asc_or_desc endend
AscDesc.new.ascdesc(:bad)# => error: argument type mismatch at parameter `asc_or_desc' of# `ascdesc' on AscDesc: expected :asc | :desc, got :baddocスタイルの# @rbs name: Tアノテーション、インラインメソッド型コメント#: () -> T、# @rbs return: T、属性#:キャスト、# @rbs @ivar: T、# @rbs override、# @rbs!生RBS埋め込みのいずれも動作します——上流rbs-inlineが受け入れるものはすべて、手書きの.rbsファイルと同等の形でRigorのRBS環境に流れ込みます。
これはRBS::Extendedではありません。# @rbsコメントは上流rbs-inlineの文法であり、プラグインがenv構築時にそれらを通常のRBSにトランスクライブします。これに対しRBS::Extendedの%a{rigor:v1:…}ディレクティブはRigor固有のアノテーションであり.rbsファイルに記述します(その他のディレクティブについてはこの章の残りを参照)。
有効にするには、プラグインgemをbundleに追加し、以下のように設定します:
plugins: - rigor-rbs-inlineファイルごとに、先頭に上流の# rbs_inline: enabledマジックコメントを書いてオプトインします——それがないファイルは影響を受けません。
注意事項:
- コアの
rigortypeアナライザーはゼロランタイム依存のまま(ADR-0)。rbs-inline上流ライブラリはコアのgemspecではなくプラグインgemの依存関係なので、オプトインしないプロジェクトは何も支払いません。 - 裸のトップレベル
defは上流rbs-inlineを通じてRBS出力を生成しません。アノテーションを有効にするには、メソッド定義をクラスまたはモジュールでラップしてください。 - rbs-inlineのパース失敗は
source-rbs-synthesis-failed:info診断として表面化し、そのファイルはインラインRBSの貢献なしにフォールバックして解析が続行されます。
完全なプラグインドキュメント、設定オプション(ブラウザプレイグラウンドが使用するrequire_magic_comment: falseホストコンテキストオーバーライドを含む)、キャッシュの契約についてはplugins/rigor-rbs-inline/README.mdを参照してください。
untypedへのフォールバック
Section titled “untypedへのフォールバック”メソッドのシグネチャにRBSが表現できない型が含まれる場合、保守的な対処はuntypedです:
def deserialize: (String) -> untypeduntypedは契約フリーのハッチ — あらゆるメソッドがそれに存在し、あらゆる引数シェイプが受け入れられます。Rigorの診断はuntypedレシーバーに対して沈黙します。正当に動的な境界(デシリアライズ、eval、プラグインエントリーポイント)に使います。失う静的解析は「これは何でもあり得る」と認めることの誠実さで補われます。
PHPStanから来た方へ — @phpstan-assertファミリー
Section titled “PHPStanから来た方へ — @phpstan-assertファミリー”PHPStanのPHPDocアノテーションに慣れている場合、RigorのRBS::Extendedディレクティブは、PHPStanが「アサート」や「型指定関数」と呼ぶポストリターン / 条件付きナローイングのプリミティブに直接マッピングされます。挙動は同一です:
「このメソッドが返した後、名前付き引数は
Tです。」
PHPStanでは@phpstan-assert、Rigorでは%a{rigor:v1:assert:}です。
| PHPStan PHPDoc | Rigor RBS::Extended | 効果 |
|---|---|---|
@phpstan-assert T $x | %a{rigor:v1:assert: x is T} | このメソッドが正常に返った後、呼び出し元のxはT。 |
@phpstan-assert-if-true T $x | %a{rigor:v1:predicate-if-true: x is T} | このメソッドが真値を返した場合、呼び出し元のxはT。 |
@phpstan-assert-if-false T $x | %a{rigor:v1:predicate-if-false: x is T} | このメソッドが偽値を返した場合、呼び出し元のxはT。 |
@phpstan-assert !T $x | %a{rigor:v1:assert: x is ~T} | このメソッドが返った後、呼び出し元のxはTではない(否定形式)。 |
@phpstan-assert-if-true !T $x | %a{rigor:v1:predicate-if-true: x is ~T} | 条件付き否定。predicate-if-falseと対称。 |
実践例 — PHPStanのドキュメントからの典型的な「assertNotNull」パターン:
class Asserts %a{rigor:v1:assert: x is ~nil} def self.not_nil: (untyped x) -> voidenddef configure(maybe) Asserts.not_nil(maybe) # maybe: (~nil)、ナローイングされた型で.upcaseが解決される maybe.upcaseendselfターゲット形式もサポートされています — PHPStanのアナログは$thisをナローイングするメソッドになります。レシーバーをselfで名指しします:
class Connection %a{rigor:v1:assert: self is Connected} def assert_connected!: () -> voidendRigorのディレクティブのグラマーはPHPStanが@phpstan-assert*ファミリーで提供するものをカバーします。ディレクティブはRBSからのみ発火します(ADR-5に従い: 戻り値では厳格に、パラメータでは寛容に); PHPStan側では関数のすぐ上のPHPDocに@phpstan-assertを直接書けます — Rigorでの等価表現は同じRBSファイルのdef行です。
コールシェイプによってアサーションを認識するプラグイン側の等価表現が必要な場合(PHPStanの「型指定拡張」)は第9章を参照してください。プラグイン契約はディレクティブが使うのと同じFact(target_kind: :self)とFact(target_kind: :parameter)キャリアを提供しているため、プラグイン作者はRubyからPHPStanのStaticMethodTypeSpecifyingExtensionに相当するものを書けます。
RBSが助けにならないとき — プラグインの逃げ道
Section titled “RBSが助けにならないとき — プラグインの逃げ道”メソッドの動作がランタイムでの引数のシェイプに依存する場合(Lisp.eval([:+, 1, 2])はIntegerを返すが、Lisp.eval([:<, 1, 2])はboolを返す)、どんなRBSシグもその関係を表現できません。それがプラグインのためのものです — 第9章とexamples/ディレクトリを参照してください。
次に読むもの
Section titled “次に読むもの”第8章はルールカタログを扱います — 各診断の意味、発火するタイミング、それが間違いまたはノイズのときの抑制方法。
© 2026 TypedDuck. Licensed under CC BY-SA 4.0.