コンテンツにスキップ

Sorbetとの共存

プロジェクトがすでにSorbetを使っているなら、rigor-sorbetプラグインを使えば、RigorがSorbetの既存のsigブロック、RBIファイル、T.let / T.cast / T.must / T.unsafeアサーションを型ソースとして読み取れます。rigor checksrb tcと並行して実行するために、何もRBSに書き直す必要はありません。

この章はSorbetを使用しているプロジェクトから来たユーザー向けです。Sorbetを使ったことがなければ、スキップしてかまいません。第1〜9章のコアハンドブックがRigorのネイティブなRBSベースのパスをカバーしています。

この章の内容 何が翻訳されるか · Sorbetの型語彙 · インライン型アサーション(T.let / T.must / …) · RBIファイル · # typed:シジル · Tapioca DSLミックスイン · T.absurd網羅性 · 競合時のティア順序 · 移行パターン · 置き換えないもの

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

Rigorはすべてのコールサイトで解析されたsigを取り上げ、チェーンされたコールが解析器の通常のディスパッチを通じて解決されます:

slug = Slug.new
slug.normalise("Alice").upcase # ✓ String#upcaseが解決される
Slug.default_length.even? # ✓ Integer#even?が解決される

.rbsファイルは不要です。プラグインはpaths:以下のすべてのRubyファイル(およびsorbet/rbi/以下のすべての.rbiファイル——以下の「RBIファイル」参照)を辿り、各sig { ... }ブロックをその直後のdefとペアにし、マッチするコールサイトで戻り型を貢献します。

プラグインはSorbetの型DSLの密な中核を翻訳します。日常的なsigのほとんどは正確に着地します。稀なまたはクラス内省が多い形式はDynamic[Top]に降格します。

Sorbet形式Rigorの表現
IntegerなどNominal["Integer"]
::Foo::BarNominal["Foo::Bar"]
T.untypedDynamic[Top]
T.anythingTop
T.noreturnBot
T.nilable(X)Union[X, Constant<nil>]
T.any(A, B, ...)Union[A, B, ...]
T.all(A, B, ...)Intersection[A, B, ...]
T::BooleanUnion[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.procT.attached_classT.self_typeT.type_parameterT::Struct / T::Enumサブクラス——は現在のところDynamic[Top]にサイレントで降格します。

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)は現在のスコープ(通常はブロック本体)の残り部分に対してselfTに絞り込みます:

arr.each do |x|
T.bind(self, MyHelper)
do_something(x) # ✓ selfはこのブロックの残りでMyHelper
end

絞り込みはエンジンのプラグイン側post_return_facts配線で実装されます——将来のPHPStanスタイルのType-Specifying Extensionプラグインがカスタムアサーションコール後に引数変数を絞り込むために使うのと同じ基板です。

T.bindは非selfの第1引数をサイレントで拒否します(Sorbetの契約(contract)に一致——bindはself専用)。

プラグインはデフォルトでsorbet/rbi/**/*.rbiを再帰的に辿り、各.rbiをRubyソースとして扱います。標準のTapiocaサブディレクトリ(gems/annotations/dsl/shims/)はすべて、親ルートに再帰する副作用として参加します。.rigor.ymlconfig.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:マジックコメントを読み取ります。挙動は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.letT.castT.mustT.must_becauseT.unsafeT.reveal_typeT.assert_type!T.bind)はenforce_sigilでゲートされません。ユーザーはそれらのコールを意図的に書いており、ファイルのシジルに関係なく発火します。

Sorbet strictの「すべてのメソッドにsigが必要」という要件と、strong-modeのT.untyped拒否は意図的に反映されていません。それらのチェックはsrb tcにあります。Rigor自身の.rigor.ymlseverity_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
end
end

プラグインは辿り中にモジュールの修飾名の下にsigを記録し、ルックアップ時にホストクラスに引き上げます。つまりpost.bodyPost::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
end
end

Post.find(42)はextendされたモジュールのインスタンス側を通じて解決されます。

T.absurd(x)はcase/when網羅性のSorbetのイディオムです:「ここに来たなら、型システムが道を見失っている。」プラグインはすべてのT.absurdコールをBot(空の型——可能な値なし)であり、かつ例外を発生させるものとして扱うため、エンジンの既存のフロー解析はコール後のコードを到達不能として扱います:

case x
when 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のプラグインティアに座ります:

  1. 精度ティア — 定数フォールド、シェイプディスパッチ、ブロックフォールドなど。
  2. プラグイン貢献rigor-sorbetのsigおよびアサーション翻訳を含む。
  3. RBSバックドディスパッチ — プロジェクトsig/RBS::Inline、バンドルされたstdlib。
  4. 依存関係ソース推論(ADR-10のオプトインウォーカー)。
  5. ユーザークラスフォールバックObject / Classの祖先)。

貢献マージャー(docs/internal-spec/flow-contribution-merger.mdに文書化されたv0.1.0の基板)は競合時にRBSを権威として保持します——Sorbet sigは絞り込みを許可されますが矛盾は許可されません。Sorbet sigを優先させたいユーザーは競合するRBSを削除すべきで、その逆ではありません。逆方向(Sorbetが勝つ)は、サードパーティDSLアノテーションが作成されたRBSを上書きすることを許可し、信頼モデルを逆転させます。

プラグインは強制的な移行ではなく漸進的な共存のために設計されています。3つの一般的な形状:

  1. 両方の静的チェッカーを並行して実行するsrb tcがその診断を生成し続け、rigor checkが独自の診断を生成します。両者はシェイプエラーで重複し、各ツールが発見するものを補完します——SorbetはT.let / T.cast / RBIをより深くカバーし、Rigorはリテラル文字列ナローイング、リファインメント(refinement、篩型とも)キャリア(carrier)、プラグインDSL、依存関係ソース推論をカバーします。
  2. Sorbetはsig、Rigorはナローイング。権威あるsigはsig { ... }ブロック(またはsorbet-runtime対応のRBIツリー)に残り、Rigorはそれらを入力として読み取り、その上に独自のナローイングを追加します。
  3. 時間をかけてSorbet → RBS。新しいコードはRBSとして着地し、既存のSorbet sigは周囲のサブシステムが変更されるまで残ります。プラグインはSorbetサーフェス(surface)が縮小する間も実行され続けます。

プラグインが置き換えないもの

Section titled “プラグインが置き換えないもの”

Rigorのrigor-sorbetアダプタは入力側のみです。Sorbetの構文を読み取り語彙を翻訳しますが、Sorbetの型チェッカーを実行せず、sorbet-runtimeを同梱せず、Sorbetのランタイム保証を強制しません。Gemfileからsorbetsorbet-runtimeを削除すると、プラグインは引き続きsigsを読み取ります(アダプタのミニインタープリタはSorbetをロードしません)が、少なくともランタイムgem(またはトップレベルのT定数で4つのシングルトンメソッドをスタブする——プラグインのデモが独自のユニットテストでこれを行っています)を保持しないかぎり、T.let / T.cast / T.must / T.unsafeコールはランタイムでNameErrorを発生させます。

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