rigortype 0.2.1 → 0.2.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/README.md +41 -14
- 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 +569 -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 +539 -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 +1 -1
- data/lib/rigor/cli/docs_command.rb +248 -0
- data/lib/rigor/cli/skill_command.rb +103 -41
- data/lib/rigor/cli/skill_describe.rb +346 -0
- data/lib/rigor/cli/triage_command.rb +8 -2
- data/lib/rigor/cli/triage_renderer.rb +4 -0
- data/lib/rigor/cli.rb +25 -3
- data/lib/rigor/inference/method_dispatcher/constant_folding.rb +124 -32
- data/lib/rigor/inference/method_dispatcher/shape_dispatch.rb +37 -6
- data/lib/rigor/inference/scope_indexer.rb +87 -89
- data/lib/rigor/plugin/isolation.rb +5 -5
- data/lib/rigor/plugin/loader.rb +4 -2
- data/lib/rigor/triage/catalogue.rb +16 -1
- data/lib/rigor/triage.rb +30 -7
- 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 +90 -1
|
@@ -0,0 +1,399 @@
|
|
|
1
|
+
# Appendix — Coming from Go
|
|
2
|
+
|
|
3
|
+
If your mental model of "types" was set by Go, this appendix
|
|
4
|
+
maps Rigor's vocabulary onto the concepts you already know. The
|
|
5
|
+
two have a real point of contact that surprises people: Go's
|
|
6
|
+
*implicitly satisfied* interface is exactly how Rigor's
|
|
7
|
+
structural typing works. The single most-misunderstood feature
|
|
8
|
+
in Rigor — "where do I declare that this class implements the
|
|
9
|
+
interface?" — is the one a Go programmer already understands by
|
|
10
|
+
reflex, the way you always did in Go.
|
|
11
|
+
|
|
12
|
+
This is a translation table plus a discussion of where the two
|
|
13
|
+
diverge. Go is small, compiled, and deliberately spare —
|
|
14
|
+
no sum types, no inheritance, errors as values, zero values
|
|
15
|
+
everywhere. Rigor adds the things Go leaves out (unions,
|
|
16
|
+
refinements, literal types, nil-narrowing) and drops the things
|
|
17
|
+
Go needs as a compiled language (the build gate itself).
|
|
18
|
+
|
|
19
|
+
## The five-second pitch
|
|
20
|
+
|
|
21
|
+
| Question | Go | Rigor |
|
|
22
|
+
| --- | --- | --- |
|
|
23
|
+
| Interface membership | Implicit — have the methods, satisfy the interface | Implicit — same structural rule |
|
|
24
|
+
| When does the checker run? | At compile time; nothing runs until it passes | After the fact, on code that already runs |
|
|
25
|
+
| The "I don't know" type | `interface{}` / `any` | `Dynamic[top]` — direct analogue |
|
|
26
|
+
| Where do annotations live? | In source; `:=` infers locals | In `.rbs` files; whole bodies inferred |
|
|
27
|
+
| Sum types | None — Go has no union types | First-class `T \| U` unions |
|
|
28
|
+
| Errors | Returned as values (`(T, error)`) | Raised; not tracked in the signature |
|
|
29
|
+
|
|
30
|
+
Go and Rigor share the structural-typing instinct and the
|
|
31
|
+
`any`-as-escape-hatch instinct. They part ways on the build
|
|
32
|
+
model: Go's checker is a gate the program passes before it
|
|
33
|
+
exists, while Rigor's is an advisor over a program that already
|
|
34
|
+
runs. They also part ways on richness, where Go is intentionally
|
|
35
|
+
minimal and Rigor adds unions, literal types, and refinements.
|
|
36
|
+
|
|
37
|
+
## Type vocabulary mapping
|
|
38
|
+
|
|
39
|
+
| Go | Rigor | Notes |
|
|
40
|
+
| --- | --- | --- |
|
|
41
|
+
| `int` / `int64` / `uint` | `Integer` | Ruby integers are arbitrary-precision; no width or signedness. |
|
|
42
|
+
| `float64` / `float32` | `Float` | `Numeric` is the common supertype. |
|
|
43
|
+
| `bool` | `bool` (`Constant<true> \| Constant<false>`) | Structurally a union of two constants. |
|
|
44
|
+
| `string` | `String` | |
|
|
45
|
+
| `byte` / `rune` | `Integer` | Ruby has no distinct byte/rune type. |
|
|
46
|
+
| `nil` | `nil` (`Constant<nil>`) | Go's typed-nil subtleties have no Ruby analogue — there is one `nil`. |
|
|
47
|
+
| `interface{}` / `any` | `Dynamic[top]` | The "be silent here" carrier — a direct match. |
|
|
48
|
+
| `error` | (no type — Ruby raises) | See [Errors as values](#errors-as-values-vs-raising). |
|
|
49
|
+
| `[]T` (slice) | `Array[T]` | |
|
|
50
|
+
| `[N]T` (array) | `Tuple[…]` or `Array[T]` | A fixed-length literal gets a `Tuple`; otherwise `Array[T]`. |
|
|
51
|
+
| `map[K]V` | `Hash[K, V]` | |
|
|
52
|
+
| `struct{ X int; Y int }` | `Point = Data.define(:x, :y)` | See [Structs ↔ Data.define](#structs--datadefine). |
|
|
53
|
+
| `interface{ Draw() string }` | RBS `interface` (structural) | The direct analogue — see below. |
|
|
54
|
+
| `*T` (pointer, nil-able) | `T?` (i.e. `T \| nil`) | A nil-able pointer narrows like `T?`. |
|
|
55
|
+
| `iota` const group | `Constant<…>` union | Go's enum idiom; Rigor uses a union of constants or symbols. |
|
|
56
|
+
| `[T any]` (generics, 1.18+) | RBS `[T]` type parameter | |
|
|
57
|
+
| (no sum types) | `T \| U` | Go has no unions at all; a Rigor addition. |
|
|
58
|
+
| (no literal types) | `Constant<42>` / `Constant<"hi">` | Go has no literal types; a Rigor novelty. |
|
|
59
|
+
|
|
60
|
+
## Structural interfaces — the part you already know
|
|
61
|
+
|
|
62
|
+
This is the section that makes Go programmers feel at home. In
|
|
63
|
+
Go, a type satisfies an interface by *having its methods* — no
|
|
64
|
+
`implements` keyword, no declaration of intent:
|
|
65
|
+
|
|
66
|
+
```go
|
|
67
|
+
type Drawable interface { Draw() string }
|
|
68
|
+
|
|
69
|
+
type Button struct{ /* … */ }
|
|
70
|
+
func (b Button) Draw() string { return "[button]" }
|
|
71
|
+
// Button satisfies Drawable. You never said so.
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
Rigor's RBS `interface` works exactly this way:
|
|
75
|
+
|
|
76
|
+
```ruby
|
|
77
|
+
# An RBS structural interface
|
|
78
|
+
interface _Drawable
|
|
79
|
+
def draw: () -> String
|
|
80
|
+
end
|
|
81
|
+
```
|
|
82
|
+
|
|
83
|
+
Any Ruby object that responds to `draw` returning a `String`
|
|
84
|
+
satisfies `_Drawable` — you never write `implements`, never
|
|
85
|
+
declare conformance. This is the **one** word in Rigor that
|
|
86
|
+
trips up readers arriving from Java or C#, where `interface`
|
|
87
|
+
means a *nominal* contract you must declare. For you it needs no
|
|
88
|
+
explanation: it is Go's interface, including the convention of
|
|
89
|
+
keeping interfaces small.
|
|
90
|
+
|
|
91
|
+
Rigor goes one step further than Go: it infers anonymous object
|
|
92
|
+
*shapes* and capability roles even when no interface is
|
|
93
|
+
declared at all — the equivalent of Go inferring `interface{
|
|
94
|
+
Draw() string }` from how you used a value. The
|
|
95
|
+
[structural-typing appendix](appendix-protocols-and-structural-typing.md)
|
|
96
|
+
is the canonical explainer.
|
|
97
|
+
|
|
98
|
+
## Narrowing — type switch / assertion
|
|
99
|
+
|
|
100
|
+
Go narrows an `interface{}` value through type assertions and
|
|
101
|
+
type switches. Rigor has direct analogues.
|
|
102
|
+
|
|
103
|
+
| Go | Rigor |
|
|
104
|
+
| --- | --- |
|
|
105
|
+
| `if x != nil` | `if x` (strips `nil`), or `unless x.nil?` |
|
|
106
|
+
| `v, ok := x.(string)` (comma-ok assertion) | `x.is_a?(String)` narrowing in an `if` |
|
|
107
|
+
| `switch v := x.(type) { case string: … }` | `case x; in String => v` |
|
|
108
|
+
| `x.(T)` (assertion, panics on fail) | (no panicking assertion) — `is_a?` guard, or `T.cast` via [`rigor-sorbet`](../../plugins/rigor-sorbet/) |
|
|
109
|
+
| user func returning `bool` | `%a{rigor:v1:predicate-if-true x is Foo}` directive |
|
|
110
|
+
|
|
111
|
+
Go's type switch is *not* exhaustive — you can omit cases and
|
|
112
|
+
fall through `default` — and neither is Rigor's `case`. Both
|
|
113
|
+
report what they can prove, not what you forgot. (Where Rigor
|
|
114
|
+
adds value is the dual: if a `case`/`in` clause can *never*
|
|
115
|
+
match because earlier clauses already covered its type, the
|
|
116
|
+
`flow.unreachable-clause` rule from
|
|
117
|
+
[ADR-47](../adr/47-narrowing-driven-clause-reachability.md) says
|
|
118
|
+
so.)
|
|
119
|
+
|
|
120
|
+
## Errors as values vs raising
|
|
121
|
+
|
|
122
|
+
Go's defining idiom — `(T, error)` returned together, checked
|
|
123
|
+
with `if err != nil` — has no direct Rigor analogue, because
|
|
124
|
+
Ruby signals failure by *raising*, not returning. A Go function
|
|
125
|
+
becomes a Ruby method that returns `T` and raises on the error
|
|
126
|
+
path:
|
|
127
|
+
|
|
128
|
+
```go
|
|
129
|
+
func parse(s string) (int, error) { return strconv.Atoi(s) }
|
|
130
|
+
|
|
131
|
+
n, err := parse("x")
|
|
132
|
+
if err != nil { return err }
|
|
133
|
+
```
|
|
134
|
+
|
|
135
|
+
```ruby
|
|
136
|
+
def parse(s) # returns Integer, raises on bad input
|
|
137
|
+
Integer(s)
|
|
138
|
+
end
|
|
139
|
+
|
|
140
|
+
n = parse("x") # raises ArgumentError on the error path
|
|
141
|
+
```
|
|
142
|
+
|
|
143
|
+
Rigor does not track the exception as part of the signature —
|
|
144
|
+
there is no typed error return and no `if err != nil` shape to
|
|
145
|
+
narrow. If you want to keep Go's value-returning style, you
|
|
146
|
+
*can* return a `Tuple` (`[value, error]` or `[:ok, value]` /
|
|
147
|
+
`[:error, reason]`) and pattern-match it with `case`/`in`;
|
|
148
|
+
Rigor types the `Tuple` and the union precisely. But that is a
|
|
149
|
+
port of the Go idiom, not idiomatic Ruby.
|
|
150
|
+
|
|
151
|
+
## nil, zero values, and absence
|
|
152
|
+
|
|
153
|
+
Go has no null-the-type, but it has nil-able pointers, maps,
|
|
154
|
+
slices, and interfaces — and *zero values*: an unset `int` is
|
|
155
|
+
`0`, an unset `string` is `""`, an unset pointer is `nil`.
|
|
156
|
+
|
|
157
|
+
Ruby has no zero-value rule. An unset instance variable reads
|
|
158
|
+
`nil`; an unset *local* raises `NameError` (it does not silently
|
|
159
|
+
become a zero). So:
|
|
160
|
+
|
|
161
|
+
- A Go nil-able `*T` maps onto Rigor's `T?` — `T | nil`,
|
|
162
|
+
narrowed by an `x.nil?` / `if x` guard, the way you write
|
|
163
|
+
`if p != nil` in Go.
|
|
164
|
+
- Go's zero-value-on-declare convenience has no Ruby analogue;
|
|
165
|
+
Ruby forces you to assign before use, and Rigor's flow
|
|
166
|
+
analysis tracks that a never-assigned local is not in scope.
|
|
167
|
+
|
|
168
|
+
The upshot: the `if err != nil` / `if ptr != nil` reflex
|
|
169
|
+
translates straight to `unless x.nil?`, and Rigor narrows the
|
|
170
|
+
non-nil branch exactly as you would expect.
|
|
171
|
+
|
|
172
|
+
## Structs ↔ `Data.define`
|
|
173
|
+
|
|
174
|
+
A Go `struct` used as an immutable value maps onto Ruby's
|
|
175
|
+
`Data.define` — value-equal, member-shaped, frozen. Rigor models
|
|
176
|
+
it natively ([ADR-48](../adr/48-data-struct-value-folding.md)).
|
|
177
|
+
|
|
178
|
+
```go
|
|
179
|
+
type Point struct{ X, Y int }
|
|
180
|
+
p := Point{X: 1, Y: 2}
|
|
181
|
+
a := p.X // a : int
|
|
182
|
+
q := p
|
|
183
|
+
q.X = 9 // Go structs are mutable; this copies-then-mutates
|
|
184
|
+
```
|
|
185
|
+
|
|
186
|
+
```ruby
|
|
187
|
+
Point = Data.define(:x, :y)
|
|
188
|
+
p = Point.new(1, 2)
|
|
189
|
+
assert_type("1", p.x) # member value is folded, not just Integer
|
|
190
|
+
q = p.with(x: 9) # immutable update — returns a new Point
|
|
191
|
+
```
|
|
192
|
+
|
|
193
|
+
Two notes for a Go reader:
|
|
194
|
+
|
|
195
|
+
- **Member values fold.** `Point.new(1, 2).x` is `1`,
|
|
196
|
+
not merely `Integer`. Go erases the literal; Rigor keeps it
|
|
197
|
+
(subject to the folding budget).
|
|
198
|
+
- **`Data` is immutable.** Unlike a Go `struct`, `Data.define`
|
|
199
|
+
produces frozen values; you get a new one via `Data#with`. If
|
|
200
|
+
you need Go's mutable struct, Ruby's `Struct` is the closer
|
|
201
|
+
fit — but Rigor does not yet value-fold `Struct`, because its
|
|
202
|
+
mutability breaks the soundness story (see
|
|
203
|
+
[Chapter 6](06-classes.md)).
|
|
204
|
+
|
|
205
|
+
## Sum types Go doesn't have
|
|
206
|
+
|
|
207
|
+
This is the gap that bites Go programmers most, and the place
|
|
208
|
+
Rigor most clearly adds something. Go has **no union types** —
|
|
209
|
+
no way to say "this is a `Circle` or a `Rectangle`." The
|
|
210
|
+
workarounds are a sealed interface with unexported methods, or
|
|
211
|
+
an `interface{}` plus a type switch with a `default: panic`.
|
|
212
|
+
|
|
213
|
+
Rigor has first-class unions, and a closed set of variants is
|
|
214
|
+
simply a union of `Data.define` types dispatched with `case`/`in`:
|
|
215
|
+
|
|
216
|
+
```ruby
|
|
217
|
+
Circle = Data.define(:radius)
|
|
218
|
+
Rectangle = Data.define(:w, :h)
|
|
219
|
+
|
|
220
|
+
def area(s) # s : Circle | Rectangle
|
|
221
|
+
case s
|
|
222
|
+
in Circle then Math::PI * s.radius * s.radius
|
|
223
|
+
in Rectangle then s.w * s.h
|
|
224
|
+
end
|
|
225
|
+
end
|
|
226
|
+
```
|
|
227
|
+
|
|
228
|
+
No marker interface, no `panic` default. The union is a real
|
|
229
|
+
type Rigor tracks, and `in Circle` narrows `s` to `Circle` along
|
|
230
|
+
that clause. This is the single biggest expressiveness gain
|
|
231
|
+
moving from Go's type system to Rigor's.
|
|
232
|
+
|
|
233
|
+
## Refinements and defined types
|
|
234
|
+
|
|
235
|
+
Go's `type Celsius float64` makes a *nominally distinct* type
|
|
236
|
+
from `float64` — but it cannot say "a positive `Celsius`" or "a
|
|
237
|
+
non-empty string." Rigor cannot reproduce Go's defined-type
|
|
238
|
+
nominal distinctness (`type Celsius float64` collapses to
|
|
239
|
+
`Float` in Rigor), but it adds the thing Go lacks: refinement
|
|
240
|
+
carriers that encode an invariant on the value.
|
|
241
|
+
|
|
242
|
+
| Rigor refinement | Go idiom | Comment |
|
|
243
|
+
| --- | --- | --- |
|
|
244
|
+
| `non-empty-string` | runtime `if len(s) == 0` check | Rigor produces it from `unless s.empty?`. |
|
|
245
|
+
| `positive-int` | runtime `if n <= 0` check | Rigor narrows from `n > 0`. |
|
|
246
|
+
| `int<1, 9>` | runtime range check | Rigor's range carrier handles arbitrary bounds. |
|
|
247
|
+
| `numeric-string` | `strconv.Atoi` + error check | No type-level analogue in Go. |
|
|
248
|
+
| `non-empty-array[T]` | runtime `len(xs) == 0` check | Rigor produces it from `unless arr.empty?`. |
|
|
249
|
+
|
|
250
|
+
So the trade runs both ways: Go gives you nominal newtypes
|
|
251
|
+
Rigor flattens, and Rigor gives you value-level refinements Go
|
|
252
|
+
has to check at runtime.
|
|
253
|
+
|
|
254
|
+
## Generics
|
|
255
|
+
|
|
256
|
+
Go added generics in 1.18; RBS has had them longer, and Rigor
|
|
257
|
+
reads them. The surfaces are close for the common cases.
|
|
258
|
+
|
|
259
|
+
| Go | Rigor (via RBS) |
|
|
260
|
+
| --- | --- |
|
|
261
|
+
| `func Id[T any](x T) T` | `def id: [T] (T) -> T` |
|
|
262
|
+
| `[]T` | `Array[T]` |
|
|
263
|
+
| `map[K]V` | `Hash[K, V]` |
|
|
264
|
+
| `[T constraints.Ordered]` | `[T < Comparable[T]]` bound |
|
|
265
|
+
| type sets / unions in constraints | RBS bounds (narrower) |
|
|
266
|
+
|
|
267
|
+
Go's constraint type-sets and Rigor's RBS bounds are both
|
|
268
|
+
modest by design; neither tries to be as type-level-expressive
|
|
269
|
+
as a fully dependent system.
|
|
270
|
+
|
|
271
|
+
## Severity, suppression, and "strict mode"
|
|
272
|
+
|
|
273
|
+
| Go | Rigor |
|
|
274
|
+
| --- | --- |
|
|
275
|
+
| `go vet` / linter severity | `severity_profile: lenient` / `balanced` / `strict` |
|
|
276
|
+
| `//nolint:rule` (golangci-lint) | `# rigor:disable <rule>` |
|
|
277
|
+
| file-level lint disable | `# rigor:disable-file all` |
|
|
278
|
+
| `go build` (the gate) | `rigor check lib` (the advisor) |
|
|
279
|
+
|
|
280
|
+
The mental shift: `go build` is the gate — code does not ship
|
|
281
|
+
until it compiles. `rigor check` is an advisor over code that
|
|
282
|
+
already runs, tuned with severity profiles and adopted
|
|
283
|
+
incrementally via baselines.
|
|
284
|
+
|
|
285
|
+
## What Go has and Rigor does not
|
|
286
|
+
|
|
287
|
+
Be honest about what you give up:
|
|
288
|
+
|
|
289
|
+
- **The compile gate.** Go does not run until it builds. Rigor
|
|
290
|
+
is advisory; the program runs regardless of what it reports.
|
|
291
|
+
- **Defined-type nominal distinctness.** `type Celsius float64`
|
|
292
|
+
is a distinct type in Go; Rigor flattens it to `Float`.
|
|
293
|
+
- **Goroutines and channel types.** `chan T`, `select`, the
|
|
294
|
+
concurrency type machinery — no Rigor analogue.
|
|
295
|
+
- **Zero values.** Go's declare-and-it-is-zero convenience has
|
|
296
|
+
no place in Ruby's assign-before-use model.
|
|
297
|
+
- **A single binary and compile-time guarantees.** Compile-model
|
|
298
|
+
benefits a runtime advisor does not provide.
|
|
299
|
+
|
|
300
|
+
## What Rigor has and Go does not
|
|
301
|
+
|
|
302
|
+
The other direction — and for Go the list is long, because Go
|
|
303
|
+
is deliberately minimal:
|
|
304
|
+
|
|
305
|
+
- **Union / sum types.** The big one: `T | U` is first-class in
|
|
306
|
+
Rigor and absent from Go. Closed variant sets need no marker
|
|
307
|
+
interface.
|
|
308
|
+
- **Literal / constant types.** `Constant<42>`, `Constant<:ok>`,
|
|
309
|
+
`Constant<"FOO">`. Go has no literal types; the nearest is an
|
|
310
|
+
`iota` const group.
|
|
311
|
+
- **Constant folding through method calls.** `"foo".upcase` is
|
|
312
|
+
`Constant<"FOO">`, not `string`.
|
|
313
|
+
- **Refinements.** `non-empty-string`, `positive-int`,
|
|
314
|
+
`int<1, 9>` — invariants on the value, no runtime check needed
|
|
315
|
+
to know them statically.
|
|
316
|
+
- **Inferred shapes beyond interfaces.** Rigor infers anonymous
|
|
317
|
+
object shapes and capability roles, not just satisfaction of a
|
|
318
|
+
declared `interface`.
|
|
319
|
+
- **No-false-positives stance.** Silent on `Dynamic[top]`
|
|
320
|
+
receivers rather than complaining; the `interface{}` you
|
|
321
|
+
haven't narrowed yet costs you nothing.
|
|
322
|
+
- **No annotation tax beyond `:=`.** Go infers `:=` locals;
|
|
323
|
+
Rigor infers whole method bodies *and* across `def`
|
|
324
|
+
boundaries. A zero-`.rbs` project still yields useful
|
|
325
|
+
diagnostics.
|
|
326
|
+
|
|
327
|
+
## A migration vignette
|
|
328
|
+
|
|
329
|
+
You are porting a Go package — an interface, a couple of
|
|
330
|
+
implementers, and a constructor that can fail — to Ruby. The
|
|
331
|
+
original:
|
|
332
|
+
|
|
333
|
+
```go
|
|
334
|
+
type Shape interface{ Area() float64 }
|
|
335
|
+
|
|
336
|
+
type Circle struct{ Radius float64 }
|
|
337
|
+
func (c Circle) Area() float64 { return math.Pi * c.Radius * c.Radius }
|
|
338
|
+
|
|
339
|
+
type Rectangle struct{ W, H float64 }
|
|
340
|
+
func (r Rectangle) Area() float64 { return r.W * r.H }
|
|
341
|
+
|
|
342
|
+
func parseRadius(s string) (float64, error) { return strconv.ParseFloat(s, 64) }
|
|
343
|
+
```
|
|
344
|
+
|
|
345
|
+
The Rigor approach — duck-typed implementers, an inferred
|
|
346
|
+
structural interface, and a raising parser:
|
|
347
|
+
|
|
348
|
+
```ruby
|
|
349
|
+
# lib/shape.rb
|
|
350
|
+
Circle = Data.define(:radius) do
|
|
351
|
+
def area = Math::PI * radius * radius
|
|
352
|
+
end
|
|
353
|
+
|
|
354
|
+
Rectangle = Data.define(:w, :h) do
|
|
355
|
+
def area = w * h
|
|
356
|
+
end
|
|
357
|
+
|
|
358
|
+
def parse_radius(s) # returns Float, raises on bad input
|
|
359
|
+
Float(s)
|
|
360
|
+
end
|
|
361
|
+
```
|
|
362
|
+
|
|
363
|
+
What carries over and what changes:
|
|
364
|
+
|
|
365
|
+
- The `Shape` interface needs no Ruby declaration. Any object
|
|
366
|
+
with `area` satisfies the structural interface; if you *want*
|
|
367
|
+
to name it, an RBS `interface _Shape` works exactly like Go's,
|
|
368
|
+
satisfied implicitly.
|
|
369
|
+
- `Circle` and `Rectangle` become `Data.define` values with
|
|
370
|
+
methods — immutable where Go's structs were mutable.
|
|
371
|
+
- `(float64, error)` becomes a `Float` return that raises. The
|
|
372
|
+
`if err != nil` check at the call site becomes a `rescue`, or
|
|
373
|
+
you keep the value-style by returning a `Tuple` and matching
|
|
374
|
+
it with `case`/`in`.
|
|
375
|
+
- If you dispatch over the shapes with a `case`/`in`, you get
|
|
376
|
+
the union type Go could not express — and Rigor's
|
|
377
|
+
`flow.unreachable-clause` will flag a clause that can never
|
|
378
|
+
match.
|
|
379
|
+
|
|
380
|
+
## What's next
|
|
381
|
+
|
|
382
|
+
You probably do not need to read the rest of the handbook
|
|
383
|
+
sequentially. Useful pointers:
|
|
384
|
+
|
|
385
|
+
- [Protocols and structural typing](appendix-protocols-and-structural-typing.md)
|
|
386
|
+
— the canonical page on RBS interfaces, the direct analogue to
|
|
387
|
+
Go's implicit interfaces, and how they differ from ADR-28
|
|
388
|
+
protocol contracts.
|
|
389
|
+
- [Chapter 3 — Narrowing](03-narrowing.md) for the flow rules —
|
|
390
|
+
the analogue to type switches and assertions.
|
|
391
|
+
- [Chapter 6 — Classes](06-classes.md) for `Data.define`, the
|
|
392
|
+
`struct` analogue, and its value-folding.
|
|
393
|
+
|
|
394
|
+
If you want to compare against another tool, the sibling
|
|
395
|
+
appendix pages cover [TypeScript](appendix-typescript.md),
|
|
396
|
+
[PHPStan](appendix-phpstan.md), [mypy](appendix-mypy.md),
|
|
397
|
+
[Steep](appendix-steep.md), [TypeProf](appendix-typeprof.md),
|
|
398
|
+
[Java / C#](appendix-java-csharp.md), [Rust](appendix-rust.md),
|
|
399
|
+
and [Elixir](appendix-elixir.md).
|