rigortype 0.2.0 → 0.2.2

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 (142) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +82 -20
  3. data/data/core_overlay/numeric.rbs +33 -0
  4. data/data/core_overlay/pathname.rbs +25 -0
  5. data/data/core_overlay/string_scanner.rbs +28 -0
  6. data/data/gem_overlay/activesupport/core_ext.rbs +473 -0
  7. data/data/vendored_gem_sigs/ast/ast.rbs +130 -0
  8. data/data/vendored_gem_sigs/bcrypt/bcrypt.rbs +47 -0
  9. data/data/vendored_gem_sigs/bundler/bundler.rbs +238 -0
  10. data/data/vendored_gem_sigs/cgi/cgi_extras.rbs +34 -0
  11. data/data/vendored_gem_sigs/did_you_mean/did_you_mean_extras.rbs +34 -0
  12. data/data/vendored_gem_sigs/idn-ruby/idn.rbs +54 -0
  13. data/data/vendored_gem_sigs/mysql2/client.rbs +55 -0
  14. data/data/vendored_gem_sigs/mysql2/error.rbs +5 -0
  15. data/data/vendored_gem_sigs/mysql2/result.rbs +31 -0
  16. data/data/vendored_gem_sigs/mysql2/statement.rbs +5 -0
  17. data/data/vendored_gem_sigs/nokogiri/nokogiri.rbs +2332 -0
  18. data/data/vendored_gem_sigs/nokogiri/nokogiri_html5.rbs +47 -0
  19. data/data/vendored_gem_sigs/pg/pg.rbs +212 -0
  20. data/data/vendored_gem_sigs/prism/prism_supplement.rbs +44 -0
  21. data/data/vendored_gem_sigs/redis/errors.rbs +50 -0
  22. data/data/vendored_gem_sigs/redis/future.rbs +5 -0
  23. data/data/vendored_gem_sigs/redis/redis.rbs +348 -0
  24. data/data/vendored_gem_sigs/redis/redis_extras.rbs +130 -0
  25. data/data/vendored_gem_sigs/rubygems/rubygems_extras.rbs +226 -0
  26. data/docs/handbook/01-getting-started.md +311 -0
  27. data/docs/handbook/02-everyday-types.md +337 -0
  28. data/docs/handbook/03-narrowing.md +359 -0
  29. data/docs/handbook/04-tuples-and-shapes.md +321 -0
  30. data/docs/handbook/05-methods-and-blocks.md +339 -0
  31. data/docs/handbook/06-classes.md +305 -0
  32. data/docs/handbook/07-rbs-and-extended.md +427 -0
  33. data/docs/handbook/08-understanding-errors.md +373 -0
  34. data/docs/handbook/09-plugins.md +241 -0
  35. data/docs/handbook/10-sorbet.md +347 -0
  36. data/docs/handbook/11-sig-gen.md +312 -0
  37. data/docs/handbook/12-lightweight-hkt.md +333 -0
  38. data/docs/handbook/README.md +275 -0
  39. data/docs/handbook/appendix-elixir.md +370 -0
  40. data/docs/handbook/appendix-go.md +399 -0
  41. data/docs/handbook/appendix-java-csharp.md +470 -0
  42. data/docs/handbook/appendix-liskov.md +580 -0
  43. data/docs/handbook/appendix-mypy.md +370 -0
  44. data/docs/handbook/appendix-phpstan.md +338 -0
  45. data/docs/handbook/appendix-protocols-and-structural-typing.md +292 -0
  46. data/docs/handbook/appendix-rust.md +446 -0
  47. data/docs/handbook/appendix-steep.md +336 -0
  48. data/docs/handbook/appendix-type-theory.md +1662 -0
  49. data/docs/handbook/appendix-typeprof.md +416 -0
  50. data/docs/handbook/appendix-typescript.md +332 -0
  51. data/docs/install.md +189 -0
  52. data/docs/llms.txt +72 -0
  53. data/docs/manual/01-installation.md +342 -0
  54. data/docs/manual/02-cli-reference.md +557 -0
  55. data/docs/manual/03-configuration.md +152 -0
  56. data/docs/manual/04-diagnostics.md +206 -0
  57. data/docs/manual/05-inspecting-types.md +109 -0
  58. data/docs/manual/06-baseline.md +104 -0
  59. data/docs/manual/07-plugins.md +92 -0
  60. data/docs/manual/08-skills.md +143 -0
  61. data/docs/manual/09-editor-integration.md +245 -0
  62. data/docs/manual/10-mcp-server.md +532 -0
  63. data/docs/manual/11-ci.md +274 -0
  64. data/docs/manual/12-caching.md +116 -0
  65. data/docs/manual/13-troubleshooting.md +120 -0
  66. data/docs/manual/14-rails-quickstart.md +332 -0
  67. data/docs/manual/15-type-protection-coverage.md +204 -0
  68. data/docs/manual/16-rbs-extended-annotations.md +190 -0
  69. data/docs/manual/17-driving-improvement.md +160 -0
  70. data/docs/manual/README.md +87 -0
  71. data/docs/manual/ci-templates/README.md +58 -0
  72. data/docs/manual/plugins/README.md +86 -0
  73. data/docs/manual/plugins/rigor-actioncable.md +78 -0
  74. data/docs/manual/plugins/rigor-actionmailer.md +74 -0
  75. data/docs/manual/plugins/rigor-actionpack.md +80 -0
  76. data/docs/manual/plugins/rigor-activejob.md +58 -0
  77. data/docs/manual/plugins/rigor-activerecord.md +102 -0
  78. data/docs/manual/plugins/rigor-activestorage.md +74 -0
  79. data/docs/manual/plugins/rigor-activesupport-core-ext.md +86 -0
  80. data/docs/manual/plugins/rigor-devise.md +70 -0
  81. data/docs/manual/plugins/rigor-dry-schema.md +56 -0
  82. data/docs/manual/plugins/rigor-dry-struct.md +60 -0
  83. data/docs/manual/plugins/rigor-dry-types.md +59 -0
  84. data/docs/manual/plugins/rigor-dry-validation.md +62 -0
  85. data/docs/manual/plugins/rigor-factorybot.md +76 -0
  86. data/docs/manual/plugins/rigor-graphql.md +89 -0
  87. data/docs/manual/plugins/rigor-hanami.md +83 -0
  88. data/docs/manual/plugins/rigor-mangrove.md +73 -0
  89. data/docs/manual/plugins/rigor-minitest.md +86 -0
  90. data/docs/manual/plugins/rigor-pundit.md +72 -0
  91. data/docs/manual/plugins/rigor-rails-i18n.md +92 -0
  92. data/docs/manual/plugins/rigor-rails-routes.md +94 -0
  93. data/docs/manual/plugins/rigor-rails.md +44 -0
  94. data/docs/manual/plugins/rigor-rbs-inline.md +83 -0
  95. data/docs/manual/plugins/rigor-rspec-rails.md +72 -0
  96. data/docs/manual/plugins/rigor-rspec.md +86 -0
  97. data/docs/manual/plugins/rigor-shoulda-matchers.md +78 -0
  98. data/docs/manual/plugins/rigor-sidekiq.md +78 -0
  99. data/docs/manual/plugins/rigor-sinatra.md +61 -0
  100. data/docs/manual/plugins/rigor-sorbet.md +63 -0
  101. data/docs/manual/plugins/rigor-statesman.md +75 -0
  102. data/docs/manual/plugins/rigor-typescript-utility-types.md +71 -0
  103. data/exe/rigor +1 -1
  104. data/lib/rigor/analysis/incremental_session.rb +4 -2
  105. data/lib/rigor/analysis/run_stats.rb +13 -1
  106. data/lib/rigor/analysis/runner.rb +54 -12
  107. data/lib/rigor/cli/check_command.rb +26 -3
  108. data/lib/rigor/cli/coverage_command.rb +67 -92
  109. data/lib/rigor/cli/coverage_mutation.rb +149 -0
  110. data/lib/rigor/cli/docs_command.rb +248 -0
  111. data/lib/rigor/cli/fused_protection_renderer.rb +67 -0
  112. data/lib/rigor/cli/fused_protection_report.rb +76 -0
  113. data/lib/rigor/cli/skill_command.rb +103 -41
  114. data/lib/rigor/cli/skill_describe.rb +346 -0
  115. data/lib/rigor/cli.rb +25 -3
  116. data/lib/rigor/config_audit.rb +152 -0
  117. data/lib/rigor/configuration.rb +12 -0
  118. data/lib/rigor/environment/rbs_loader.rb +27 -0
  119. data/lib/rigor/environment.rb +49 -1
  120. data/lib/rigor/inference/method_dispatcher/constant_folding.rb +140 -38
  121. data/lib/rigor/inference/method_dispatcher/shape_dispatch.rb +37 -6
  122. data/lib/rigor/inference/scope_indexer.rb +87 -89
  123. data/lib/rigor/inference/statement_evaluator.rb +27 -0
  124. data/lib/rigor/plugin/isolation.rb +5 -5
  125. data/lib/rigor/plugin/loader.rb +4 -2
  126. data/lib/rigor/protection/diagnostic_oracle.rb +51 -0
  127. data/lib/rigor/protection/mutation_scanner.rb +98 -38
  128. data/lib/rigor/protection/mutator.rb +21 -0
  129. data/lib/rigor/protection/test_suite_oracle.rb +68 -0
  130. data/lib/rigor/signature_path_audit.rb +92 -0
  131. data/lib/rigor/version.rb +1 -1
  132. data/skills/rigor-ask/SKILL.md +172 -0
  133. data/skills/rigor-doctor/SKILL.md +87 -0
  134. data/skills/rigor-editor-setup/SKILL.md +114 -0
  135. data/skills/rigor-mcp-setup/SKILL.md +117 -0
  136. data/skills/rigor-monkeypatch-resolve/SKILL.md +79 -0
  137. data/skills/rigor-next-steps/SKILL.md +113 -0
  138. data/skills/rigor-plugin-tune/SKILL.md +79 -0
  139. data/skills/rigor-protection-uplift/SKILL.md +133 -0
  140. data/skills/rigor-rbs-setup/SKILL.md +128 -0
  141. data/skills/rigor-upgrade/SKILL.md +79 -0
  142. metadata +120 -1
@@ -0,0 +1,446 @@
1
+ # Appendix — Coming from Rust
2
+
3
+ If your mental model of "types" was set by Rust, this appendix
4
+ maps Rigor's vocabulary onto the concepts you already know.
5
+ Rust and Rigor sit at opposite ends of one axis: Rust is
6
+ ahead-of-time, sound, and refuses to compile anything it cannot
7
+ prove safe; Rigor analyses Ruby that already runs and stays
8
+ silent on anything it cannot prove *wrong*. But they meet
9
+ often on the others: sum types, exhaustive
10
+ matching, the absence of a billion-dollar null.
11
+
12
+ This is a translation table plus a discussion of the places
13
+ where the two systems make genuinely different choices. Those
14
+ are where your Rust reflexes will mislead you. The biggest one:
15
+ Rust's type checker is a gate the program must pass before it
16
+ exists; Rigor's is an advisor over a program that already runs.
17
+ There is no borrow checker, no ownership, and no "it does not
18
+ compile." The Ruby ran, and Rigor tells you where it can prove
19
+ a type goes wrong.
20
+
21
+ ## The five-second pitch
22
+
23
+ | Question | Rust | Rigor |
24
+ | --- | --- | --- |
25
+ | When does the checker run? | At compile time; nothing runs until it passes | After the fact, on code that already runs |
26
+ | Soundness stance | Sound — a type error is a hard stop | No-false-positives — silent unless it can prove the error |
27
+ | Where do annotations live? | In source; locals inferred, signatures explicit | In `.rbs` files; whole bodies inferred |
28
+ | The "I don't know" type | (none — Rust has no escape hatch) | `Dynamic[top]` — silent at the boundary |
29
+ | Null | Does not exist (`Option<T>` instead) | `nil` exists; narrowed away like Rust narrows `Option` |
30
+ | Identity of types | Nominal, with trait coherence | Nominal + structural facets |
31
+
32
+ Rust earns its guarantees by refusing to run until every value
33
+ is accounted for. Rigor takes the opposite bet: the program
34
+ runs, most of it is fine, and the analyzer should only speak
35
+ when it can *prove* a problem — never frighten working code over
36
+ a worst-case it cannot rule out. If "the compiler is always
37
+ right and I obey it" is your reflex, the one to retrain is that
38
+ Rigor is an advisor, not a gate.
39
+
40
+ ## Type vocabulary mapping
41
+
42
+ | Rust | Rigor | Notes |
43
+ | --- | --- | --- |
44
+ | `i8` / `i32` / `i64` / `u32` / `usize` | `Integer` | Ruby integers are arbitrary-precision; no width or signedness in the type. |
45
+ | `f32` / `f64` | `Float` | `Numeric` is the common supertype. |
46
+ | `bool` | `bool` (`Constant<true> \| Constant<false>`) | Structurally a union of two constants. |
47
+ | `char` / `&str` / `String` | `String` | Ruby has one string type; no borrow distinction. |
48
+ | `()` (unit) | `nil` / `void` | `void` when the caller must ignore the result; `nil` as a value. |
49
+ | `!` (never) | `Bot` | Empty type — unreachable branches, `raise`-only bodies. |
50
+ | `Option<T>` | `T?` (i.e. `T \| nil`) | See [Option and Result](#option-and-result). |
51
+ | `Result<T, E>` | (no single carrier — Ruby raises) | See [Option and Result](#option-and-result). |
52
+ | `Vec<T>` / `&[T]` | `Array[T]` | |
53
+ | `HashMap<K, V>` | `Hash[K, V]` | |
54
+ | `HashSet<T>` | `Set[T]` | |
55
+ | `(i32, String)` (tuple) | `Tuple[Integer, String]` | Same per-position model. |
56
+ | `struct Point { x: i32, y: i32 }` | `Point = Data.define(:x, :y)` | See [Structs ↔ Data.define](#structs--datadefine). |
57
+ | `enum E { A, B(i32) }` (sum type) | union of the variants | See [Sum types & exhaustiveness](#sum-types-and-exhaustiveness). |
58
+ | `trait T { … }` | RBS `interface` (structural) | Nominal in Rust, structural in Rigor — see below. |
59
+ | `<T: Trait>` / `where T: Trait` | RBS `[T < Bound]` bounded parameter | |
60
+ | `dyn Trait` | a structural interface type | Dynamic dispatch over the method set. |
61
+ | `Box<dyn Any>` | `Dynamic[top]` | The closest Rust has to "silence the checker"; Rigor reaches for it routinely, Rust almost never. |
62
+ | (no literal types) | `Constant<42>` / `Constant<"hi">` | Rust has no literal types (const generics aside); a Rigor novelty. |
63
+
64
+ ## Option and Result
65
+
66
+ Rust's two famous enums split cleanly: one maps almost exactly
67
+ onto Rigor, the other does not, because Ruby chose a different
68
+ error-handling model.
69
+
70
+ **`Option<T>` ↔ `T?`.** This is a near-perfect match.
71
+ `Option<T>` is "a `T` or nothing"; Rigor's `T?` is `T | nil`,
72
+ and the narrowing mirrors `match` / `if let`:
73
+
74
+ ```rust
75
+ fn length(s: Option<String>) -> usize {
76
+ match s {
77
+ Some(v) => v.len(),
78
+ None => 0,
79
+ }
80
+ }
81
+ ```
82
+
83
+ ```ruby
84
+ def length(s) # s : String? (RBS-declared)
85
+ return 0 if s.nil?
86
+ s.length # s : String — nil stripped by the guard
87
+ end
88
+ ```
89
+
90
+ The reflex to drop is `unwrap()`. In Rust you reach for
91
+ `.unwrap()` / `.expect()` when you *know* it is `Some`. Rigor
92
+ has no in-source assertion that lies to the checker; the
93
+ equivalents are a `nil?` guard (checked, not asserted) or
94
+ `T.must` via the [`rigor-sorbet`](../../plugins/rigor-sorbet/)
95
+ plugin (see [Chapter 10](10-sorbet.md)).
96
+
97
+ **`Result<T, E>` ↔ exceptions.** Here the models diverge. Ruby
98
+ signals failure by *raising*, not by returning a tagged value,
99
+ so there is no single `Result` carrier. A Rust function
100
+ returning `Result<T, E>` becomes a Ruby method that returns `T`
101
+ and raises on the error path:
102
+
103
+ ```rust
104
+ fn parse(s: &str) -> Result<i32, ParseIntError> { s.parse() }
105
+ ```
106
+
107
+ ```ruby
108
+ def parse(s) # returns Integer, raises on bad input
109
+ Integer(s)
110
+ end
111
+ ```
112
+
113
+ Rigor does not track the exception type as part of the
114
+ signature — there is no typed `throws` and no `?` operator. If
115
+ you want to keep Rust's value-returning style, you *can* return
116
+ a tagged tuple (`[:ok, value]` / `[:error, reason]`) and pattern-
117
+ match it with `case`/`in`, and Rigor will type the `Tuple` and
118
+ the union precisely. That is a deliberate port of the Rust
119
+ idiom, not the idiomatic Ruby.
120
+
121
+ ## Narrowing — `match` / `if let`
122
+
123
+ Rust narrows through `match`, `if let`, and pattern binding.
124
+ Rigor has direct analogues; the behaviour matches even though
125
+ the surface differs.
126
+
127
+ | Rust | Rigor |
128
+ | --- | --- |
129
+ | `match x { … }` | `case x; in …` |
130
+ | `if let Some(v) = x` | `if x` (strips `nil`), or `case x; in val` |
131
+ | `if let Pat = x { … }` (binding) | `case x; in Pat => v` |
132
+ | `x as i64` (numeric cast) | `x.to_i` / `Integer(x)` — a real conversion, not a type cast |
133
+ | `x.unwrap()` | (no in-source assertion) — `nil?` guard, or `T.must` via `rigor-sorbet` |
134
+ | `matches!(x, Pat)` | `case x; in Pat then true; else false; end`, or an `is_a?` predicate |
135
+ | guard `Pat if cond =>` | `in Pat if cond` (pattern guard) |
136
+
137
+ The structural-pattern part of `match` maps onto `case`/`in`
138
+ one-to-one: `in Circle => c` narrows `x` to `Circle` along that
139
+ clause exactly as `Circle(c) =>` does in Rust.
140
+
141
+ ## Sum types and exhaustiveness
142
+
143
+ Rust's `enum` is an algebraic sum type, and `match` over it is
144
+ *compiler-enforced* exhaustive — miss a variant and it does not
145
+ compile. Rigor models the data the same way but approaches
146
+ exhaustiveness from the dual side.
147
+
148
+ A Rust `enum` whose variants carry data becomes, in Ruby, one
149
+ `Data.define` per variant plus a union of them:
150
+
151
+ ```rust
152
+ enum Shape {
153
+ Circle { radius: f64 },
154
+ Rectangle { w: f64, h: f64 },
155
+ }
156
+
157
+ fn area(s: Shape) -> f64 {
158
+ match s {
159
+ Shape::Circle { radius } => PI * radius * radius,
160
+ Shape::Rectangle { w, h } => w * h,
161
+ }
162
+ }
163
+ ```
164
+
165
+ ```ruby
166
+ Circle = Data.define(:radius)
167
+ Rectangle = Data.define(:w, :h)
168
+
169
+ def area(s)
170
+ case s
171
+ in Circle then Math::PI * s.radius * s.radius
172
+ in Rectangle then s.w * s.h
173
+ end
174
+ end
175
+ ```
176
+
177
+ The crucial difference is *direction*.
178
+ [ADR-47](../adr/47-narrowing-driven-clause-reachability.md)'s
179
+ `flow.unreachable-clause` rule fires when a clause is provably
180
+ *dead* — its subject already narrowed to `Bot` by prior clauses
181
+ or prior exhaustion:
182
+
183
+ ```ruby
184
+ case shape
185
+ in Circle then shape.radius
186
+ in Rectangle then shape.width * shape.height
187
+ in Circle then "…" # flow.unreachable-clause — Circle already covered
188
+ end
189
+ ```
190
+
191
+ Rust **requires** you to cover every variant and rejects a
192
+ non-exhaustive `match`. Rigor does the dual — it reports clauses
193
+ that can never run, but it does **not** force you to handle every
194
+ variant. A `case` that omits a branch is not an error; an
195
+ *unreachable* clause is. This follows the no-false-positives
196
+ stance: an omitted branch may be intentional (that variant
197
+ cannot reach this point), and Rigor will not frighten working
198
+ code over it. If you want the missing-arm safety back, a
199
+ trailing `else raise` makes the omission explicit — the analogue
200
+ of Rust's `_ => unreachable!()`.
201
+
202
+ ## Structs ↔ `Data.define`
203
+
204
+ A Rust `struct` maps onto Ruby's `Data.define` — an immutable,
205
+ value-equal, member-shaped aggregate. Rigor models it natively
206
+ ([ADR-48](../adr/48-data-struct-value-folding.md)).
207
+
208
+ ```rust
209
+ struct Point { x: i32, y: i32 }
210
+ let p = Point { x: 1, y: 2 };
211
+ let a = p.x; // a : i32
212
+ let q = Point { x: 9, ..p }; // functional update
213
+ ```
214
+
215
+ ```ruby
216
+ Point = Data.define(:x, :y)
217
+ p = Point.new(1, 2)
218
+ assert_type("1", p.x) # member value is folded, not just Integer
219
+ q = p.with(x: 9) # Data#with ↔ Rust's ..p update
220
+ ```
221
+
222
+ Two things go beyond Rust here:
223
+
224
+ - **Member values fold.** `Point.new(1, 2).x` is `1`,
225
+ not merely `Integer`. Rust erases the literal at
226
+ construction; Rigor keeps it (subject to the folding budget).
227
+ - **`with` is first-class.** `Data#with` is the analogue of
228
+ Rust's `..p` functional-update syntax, and Rigor types the
229
+ result with the overridden member folded in.
230
+
231
+ Ruby's mutable `Struct` is deliberately not folded the same way
232
+ yet; its mutability breaks the value-folding soundness story.
233
+ See [Chapter 6](06-classes.md).
234
+
235
+ ## Traits ↔ RBS interfaces
236
+
237
+ Rust traits and Rigor's structural interfaces both describe "a
238
+ type that has these methods," but they differ on *how
239
+ membership is decided*, and the difference is the same one
240
+ Go programmers feel from the other side.
241
+
242
+ A Rust trait is **nominal with coherence**: a type has the trait
243
+ only if there is an explicit `impl Trait for Type`, and the
244
+ orphan rule governs where that `impl` may live. Rigor's RBS
245
+ `interface` is **structural**: any object with the right methods
246
+ satisfies it, no declaration of intent, no coherence rule. This
247
+ is Go's `interface`, not Rust's `trait`.
248
+
249
+ ```ruby
250
+ # An RBS structural interface
251
+ interface _Drawable
252
+ def draw: () -> String
253
+ end
254
+ ```
255
+
256
+ Any Ruby object that responds to `draw` returning a `String`
257
+ satisfies `_Drawable` — you never write `impl _Drawable for …`.
258
+ Rigor also infers anonymous object *shapes* and capability roles
259
+ without any interface declared at all. The
260
+ [structural-typing appendix](appendix-protocols-and-structural-typing.md)
261
+ is the canonical explainer; the short version is "traits you do
262
+ not have to implement on purpose."
263
+
264
+ ## Refinements vs the newtype pattern
265
+
266
+ When a Rust invariant outruns the type system — "a string that
267
+ is non-empty," "an integer in 1..=9" — you reach for the newtype
268
+ pattern: a `struct NonEmptyString(String)` with a validating
269
+ constructor. Rigor has first-class refinement carriers instead;
270
+ the invariant rides on the ordinary type, produced automatically
271
+ by narrowing.
272
+
273
+ | Rigor refinement | Rust idiom | Comment |
274
+ | --- | --- | --- |
275
+ | `non-empty-string` | `struct NonEmptyString(String)` newtype | Rigor produces it from `unless s.empty?`, no wrapper. |
276
+ | `positive-int` | `struct PositiveInt(u32)` newtype | Rigor narrows from `n > 0`. |
277
+ | `int<1, 9>` | newtype + range check, or const generics gymnastics | Rigor's range carrier handles arbitrary bounds directly. |
278
+ | `numeric-string` | newtype wrapping validated parse | No type-level analogue. |
279
+ | `non-empty-array[T]` | newtype over `Vec<T>` | Rigor produces it from `unless arr.empty?`. |
280
+
281
+ If you have ever written a newtype purely to encode an invariant
282
+ the compiler could not express on the base type, this is the
283
+ part of Rigor that earns its keep: no wrapper allocation, the
284
+ invariant narrowed from a plain `if`.
285
+
286
+ ## Generics
287
+
288
+ RBS generics are what Rigor reads; they are more conservative
289
+ than Rust's. RBS supports class- and method-level type
290
+ parameters with bounds, but does not infer call-site
291
+ instantiation as eagerly, and has nothing like trait-bound
292
+ dispatch resolution.
293
+
294
+ | Rust | Rigor (via RBS) |
295
+ | --- | --- |
296
+ | `fn id<T>(x: T) -> T` | `def id: [T] (T) -> T` |
297
+ | `Vec<T>` | `Array[T]` |
298
+ | `HashMap<K, V>` | `Hash[K, V]` |
299
+ | `fn f<T: Ord>(x: T)` | `def f: [T < Comparable[T]] (T) -> void` |
300
+ | `impl Trait` return | a structural interface return type |
301
+
302
+ Rust's associated types, higher-ranked trait bounds, and const
303
+ generics have no RBS analogue. Rigor's generics are deliberately
304
+ modest so the analyzer stays fast on real Ruby.
305
+
306
+ ## Severity, suppression, and "strict mode"
307
+
308
+ | Rust | Rigor |
309
+ | --- | --- |
310
+ | `#![deny(warnings)]` / lint levels | `severity_profile: lenient` / `balanced` / `strict` |
311
+ | `#[allow(lint_name)]` | `# rigor:disable <rule>` |
312
+ | crate-level `#![allow(…)]` | `# rigor:disable-file all` |
313
+ | `cargo check` (the gate) | `rigor check lib` (the advisor) |
314
+
315
+ The mental shift: `cargo check` is part of the build, so code does
316
+ not ship until it passes. `rigor check` is not a gate the
317
+ program must clear; it is a diagnostic surface you tune with
318
+ severity profiles and adopt incrementally via baselines. The
319
+ program already runs.
320
+
321
+ ## What Rust has and Rigor does not
322
+
323
+ Be honest about what you give up:
324
+
325
+ - **Ownership and borrowing.** Rust's defining feature has no
326
+ Rigor analogue — Ruby is garbage-collected and aliases freely.
327
+ Rigor does not model lifetimes, moves, or `&mut` exclusivity.
328
+ (This is not a gap Rigor is trying to fill; it is a different
329
+ language's contract.)
330
+ - **Enforced exhaustiveness.** A non-exhaustive `match` is a
331
+ Rust compile error. Rigor reports *unreachable* clauses, not
332
+ *missing* ones — by design (see above).
333
+ - **No-null guarantee.** Rust *eliminates* null; `Option<T>` is
334
+ the only absence. Rigor's `nil` still exists — it narrows it
335
+ away, but it cannot promise a value is never `nil` the way
336
+ Rust's type system can.
337
+ - **`Result` / the `?` operator.** Typed, value-level error
338
+ propagation has no Rigor analogue; Ruby raises.
339
+ - **Trait coherence and associated types.** The `impl`-based,
340
+ orphan-ruled trait machinery is outside RBS's structural model.
341
+ - **Const generics, zero-cost guarantees, `unsafe`.** All
342
+ compile-model concepts with no place in a runtime advisor.
343
+
344
+ ## What Rigor has and Rust does not
345
+
346
+ The other direction:
347
+
348
+ - **Literal / constant types.** `Constant<42>`, `Constant<:ok>`,
349
+ `Constant<"FOO">`. Rust has no literal types; the nearest is a
350
+ unit-variant `enum`, hand-declared. Rigor infers them from
351
+ ordinary values.
352
+ - **Constant folding through method calls.** `"foo".upcase` is
353
+ `Constant<"FOO">`, not `String`. Rigor catalogues which
354
+ built-in methods are pure and folds through them.
355
+ - **Refinements without a newtype.** Invariants on the ordinary
356
+ type, no wrapper struct and no validating constructor.
357
+ - **Structural facets without an `impl`.** A Ruby object that has
358
+ the right methods satisfies an RBS `interface` with no
359
+ declaration of intent — and Rigor infers anonymous shapes and
360
+ capability roles besides.
361
+ - **No-false-positives stance.** Rigor stays silent on
362
+ `Dynamic[top]` receivers rather than complaining. You will
363
+ never see a diagnostic whose honest answer is "well, the
364
+ checker cannot know."
365
+ - **No annotation tax.** `rigor check` on a Ruby project with
366
+ zero `.rbs` files still yields useful diagnostics from
367
+ inference. Adding `.rbs` is incremental — every file you skip
368
+ is `Dynamic[top]` at the boundary, not an error.
369
+
370
+ ## A migration vignette
371
+
372
+ You are porting a Rust module — a sum type with an exhaustive
373
+ `match` and an `Option`-returning lookup — to Ruby. The
374
+ original:
375
+
376
+ ```rust
377
+ enum Shape {
378
+ Circle { radius: f64 },
379
+ Rectangle { w: f64, h: f64 },
380
+ }
381
+
382
+ fn area(s: &Shape) -> f64 {
383
+ match s {
384
+ Shape::Circle { radius } => PI * radius * radius,
385
+ Shape::Rectangle { w, h } => w * h,
386
+ }
387
+ }
388
+
389
+ fn first_circle(shapes: &[Shape]) -> Option<&Shape> {
390
+ shapes.iter().find(|s| matches!(s, Shape::Circle { .. }))
391
+ }
392
+ ```
393
+
394
+ The Rigor approach — `Data.define` for the variants, `case`/`in`
395
+ for the dispatch, `T?` for the optional, no annotations:
396
+
397
+ ```ruby
398
+ # lib/shape.rb
399
+ Circle = Data.define(:radius)
400
+ Rectangle = Data.define(:w, :h)
401
+
402
+ def area(s)
403
+ case s
404
+ in Circle then Math::PI * s.radius * s.radius
405
+ in Rectangle then s.w * s.h
406
+ end
407
+ end
408
+
409
+ def first_circle(shapes)
410
+ shapes.find { |s| s.is_a?(Circle) } # returns Circle? (Circle | nil)
411
+ end
412
+ ```
413
+
414
+ What carries over and what changes:
415
+
416
+ - The `enum` variants become `Data.define` — immutable, value-
417
+ equal, member-shaped, and Rigor folds their member reads.
418
+ - The `match` becomes `case`/`in`; `in Circle` narrows `s`
419
+ exactly as `Shape::Circle { .. }` does.
420
+ - `Option<&Shape>` becomes a plain `Circle?` return —
421
+ `Array#find` yields the element or `nil`, and a `nil?` guard
422
+ at the call site narrows it, the way `if let Some(c)` would.
423
+ - The exhaustive `match` loses its enforced totality. Rigor will
424
+ not demand the third variant you have not written; it will
425
+ only tell you if a clause you *did* write can never run. Add a
426
+ trailing `else raise` if you want Rust's `_ => unreachable!()`.
427
+
428
+ ## What's next
429
+
430
+ You probably do not need to read the rest of the handbook
431
+ sequentially. Useful pointers:
432
+
433
+ - [Chapter 3 — Narrowing](03-narrowing.md) for the flow rules —
434
+ the direct analogue to `match` / `if let` narrowing.
435
+ - [Chapter 6 — Classes](06-classes.md) for `Data.define`, the
436
+ `struct` analogue, and its value-folding.
437
+ - [Chapter 7 — RBS and `RBS::Extended`](07-rbs-and-extended.md)
438
+ for the directive grammar — `predicate-if-true` is the
439
+ user-defined narrowing analogue.
440
+
441
+ If you want to compare against another tool, the sibling
442
+ appendix pages cover [TypeScript](appendix-typescript.md),
443
+ [PHPStan](appendix-phpstan.md), [mypy](appendix-mypy.md),
444
+ [Steep](appendix-steep.md), [TypeProf](appendix-typeprof.md),
445
+ [Java / C#](appendix-java-csharp.md), [Go](appendix-go.md), and
446
+ [Elixir](appendix-elixir.md).