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,470 @@
1
+ # Appendix — Coming from Java or C#
2
+
3
+ If your mental model of "static types" was set by Java or C#,
4
+ this appendix maps Rigor's vocabulary onto the concepts you
5
+ already know. Both languages bring nearly the same reflexes to
6
+ Ruby — nominal-first, annotate-everything, generics, records,
7
+ sealed hierarchies, pattern-matching `switch` — so one page
8
+ serves both, with the few places Java and C# diverge called out
9
+ inline.
10
+
11
+ The examples assume a modern LTS baseline: **Java 21** (records,
12
+ sealed types, pattern-matching `switch`, record patterns) and
13
+ **modern C#** on .NET 8 (nullable reference types, records,
14
+ `switch` expressions, declaration-site variance). Where a
15
+ feature is newer than that, the page says so.
16
+
17
+ This is a translation table plus a discussion of the places
18
+ where Rigor makes a different choice. Those are where your
19
+ Java / C# reflexes will mislead you, and for these two languages
20
+ the single biggest one is the direction of the default: you
21
+ annotate first and the compiler infers locally; Rigor infers
22
+ first and asks for annotations only at the edges.
23
+
24
+ ## The five-second pitch
25
+
26
+ | Question | Java / C# | Rigor |
27
+ | --- | --- | --- |
28
+ | Where do annotations live? | In source, on every declaration | In `.rbs` files alongside `.rb` |
29
+ | Who writes them? | The author (always) | The author OR inference |
30
+ | Default for an unannotated value | There is no unannotated value (except `var` locals) | Inferred precisely or `Dynamic[top]` |
31
+ | Identity of types | Nominal (a class `implements` / `: IFace`) | Nominal + structural facets |
32
+ | Inference scope | Local only (`var` / `var`) | Whole-method body, across `def` boundaries |
33
+ | When do diagnostics fire? | Whenever a type does not check | Only when Rigor can **prove** the unsoundness |
34
+
35
+ Java and C# are *ahead-of-time, soundness-first* type systems:
36
+ nothing runs until every declaration type-checks. Rigor is the
37
+ opposite stance — it analyses Ruby that already runs and stays
38
+ silent on anything it cannot prove wrong. The reflex to retrain
39
+ is "the type checker is a gate the program must pass": in Rigor
40
+ the program already passed (it runs), and the analyzer is an
41
+ advisor that only speaks when it is sure.
42
+
43
+ ## Type vocabulary mapping
44
+
45
+ | Java | C# | Rigor | Notes |
46
+ | --- | --- | --- | --- |
47
+ | `String` | `string` | `String` | Display drops `Nominal[]`. |
48
+ | `int` / `long` | `int` / `long` | `Integer` | Ruby integers are arbitrary-precision; no `int`/`long` split. |
49
+ | `double` / `float` | `double` / `float` | `Float` | `Numeric` is the common supertype. |
50
+ | `boolean` | `bool` | `bool` (`Constant<true> \| Constant<false>`) | `bool` is structurally a union of two constants. |
51
+ | `null` | `null` | `nil` (`Constant<nil>`) | Ruby has one no-value; C#'s `null` and `default` collapse to `nil`. |
52
+ | `Object` | `object` | `Object` / `Top` | `Top` is the universal supertype when you mean "anything". |
53
+ | `void` | `void` | `void` | Same idea — caller must not consume the value. |
54
+ | (none) | (none) | `Bot` | Empty type — unreachable branches, `raise`-only bodies. Java's `Void` / C#'s `Never` (proposed) are the nearest spellings. |
55
+ | `Object` (untyped boundary) | `dynamic` | `Dynamic[top]` | C#'s `dynamic` is the closest analogue — the "be silent here" carrier. |
56
+ | `T[]` / `List<T>` | `T[]` / `List<T>` / `IEnumerable<T>` | `Array[T]` | |
57
+ | `Map<K, V>` | `Dictionary<K, V>` / `IDictionary<K, V>` | `Hash[K, V]` | |
58
+ | `Set<T>` | `HashSet<T>` / `ISet<T>` | `Set[T]` | |
59
+ | `record Point(int x, int y)` | `record Point(int X, int Y)` | `Point = Data.define(:x, :y)` | See [Records ↔ Data.define](#records--datadefine). |
60
+ | `Optional<T>` | `T?` (nullable reference type) | `T?` (i.e. `T \| nil`) | Java models it as a *container*; C# as a *type modifier*. See [Nullability](#nullability). |
61
+ | `enum Color { RED, GREEN }` | `enum Color { Red, Green }` | `Constant<:red> \| Constant<:green>` (Symbol union) | Ruby has no native enum; the [`rigor-mangrove`](../../plugins/) plugin types richer enum DSLs. |
62
+ | `sealed interface Shape permits …` | `abstract` base + sealed hierarchy | union of the subtypes | See [Sealed types & exhaustiveness](#sealed-types-and-exhaustiveness). |
63
+ | `<T>` (generic) | `<T>` (generic) | RBS `[T]` type parameter | |
64
+ | `? extends T` (use-site) | `out T` (declaration-site) | covariant type parameter | See [Generics & variance](#generics-and-variance). |
65
+ | `? super T` (use-site) | `in T` (declaration-site) | contravariant type parameter | |
66
+ | `var x = …` | `var x = …` | (no spelling — every local is inferred) | Rigor infers *all* locals, not just `var`-declared ones. |
67
+ | (no literal types) | (no literal types) | `Constant<42>` / `Constant<"hi">` | Neither language has literal types; this is a Rigor novelty — see below. |
68
+ | `Stream<T>` / Streams API | `IEnumerable<T>` / LINQ | `Enumerable` (returns typed, no query layer) | Element types flow; there is no type-level query algebra. |
69
+
70
+ ## Nominal-first vs inference-first
71
+
72
+ In Java and C# a value's type is whatever its declaration says.
73
+ `var` exists, but it is *local* inference — the compiler fills
74
+ in a type you could have written, and it never crosses a method
75
+ boundary. Field types, parameter types, and return types are
76
+ always authored.
77
+
78
+ Rigor turns this around. It infers across the whole method body
79
+ and *through* in-source `def` boundaries — a method with no
80
+ `.rbs` still binds its callers to the return type inferred from
81
+ its body:
82
+
83
+ ```ruby
84
+ def classify(n)
85
+ return :zero if n.zero?
86
+ return :positive if n.positive?
87
+ :negative
88
+ end
89
+
90
+ result = classify(7)
91
+ ```
92
+
93
+ The C# equivalent demands the parameter type and the return
94
+ type as authored annotations, and even with both, `switch`
95
+ returns `string`, not the three-way literal union — C# has no
96
+ literal types to carry it:
97
+
98
+ ```csharp
99
+ string Classify(int n) =>
100
+ n == 0 ? "zero"
101
+ : n > 0 ? "positive"
102
+ : "negative";
103
+ // result : string
104
+ ```
105
+
106
+ When you DO need to write a sig — at a public boundary, when a
107
+ body is too dynamic, when you want to *enforce* a parameter
108
+ shape rather than observe it — it goes into `sig/<file>.rbs`,
109
+ never into the `.rb` source. That separation is deliberate (see
110
+ [ADR-1](../adr/1-types.md) and [ADR-5](../adr/5-robustness-principle.md));
111
+ it is the analogue of keeping declarations out of your method
112
+ bodies, except Rigor keeps them out of the *file*.
113
+
114
+ ## Narrowing — `instanceof`, `is`, pattern `switch`
115
+
116
+ Both languages have flow-sensitive narrowing, and modern Java /
117
+ C# added pattern binding (`instanceof String s`, `is string s`).
118
+ Rigor has direct analogues; the vocabulary differs, the
119
+ behaviour matches.
120
+
121
+ | Java | C# | Rigor |
122
+ | --- | --- | --- |
123
+ | `if (x != null)` | `if (x is not null)` | `if x` (strips `false` / `nil`) or `unless x.nil?` |
124
+ | `x instanceof String` | `x is string` | `x.is_a?(String)` |
125
+ | `x instanceof String s` (binding) | `x is string s` (binding) | `case x; in String => s` |
126
+ | `switch (x) { case Foo f -> … }` | `switch (x) { case Foo f => … }` | `case x; in Foo => f` |
127
+ | `(Foo) x` (cast) | `(Foo)x` (cast) | (no in-source cast) — `is_a?` guard, or `T.cast` via [`rigor-sorbet`](../../plugins/rigor-sorbet/) |
128
+ | `Objects.requireNonNull(x)` | `x!` (null-forgiving) | (no in-source assertion) — `unless x.nil?`, or `T.must` via `rigor-sorbet` |
129
+ | user method returning `boolean` | user method returning `bool` | `%a{rigor:v1:predicate-if-true x is Foo}` directive on the predicate |
130
+
131
+ The reflex to drop is the **cast**. In Java/C# you reach for
132
+ `(Foo) x` or C#'s null-forgiving `x!` whenever the compiler
133
+ disagrees with you. Rigor has no in-source cast. The
134
+ equivalents are:
135
+
136
+ 1. **Add a guard.** `unless x.nil?; x.upcase; end` is the
137
+ idiomatic move — and unlike `x!`, it is checked, not asserted.
138
+ 2. **Tighten an `.rbs`.** Often the underlying issue is a
139
+ library sig that is too loose.
140
+ 3. **Use the `rigor-sorbet` plugin.** Adopt `T.let` / `T.cast`
141
+ / `T.must` if you want in-source assertions; see
142
+ [Chapter 10](10-sorbet.md).
143
+
144
+ ## Records ↔ `Data.define`
145
+
146
+ A Java `record` or a C# positional `record` maps almost exactly
147
+ onto Ruby's `Data.define` — an immutable, value-equal, member-
148
+ shaped aggregate. Rigor models it natively
149
+ ([ADR-48](../adr/48-data-struct-value-folding.md)).
150
+
151
+ ```java
152
+ // Java 21
153
+ record Point(int x, int y) {}
154
+ var p = new Point(1, 2);
155
+ int a = p.x(); // a : int
156
+ Point q = p.withX(...); // (no built-in wither; you write it)
157
+ ```
158
+
159
+ ```csharp
160
+ // modern C#
161
+ record Point(int X, int Y);
162
+ var p = new Point(1, 2);
163
+ int a = p.X; // a : int
164
+ var q = p with { X = 9 }; // non-destructive mutation
165
+ ```
166
+
167
+ ```ruby
168
+ Point = Data.define(:x, :y)
169
+ p = Point.new(1, 2)
170
+ assert_type("1", p.x) # member value is folded, not just Integer
171
+ q = p.with(x: 9) # Data#with ↔ C#'s `with` expression
172
+ ```
173
+
174
+ Two things go further than either language:
175
+
176
+ - **Member values fold.** `Point.new(1, 2).x` is
177
+ `1`, not merely `Integer`. Java and C# erase the
178
+ literal at construction; Rigor keeps it (subject to the usual
179
+ folding budget).
180
+ - **`with` is first-class.** Ruby's `Data#with` is the direct
181
+ analogue of C#'s `with` expression, and Rigor types the
182
+ result with the overridden member folded in. (Java has no
183
+ built-in wither.)
184
+
185
+ `Struct` — Ruby's *mutable* sibling — is deliberately not folded
186
+ the same way yet; its mutability breaks the value-folding
187
+ soundness story. See [Chapter 6](06-classes.md).
188
+
189
+ ## Nullability
190
+
191
+ This is the one axis where Java and C# diverge enough to matter,
192
+ and where C# lands closer to Rigor than Java does.
193
+
194
+ **C#** (nullable reference types, C# 8+): `string?` is a *type
195
+ modifier*. The compiler tracks nullability flow-sensitively and
196
+ warns on a possible-null dereference. This is almost exactly
197
+ Rigor's model — `T?` is `T | nil`, and the narrowing is the
198
+ same:
199
+
200
+ ```csharp
201
+ int Length(string? s) {
202
+ if (s is null) return 0;
203
+ return s.Length; // s : string — null stripped by the flow
204
+ }
205
+ ```
206
+
207
+ ```ruby
208
+ def length(s) # s : String? (RBS-declared)
209
+ return 0 if s.nil?
210
+ s.length # s : String — nil stripped by .nil?
211
+ end
212
+ ```
213
+
214
+ **Java**: there is no nullable *type*. You either reach for
215
+ `Optional<T>` (a container you must `.map` / `.orElse` through)
216
+ or an annotation like `@Nullable` that the compiler does not
217
+ enforce. Rigor's `T?` is closer to C#'s `string?` than to Java's
218
+ `Optional<T>` — it is a union the flow narrows, not a wrapper you
219
+ unwrap. If you are porting Java `Optional<T>`-returning code, the
220
+ idiomatic Ruby is a plain `T?` return plus a `nil?` guard at the
221
+ call site, not a wrapper object.
222
+
223
+ One difference from C#: Rigor's nullability is **always on** and
224
+ **never forces** you. C#'s NRT warnings can be silenced with
225
+ `!`; Rigor will not fire `possible-nil` unless it can
226
+ prove the receiver is `nil` on some path — there is no nullable-
227
+ context to enable and no forgiving operator to reach for.
228
+
229
+ ## Generics and variance
230
+
231
+ RBS generics are what Rigor reads, and they are more
232
+ conservative than either language's. RBS supports class-level
233
+ and method-level type parameters with bounds, but does not
234
+ infer call-site instantiation as eagerly as C#'s or Java's
235
+ target-typing.
236
+
237
+ | Java | C# | Rigor (via RBS) |
238
+ | --- | --- | --- |
239
+ | `<T> T id(T x)` | `T Id<T>(T x)` | `def id: [T] (T) -> T` |
240
+ | `List<T>` | `List<T>` | `Array[T]` |
241
+ | `Map<K, V>` | `Dictionary<K, V>` | `Hash[K, V]` |
242
+ | `List<? extends Animal>` (use-site) | `IEnumerable<out Animal>` (declaration-site) | covariant `[out T]` parameter |
243
+ | `Consumer<? super Cat>` (use-site) | `IComparer<in Cat>` (declaration-site) | contravariant `[in T]` parameter |
244
+ | bounded `<T extends Comparable<T>>` | `where T : IComparable<T>` | `[T < Comparable[T]]` bound |
245
+
246
+ The variance story is the notable gap. C# pins variance at the
247
+ *declaration* (`in` / `out` on the interface), Java pins it at
248
+ the *use* (wildcards on each reference). RBS uses declaration-
249
+ site variance markers like C#, but the surface is narrower and
250
+ Rigor leans on its structural facets and refinements for cases
251
+ where you would reach for a wildcard in Java.
252
+
253
+ ## Sealed types and exhaustiveness
254
+
255
+ Java's `sealed interface … permits` and C#'s sealed hierarchies
256
+ let the compiler prove a `switch` is exhaustive, and *error* if
257
+ it is not. Rigor approaches the same shape from the other side.
258
+
259
+ A closed set of subtypes is a union in Rigor, and the flow
260
+ engine tracks which `case`/`when` and `case`/`in` clauses can
261
+ still match. [ADR-47](../adr/47-narrowing-driven-clause-reachability.md)'s
262
+ `flow.unreachable-clause` rule fires when a clause is provably
263
+ dead — its subject has already been narrowed to `Bot` by the
264
+ prior clauses (per-clause disjointness) or by prior exhaustion:
265
+
266
+ ```ruby
267
+ case shape
268
+ in Circle then shape.radius
269
+ in Rectangle then shape.width * shape.height
270
+ in Circle then "…" # flow.unreachable-clause — Circle already covered
271
+ end
272
+ ```
273
+
274
+ The difference in *direction*: Java and C# **require**
275
+ exhaustiveness and reject a `switch` that misses a case. Rigor
276
+ does the dual — it reports clauses that can never run, but it
277
+ does **not** force you to handle every variant. A non-exhaustive
278
+ `case` is not an error in Rigor; an *unreachable* clause is a
279
+ diagnostic. This follows Rigor's no-false-positives stance: a
280
+ `case` that omits a branch may be intentional (the omitted
281
+ variant cannot reach this point), and Rigor will not frighten
282
+ working code over it.
283
+
284
+ ## Refinement carriers — the part neither language has
285
+
286
+ Neither Java nor C# can say "string of length ≥ 1" or "integer
287
+ in 1..9" at the type level. You reach for a constructor that
288
+ throws, a value object, or a runtime check. Rigor has first-
289
+ class refinement carriers, produced automatically by narrowing.
290
+
291
+ | Rigor refinement | Java / C# closest | Comment |
292
+ | --- | --- | --- |
293
+ | `non-empty-string` | a `NonEmptyString` value class wrapping validation | Rigor produces it from `unless s.empty?`, no wrapper type. |
294
+ | `positive-int` | a `PositiveInt` value object, or a runtime guard | Rigor narrows from `n > 0`. |
295
+ | `int<1, 9>` | an `enum` of nine constants, or a range check | Rigor's range carrier handles arbitrary bounds without enumerating them. |
296
+ | `numeric-string` | `string` + `int.TryParse` discipline | No type-level analogue in either language. |
297
+ | `non-empty-array[T]` | a non-empty-collection value class | Rigor produces it from `unless arr.empty?`. |
298
+
299
+ If you have ever written a `PositiveInt` value class purely to
300
+ encode an invariant the type system could not, this is the part
301
+ of Rigor that earns its keep — the invariant rides on the
302
+ ordinary `Integer`, no wrapper allocation, narrowed from a plain
303
+ `if`.
304
+
305
+ ## Severity, suppression, and "strict mode"
306
+
307
+ | Java / C# | Rigor |
308
+ | --- | --- |
309
+ | `-Xlint` / `<TreatWarningsAsErrors>` / analyzer severity | `severity_profile: lenient` / `balanced` / `strict` |
310
+ | C# `<Nullable>enable</Nullable>` | Always-on nil-narrowing in Rigor (no context to enable) |
311
+ | `@SuppressWarnings("…")` / `#pragma warning disable` | `# rigor:disable <rule>` |
312
+ | file-level `#pragma warning disable` at top | `# rigor:disable-file all` |
313
+ | `javac` / `dotnet build` (the gate) | `rigor check lib` (the advisor) |
314
+
315
+ The mental shift: in Java/C# the type checker is part of the
316
+ build — code does not ship until it passes. `rigor check` is not
317
+ a build step the program must pass; it is a diagnostic gate you
318
+ tune with severity profiles and adopt incrementally via
319
+ baselines. The program already runs.
320
+
321
+ ## Where Java and C# differ
322
+
323
+ For most of this page Java and C# move together. The places they
324
+ do not, and which way each leans relative to Rigor:
325
+
326
+ - **Nullability** (covered above): C#'s `string?` is close to
327
+ Rigor's `T?`; Java's `Optional<T>` is a container, further
328
+ away.
329
+ - **Variance**: C# is declaration-site (`in` / `out`), like RBS;
330
+ Java is use-site (wildcards). RBS readers from C# will find
331
+ the variance markers familiar.
332
+ - **`dynamic`**: C# has a genuine `dynamic` type — the direct
333
+ analogue of `Dynamic[top]`. Java's nearest equivalent is an
334
+ `Object` reference plus reflection, with no "silence the
335
+ checker" semantics.
336
+ - **Value types**: C#'s `struct` / `record struct` carry value
337
+ semantics that Rigor does not model (Ruby has no value-vs-
338
+ reference distinction at this layer). Java has no user value
339
+ types at the modern LTS (Project Valhalla is not yet GA).
340
+ - **Checked exceptions**: Java's `throws` clause is a typed
341
+ effect Rigor has no analogue for; C# (like Rigor) does not
342
+ track exceptions in the type system.
343
+
344
+ ## What Java / C# have and Rigor does not
345
+
346
+ Be honest about what you give up:
347
+
348
+ - **Enforced exhaustiveness.** A sealed `switch` that misses a
349
+ variant is a compile error in Java/C#. Rigor reports
350
+ *unreachable* clauses, not *missing* ones — by design (see
351
+ above).
352
+ - **Compiler-enforced nullability.** C#'s NRT *warns* you into
353
+ handling null. Rigor narrows nil but never forces a guard it
354
+ cannot prove is needed.
355
+ - **Checked exceptions.** Java's `throws` has no Rigor analogue.
356
+ - **Value-type semantics.** C#'s `struct` / `record struct`
357
+ value model is outside Rigor's surface.
358
+ - **Type-level computation.** Neither Java nor C# is as
359
+ type-level-expressive as, say, TypeScript, but both have more
360
+ generic machinery (higher-kinded-ish patterns, complex bounds)
361
+ than RBS exposes. Rigor's generics are deliberately modest so
362
+ the analyzer stays fast on real Ruby.
363
+ - **IDE completeness.** Decades of IntelliJ / Visual Studio
364
+ investment back Java and C#. Rigor ships diagnostics, `rigor
365
+ type-of`, and an in-process Language Server (`rigor lsp`)
366
+ today; its IDE tooling depth still trails the JetBrains /
367
+ Visual Studio ecosystems.
368
+
369
+ ## What Rigor has and Java / C# do not
370
+
371
+ The other direction — and for these two languages the list is
372
+ longer than you might expect, because neither has literal types:
373
+
374
+ - **Literal / constant types.** `Constant<42>`, `Constant<:zero>`,
375
+ `Constant<"FOO">`. Java and C# have *no* literal types — the
376
+ closest is an `enum`, and that only covers a hand-declared
377
+ set. Rigor infers them from ordinary values.
378
+ - **Constant folding through method calls.** `"foo".upcase` is
379
+ `Constant<"FOO">`, not `String`. Rigor catalogues which
380
+ built-in methods are pure and folds through them.
381
+ - **First-class refinements.** `non-empty-string`, `positive-int`,
382
+ `int<1, 9>`, `numeric-string` — invariants on ordinary types,
383
+ no value-class wrapper.
384
+ - **Structural facets without a declaration.** A Ruby object that
385
+ has the right methods satisfies an RBS `interface` (a
386
+ *structural* interface — Go's `interface`, not Java's nominal
387
+ `implements`). You do not declare conformance; Rigor infers
388
+ the shape. See the
389
+ [structural-typing appendix](appendix-protocols-and-structural-typing.md).
390
+ - **No-false-positives stance.** Rigor stays silent on
391
+ `Dynamic[top]` receivers rather than complaining. You will
392
+ never see a Rigor diagnostic whose honest answer is "well,
393
+ technically the checker cannot know."
394
+ - **No annotation tax.** `rigor check` on a Ruby project with
395
+ zero `.rbs` files still yields useful diagnostics from
396
+ inference. Java and C# infer only `var` locals; everything
397
+ else you author. Adding `.rbs` to Rigor is incremental — every
398
+ file you skip is `Dynamic[top]` at the boundary, not an error.
399
+
400
+ ## A migration vignette
401
+
402
+ You are porting a C# domain model — a sealed-ish shape
403
+ hierarchy with a `switch` over it — to Ruby. The original:
404
+
405
+ ```csharp
406
+ abstract record Shape;
407
+ record Circle(double Radius) : Shape;
408
+ record Rectangle(double W, double H) : Shape;
409
+
410
+ double Area(Shape s) => s switch {
411
+ Circle c => Math.PI * c.Radius * c.Radius,
412
+ Rectangle r => r.W * r.H,
413
+ _ => throw new ArgumentException(),
414
+ };
415
+ ```
416
+
417
+ The Rigor approach — `Data.define` for the records, `case`/`in`
418
+ for the dispatch, and no annotations:
419
+
420
+ ```ruby
421
+ # lib/shape.rb
422
+ Circle = Data.define(:radius)
423
+ Rectangle = Data.define(:w, :h)
424
+
425
+ def area(s)
426
+ case s
427
+ in Circle then Math::PI * s.radius * s.radius
428
+ in Rectangle then s.w * s.h
429
+ end
430
+ end
431
+ ```
432
+
433
+ What carries over and what changes:
434
+
435
+ - The records become `Data.define` — immutable, value-equal,
436
+ member-shaped, and Rigor folds their member reads (see
437
+ [Records ↔ Data.define](#records--datadefine)).
438
+ - The `switch` patterns become `case`/`in`; `in Circle` narrows
439
+ `s` to `Circle` along that clause exactly as C#'s `case Circle
440
+ c` does.
441
+ - The `_ => throw` arm is gone. C# needs it to satisfy
442
+ exhaustiveness; Rigor does not demand it. If a third variant
443
+ appears later and reaches `area`, the result is a `nil` return
444
+ on the unmatched path — and if you add an `in OtherShape`
445
+ clause that can never match, `flow.unreachable-clause` tells
446
+ you. Rigor reports the *dead* clause, never the *missing* one.
447
+
448
+ If you want the missing-arm safety back, that is a deliberate
449
+ opt-in, not a default: a trailing `else raise` makes the
450
+ omission explicit and Rigor types the body accordingly.
451
+
452
+ ## What's next
453
+
454
+ You probably do not need to read the rest of the handbook
455
+ sequentially. Useful pointers:
456
+
457
+ - [Chapter 3 — Narrowing](03-narrowing.md) for the flow rules —
458
+ the direct analogue to `instanceof` / `is` pattern narrowing.
459
+ - [Chapter 6 — Classes](06-classes.md) for `Data.define`,
460
+ instance-side vs class-side, and `attr_accessor`.
461
+ - [Chapter 7 — RBS and `RBS::Extended`](07-rbs-and-extended.md)
462
+ for the directive grammar — `predicate-if-true` is the
463
+ user-defined type-guard analogue.
464
+
465
+ If you want to compare against another tool, the sibling
466
+ appendix pages cover [TypeScript](appendix-typescript.md),
467
+ [PHPStan](appendix-phpstan.md), [mypy](appendix-mypy.md),
468
+ [Steep](appendix-steep.md), [TypeProf](appendix-typeprof.md),
469
+ [Rust](appendix-rust.md), [Go](appendix-go.md), and
470
+ [Elixir](appendix-elixir.md).