コンテンツにスキップ

メソッドとブロック

この章では、Rigorがメソッド呼び出しについて知っていること — レシーバーの型、引数の型、推論された戻り値型、ブロックが付属している場合のブロックパラメータ — を扱います。いくつかのセクションは呼び出し元診断のリファレンスも兼ねており、見出しにルールIDが現れます。

この章の内容 メソッドディスパッチ · 引数型(call.argument-type-mismatch · アリティ(call.wrong-arity · 未定義メソッド(call.undefined-method · nilレシーバー(call.possible-nil-receiver · 戻り値型推論 · 戻り値の不一致(def.return-type-mismatch · ブロックパラメータ · 番号付きパラメータとit · ブロックローカル · クロージャエスケープ

メソッドディスパッチ — 呼び出し元でRigorが見るもの

Section titled “メソッドディスパッチ — 呼び出し元でRigorが見るもの”

Rigorがreceiver.method(args, &block)に遭遇したとき、結果を生成する最初のものを採用しながら、固定されたディスパッチ層のシーケンスを実行します:

  1. 定数たたみ込み。 すべての引数がConstant<...>または定数のタプルで、レシーバーが既知の名前的クラスで、メソッドがクラスごとの「純粋な」カタログにある場合。Rigorは解析時にメソッドを実行して結果を返します。1 + 2Constant<3>[1, 2, 3].firstConstant<1>
  2. シェイプ(shape)ディスパッチ。 レシーバーがTuple / HashShape / IntegerRange / リファインメント(refinement、篩型とも)を持ち、メソッドにシェイプごとのルールがある場合。Tuple[A, B, C].sizeConstant<3>; int<0, max>.zero?Constant<true> | Constant<false>
  3. RBSディスパッチ。 クラスにそのメソッドのRBSシグがある場合。引数型がパラメータ契約(contract)に対してチェックされます(後述);戻り値型はシグから読まれ、RBS::Extendedディレクティブによって締め付けられることがあります。
  4. インソースディスパッチ。 クラスにRBSはないが、Rigorがプロジェクト内のdef(またはdefine_methodattr_*)を見つけた場合。パラメータ型はチェックされません(契約がない);戻り値型はメソッド本体から推論されます。
  5. フォールバック。 上記のいずれも当てはまらない — 呼び出しはDynamic[Top]を返し、沈黙を保ちます。

「最初に一致したものが勝つ」カスケード構造が、厳密なRBSシグ + RBS::Extendedディレクティブを持つメソッドがインソース本体の推論された戻り値型をオーバーライドする理由です。シグレベルでの締め付けは、RBSが表現するよりも狭い戻り値型を持つドメイン固有のメソッドについてRigorに教える、サポートされた方法です。

引数型付け — call.argument-type-mismatch

Section titled “引数型付け — call.argument-type-mismatch”

メソッドにRBSシグ(またはRBS::Extendedパラメータオーバーライド)がある場合、Rigorは各位置引数/キーワード引数を宣言されたパラメータ型に対してチェックします:

class Slug
%a{rigor:v1:param: id is non-empty-string}
def normalise: (::String id) -> ::String
end
Slug.new.normalise("hello") # OK — Constant<"hello">はnon-empty-stringを満たす
Slug.new.normalise("") # error: argument-type-mismatch
# ("" はnon-empty-stringが除外する
# 唯一の値)
Slug.new.normalise(some_str) # some_strが空であることをRigorが
# 証明できない場合はOK;
# 「どちらかの可能性がある」ケースでは
# Rigorは沈黙します

call.argument-type-mismatchは、Rigorが引数がパラメータ契約を満たせないことを証明できる場合にのみ発火します。「空の可能性がある」は沈黙します — 偽陽性なしルール。

レシーバークラスが静的に既知で、メソッドが発見可能(RBSシグまたはインソースdef)な場合、Rigorは引数の数をメソッドのアリティに対してチェックします:

[1, 2, 3].rotate(1, 2)
# error: wrong number of arguments to `rotate' on Array
# (given 2, expected 0..1)

アリティチェックは省略可能な位置引数、スプラット、キーワード引数、オーバーロードシグネチャを考慮します。メソッドがオーバーロードされている場合、指定されたアリティを受け入れるすべてのオーバーロードが候補です — Rigorはどのオーバーロードも受け入れない場合にのみアリティをフラグします。

レシーバークラスが静的に既知で、メソッドが(RBSシグ、インソースdef、インソースattr、Data.defineアクセサの)いずれにもない場合、Rigorは呼び出しをフラグします:

"hello".no_such_method
# error: undefined method `no_such_method' for "hello"

このルールは意図的に保守的です: 呼び出しがフラグされるのは、レシーバー型が静的に既知で、メソッドカタログが列挙可能な場合のみです。Dynamic[Top]レシーバー、メソッド本体内の暗黙的selfの呼び出し、定数宣言エイリアスクラス(YAMLPsych)は沈黙します。

レシーバーの型がT | nilで、呼び出されたメソッドがNilClassで定義されていない場合、Rigorはフラグします:

def shout(name)
name.upcase # warning: name: String?のとき
end

修正は通常ガードを追加することです:

def shout(name)
return "" if name.nil?
name.upcase # name: Stringになった
end

このルールはRigorが出荷する最も高価値な診断の1つです — 非自明なRubyコードベースに散在するnilへのNoMethodErrorクラッシュのファミリー全体を捉えます。

インソースメソッドの戻り値型推論

Section titled “インソースメソッドの戻り値型推論”

RBSシグなしでdefを書くと、Rigorはメソッド本体から戻り値型を推論します。推論された型は最後の式が評価するものです:

def double(n)
n * 2
end
double(5) # Constant<10> — Rigorが呼び出しをたたみ込む

本体に複数のブランチがある場合、戻り値型は到達可能なすべての終端式のユニオン(union、合併型とも)です:

def kind(x)
if x.is_a?(Integer)
:int
elsif x.is_a?(String)
:str
end
end
kind(7) # Constant<:int>
kind("hi") # Constant<:str>
kind(:nope) # Constant<nil> — ifのelse分岐が欠けている
# ことによる暗黙のnil

本体途中のreturnは期待通りに動作します;明示的なraiseはそのブランチをユニオンから除外します(内部的にbotキャリア(carrier))。

メソッドにRBS宣言の戻り値型と推論された型の両方がある場合、Rigorは推論された型が宣言された型に適合するかチェックします:

class Slug
def normalise: (::String) -> ::String
end
def.return-type-mismatch
class Slug
def normalise(s)
s.empty? ? nil : s.upcase # warning:
# (宣言はString、推論は
# String | nil)
end
end

このルールはcall.argument-type-mismatchの対称的な対応物です: 引数側は「呼び出し元が間違った型を渡した」;戻り値側は「私が呼び出し元に間違った型を返した」。

メソッドがブロックを受け取るとき、Rigorはレシーバーメソッドのシグネチャに基づいてブロックパラメータをバインドします。バンドルされたカタログのすべてのブロック使用メソッドにはメソッドごとのルールがあります:

[1, 2, 3].each do |n|
assert_type("Constant<1> | Constant<2> | Constant<3>", n)
end
%w[a b c].each_with_index do |word, idx|
assert_type("Constant<\"a\"> | Constant<\"b\"> | Constant<\"c\">", word)
assert_type("non-negative-int", idx)
end
{name: "Alice", age: 30}.each_pair do |key, value|
assert_type("Constant<:name> | Constant<:age>", key)
assert_type("Constant<\"Alice\"> | Constant<30>", value)
end

位置ごとのバインディングはタプル、ハッシュシェイプ、範囲に対して機能します。レシーバーが拡幅されている(Tuple[…]ではなくArray[T])場合、ブロックパラメータは要素型Tです。

受信メソッドにメソッドごとのルールがない場合、ブロックパラメータはDynamic[Top]にフォールバックします。プロジェクトソースに書いたカスタムなブロック使用メソッドは、インソースディスパッチ層によって見られます — Rigorが本体を走査してyield呼び出しからパラメータ型を推論します — しかしその解析はカタログに載っている組み込みよりも制限があります。

_1_2、…、およびRuby 3.4のitは明示的なパラメータとまったく同じようにバインドされます:

[1, 2, 3].each { _1.succ }
# _1: Constant<1> | Constant<2> | Constant<3>
[10, 20, 30].each { it.to_s }
# it: 明示的な形式と同じ

ブロックローカル宣言(do |i; x|

Section titled “ブロックローカル宣言(do |i; x|)”

;を接頭辞にした名前は、同名の外側ローカル変数をシャドウする新しいブロックローカル変数を導入します。Rigorはブロックエントリー時にこれらのローカル変数をConstant<nil>にバインドします — Rubyのランタイムセマンティクス — そしてブロック内の書き込みをそのブロックにローカルなものとして扱います:

x = 100
[1, 2, 3].each do |i; x|
# x: Constant<nil> この時点で — ブロックローカルなシャドウ
x = i * 2
# x: Constant<2> | Constant<4> | Constant<6>
end
assert_type("Constant<100>", x) # 外側のxは変更されていない

クロージャエスケープとキャプチャされたローカル変数

Section titled “クロージャエスケープとキャプチャされたローカル変数”

ブロックが外側のローカル変数をキャプチャすると、そのローカル変数へのブロックの書き込みが呼び出し後のローカル変数のビューに影響します。非エスケープとして既知のメソッド(Array#eachtapなど)では、呼び出し後のナローイング(narrowing)が保持されます;エスケープするメソッド(Thread.newdefine_methodなど)では、ブロックが任意の後のタイミングで発火する可能性があるため、解析器はキャプチャされたローカル変数のナローイングを除去します。

これは保守的な判断です: エスケープ後のナローイング事実をランタイムが違反するかもしれないという主張をするよりも、広げすぎるほうがよいです。

第6章はクラス側を扱います: Rigorがself、定数ルックアップ、attr_*宣言、クラスメソッドとインスタンスメソッドの区別をどう型付けするか。

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