Sorbetとの共存
プロジェクトがすでにSorbetを使っているなら、rigor-sorbetプラグインを使えば、RigorがSorbetの既存のsigブロック、RBIファイル、T.let / T.cast / T.must / T.unsafeアサーションを型ソースとして読み取れます。rigor checkをsrb tcと並行して実行するために、何もRBSに書き直す必要はありません。
この章はSorbetを使用しているプロジェクトから来たユーザー向けです。Sorbetを使ったことがなければ、スキップしてかまいません。第1〜9章のコアハンドブックがRigorのネイティブなRBSベースのパスをカバーしています。
この章の内容 何が翻訳されるか · Sorbetの型語彙 · インライン型アサーション(
T.let/T.must/ …) · RBIファイル ·# typed:シジル · Tapioca DSLミックスイン ·T.absurd網羅性 · 競合時のティア順序 · 移行パターン · 置き換えないもの
何が翻訳されるか
Section titled “何が翻訳されるか”sigブロックが前置されたメソッドがある場合:
class Slug extend T::Sig
sig { params(name: String).returns(String) } def normalise(name) name.downcase.gsub(/\s+/, "-") end
sig { returns(Integer) } def self.default_length 32 endendRigorはすべてのコールサイトで解析されたsigを取り上げ、チェーンされたコールが解析器の通常のディスパッチを通じて解決されます:
slug = Slug.newslug.normalise("Alice").upcase # ✓ String#upcaseが解決されるSlug.default_length.even? # ✓ Integer#even?が解決される.rbsファイルは不要です。プラグインはpaths:以下のすべてのRubyファイル(およびsorbet/rbi/以下のすべての.rbiファイル——以下の「RBIファイル」参照)を辿り、各sig { ... }ブロックをその直後のdefとペアにし、マッチするコールサイトで戻り型を貢献します。
Sorbetの型語彙
Section titled “Sorbetの型語彙”プラグインはSorbetの型DSLの密な中核を翻訳します。日常的なsigのほとんどは正確に着地します。稀なまたはクラス内省が多い形式はDynamic[Top]に降格します。
| Sorbet形式 | Rigorの表現 |
|---|---|
Integerなど | Nominal["Integer"] |
::Foo::Bar | Nominal["Foo::Bar"] |
T.untyped | Dynamic[Top] |
T.anything | Top |
T.noreturn | Bot |
T.nilable(X) | Union[X, Constant<nil>] |
T.any(A, B, ...) | Union[A, B, ...] |
T.all(A, B, ...) | Intersection[A, B, ...] |
T::Boolean | Union[Constant<true>, Constant<false>] |
T::Array[E] | Nominal["Array", [E]] |
T::Hash[K, V] | Nominal["Hash", [K, V]] |
T::Set[E] | Nominal["Set", [E]] |
T::Range[E] | Nominal["Range", [E]] |
T::Enumerable[E] | Nominal["Enumerable", [E]] |
T::Class[T] | Singleton[T-class-name](損失あり) |
T.class_of(C) | Singleton[C] |
[A, B](sig内のタプル) | Tuple[A, B] |
{a: A, b: B} | HashShape{a: A, b: B}(クローズド) |
このテーブル外のもの——T.proc、T.attached_class、T.self_type、T.type_parameter、T::Struct / T::Enumサブクラス——は現在のところDynamic[Top]にサイレントで降格します。
インライン型アサーション
Section titled “インライン型アサーション”SorbetのT.let / T.cast / T.must / T.unsafe式はsigブロック内だけでなく、すべてのコールサイトで認識されます:
counter = T.let(0, Integer) # Constant<0>をIntegerに拡大counter.even? # ✓ Integer#even?が解決される
T.cast(some_value, String).upcase # ✓ String#upcaseが解決される
maybe = T.let(nil, T.nilable(Integer))T.must(maybe).bit_length # ✓ nilを除去 → Integer # その後Integer#bit_lengthが解決される
T.unsafe(opaque).any_method_at_all # ✓ サイレント — 戻りはDynamic[Top]T.must_because(expr, "explanation")はT.mustのエイリアスとして認識されます——静的挙動は同じ(nilを除去)で、第2引数の文字列は情報目的のみです。
T.reveal_type(expr)はランタイムでexprをそのまま返し、コールサイトでplugin.sorbet.reveal-type :info診断として推論された静的型を表面化します。コールがチェーンされても機能しつつ、解析器が何を見ているかを確認できます:
n = T.let(3, Integer)T.reveal_type(n).even? # info: T.reveal_type inferred type: Integer # ✓ Integer#even?は引き続き解決されるT.assert_type!(expr, T)はT.castに静的部分型(subtype)チェックを加えたものです。コールはアサートされた型を返すのでチェーンされたコールはそれを通じて解決されます。推論された型が証明可能に非互換(Inference::Acceptance.accepts(...)が:noを返す)の場合、プラグインはplugin.sorbet.assert-type-mismatchを:errorとして発行します。漸進的(gradual)一貫性ルールが適用されます——Dynamic[Top]推論型と:maybe互換のシェイプ(shape)は、ランタイムチェックがカバーするためサイレントになります。
T.assert_type!("hello", Integer) # error: 証明可能に非互換T.assert_type!(some_obj, String) # silent: ユーザーを信頼T.bind(self, T)は現在のスコープ(通常はブロック本体)の残り部分に対してselfをTに絞り込みます:
arr.each do |x| T.bind(self, MyHelper) do_something(x) # ✓ selfはこのブロックの残りでMyHelperend絞り込みはエンジンのプラグイン側post_return_facts配線で実装されます——将来のPHPStanスタイルのType-Specifying Extensionプラグインがカスタムアサーションコール後に引数変数を絞り込むために使うのと同じ基板です。
T.bindは非selfの第1引数をサイレントで拒否します(Sorbetの契約(contract)に一致——bindはself専用)。
RBIファイル
Section titled “RBIファイル”プラグインはデフォルトでsorbet/rbi/**/*.rbiを再帰的に辿り、各.rbiをRubyソースとして扱います。標準のTapiocaサブディレクトリ(gems/、annotations/、dsl/、shims/)はすべて、親ルートに再帰する副作用として参加します。.rigor.ymlのconfig.rbi_paths:で場所をオーバーライドするか、[]に設定してオプトアウトできます:
plugins: - gem: rigor-sorbet config: rbi_paths: [] # RBIローディングを無効化 # rbi_paths: ["sorbet/rbi", "vendor/rbi"] # ベンダーツリーを追加プロジェクトsig(paths:以下の.rbファイル)とRBI sig(rbi_paths:以下の.rbiファイル)は同じ実行ごとのカタログにフィードされるため、どちらのソースで宣言されたメソッドもコールサイトで同じように解決されます。
Sorbet # typed:シジル
Section titled “Sorbet # typed:シジル”プラグインは各ファイルの先頭からSorbetの# typed:マジックコメントを読み取ります。挙動はenforce_sigil設定ノブ(デフォルトtrue)に依存します:
| シジル | enforce_sigil: true(デフォルト) | enforce_sigil: false |
|---|---|---|
# typed: ignore | 完全にスキップ; sigsもパースエラーも記録されない。 | 同上。 |
シジルなし / false | パースエラー診断のために辿られるが、sigsは記録されない。 | Sigsが記録される。 |
# typed: true以上 | Sigsが記録される。 | Sigsが記録される。 |
デフォルトはSorbet自身の契約を反映します: # typed: falseでは型が強制されないので、Rigorもそれらのファイルからの絞り込みを表面化しません。enforce_sigil: falseをプラグイン設定で設定することで、ゲート前の挙動にオプトイン(シジルに関係なく、解析可能なすべてのファイルのsigsがカタログに着地する)できます。
アサーション認識器(T.let、T.cast、T.must、T.must_because、T.unsafe、T.reveal_type、T.assert_type!、T.bind)はenforce_sigilでゲートされません。ユーザーはそれらのコールを意図的に書いており、ファイルのシジルに関係なく発火します。
Sorbet strictの「すべてのメソッドにsigが必要」という要件と、strong-modeのT.untyped拒否は意図的に反映されていません。それらのチェックはsrb tcにあります。Rigor自身の.rigor.ymlのseverity_profile設定が類似のフィルタリングをカバーします。
Tapioca DSL — ミックスインパターン
Section titled “Tapioca DSL — ミックスインパターン”TapiocaのstanderdなDSL RBI形式は、ホストクラスにinclude / extendされる生成モジュールにsigsを宣言します:
class Post include GeneratedAttributeMethods module GeneratedAttributeMethods sig { returns(::String) } def body; end endendプラグインは辿り中にモジュールの修飾名の下にsigを記録し、ルックアップ時にホストクラスに引き上げます。つまりpost.bodyはPost::GeneratedAttributeMethods#bodyを通じて正しく解決されます——手動のフラット化は不要で、sorbet/rbi/shims/の手書きシムとrbi-centralのコミュニティアノテーションにも同じトリックが機能します。
extend MはMのインスタンスメソッドをextendするクラスのシングルトン側に正しく引き上げ、Rubyのランタイム挙動に一致します:
class Post extend GeneratedClassMethods module GeneratedClassMethods sig { params(id: Integer).returns(Post) } def find(id); end endendPost.find(42)はextendされたモジュールのインスタンス側を通じて解決されます。
T.absurd網羅性
Section titled “T.absurd網羅性”T.absurd(x)はcase/when網羅性のSorbetのイディオムです:「ここに来たなら、型システムが道を見失っている。」プラグインはすべてのT.absurdコールをBot(空の型——可能な値なし)であり、かつ例外を発生させるものとして扱うため、エンジンの既存のフロー解析はコール後のコードを到達不能として扱います:
case xwhen A then handle_a(x)when B then handle_b(x)else T.absurd(x) # elseブランチが到達不能であることをアサートend判別子が完全に網羅されると、T.absurdコールはデッドコードに座り何も貢献しません。caseブランチが欠落している場合、T.absurdコールでの判別子の型にはまだ許容可能な値があり、プラグインはplugin.sorbet.absurd-reachableを警告として表面化します:
demo.rb:42:5: warning: `T.absurd` is reachable: the discriminant did not narrow to `T.noreturn`. Either add the missing case branch above the `else`, or remove the `T.absurd(...)` call. [plugin.sorbet.absurd-reachable]検出の精度はRigorのフローセンシティブ(flow-sensitive)なナローイング(narrowing)に従います——is_a? / kind_of? / nil?は正確に機能します。シンボル列挙型に対するナローイングはv0.1.3時点ではそれほど正確ではないため、完全に網羅されたシンボルケースが偽陽性警告を発することがあります。
ティア順序 — 競合時に何が勝つか
Section titled “ティア順序 — 競合時に何が勝つか”メソッドがSorbet sigとRBS sigの両方を持つ場合、RBSが勝ちます。Sorbet sigはRigorのプラグインティアに座ります:
- 精度ティア — 定数フォールド、シェイプディスパッチ、ブロックフォールドなど。
- プラグイン貢献 —
rigor-sorbetのsigおよびアサーション翻訳を含む。 - RBSバックドディスパッチ — プロジェクト
sig/、RBS::Inline、バンドルされたstdlib。 - 依存関係ソース推論(ADR-10のオプトインウォーカー)。
- ユーザークラスフォールバック(
Object/Classの祖先)。
貢献マージャー(docs/internal-spec/flow-contribution-merger.mdに文書化されたv0.1.0の基板)は競合時にRBSを権威として保持します——Sorbet sigは絞り込みを許可されますが矛盾は許可されません。Sorbet sigを優先させたいユーザーは競合するRBSを削除すべきで、その逆ではありません。逆方向(Sorbetが勝つ)は、サードパーティDSLアノテーションが作成されたRBSを上書きすることを許可し、信頼モデルを逆転させます。
移行パターン
Section titled “移行パターン”プラグインは強制的な移行ではなく漸進的な共存のために設計されています。3つの一般的な形状:
- 両方の静的チェッカーを並行して実行する。
srb tcがその診断を生成し続け、rigor checkが独自の診断を生成します。両者はシェイプエラーで重複し、各ツールが発見するものを補完します——SorbetはT.let/T.cast/ RBIをより深くカバーし、Rigorはリテラル文字列ナローイング、リファインメント(refinement、篩型とも)キャリア(carrier)、プラグインDSL、依存関係ソース推論をカバーします。 - Sorbetはsig、Rigorはナローイング。権威あるsigは
sig { ... }ブロック(またはsorbet-runtime対応のRBIツリー)に残り、Rigorはそれらを入力として読み取り、その上に独自のナローイングを追加します。 - 時間をかけてSorbet → RBS。新しいコードはRBSとして着地し、既存のSorbet sigは周囲のサブシステムが変更されるまで残ります。プラグインはSorbetサーフェス(surface)が縮小する間も実行され続けます。
プラグインが置き換えないもの
Section titled “プラグインが置き換えないもの”Rigorのrigor-sorbetアダプタは入力側のみです。Sorbetの構文を読み取り語彙を翻訳しますが、Sorbetの型チェッカーを実行せず、sorbet-runtimeを同梱せず、Sorbetのランタイム保証を強制しません。Gemfileからsorbetとsorbet-runtimeを削除すると、プラグインは引き続きsigsを読み取ります(アダプタのミニインタープリタはSorbetをロードしません)が、少なくともランタイムgem(またはトップレベルのT定数で4つのシングルトンメソッドをスタブする——プラグインのデモが独自のユニットテストでこれを行っています)を保持しないかぎり、T.let / T.cast / T.must / T.unsafeコールはランタイムでNameErrorを発生させます。
次に読むもの
Section titled “次に読むもの”- 全機能マトリックスとアーキテクチャサーフェスは
plugins/rigor-sorbet/README.mdにあります。 - 設計根拠 + スライスプランは
docs/adr/11-sorbet-input-adapter.mdにあります。 docs/notes/20260503-steep-cross-check-triage.mdのクロスチェッカートリアージレポートは、他の静的チェッカーが見逃すsigドリフトをRigorの解析器が日常的に表面化する方法を示しています——各ツールが実際に何を発見するかを比較するときに役立ちます。
© 2026 TypedDuck. Licensed under CC BY-SA 4.0.