rigortype 0.2.1 → 0.2.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (109) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +41 -14
  3. data/docs/handbook/01-getting-started.md +311 -0
  4. data/docs/handbook/02-everyday-types.md +337 -0
  5. data/docs/handbook/03-narrowing.md +359 -0
  6. data/docs/handbook/04-tuples-and-shapes.md +321 -0
  7. data/docs/handbook/05-methods-and-blocks.md +339 -0
  8. data/docs/handbook/06-classes.md +305 -0
  9. data/docs/handbook/07-rbs-and-extended.md +427 -0
  10. data/docs/handbook/08-understanding-errors.md +373 -0
  11. data/docs/handbook/09-plugins.md +241 -0
  12. data/docs/handbook/10-sorbet.md +347 -0
  13. data/docs/handbook/11-sig-gen.md +312 -0
  14. data/docs/handbook/12-lightweight-hkt.md +333 -0
  15. data/docs/handbook/README.md +275 -0
  16. data/docs/handbook/appendix-elixir.md +370 -0
  17. data/docs/handbook/appendix-go.md +399 -0
  18. data/docs/handbook/appendix-java-csharp.md +470 -0
  19. data/docs/handbook/appendix-liskov.md +580 -0
  20. data/docs/handbook/appendix-mypy.md +370 -0
  21. data/docs/handbook/appendix-phpstan.md +338 -0
  22. data/docs/handbook/appendix-protocols-and-structural-typing.md +292 -0
  23. data/docs/handbook/appendix-rust.md +446 -0
  24. data/docs/handbook/appendix-steep.md +336 -0
  25. data/docs/handbook/appendix-type-theory.md +1662 -0
  26. data/docs/handbook/appendix-typeprof.md +416 -0
  27. data/docs/handbook/appendix-typescript.md +332 -0
  28. data/docs/install.md +189 -0
  29. data/docs/llms.txt +72 -0
  30. data/docs/manual/01-installation.md +342 -0
  31. data/docs/manual/02-cli-reference.md +569 -0
  32. data/docs/manual/03-configuration.md +152 -0
  33. data/docs/manual/04-diagnostics.md +206 -0
  34. data/docs/manual/05-inspecting-types.md +109 -0
  35. data/docs/manual/06-baseline.md +104 -0
  36. data/docs/manual/07-plugins.md +92 -0
  37. data/docs/manual/08-skills.md +143 -0
  38. data/docs/manual/09-editor-integration.md +245 -0
  39. data/docs/manual/10-mcp-server.md +539 -0
  40. data/docs/manual/11-ci.md +274 -0
  41. data/docs/manual/12-caching.md +116 -0
  42. data/docs/manual/13-troubleshooting.md +120 -0
  43. data/docs/manual/14-rails-quickstart.md +332 -0
  44. data/docs/manual/15-type-protection-coverage.md +204 -0
  45. data/docs/manual/16-rbs-extended-annotations.md +190 -0
  46. data/docs/manual/17-driving-improvement.md +160 -0
  47. data/docs/manual/README.md +87 -0
  48. data/docs/manual/ci-templates/README.md +58 -0
  49. data/docs/manual/plugins/README.md +86 -0
  50. data/docs/manual/plugins/rigor-actioncable.md +78 -0
  51. data/docs/manual/plugins/rigor-actionmailer.md +74 -0
  52. data/docs/manual/plugins/rigor-actionpack.md +80 -0
  53. data/docs/manual/plugins/rigor-activejob.md +58 -0
  54. data/docs/manual/plugins/rigor-activerecord.md +102 -0
  55. data/docs/manual/plugins/rigor-activestorage.md +74 -0
  56. data/docs/manual/plugins/rigor-activesupport-core-ext.md +86 -0
  57. data/docs/manual/plugins/rigor-devise.md +70 -0
  58. data/docs/manual/plugins/rigor-dry-schema.md +56 -0
  59. data/docs/manual/plugins/rigor-dry-struct.md +60 -0
  60. data/docs/manual/plugins/rigor-dry-types.md +59 -0
  61. data/docs/manual/plugins/rigor-dry-validation.md +62 -0
  62. data/docs/manual/plugins/rigor-factorybot.md +76 -0
  63. data/docs/manual/plugins/rigor-graphql.md +89 -0
  64. data/docs/manual/plugins/rigor-hanami.md +83 -0
  65. data/docs/manual/plugins/rigor-mangrove.md +73 -0
  66. data/docs/manual/plugins/rigor-minitest.md +86 -0
  67. data/docs/manual/plugins/rigor-pundit.md +72 -0
  68. data/docs/manual/plugins/rigor-rails-i18n.md +92 -0
  69. data/docs/manual/plugins/rigor-rails-routes.md +94 -0
  70. data/docs/manual/plugins/rigor-rails.md +44 -0
  71. data/docs/manual/plugins/rigor-rbs-inline.md +83 -0
  72. data/docs/manual/plugins/rigor-rspec-rails.md +72 -0
  73. data/docs/manual/plugins/rigor-rspec.md +86 -0
  74. data/docs/manual/plugins/rigor-shoulda-matchers.md +78 -0
  75. data/docs/manual/plugins/rigor-sidekiq.md +78 -0
  76. data/docs/manual/plugins/rigor-sinatra.md +61 -0
  77. data/docs/manual/plugins/rigor-sorbet.md +63 -0
  78. data/docs/manual/plugins/rigor-statesman.md +75 -0
  79. data/docs/manual/plugins/rigor-typescript-utility-types.md +71 -0
  80. data/exe/rigor +1 -1
  81. data/lib/rigor/analysis/incremental_session.rb +4 -2
  82. data/lib/rigor/analysis/run_stats.rb +13 -1
  83. data/lib/rigor/analysis/runner.rb +54 -12
  84. data/lib/rigor/cli/check_command.rb +1 -1
  85. data/lib/rigor/cli/docs_command.rb +248 -0
  86. data/lib/rigor/cli/skill_command.rb +103 -41
  87. data/lib/rigor/cli/skill_describe.rb +346 -0
  88. data/lib/rigor/cli/triage_command.rb +8 -2
  89. data/lib/rigor/cli/triage_renderer.rb +4 -0
  90. data/lib/rigor/cli.rb +25 -3
  91. data/lib/rigor/inference/method_dispatcher/constant_folding.rb +124 -32
  92. data/lib/rigor/inference/method_dispatcher/shape_dispatch.rb +37 -6
  93. data/lib/rigor/inference/scope_indexer.rb +87 -89
  94. data/lib/rigor/plugin/isolation.rb +5 -5
  95. data/lib/rigor/plugin/loader.rb +4 -2
  96. data/lib/rigor/triage/catalogue.rb +16 -1
  97. data/lib/rigor/triage.rb +30 -7
  98. data/lib/rigor/version.rb +1 -1
  99. data/skills/rigor-ask/SKILL.md +172 -0
  100. data/skills/rigor-doctor/SKILL.md +87 -0
  101. data/skills/rigor-editor-setup/SKILL.md +114 -0
  102. data/skills/rigor-mcp-setup/SKILL.md +117 -0
  103. data/skills/rigor-monkeypatch-resolve/SKILL.md +79 -0
  104. data/skills/rigor-next-steps/SKILL.md +113 -0
  105. data/skills/rigor-plugin-tune/SKILL.md +79 -0
  106. data/skills/rigor-protection-uplift/SKILL.md +133 -0
  107. data/skills/rigor-rbs-setup/SKILL.md +128 -0
  108. data/skills/rigor-upgrade/SKILL.md +79 -0
  109. metadata +90 -1
@@ -0,0 +1,321 @@
1
+ # Tuples and hash shapes
2
+
3
+ `Tuple` and `HashShape` are how Rigor gives precise types to
4
+ heterogeneous arrays and known-key hashes. They look a lot like
5
+ Ruby's `Array` and `Hash` from the outside (and erase to those
6
+ nominal types when crossing an RBS boundary), but inside Rigor
7
+ they carry the per-position / per-key types that ordinary
8
+ `Array[T]` / `Hash[K, V]` would lose.
9
+
10
+ ## Tuples — heterogeneous arrays
11
+
12
+ When the analyzer can prove the layout of an array literal, it
13
+ produces `Tuple[…]` rather than `Array[T]`:
14
+
15
+ ```ruby
16
+ arr = [1, "two", :three]
17
+ # Tuple[Constant<1>, Constant<"two">, Constant<:three>]
18
+ ```
19
+
20
+ The most common ways tuples appear in real code:
21
+
22
+ ```ruby
23
+ # Multiple-assignment destructuring is per-position.
24
+ first, second, third = [10, 20, 30]
25
+ assert_type("10", first)
26
+ assert_type("20", second)
27
+ assert_type("30", third)
28
+
29
+ # divmod returns a 2-tuple.
30
+ quotient, remainder = 17.divmod(5)
31
+ assert_type("3", quotient)
32
+ assert_type("2", remainder)
33
+
34
+ # Each-with-index yields a 2-tuple.
35
+ %w[a b c].each_with_index do |elt, idx|
36
+ assert_type("\"a\" | \"b\" | \"c\"", elt)
37
+ assert_type("non-negative-int", idx)
38
+ end
39
+ ```
40
+
41
+ Indexed access into a tuple stays per-position:
42
+
43
+ ```ruby
44
+ arr = [1, "two", :three]
45
+ arr[0] # Constant<1>
46
+ arr[1] # Constant<"two">
47
+ arr[-1] # Constant<:three>
48
+ arr[5] # Constant<nil> — out of bounds
49
+ ```
50
+
51
+ Slicing with `[start, length]` or `[range]` produces a tuple
52
+ of the matching elements:
53
+
54
+ ```ruby
55
+ arr = [10, 20, 30, 40, 50]
56
+ arr[1..3] # Tuple[Constant<20>, Constant<30>, Constant<40>]
57
+ arr[2, 2] # Tuple[Constant<30>, Constant<40>]
58
+ ```
59
+
60
+ ## Tuples through `map`, `select`, and friends
61
+
62
+ When you call an Enumerable method on a tuple, Rigor evaluates
63
+ the block once per element with the per-position type
64
+ substituted, then unions the results:
65
+
66
+ ```ruby
67
+ arr = [1, 2, 3]
68
+ doubled = arr.map { |n| n * 2 }
69
+ # Tuple[Constant<2>, Constant<4>, Constant<6>]
70
+
71
+ mixed = [1, "two", :three]
72
+ strings = mixed.map { |x| x.to_s }
73
+ # Tuple[Constant<"1">, Constant<"two">, Constant<"three">]
74
+ ```
75
+
76
+ `select` and `filter_map` widen to `Array[Element]` because
77
+ the resulting size depends on the predicate, not the
78
+ positions. `find` returns the union of the elements (or `nil`
79
+ when no element matches statically).
80
+
81
+ ## Tuples widen — when and why
82
+
83
+ A `Tuple` widens to `Array[T]` when its size grows past the
84
+ configurable union budget, when an unknown-shape array is
85
+ concatenated to it, or when it crosses an RBS-declared
86
+ parameter typed as `Array[T]`. The widening is deterministic
87
+ and documented in
88
+ [`docs/type-specification/inference-budgets.md`](../type-specification/inference-budgets.md).
89
+
90
+ Widening is safe (`Array[T]` is a strictly less precise view
91
+ of the same value), but you lose the per-position information.
92
+ If you find yourself writing code where `[a, b, c]` should
93
+ type-check precisely but does not, look for a method
94
+ in the chain that takes `Array[T]` rather than a tuple, or a
95
+ `+` / `concat` against a wider array.
96
+
97
+ ## Hash shapes — known-key hashes
98
+
99
+ The hash analogue is `HashShape`:
100
+
101
+ ```ruby
102
+ user = { name: "Alice", age: 30, admin: false }
103
+ # { name: "Alice", age: 30, admin: false }
104
+
105
+ assert_type("\"Alice\"", user[:name])
106
+ assert_type("30", user[:age])
107
+ assert_type("false", user[:admin])
108
+ ```
109
+
110
+ Hash shapes have a few extra dimensions over tuples:
111
+
112
+ - **Required vs optional keys.** Was the key written
113
+ unconditionally in the literal, or merged in conditionally?
114
+ - **Open vs closed.** Can the value carry extra keys beyond
115
+ the listed ones?
116
+ - **Read-only entries.** Has Rigor seen a write to the key, or
117
+ only reads?
118
+
119
+ Rigor tracks all three but exposes them mostly through the
120
+ narrowing rules — most users do not need to think about them
121
+ directly.
122
+
123
+ ## Hash shapes through method calls
124
+
125
+ ```ruby
126
+ config = { host: "example.com", port: 8080 }
127
+ # HashShape{host: Constant<"example.com">, port: Constant<8080>}
128
+
129
+ config.fetch(:host) # Constant<"example.com">
130
+ config.fetch(:host, "x") # Constant<"example.com"> (default unused)
131
+ config[:port] # Constant<8080>
132
+ config.key?(:host) # Constant<true> — proven
133
+ config.empty? # Constant<false> — proven
134
+ config.size # Constant<2>
135
+ ```
136
+
137
+ ## Keyword-argument hashes
138
+
139
+ When you call a method with keyword arguments, the implicit
140
+ hash shape is what Rigor type-checks against:
141
+
142
+ ```ruby
143
+ def connect(host:, port: 80)
144
+ # ...
145
+ end
146
+
147
+ connect(host: "example.com") # OK (port defaults)
148
+ connect(host: "example.com", port: 80) # OK
149
+ connect(host: "example.com", port: "8080") # warning when
150
+ # port: Integer
151
+ # is required
152
+ ```
153
+
154
+ Hash shapes flow through `**` splat and double-splat
155
+ operations, so `connect(**opts)` where `opts` is a known
156
+ shape narrows correctly.
157
+
158
+ ## Splat composition
159
+
160
+ Splatting one tuple into another preserves the per-position
161
+ information when the splat is at a fixed position:
162
+
163
+ ```ruby
164
+ head = [1, 2]
165
+ tail = [3, 4]
166
+ arr = [*head, *tail]
167
+ # Tuple[Constant<1>, Constant<2>, Constant<3>, Constant<4>]
168
+
169
+ with_middle = [*head, "X", *tail]
170
+ # Tuple[Constant<1>, Constant<2>, Constant<"X">,
171
+ # Constant<3>, Constant<4>]
172
+ ```
173
+
174
+ Same for double-splat into hash shapes:
175
+
176
+ ```ruby
177
+ defaults = { port: 80, ssl: false }
178
+ overrides = { port: 443, ssl: true }
179
+ final = { **defaults, **overrides }
180
+ # HashShape{port: Constant<443>, ssl: Constant<true>}
181
+ # (the override wins per Ruby semantics)
182
+ ```
183
+
184
+ ## Pattern matching destructuring
185
+
186
+ `case x in [a, b, c]` narrows `a` / `b` / `c` per-position
187
+ exactly like multiple-assignment:
188
+
189
+ ```ruby
190
+ case [10, 20, 30]
191
+ in [first, _, third]
192
+ assert_type("Dynamic[top]", first) # pattern bindings are not constant-folded
193
+ assert_type("Dynamic[top]", third)
194
+ end
195
+ ```
196
+
197
+ Hash patterns work the same way:
198
+
199
+ ```ruby
200
+ case { name: "Alice", age: 30 }
201
+ in { name:, age: }
202
+ assert_type("Dynamic[top]", name) # pattern bindings are not constant-folded
203
+ assert_type("Dynamic[top]", age)
204
+ end
205
+ ```
206
+
207
+ An alternation pattern (`Integer | String => x`) produces a
208
+ union for the captured local — see
209
+ [Chapter 3](03-narrowing.md) for the underlying narrowing
210
+ rule.
211
+
212
+ ## When the layout is not provable
213
+
214
+ If even one element of an array literal has a non-Constant,
215
+ non-tuple-shaped type, Rigor falls back to `Array[T]` where
216
+ `T` is the union of element types — still useful, just not
217
+ per-position:
218
+
219
+ ```ruby
220
+ arr = [1, ARGV.first]
221
+ # Array[Constant<1> | String?]
222
+ ```
223
+
224
+ The same goes for hashes whose keys are not provably symbol /
225
+ string literals — Rigor produces `Hash[K, V]` rather than
226
+ `HashShape`.
227
+
228
+ ## Deriving new shapes — `pick_of` / `omit_of` / `partial_of` / `required_of` / `readonly_of`
229
+
230
+ When you have a `HashShape` (or a `Tuple`) and want a derived
231
+ shape that keeps some fields, drops others, or flips the
232
+ required-ness, Rigor exposes five **shape-projection type
233
+ functions** on `Type::Combinator`. They mirror TypeScript's
234
+ `Pick` / `Omit` / `Partial` / `Required` / `Readonly` utility
235
+ types but are first-class Rigor operations, not a TS bolt-on.
236
+ Each preserves the source's existing classification (required /
237
+ optional / read-only / extra-keys policy) on the entries it
238
+ keeps.
239
+
240
+ | Projection | What it does | TypeScript analogue |
241
+ | --- | --- | --- |
242
+ | `pick_of[T, K]` | Keep only the entries whose key is in the literal-key union `K`. On `Tuple`, `K` is an integer-index union. | `Pick<T, K>` |
243
+ | `omit_of[T, K]` | Drop the entries whose key is in `K`; keep the rest. | `Omit<T, K>` |
244
+ | `partial_of[T]` | Flip every required entry to optional. **Does not** widen value types to `nil` — Rigor distinguishes "key absent" from "key present with `nil` value". | `Partial<T>` |
245
+ | `required_of[T]` | Inverse of `partial_of`. Every optional entry becomes required. | `Required<T>` |
246
+ | `readonly_of[T]` | Mark every entry as read-only in the current view. Does NOT prove the underlying object is frozen — it is a view-level marker. | `Readonly<T>` |
247
+
248
+ These show up in two surfaces:
249
+
250
+ ### As `RBS::Extended` directive payloads
251
+
252
+ The projection name is part of the directive grammar — the
253
+ parser accepts Symbol / String literals and `|`-unions inside
254
+ the type-arg position, so you can author the key set inline:
255
+
256
+ ```rbs
257
+ class UserView
258
+ # The runtime returns the full user hash; the view exposes
259
+ # only :name and :email to its caller. The directive narrows
260
+ # the return-side HashShape to those two entries.
261
+ %a{rigor:v1:return: pick_of[UserHash, :name | :email]}
262
+ def public_attrs: () -> ::Hash[::Symbol, ::String]
263
+ end
264
+ ```
265
+
266
+ Inside an analysed file, the call site's result type is the
267
+ projected HashShape rather than the raw `Hash[Symbol, String]`
268
+ the underlying RBS sig advertises.
269
+
270
+ ### Through the opt-in TypeScript-utility-types plugin
271
+
272
+ If you prefer the TS spellings (`Pick<T, K>` etc.) in
273
+ directives, opt into the
274
+ [`rigor-typescript-utility-types`](../../plugins/rigor-typescript-utility-types/)
275
+ plugin. The plugin registers a `Plugin::TypeNodeResolver` that
276
+ translates each TS name onto the canonical projection:
277
+
278
+ ```yaml
279
+ # .rigor.yml
280
+ plugins:
281
+ - gem: rigor-typescript-utility-types
282
+ ```
283
+
284
+ ```rbs
285
+ %a{rigor:v1:return: Pick[UserHash, "name" | "email"]}
286
+ ```
287
+
288
+ The plugin chain resolves `Pick[…]` to `pick_of[…]` before the
289
+ analyzer sees it — the inferred result is identical to the
290
+ direct `pick_of` spelling. The plugin is purely a naming
291
+ convenience.
292
+
293
+ ### Lossy projection
294
+
295
+ The projections fire only on carriers that preserve shape
296
+ information (`HashShape` and, for `pick_of` / `omit_of`,
297
+ `Tuple`). Applying them to a plain `Hash[K, V]` or any other
298
+ non-shape input is **lossy** — the projection silently
299
+ degrades to the input type and Rigor records a
300
+ [`dynamic.shape.lossy-projection`](../type-specification/diagnostic-policy.md)
301
+ `:info` diagnostic so you can audit the call site.
302
+
303
+ ```rbs
304
+ class C
305
+ # `User` here is `Nominal[User]`, not a HashShape, so the
306
+ # projection cannot narrow anything. The directive is
307
+ # accepted but `:info` records the lossy degrade.
308
+ %a{rigor:v1:return: pick_of[User, :name]}
309
+ def render: () -> ::User
310
+ end
311
+ ```
312
+
313
+ The fix is usually to author a `HashShape` carrier (or use
314
+ `Data.define` / a `Struct`) instead of a bare `Nominal`.
315
+
316
+ ## What's next
317
+
318
+ Chapter 5 covers the function side: how Rigor types method
319
+ parameters and return values, how block parameters are bound
320
+ through Enumerable iteration, and how arity / parameter-type
321
+ mismatches surface as `call.*` diagnostics.
@@ -0,0 +1,339 @@
1
+ # Methods and blocks
2
+
3
+ This chapter covers what Rigor knows about method calls — the
4
+ receiver's type, the argument types, the inferred return
5
+ type, and the block parameters when a block is attached.
6
+ Several sections double as the reference for a call-site
7
+ diagnostic, so the rule IDs appear in the headings.
8
+
9
+ ## Method dispatch — what Rigor sees at a call site
10
+
11
+ When Rigor encounters `receiver.method(args, &block)`, it
12
+ runs through a fixed sequence of dispatch tiers, taking the
13
+ first one that produces a result:
14
+
15
+ 1. **Constant folding.** Every argument is a `Constant<...>`
16
+ or a tuple of constants, the receiver is a known
17
+ nominal class, and the method is in the per-class
18
+ "pure" catalog. Rigor invokes the method at lint time
19
+ and returns the result. `1 + 2` → `Constant<3>`,
20
+ `[1, 2, 3].first` → `Constant<1>`.
21
+ 2. **Shape dispatch.** The receiver carries a `Tuple` /
22
+ `HashShape` / `IntegerRange` / refinement and the method
23
+ has a per-shape rule. `Tuple[A, B, C].size` →
24
+ `Constant<3>`; `int<0, max>.zero?` → `Constant<true> |
25
+ Constant<false>`.
26
+ 3. **RBS dispatch.** The class has an RBS sig for the method.
27
+ Argument types are checked against the parameter contract
28
+ (more on this below); the return type is read from the sig
29
+ and may be tightened by `RBS::Extended` directives.
30
+ 4. **In-source dispatch.** The class has no RBS but Rigor
31
+ discovered a `def` (or `define_method`, `attr_*`) in the
32
+ project. Parameter types are not checked (no contract);
33
+ the return type is inferred from the method body.
34
+ 5. **Fallback.** None of the above — the call returns
35
+ `Dynamic[top]` and stays silent.
36
+
37
+ The cascading "first match wins" structure is why a method
38
+ with a tight RBS sig + an `RBS::Extended` directive overrides
39
+ the in-source body's inferred return type. Tightening at the
40
+ sig level is the supported way to teach Rigor about a
41
+ domain-specific method whose return type is narrower than
42
+ RBS expresses.
43
+
44
+ ## Argument typing — `call.argument-type-mismatch`
45
+
46
+ When the method has an RBS sig (or an `RBS::Extended`
47
+ parameter override), Rigor checks each positional / keyword
48
+ argument against the declared parameter type:
49
+
50
+ ```rbs
51
+ class Slug
52
+ %a{rigor:v1:param: id is non-empty-string}
53
+ def normalise: (::String id) -> ::String
54
+ end
55
+ ```
56
+
57
+ ```ruby
58
+ Slug.new.normalise("hello") # OK — Constant<"hello"> accepted
59
+ # by non-empty-string
60
+
61
+ Slug.new.normalise("") # error: argument-type-mismatch
62
+ # ("" is the one value
63
+ # non-empty-string excludes)
64
+
65
+ Slug.new.normalise(some_str) # OK if Rigor cannot prove some_str
66
+ # is empty; Rigor stays silent on
67
+ # "could be either" cases.
68
+ ```
69
+
70
+ `call.argument-type-mismatch` only fires when Rigor can
71
+ **prove** the argument cannot satisfy the parameter contract.
72
+ "Possibly empty" stays silent — the no-false-positives rule.
73
+
74
+ ## Arity — `call.wrong-arity`
75
+
76
+ When the receiver class is statically known and the method is
77
+ discoverable (RBS sig or in-source `def`), Rigor checks the
78
+ number of arguments against the method's arity:
79
+
80
+ ```ruby
81
+ [1, 2, 3].rotate(1, 2)
82
+ # error: wrong number of arguments to `rotate' on Array
83
+ # (given 2, expected 0..1)
84
+ ```
85
+
86
+ Arity checking respects optional positional, splat, keyword
87
+ arguments, and overload signatures. When the method is
88
+ overloaded, every overload that accepts the given arity is a
89
+ candidate — Rigor only flags arity when **no** overload
90
+ accepts.
91
+
92
+ ## `call.undefined-method`
93
+
94
+ When the receiver class is statically known and the method is
95
+ not in any of (RBS sig, in-source `def`, in-source attr,
96
+ `Data.define` accessor), Rigor flags the call:
97
+
98
+ ```ruby
99
+ "hello".no_such_method
100
+ # error: undefined method `no_such_method' for "hello"
101
+ ```
102
+
103
+ The rule is **deliberately conservative**: a call only fires
104
+ when the receiver type is statically known and the method
105
+ catalogue is enumerable. `Dynamic[top]` receivers, implicit-
106
+ self calls inside method bodies, and constant-decl alias
107
+ classes (`YAML` → `Psych`) are silenced.
108
+
109
+ ## `call.possible-nil-receiver`
110
+
111
+ When the receiver's type is `T | nil` and the method called
112
+ on it is not defined on `NilClass`, Rigor flags it:
113
+
114
+ ```ruby
115
+ def shout(name)
116
+ name.upcase # warning if name: String?
117
+ end
118
+ ```
119
+
120
+ The fix is usually a guard:
121
+
122
+ ```ruby
123
+ def shout(name)
124
+ return "" if name.nil?
125
+ name.upcase # name: String now
126
+ end
127
+ ```
128
+
129
+ This rule is one of the highest-value diagnostics Rigor ships.
130
+ It catches the entire family of `NoMethodError on nil`
131
+ crashes that pepper any non-trivial Ruby code base.
132
+
133
+ ## Return-type inference for in-source methods
134
+
135
+ When you write a `def` without an RBS sig, Rigor infers the
136
+ return type from the method body. The inferred type is
137
+ whatever the last expression evaluates to:
138
+
139
+ ```ruby
140
+ def double(n)
141
+ n * 2
142
+ end
143
+
144
+ double(5) # Constant<10> — Rigor folds the call
145
+ ```
146
+
147
+ When the body has multiple branches, the return type is the
148
+ union of every reachable terminal expression:
149
+
150
+ ```ruby
151
+ def kind(x)
152
+ if x.is_a?(Integer)
153
+ :int
154
+ elsif x.is_a?(String)
155
+ :str
156
+ end
157
+ end
158
+
159
+ kind(7) # Constant<:int>
160
+ kind("hi") # Constant<:str>
161
+ kind(:nope) # Constant<nil> — the implicit nil from
162
+ # the if's missing else branch
163
+ ```
164
+
165
+ `return` mid-body works as expected; explicit `raise` excludes
166
+ that branch from the union (a `bot` carrier internally).
167
+
168
+ ### Recursive methods
169
+
170
+ A recursive method infers a precise return type rather than
171
+ collapsing to `Dynamic[top]`. When the recursive call has
172
+ fully-constant arguments, Rigor unrolls it (under a hard frame
173
+ budget) and folds the result:
174
+
175
+ ```ruby
176
+ def factorial(n)
177
+ n <= 1 ? 1 : n * factorial(n - 1)
178
+ end
179
+
180
+ factorial(5) # Constant<120>
181
+ ```
182
+
183
+ When the arguments are not constant, Rigor reaches a fixpoint
184
+ *return summary* instead — so a pass-through recursion returns
185
+ the right type rather than a contaminated one:
186
+
187
+ ```ruby
188
+ def last_step(n)
189
+ n <= 0 ? :done : last_step(n - 1)
190
+ end
191
+
192
+ last_step(some_int) # Constant<:done>
193
+ ```
194
+
195
+ Both paths are hard-capped and degrade to the previous
196
+ base-type behaviour when they cannot converge. Mutual
197
+ recursion across two methods terminates, degrading to a safe
198
+ `Dynamic[top]` floor where a precise summary is unavailable.
199
+
200
+ ## `def.return-type-mismatch`
201
+
202
+ When a method has both an RBS-declared return type and an
203
+ inferred one, Rigor checks that the inferred fits the
204
+ declared:
205
+
206
+ ```rbs
207
+ class Slug
208
+ def normalise: (::String) -> ::String
209
+ end
210
+ ```
211
+
212
+ ```ruby
213
+ class Slug
214
+ def normalise(s)
215
+ s.empty? ? nil : s.upcase # warning:
216
+ # def.return-type-mismatch
217
+ # (declared String, inferred
218
+ # String | nil)
219
+ end
220
+ end
221
+ ```
222
+
223
+ The rule is the symmetric counterpart of
224
+ `call.argument-type-mismatch`: argument-side is "the caller
225
+ gave me a wrong type"; return-side is "I gave my caller a
226
+ wrong type."
227
+
228
+ ## Block parameters
229
+
230
+ When a method takes a block, Rigor binds the block parameters
231
+ based on the receiver method's signature. Every block-using
232
+ method in the bundled catalog has a per-method rule:
233
+
234
+ ```ruby
235
+ [1, 2, 3].each do |n|
236
+ assert_type("1 | 2 | 3", n)
237
+ end
238
+
239
+ %w[a b c].each_with_index do |word, idx|
240
+ assert_type("\"a\" | \"b\" | \"c\"", word)
241
+ assert_type("non-negative-int", idx)
242
+ end
243
+
244
+ {name: "Alice", age: 30}.each_pair do |key, value|
245
+ assert_type(":age | :name", key)
246
+ assert_type("\"Alice\" | 30", value)
247
+ end
248
+ ```
249
+
250
+ Per-position binding works for tuples, hash shapes, and
251
+ ranges. When the receiver is widened (`Array[T]` instead of
252
+ `Tuple[…]`), the block parameter is the element type `T`.
253
+
254
+ When the receiving method does not have a per-method rule,
255
+ the block parameter falls back to `Dynamic[top]`. Custom
256
+ block-using methods you write in your project's source are
257
+ seen by the in-source dispatch tier — Rigor walks the body to
258
+ infer the parameter type from `yield` calls — but that
259
+ analysis is more limited than the catalogued built-ins.
260
+
261
+ ## Numbered parameters and `it`
262
+
263
+ `_1`, `_2`, ..., and Ruby 3.4's `it` are bound exactly like
264
+ explicit parameters:
265
+
266
+ ```ruby
267
+ [1, 2, 3].each { _1.succ }
268
+ # _1: Constant<1> | Constant<2> | Constant<3>
269
+
270
+ [10, 20, 30].each { it.to_s }
271
+ # it: same as the explicit form
272
+ ```
273
+
274
+ ## Block-local declarations (`do |i; x|`)
275
+
276
+ The `;`-prefixed names introduce a fresh block-local
277
+ variable that shadows any outer local of the same name. Rigor
278
+ binds these locals to `Constant<nil>` at block entry — Ruby's
279
+ runtime semantics — and treats writes inside the block as
280
+ local to the block:
281
+
282
+ ```ruby
283
+ x = 100
284
+ [1, 2, 3].each do |i; x|
285
+ # x: Constant<nil> at this point — the block-local shadow
286
+ x = i * 2
287
+ # x: Constant<2> | Constant<4> | Constant<6>
288
+ end
289
+
290
+ assert_type("100", x) # outer x untouched
291
+ ```
292
+
293
+ ## Closure escape and captured locals
294
+
295
+ When a block captures an outer local, the block's writes to
296
+ that local flow back into the post-call view of the local. For
297
+ known non-escaping methods (`Array#each`, `tap`, …) the
298
+ write-back is applied; for escaping methods (`Thread.new`,
299
+ `define_method`, …) the analyzer drops captured-local facts
300
+ because the block could fire arbitrarily later.
301
+
302
+ The write-back covers both *rebinding* a local and *mutating a
303
+ collection* the block was building up. A rebound accumulator
304
+ widens to its base type rather than keeping a stale seed, and a
305
+ collection grown inside the block carries the appended element
306
+ types:
307
+
308
+ ```ruby
309
+ total = 0
310
+ [1, 2, 3].each { |n| total += n }
311
+ # total: Integer — the rebind is written back (not a stale 0)
312
+
313
+ out = [0]
314
+ [1, 2, 3].each { |x| out << x }
315
+ # out: Array[Integer] — the appended element type joins in,
316
+ # not just the seed's Constant<0>
317
+
318
+ squares = [1, 2, 3].each_with_object([]) { |n, acc| acc << n * n }
319
+ # squares: Array[Integer]
320
+ ```
321
+
322
+ The same write-back applies to `while` / `until` loop
323
+ bodies — an accumulator a loop rebinds no longer keeps a stale
324
+ single-pass constant. The analysis runs the body to a small
325
+ fixpoint and widens (it never folds a loop accumulator to a
326
+ value the runtime would exceed). A `for` loop joins a single
327
+ body pass rather than running this fixpoint. For an *escaping* or unknown
328
+ block that mutates a captured collection, the local widens to
329
+ its bare-collection floor instead of an unsoundly-precise seed.
330
+
331
+ This is the conservative call: better to widen too much than
332
+ to claim narrowed-after-escape facts that the runtime might
333
+ violate.
334
+
335
+ ## What's next
336
+
337
+ Chapter 6 covers the class side: how Rigor types `self`,
338
+ constant lookup, `attr_*` declarations, and the
339
+ class-vs-instance method distinction.