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.
- checksums.yaml +4 -4
- data/README.md +82 -20
- data/data/core_overlay/numeric.rbs +33 -0
- data/data/core_overlay/pathname.rbs +25 -0
- data/data/core_overlay/string_scanner.rbs +28 -0
- data/data/gem_overlay/activesupport/core_ext.rbs +473 -0
- data/data/vendored_gem_sigs/ast/ast.rbs +130 -0
- data/data/vendored_gem_sigs/bcrypt/bcrypt.rbs +47 -0
- data/data/vendored_gem_sigs/bundler/bundler.rbs +238 -0
- data/data/vendored_gem_sigs/cgi/cgi_extras.rbs +34 -0
- data/data/vendored_gem_sigs/did_you_mean/did_you_mean_extras.rbs +34 -0
- data/data/vendored_gem_sigs/idn-ruby/idn.rbs +54 -0
- data/data/vendored_gem_sigs/mysql2/client.rbs +55 -0
- data/data/vendored_gem_sigs/mysql2/error.rbs +5 -0
- data/data/vendored_gem_sigs/mysql2/result.rbs +31 -0
- data/data/vendored_gem_sigs/mysql2/statement.rbs +5 -0
- data/data/vendored_gem_sigs/nokogiri/nokogiri.rbs +2332 -0
- data/data/vendored_gem_sigs/nokogiri/nokogiri_html5.rbs +47 -0
- data/data/vendored_gem_sigs/pg/pg.rbs +212 -0
- data/data/vendored_gem_sigs/prism/prism_supplement.rbs +44 -0
- data/data/vendored_gem_sigs/redis/errors.rbs +50 -0
- data/data/vendored_gem_sigs/redis/future.rbs +5 -0
- data/data/vendored_gem_sigs/redis/redis.rbs +348 -0
- data/data/vendored_gem_sigs/redis/redis_extras.rbs +130 -0
- data/data/vendored_gem_sigs/rubygems/rubygems_extras.rbs +226 -0
- data/docs/handbook/01-getting-started.md +311 -0
- data/docs/handbook/02-everyday-types.md +337 -0
- data/docs/handbook/03-narrowing.md +359 -0
- data/docs/handbook/04-tuples-and-shapes.md +321 -0
- data/docs/handbook/05-methods-and-blocks.md +339 -0
- data/docs/handbook/06-classes.md +305 -0
- data/docs/handbook/07-rbs-and-extended.md +427 -0
- data/docs/handbook/08-understanding-errors.md +373 -0
- data/docs/handbook/09-plugins.md +241 -0
- data/docs/handbook/10-sorbet.md +347 -0
- data/docs/handbook/11-sig-gen.md +312 -0
- data/docs/handbook/12-lightweight-hkt.md +333 -0
- data/docs/handbook/README.md +275 -0
- data/docs/handbook/appendix-elixir.md +370 -0
- data/docs/handbook/appendix-go.md +399 -0
- data/docs/handbook/appendix-java-csharp.md +470 -0
- data/docs/handbook/appendix-liskov.md +580 -0
- data/docs/handbook/appendix-mypy.md +370 -0
- data/docs/handbook/appendix-phpstan.md +338 -0
- data/docs/handbook/appendix-protocols-and-structural-typing.md +292 -0
- data/docs/handbook/appendix-rust.md +446 -0
- data/docs/handbook/appendix-steep.md +336 -0
- data/docs/handbook/appendix-type-theory.md +1662 -0
- data/docs/handbook/appendix-typeprof.md +416 -0
- data/docs/handbook/appendix-typescript.md +332 -0
- data/docs/install.md +189 -0
- data/docs/llms.txt +72 -0
- data/docs/manual/01-installation.md +342 -0
- data/docs/manual/02-cli-reference.md +557 -0
- data/docs/manual/03-configuration.md +152 -0
- data/docs/manual/04-diagnostics.md +206 -0
- data/docs/manual/05-inspecting-types.md +109 -0
- data/docs/manual/06-baseline.md +104 -0
- data/docs/manual/07-plugins.md +92 -0
- data/docs/manual/08-skills.md +143 -0
- data/docs/manual/09-editor-integration.md +245 -0
- data/docs/manual/10-mcp-server.md +532 -0
- data/docs/manual/11-ci.md +274 -0
- data/docs/manual/12-caching.md +116 -0
- data/docs/manual/13-troubleshooting.md +120 -0
- data/docs/manual/14-rails-quickstart.md +332 -0
- data/docs/manual/15-type-protection-coverage.md +204 -0
- data/docs/manual/16-rbs-extended-annotations.md +190 -0
- data/docs/manual/17-driving-improvement.md +160 -0
- data/docs/manual/README.md +87 -0
- data/docs/manual/ci-templates/README.md +58 -0
- data/docs/manual/plugins/README.md +86 -0
- data/docs/manual/plugins/rigor-actioncable.md +78 -0
- data/docs/manual/plugins/rigor-actionmailer.md +74 -0
- data/docs/manual/plugins/rigor-actionpack.md +80 -0
- data/docs/manual/plugins/rigor-activejob.md +58 -0
- data/docs/manual/plugins/rigor-activerecord.md +102 -0
- data/docs/manual/plugins/rigor-activestorage.md +74 -0
- data/docs/manual/plugins/rigor-activesupport-core-ext.md +86 -0
- data/docs/manual/plugins/rigor-devise.md +70 -0
- data/docs/manual/plugins/rigor-dry-schema.md +56 -0
- data/docs/manual/plugins/rigor-dry-struct.md +60 -0
- data/docs/manual/plugins/rigor-dry-types.md +59 -0
- data/docs/manual/plugins/rigor-dry-validation.md +62 -0
- data/docs/manual/plugins/rigor-factorybot.md +76 -0
- data/docs/manual/plugins/rigor-graphql.md +89 -0
- data/docs/manual/plugins/rigor-hanami.md +83 -0
- data/docs/manual/plugins/rigor-mangrove.md +73 -0
- data/docs/manual/plugins/rigor-minitest.md +86 -0
- data/docs/manual/plugins/rigor-pundit.md +72 -0
- data/docs/manual/plugins/rigor-rails-i18n.md +92 -0
- data/docs/manual/plugins/rigor-rails-routes.md +94 -0
- data/docs/manual/plugins/rigor-rails.md +44 -0
- data/docs/manual/plugins/rigor-rbs-inline.md +83 -0
- data/docs/manual/plugins/rigor-rspec-rails.md +72 -0
- data/docs/manual/plugins/rigor-rspec.md +86 -0
- data/docs/manual/plugins/rigor-shoulda-matchers.md +78 -0
- data/docs/manual/plugins/rigor-sidekiq.md +78 -0
- data/docs/manual/plugins/rigor-sinatra.md +61 -0
- data/docs/manual/plugins/rigor-sorbet.md +63 -0
- data/docs/manual/plugins/rigor-statesman.md +75 -0
- data/docs/manual/plugins/rigor-typescript-utility-types.md +71 -0
- data/exe/rigor +1 -1
- data/lib/rigor/analysis/incremental_session.rb +4 -2
- data/lib/rigor/analysis/run_stats.rb +13 -1
- data/lib/rigor/analysis/runner.rb +54 -12
- data/lib/rigor/cli/check_command.rb +26 -3
- data/lib/rigor/cli/coverage_command.rb +67 -92
- data/lib/rigor/cli/coverage_mutation.rb +149 -0
- data/lib/rigor/cli/docs_command.rb +248 -0
- data/lib/rigor/cli/fused_protection_renderer.rb +67 -0
- data/lib/rigor/cli/fused_protection_report.rb +76 -0
- data/lib/rigor/cli/skill_command.rb +103 -41
- data/lib/rigor/cli/skill_describe.rb +346 -0
- data/lib/rigor/cli.rb +25 -3
- data/lib/rigor/config_audit.rb +152 -0
- data/lib/rigor/configuration.rb +12 -0
- data/lib/rigor/environment/rbs_loader.rb +27 -0
- data/lib/rigor/environment.rb +49 -1
- data/lib/rigor/inference/method_dispatcher/constant_folding.rb +140 -38
- data/lib/rigor/inference/method_dispatcher/shape_dispatch.rb +37 -6
- data/lib/rigor/inference/scope_indexer.rb +87 -89
- data/lib/rigor/inference/statement_evaluator.rb +27 -0
- data/lib/rigor/plugin/isolation.rb +5 -5
- data/lib/rigor/plugin/loader.rb +4 -2
- data/lib/rigor/protection/diagnostic_oracle.rb +51 -0
- data/lib/rigor/protection/mutation_scanner.rb +98 -38
- data/lib/rigor/protection/mutator.rb +21 -0
- data/lib/rigor/protection/test_suite_oracle.rb +68 -0
- data/lib/rigor/signature_path_audit.rb +92 -0
- data/lib/rigor/version.rb +1 -1
- data/skills/rigor-ask/SKILL.md +172 -0
- data/skills/rigor-doctor/SKILL.md +87 -0
- data/skills/rigor-editor-setup/SKILL.md +114 -0
- data/skills/rigor-mcp-setup/SKILL.md +117 -0
- data/skills/rigor-monkeypatch-resolve/SKILL.md +79 -0
- data/skills/rigor-next-steps/SKILL.md +113 -0
- data/skills/rigor-plugin-tune/SKILL.md +79 -0
- data/skills/rigor-protection-uplift/SKILL.md +133 -0
- data/skills/rigor-rbs-setup/SKILL.md +128 -0
- data/skills/rigor-upgrade/SKILL.md +79 -0
- 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).
|