コンテンツにスキップ

RBSと`RBS::Extended`

Rigorの推論が型を証明できないとき、次の逃げ道はRBS — Rubyのシグネチャ言語 — です。RBSが求める精密な契約(contract)を表現できないとき、RBS::Extendedがその上に小さなアノテーション表面を追加します。

この章では、通常手を伸ばす順序でその両方を扱います。

この章の内容 RBSが必要なとき · 最初のシグ · RBSシェイプが広すぎるとき · ディレクティブ文法 · リファインメント名 · 実例 — アサーションゲート · 型述語 · パラメータオーバーライド · ランタイムが強制できないオーバーライド · アノテーションの置き場所 · インラインRBS(rigor-rbs-inline · untypedへのフォールバック · PHPStanから来た方へ · プラグインの逃げ道

以下の場合に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 # ← あなたのシグ
lib/slug.rb
class Slug
def normalise(id)
id.downcase.gsub(/\s+/, "-")
end
end
sig/slug.rbs
class Slug
def normalise: (String) -> String
end

.rbsファイルをsig/に置けばRigorが自動的に拾います — .rigor.ymlの変更は不要です。デフォルト設定にはsignature_paths: [sig]があります。

その後、このコード:

Slug.new.normalise(42)

call.argument-type-mismatchを発火させます: 42はIntegerで、パラメータはStringです。

Slugの例のランタイムは常に非空の小文字文字列を返しますが、RBSシグはStringとしか言っていません。Rigorにより狭い事実を知らせたい場合、RBS::Extendedアノテーションを付けます:

class Slug
%a{rigor:v1:return: non-empty-lowercase-string}
def normalise: (String) -> String
end

これで:

s = Slug.new.normalise("Hello World")
# s: non-empty-lowercase-string
s.empty? # Constant<false> — 証明済み
s.size # positive-int — 証明済み
s == "hello-world" # bool — 等値ナローイングが適用される

.rbsファイルは依然として有効なRBSです — %a{...}はRBSアノテーション構文です。Steep / typeprof / ruby-lspはコメントとして見ます; Rigorは締め付けとして見ます。

RBS::Extendeddocs/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クラス名StringInteger::Foo::Bar
  • インポートされたリファインメント(refinement、篩型とも)名non-empty-stringlowercase-stringnumeric-stringint<5, 10>non-empty-array[Integer]literal-stringなど。
  • 否定~T~lowercase-stringは「非小文字string」を意味します。

完全なカタログはdocs/type-specification/imported-built-in-types.mdにあります。短いリファレンス:

ファミリー名前
空/非空non-empty-stringnon-empty-array[T]non-empty-hash[K, V]
整数範囲positive-intnon-negative-intnegative-intnon-positive-intnon-zero-intint<min, max>
文字列述語lowercase-stringuppercase-stringnumeric-stringdecimal-int-stringoctal-int-stringhex-int-stringliteral-string
ペアになった補完non-lowercase-stringnon-uppercase-stringnon-numeric-string
合成non-empty-lowercase-stringnon-empty-uppercase-stringnon-empty-literal-string
シェイプ射影pick_of[T, K]omit_of[T, K]partial_of[T]required_of[T]readonly_of[T] — 既存のHashShape/Tupleから新しいキャリア(carrier)を派生させます。第4章 §「新しいシェイプを派生させる」を参照。
class Validator
%a{rigor:v1:assert: x is non-empty-string}
def assert_non_empty: (String x) -> void
end
def configure(host)
Validator.new.assert_non_empty(host)
# この呼び出し後、host: non-empty-string
host.size # positive-int — 証明済み
end

ランタイム側はassert_non_emptyが何をするかです(空のとき例外、ログなど)— Rigorはディレクティブのみを読みます。

class Range
%a{rigor:v1:predicate-if-true: value is Integer}
def integer?: (untyped value) -> bool
end
def double_if_int(value)
if (1..10).integer?(value)
# 真値ブランチでvalue: Integer
value * 2
else
value
end
end

これは、エンジンの組み込みis_a? / nil?ルールが認識できないカスタム型述語メソッドについてRigorに教えるためのサポートされた方法です。

実例: パラメータオーバーライド

Section titled “実例: パラメータオーバーライド”
class Slug
%a{rigor:v1:param: id is non-empty-string}
def normalise: (String id) -> String
end

これには2つの効果があります:

  1. 呼び出し元チェックSlug.new.normalise("")Constant<"">non-empty-stringを満たさないため、call.argument-type-mismatchになります。
  2. 本体側ナローイングnormaliseのメソッド本体内側で、パラメータidnon-empty-stringです。したがってid.empty?Constant<false>に還元され、id.sizepositive-intに還元されます。

ランタイムが強制できないパラメータオーバーライドが必要なとき

Section titled “ランタイムが強制できないパラメータオーバーライドが必要なとき”

ランタイム関数が不正な入力で例外を投げない場合 — nilを返す、デフォルトを返す、またはエラーを飲み込む — があります。Rigorのparam:ディレクティブは依然として呼び出し元の契約を締め付けます:

class FileLoader
%a{rigor:v1:param: path is non-empty-string}
def load: (String path) -> String?
end

FileLoader.new.load("")は、ランタイムでloadが穏やかに失敗するにもかかわらず、call.argument-type-mismatchを発火させます。ディレクティブは「本体が何を強制するか」ではなく「呼び出し元が何を渡すべきか」を表現します。

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) -> String
end

これらの%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
end
end
AscDesc.new.ascdesc(:bad)
# => error: argument type mismatch at parameter `asc_or_desc' of
# `ascdesc' on AscDesc: expected :asc | :desc, got :bad

docスタイルの# @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に追加し、以下のように設定します:

.rigor.yml
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を参照してください。

メソッドのシグネチャにRBSが表現できない型が含まれる場合、保守的な対処はuntypedです:

def deserialize: (String) -> untyped

untypedは契約フリーのハッチ — あらゆるメソッドがそれに存在し、あらゆる引数シェイプが受け入れられます。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 PHPDocRigor RBS::Extended効果
@phpstan-assert T $x%a{rigor:v1:assert: x is T}このメソッドが正常に返った後、呼び出し元のxT
@phpstan-assert-if-true T $x%a{rigor:v1:predicate-if-true: x is T}このメソッドが真値を返した場合、呼び出し元のxT
@phpstan-assert-if-false T $x%a{rigor:v1:predicate-if-false: x is T}このメソッドが偽値を返した場合、呼び出し元のxT
@phpstan-assert !T $x%a{rigor:v1:assert: x is ~T}このメソッドが返った後、呼び出し元のxTではない(否定形式)。
@phpstan-assert-if-true !T $x%a{rigor:v1:predicate-if-true: x is ~T}条件付き否定。predicate-if-falseと対称。

実践例 — PHPStanのドキュメントからの典型的な「assertNotNull」パターン:

sig/asserts.rbs
class Asserts
%a{rigor:v1:assert: x is ~nil}
def self.not_nil: (untyped x) -> void
end
lib/configure.rb
def configure(maybe)
Asserts.not_nil(maybe)
# maybe: (~nil)、ナローイングされた型で.upcaseが解決される
maybe.upcase
end

selfターゲット形式もサポートされています — PHPStanのアナログは$thisをナローイングするメソッドになります。レシーバーをselfで名指しします:

class Connection
%a{rigor:v1:assert: self is Connected}
def assert_connected!: () -> void
end

Rigorのディレクティブのグラマーは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/ディレクトリを参照してください。

第8章はルールカタログを扱います — 各診断の意味、発火するタイミング、それが間違いまたはノイズのときの抑制方法。

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