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,370 @@
1
+ # Appendix — Coming from Elixir
2
+
3
+ If your mental model of "types" was set by Elixir — Dialyzer's
4
+ success typing, `@spec` / `@type`, and the set-theoretic gradual
5
+ types now rolling into the language — this appendix maps Rigor's
6
+ vocabulary onto it. Of every page in this series, this is the
7
+ one whose *philosophy* matches Rigor most closely: both are
8
+ dynamic languages at heart, both add types gradually, both
9
+ refuse to cry wolf. Elixir's type checker only flags what it can
10
+ prove will fail; so does Rigor. That instinct (never frighten
11
+ working code) is shared DNA, not coincidence.
12
+
13
+ There is even a direct lineage: Rigor's clause-reachability rule
14
+ ([ADR-47](../adr/47-narrowing-driven-clause-reachability.md)) was
15
+ modelled on Elixir's own work on detecting impossible `case`
16
+ clauses. You are coming from one of Rigor's design influences.
17
+
18
+ This is a translation table plus a discussion of where the two
19
+ diverge (Elixir is functional, immutable, and process-oriented;
20
+ Rigor analyses an object-oriented, mutable language) and where
21
+ they line up better than you would guess.
22
+
23
+ ## The five-second pitch
24
+
25
+ | Question | Elixir | Rigor |
26
+ | --- | --- | --- |
27
+ | Origin | Dynamic; types added gradually | Dynamic; types added gradually |
28
+ | Soundness stance | Success typing — flag only provable failures | No-false-positives — silent unless it can prove the error |
29
+ | Where do annotations live? | `@spec` / `@type` in source | `.rbs` files alongside `.rb` |
30
+ | The "I don't know" type | `dynamic()` / `term()` | `Dynamic[top]` / `Top` |
31
+ | Narrowing engine | Pattern matching + guards | Pattern matching + guards + predicate methods |
32
+ | Type algebra | Set-theoretic (union / intersection / negation) | Union + type operators (`~T`, `T - U`) |
33
+
34
+ Elixir and Rigor agree on the thing that matters most: a type
35
+ checker for a dynamic language should be an advisor that earns
36
+ trust by never false-alarming, not a gate that rejects programs.
37
+ Dialyzer's "I will only tell you about a path that *cannot*
38
+ succeed" is the same contract as Rigor's "I stay silent unless I
39
+ can prove the error." If that stance is why you trust Dialyzer,
40
+ you will recognise Rigor immediately.
41
+
42
+ ## Type vocabulary mapping
43
+
44
+ | Elixir | Rigor | Notes |
45
+ | --- | --- | --- |
46
+ | `integer()` | `Integer` | Both arbitrary-precision. |
47
+ | `float()` | `Float` | `Numeric` is the common supertype. |
48
+ | `boolean()` (`true \| false`) | `bool` (`Constant<true> \| Constant<false>`) | Structurally a union of two constants in both systems. |
49
+ | `:foo` (atom) | `Constant<:foo>` (Symbol) | Atoms ↔ symbols — a direct match. See [Atoms ↔ symbols](#atoms--symbols). |
50
+ | `nil` | `nil` (`Constant<nil>`) | Elixir's `nil` is the atom `nil`; Ruby's is its own singleton. Both falsy. |
51
+ | `binary()` / `String.t()` | `String` | |
52
+ | `term()` / `any()` | `Top` / `Dynamic[top]` | `any()` for "anything"; `dynamic()` for the gradual escape hatch. |
53
+ | `none()` | `Bot` | Empty type — no inhabitants. |
54
+ | `[t]` (list) | `Array[T]` | |
55
+ | `{a, b}` (tuple) | `Tuple[A, B]` | Same per-position model. |
56
+ | `%{optional(k) => v}` (map) | `Hash[K, V]` | |
57
+ | `%{a: t}` (map with known keys) | `HashShape{a: T}` | Closed shape with known keys. |
58
+ | `%User{}` (struct) | `User = Data.define(...)` | A named, member-shaped value. |
59
+ | `[key: t]` (keyword list) | `Hash[Symbol, T]` or `Array[Tuple]` | Ruby's keyword-ish data is a `Hash`. |
60
+ | `t \| u` (set-theoretic union) | `T \| U` | Same display; same idea. |
61
+ | `dynamic()` | `Dynamic[top]` | The "be silent here" gradual carrier. |
62
+ | `(integer() -> binary())` | `^(Integer) -> String` (RBS proc syntax) | |
63
+
64
+ ## Pattern matching & guards ↔ narrowing
65
+
66
+ This is the section where an Elixir programmer feels at home,
67
+ because Ruby borrowed pattern matching in a shape close to
68
+ Elixir's — including the pin operator `^` — and Rigor's
69
+ narrowing engine is built around it.
70
+
71
+ | Elixir | Rigor |
72
+ | --- | --- |
73
+ | `case x do {:ok, v} -> … end` | `case x; in [:ok, v] then …` |
74
+ | function-head match `def f(%Circle{} = c)` | `case x; in Circle => c` (Ruby has no multi-clause heads) |
75
+ | guard `when is_integer(x)` | `if x.is_a?(Integer)`, or `in Integer` |
76
+ | guard `when x > 0` | `n > 0` narrows to `positive-int` — see [Refinements](#refinements--guards) |
77
+ | `^pinned` in a pattern | `^pinned` pin in `case`/`in` (same operator, same meaning) |
78
+ | `with {:ok, a} <- step1(), … do` | chained `case`/`in`, or guard clauses with early return |
79
+ | multi-clause function + guards | `case`/`in` with `if` guards in one method |
80
+
81
+ The one structural difference: Elixir dispatches by writing
82
+ *multiple function heads* with patterns and guards, and the
83
+ runtime picks the first that matches. Ruby has no multi-clause
84
+ `def`; you fold the clauses into a single method body with
85
+ `case`/`in`. The narrowing Rigor performs along each `in` clause
86
+ is the analogue of the type Elixir's compiler now infers for
87
+ each function head.
88
+
89
+ And the lineage runs the other way too: Rigor's
90
+ `flow.unreachable-clause` rule — which flags a `case`/`in` clause
91
+ that can never match because earlier clauses already covered its
92
+ type — was modelled directly on Elixir's clause-reachability
93
+ work ([ADR-47](../adr/47-narrowing-driven-clause-reachability.md)).
94
+ It is the feature you may know from Elixir warning you about an
95
+ impossible `case` clause, brought to Ruby.
96
+
97
+ ## Set-theoretic types & gradual `dynamic()`
98
+
99
+ Elixir's type system is *set-theoretic*: types are sets of
100
+ values, and you compose them with union, intersection, and
101
+ negation, with a `dynamic()` type marking the gradual boundary.
102
+ Rigor is built on the same union-of-values intuition (the
103
+ [value lattice](../type-specification/value-lattice.md)) and has
104
+ the operator vocabulary to match the common cases:
105
+
106
+ | Elixir | Rigor |
107
+ | --- | --- |
108
+ | `t \| u` (union) | `T \| U` |
109
+ | intersection `t and u` | `Intersection[T, U]` |
110
+ | negation `not t` | `~T` (complement) |
111
+ | difference (set minus) | `T - U` (type difference) |
112
+ | `dynamic()` | `Dynamic[top]` |
113
+
114
+ The gradual story is nearly identical in spirit: a value typed
115
+ `dynamic()` in Elixir, like a `Dynamic[top]` receiver in Rigor,
116
+ is the point where the checker steps back and stays silent
117
+ rather than guess. Both systems lean on this to stay practical
118
+ on real code, and both treat reaching for it as ordinary.
119
+
120
+ Where Elixir goes further: it lets you *author* intersections
121
+ and negations as ordinary `@type` expressions, and its inference
122
+ reasons about them throughout. Rigor uses `~T` and `T - U`
123
+ internally and in some directives, but does not yet expose a
124
+ full set-theoretic authoring surface in `.rbs`. The everyday
125
+ overlap — unions and the gradual `dynamic()` boundary — is where
126
+ the two feel the same.
127
+
128
+ ## Tagged tuples ↔ `case`/`in`
129
+
130
+ Elixir's signature idiom — `{:ok, value}` / `{:error, reason}`
131
+ returned and matched — translates almost verbatim, because Ruby
132
+ expresses the same shape with an array and `case`/`in`:
133
+
134
+ ```elixir
135
+ case Integer.parse(s) do
136
+ {n, ""} -> {:ok, n}
137
+ _ -> {:error, :invalid}
138
+ end
139
+ ```
140
+
141
+ ```ruby
142
+ def parse(s)
143
+ n = Integer(s, exception: false)
144
+ n ? [:ok, n] : [:error, :invalid]
145
+ end
146
+
147
+ case parse(s)
148
+ in [:ok, n] then n
149
+ in [:error, why] then handle(why)
150
+ end
151
+ ```
152
+
153
+ Rigor types the `Tuple` per position and the union of the two
154
+ tagged shapes precisely, and `in [:ok, n]` narrows along that
155
+ clause exactly as the Elixir pattern does. (Ruby idiom does also
156
+ reach for raising on the error path; the tagged-tuple style
157
+ ports cleanly when you want to keep it.)
158
+
159
+ ## Atoms ↔ symbols
160
+
161
+ Elixir atoms and Ruby symbols are the same idea — interned,
162
+ identity-compared name constants — and Rigor folds a symbol to
163
+ `Constant<:foo>`, the precise singleton type, not merely
164
+ `Symbol`:
165
+
166
+ ```ruby
167
+ status = :ok
168
+ assert_type(":ok", status)
169
+
170
+ # a discriminated union over atoms/symbols:
171
+ def describe(s) # s : Constant<:ok> | Constant<:error>
172
+ case s
173
+ in :ok then "fine"
174
+ in :error then "broken"
175
+ end
176
+ end
177
+ ```
178
+
179
+ This is the same modelling Elixir's set-theoretic types give an
180
+ atom — a singleton type for the specific atom — and it powers
181
+ the same discriminated-union dispatch you write with atom keys
182
+ in Elixir.
183
+
184
+ ## Protocols & behaviours
185
+
186
+ Two Elixir concepts map onto Rigor's structural typing, with one
187
+ twist of difference each.
188
+
189
+ - **Behaviours** (`@callback` in a behaviour module, `@behaviour`
190
+ on the implementer) describe a set of functions a module must
191
+ provide. Rigor's nearest analogue is an RBS `interface` — a
192
+ named set of methods — but with a key difference: an RBS
193
+ interface is satisfied **structurally** (have the methods,
194
+ satisfy it), whereas an Elixir `@behaviour` is declared.
195
+ - **Protocols** (`defprotocol` / `defimpl`) dispatch on the data
196
+ type, and a protocol is satisfied by an *explicit* `defimpl`
197
+ for that type — nominal, like Rust traits. Rigor has no
198
+ per-type `defimpl`; an object satisfies a structural interface
199
+ by responding to the methods, full stop.
200
+
201
+ So both of Elixir's "abstract over implementations" mechanisms
202
+ become Rigor's one structural-interface mechanism, which is
203
+ closer to Go's implicit interfaces than to either Elixir
204
+ construct. The
205
+ [structural-typing appendix](appendix-protocols-and-structural-typing.md)
206
+ is the canonical explainer.
207
+
208
+ ## Refinements ↔ guards
209
+
210
+ An Elixir guard like `when x > 0` constrains a value at the
211
+ clause boundary, and the new type system reasons about some such
212
+ guards. Rigor turns the same guard into a named **refinement
213
+ carrier** that rides on the ordinary type:
214
+
215
+ ```ruby
216
+ def reciprocal(n)
217
+ return nil unless n > 0
218
+ # n is positive-int here when typed as Integer; untyped params stay Dynamic[top]
219
+ 1.0 / n
220
+ end
221
+ ```
222
+
223
+ | Rigor refinement | Elixir guard / idiom | Comment |
224
+ | --- | --- | --- |
225
+ | `positive-int` | `when n > 0` | Rigor names and carries the result. |
226
+ | `non-empty-string` | `when s != ""` / `byte_size(s) > 0` | Rigor produces it from `unless s.empty?`. |
227
+ | `int<1, 9>` | `when x in 1..9` | Rigor's range carrier handles arbitrary bounds. |
228
+ | `non-empty-array[T]` | `when xs != []` | Rigor produces it from `unless arr.empty?`. |
229
+ | `numeric-string` | `Integer.parse/1` + match | No direct Elixir analogue. |
230
+
231
+ The conceptual match is strong: both systems use a *runtime
232
+ guard* as the place where a more precise type becomes known.
233
+ Rigor's addition is naming that precise type so it flows onward.
234
+
235
+ ## Severity, suppression, and "strict mode"
236
+
237
+ | Elixir | Rigor |
238
+ | --- | --- |
239
+ | Dialyzer warning selection | `severity_profile: lenient` / `balanced` / `strict` |
240
+ | `@dialyzer {:nowarn_function, …}` | `# rigor:disable <rule>` |
241
+ | module-level Dialyzer skip | `# rigor:disable-file all` |
242
+ | `mix dialyzer` (advisory) | `rigor check lib` (advisory) |
243
+
244
+ Both are advisory by nature — neither blocks the program from
245
+ running. The difference is when they run: Dialyzer over compiled
246
+ BEAM bytecode, Rigor over Ruby source. The adoption shape —
247
+ tune severity, suppress narrowly, baseline the rest — is the
248
+ same.
249
+
250
+ ## What Elixir has and Rigor does not
251
+
252
+ Be honest about what differs:
253
+
254
+ - **Authored intersections and negations.** Elixir lets you
255
+ write intersection and negation types as ordinary `@type`
256
+ expressions and reasons about them throughout. Rigor uses
257
+ `~T` / `T - U` internally but does not expose a full
258
+ set-theoretic authoring surface.
259
+ - **Multi-clause function heads.** Pattern-matching dispatch
260
+ across separate function heads, with the runtime selecting the
261
+ match, has no Ruby `def`-level analogue; you fold it into
262
+ `case`/`in`.
263
+ - **Pattern matching as pervasive assignment.** Elixir's `=` is
264
+ a match operator everywhere; Ruby's pattern matching is scoped
265
+ to `case`/`in` (and `=>` / `in` one-liners).
266
+ - **Process and concurrency types.** The BEAM's process model
267
+ has no Rigor analogue.
268
+ - **Immutability by default.** Elixir data is immutable; Ruby is
269
+ mutable, and Rigor has to reason about mutation effects that
270
+ simply do not arise in Elixir.
271
+
272
+ ## What Rigor has and Elixir does not
273
+
274
+ The other direction:
275
+
276
+ - **Shipping today, for Ruby.** Elixir's set-theoretic types are
277
+ rolling into the language incrementally; Rigor's analyzer is
278
+ here now for Ruby, with the no-false-positives stance already
279
+ in force.
280
+ - **Constant folding through method calls.** `"foo".upcase` is
281
+ `Constant<"FOO">`, not `String`. Rigor catalogues which
282
+ built-in methods are pure and folds through them — Dialyzer
283
+ and Elixir's types do not fold call results to singleton
284
+ types this way.
285
+ - **Named refinement carriers.** `non-empty-string`,
286
+ `positive-int`, `int<1, 9>`, `numeric-string` — first-class,
287
+ named, and flowed onward from a guard.
288
+ - **Inferred object shapes and capability roles.** Beyond
289
+ behaviours and protocols, Rigor infers anonymous structural
290
+ shapes from how a value is used.
291
+ - **No annotation tax.** `rigor check` on a Ruby project with
292
+ zero `.rbs` files yields useful diagnostics from inference
293
+ alone; adding `.rbs` is incremental.
294
+
295
+ ## A migration vignette
296
+
297
+ You are porting an Elixir module — a couple of pattern-matched
298
+ function heads over a struct, plus a tagged-tuple parser — to
299
+ Ruby. The original:
300
+
301
+ ```elixir
302
+ defmodule Shape do
303
+ def area(%{kind: :circle, radius: r}), do: :math.pi() * r * r
304
+ def area(%{kind: :rectangle, w: w, h: h}), do: w * h
305
+ end
306
+
307
+ def parse_radius(s) do
308
+ case Float.parse(s) do
309
+ {r, ""} -> {:ok, r}
310
+ _ -> {:error, :invalid}
311
+ end
312
+ end
313
+ ```
314
+
315
+ The Rigor approach — `case`/`in` with hash patterns folding the
316
+ clauses into one method, and a tagged-tuple parser that ports
317
+ verbatim:
318
+
319
+ ```ruby
320
+ # lib/shape.rb
321
+ def area(shape)
322
+ case shape
323
+ in {kind: :circle, radius:} then Math::PI * radius * radius
324
+ in {kind: :rectangle, w:, h:} then w * h
325
+ end
326
+ end
327
+
328
+ def parse_radius(s)
329
+ r = Float(s, exception: false)
330
+ r ? [:ok, r] : [:error, :invalid]
331
+ end
332
+ ```
333
+
334
+ What carries over and what changes:
335
+
336
+ - The multiple function heads fold into one `case`/`in`. Ruby's
337
+ hash patterns with key binding (`in {radius:}`) mirror
338
+ Elixir's map patterns (`%{radius: r}`) almost exactly.
339
+ - The atom keys (`:circle`, `:ok`) become symbols, folded to
340
+ `Constant<:circle>` / `Constant<:ok>` — the same singleton
341
+ typing Elixir's atoms get.
342
+ - The `{:ok, r}` / `{:error, _}` tagged tuples become `[:ok, r]`
343
+ / `[:error, _]` arrays, typed as a precise `Tuple` union and
344
+ matched with `case`/`in`.
345
+ - If you add an `in` clause that can never match — say a second
346
+ `:circle` arm — Rigor's `flow.unreachable-clause` flags it,
347
+ the same warning Elixir would give you about an impossible
348
+ clause.
349
+
350
+ ## What's next
351
+
352
+ You probably do not need to read the rest of the handbook
353
+ sequentially. Useful pointers:
354
+
355
+ - [Chapter 3 — Narrowing](03-narrowing.md) for the flow rules —
356
+ the analogue to guards and pattern-match narrowing.
357
+ - [Chapter 7 — RBS and `RBS::Extended`](07-rbs-and-extended.md)
358
+ for the directive grammar — `predicate-if-true` is the
359
+ user-defined guard analogue, the closest thing to teaching
360
+ Rigor a custom `is_*` guard.
361
+ - [Protocols and structural typing](appendix-protocols-and-structural-typing.md)
362
+ for how behaviours and protocols both map onto Rigor's single
363
+ structural-interface mechanism.
364
+
365
+ If you want to compare against another tool, the sibling
366
+ appendix pages cover [TypeScript](appendix-typescript.md),
367
+ [PHPStan](appendix-phpstan.md), [mypy](appendix-mypy.md),
368
+ [Steep](appendix-steep.md), [TypeProf](appendix-typeprof.md),
369
+ [Java / C#](appendix-java-csharp.md), [Rust](appendix-rust.md),
370
+ and [Go](appendix-go.md).