rigortype 0.3.0 → 0.3.1
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 +1 -1
- data/data/builtins/ruby_core/array.yml +416 -392
- data/data/builtins/ruby_core/file.yml +42 -42
- data/data/builtins/ruby_core/hash.yml +302 -302
- data/data/builtins/ruby_core/io.yml +191 -191
- data/data/builtins/ruby_core/numeric.yml +321 -366
- data/data/builtins/ruby_core/proc.yml +124 -124
- data/data/builtins/ruby_core/range.yml +21 -21
- data/data/builtins/ruby_core/rational.yml +39 -39
- data/data/builtins/ruby_core/re.yml +65 -65
- data/data/builtins/ruby_core/set.yml +106 -106
- data/data/builtins/ruby_core/struct.yml +14 -14
- data/data/core_overlay/string_scanner.rbs +6 -5
- data/docs/handbook/01-getting-started.md +22 -34
- data/docs/handbook/06-classes.md +1 -1
- data/docs/handbook/07-rbs-and-extended.md +76 -101
- data/docs/handbook/08-understanding-errors.md +114 -247
- data/docs/handbook/09-plugins.md +54 -144
- data/docs/handbook/README.md +5 -3
- data/docs/handbook/appendix-liskov.md +4 -2
- data/docs/handbook/appendix-phpstan.md +14 -7
- data/docs/handbook/appendix-steep.md +4 -2
- data/docs/handbook/appendix-type-theory.md +3 -1
- data/docs/manual/02-cli-reference.md +32 -0
- data/docs/manual/04-diagnostics.md +36 -4
- data/docs/manual/06-baseline.md +35 -1
- data/docs/manual/08-skills.md +6 -1
- data/docs/manual/09-editor-integration.md +3 -2
- data/docs/manual/plugins/rigor-actioncable.md +32 -0
- data/docs/manual/plugins/rigor-devise.md +4 -2
- data/lib/rigor/analysis/check_rules/void_value_use_collector.rb +21 -2
- data/lib/rigor/analysis/check_rules.rb +34 -13
- data/lib/rigor/analysis/run_cache_key.rb +10 -0
- data/lib/rigor/analysis/runner.rb +2 -1
- data/lib/rigor/cache/rbs_cache_producer.rb +11 -1
- data/lib/rigor/cache/rbs_environment_marshal_patch.rb +38 -0
- data/lib/rigor/cache/store.rb +99 -24
- data/lib/rigor/cli/check_command.rb +12 -6
- data/lib/rigor/cli/check_invocation.rb +84 -0
- data/lib/rigor/cli/doctor_command.rb +6 -8
- data/lib/rigor/cli/skill_command.rb +21 -1
- data/lib/rigor/cli/skill_deep_probe.rb +172 -0
- data/lib/rigor/cli/skill_describe.rb +75 -9
- data/lib/rigor/environment/default_libraries.rb +5 -4
- data/lib/rigor/environment.rb +10 -1
- data/lib/rigor/inference/method_dispatcher/overload_selector.rb +6 -1
- data/lib/rigor/language_server/buffer_resolution.rb +6 -3
- data/lib/rigor/language_server/buffer_table.rb +46 -6
- data/lib/rigor/language_server/diagnostic_publisher.rb +4 -0
- data/lib/rigor/language_server/incremental_sync.rb +159 -0
- data/lib/rigor/language_server/server.rb +19 -9
- data/lib/rigor/language_server.rb +1 -0
- data/lib/rigor/plugin/base.rb +29 -2
- data/lib/rigor/sig_gen/writer.rb +183 -47
- data/lib/rigor/version.rb +1 -1
- data/plugins/rigor-actioncable/lib/rigor/plugin/actioncable.rb +51 -1
- data/sig/rigor/cache.rbs +6 -0
- data/sig/rigor/inference/void_origin.rbs +18 -0
- data/sig/rigor/plugin/base.rbs +4 -3
- metadata +7 -3
|
@@ -1,9 +1,16 @@
|
|
|
1
1
|
# Understanding errors
|
|
2
2
|
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
when
|
|
3
|
+
A diagnostic is Rigor telling you something it proved about
|
|
4
|
+
your code. This chapter is about *reading* one: what its parts
|
|
5
|
+
mean, what each rule family is really claiming, why one fires
|
|
6
|
+
when you did not expect it, why one stays silent when you did,
|
|
7
|
+
and how to work a freshly-adopted project down to a clean run.
|
|
8
|
+
|
|
9
|
+
The reference lives one door over. The full rule catalogue,
|
|
10
|
+
each rule's evidence tier, the severity-profile table, and the
|
|
11
|
+
exact syntax of every suppression form are in
|
|
12
|
+
[Diagnostics](../manual/04-diagnostics.md) in the manual; this
|
|
13
|
+
chapter links there rather than restating it.
|
|
7
14
|
|
|
8
15
|
## Anatomy of a diagnostic
|
|
9
16
|
|
|
@@ -33,251 +40,106 @@ fires, when it doesn't, the suppression token, the authored
|
|
|
33
40
|
severity, and the per-profile severity. `rigor explain` with
|
|
34
41
|
no argument prints the index of every shipped rule.
|
|
35
42
|
|
|
36
|
-
### Confidence
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
`flow.unreachable-branch`, `flow.always-truthy-condition`, and
|
|
93
|
-
`flow.unreachable-clause` are the **reachability family** — each
|
|
94
|
-
proves a branch or `case` clause is dead. `unreachable-clause`
|
|
95
|
-
is the newest member: it watches `case <local>; when <Class>`
|
|
96
|
-
(and bare-class `case`/`in`) and fires when an earlier clause
|
|
97
|
-
already covered a member's type or the clause is disjoint from
|
|
98
|
-
the subject. It ships at `:info` under `balanced` (one notch
|
|
99
|
-
below its siblings) while its corpus false-positive gate
|
|
100
|
-
finishes; bump it with `severity_overrides:` if you want it
|
|
101
|
-
louder.
|
|
102
|
-
|
|
103
|
-
### `def.*` — method-definition rules
|
|
104
|
-
|
|
105
|
-
Fire when the body of a method violates its declared
|
|
106
|
-
contract.
|
|
107
|
-
|
|
108
|
-
| Rule | Fires when | Default severity |
|
|
109
|
-
| --- | --- | --- |
|
|
110
|
-
| `def.return-type-mismatch` | The body's last expression's inferred type cannot satisfy the RBS-declared return type. Honors `%a{rigor:v1:return: <refinement>}` overrides. | warning under `balanced` profile, error under `strict` |
|
|
111
|
-
| `def.ivar-write-mismatch` | A later `@var = ...` write's concrete class disagrees with the first write's class in the same class body (NilClass-to-clear is allowlisted). | warning under `balanced` profile, error under `strict` |
|
|
112
|
-
| `def.method-visibility-mismatch` | An explicit-receiver call targets a `Nominal[X]` whose discovered method is `:private` in the surrounding class body. | error |
|
|
113
|
-
| `def.override-visibility-reduced` | An override reduces the visibility it inherits from a project-defined ancestor (public → protected/private, protected → private), breaking a caller that holds the supertype. | warning under `balanced`, error under `strict`, suppressed under `lenient` |
|
|
114
|
-
| `def.override-return-widened` | An override's declared return widens the inherited return (covariance). Fires only on a proven violation when both sides carry an authored RBS signature. | warning under `balanced`, error under `strict`, suppressed under `lenient` |
|
|
115
|
-
| `def.override-param-narrowed` | An override narrows an inherited parameter type (contravariance), comparing matching positional parameters. Requires an authored single-overload RBS signature on both sides. | warning under `balanced`, error under `strict`, suppressed under `lenient` |
|
|
116
|
-
|
|
117
|
-
The three `def.override-*` rules are the Liskov Substitution
|
|
118
|
-
Principle signature rule applied across a project-defined
|
|
119
|
-
class/module hierarchy (superclass chain + included/prepended
|
|
120
|
-
modules, resolved cross-file). They are the conceptual subject of
|
|
43
|
+
### Confidence — reading the evidence tier
|
|
44
|
+
|
|
45
|
+
Every built-in diagnostic carries an `evidence_tier` —
|
|
46
|
+
`high` / `medium` / `low` — which is Rigor's own confidence
|
|
47
|
+
that the firing is a *true positive*, derived from the rule's
|
|
48
|
+
gates rather than from its severity. It is worth internalising
|
|
49
|
+
as a reading habit rather than a config knob: a `high` firing
|
|
50
|
+
(`call.undefined-method` on a concrete receiver) is almost
|
|
51
|
+
always a real bug and can be acted on directly, while a `low`
|
|
52
|
+
one (`call.unresolved-toplevel`) usually means the analyzer is
|
|
53
|
+
missing context — an unanalyzed file, a monkey-patch it never
|
|
54
|
+
saw — and reads better as "look at this" than "fix this". The
|
|
55
|
+
tier never feeds severity, and never changes whether a
|
|
56
|
+
diagnostic fires; it only routes your attention.
|
|
57
|
+
|
|
58
|
+
The per-rule tiers, and the `documentation_url` field that
|
|
59
|
+
rides alongside them in `--format json`, are in
|
|
60
|
+
[manual — Evidence tier](../manual/04-diagnostics.md#evidence-tier).
|
|
61
|
+
|
|
62
|
+
## The five families
|
|
63
|
+
|
|
64
|
+
Every rule ID reads `family.rule`, and the family tells you
|
|
65
|
+
what kind of proof failed. The catalogue — every rule, what it
|
|
66
|
+
fires on, its evidence tier — is in
|
|
67
|
+
[manual — Diagnostics](../manual/04-diagnostics.md#catalogue).
|
|
68
|
+
What follows is what each family is *about*.
|
|
69
|
+
|
|
70
|
+
**`call.*` — the call site's shape is wrong.** An undefined
|
|
71
|
+
method, an arity no signature accepts, an argument whose type
|
|
72
|
+
provably violates the parameter contract, a receiver that
|
|
73
|
+
might be `nil`. These are the highest-volume diagnostics on
|
|
74
|
+
real-world code, and also the most refined: every one of them
|
|
75
|
+
fires only when Rigor can prove the underlying fact about a
|
|
76
|
+
statically-known receiver, which is why a `call.*` firing is
|
|
77
|
+
usually worth reading first.
|
|
78
|
+
|
|
79
|
+
**`flow.*` — the control flow itself is unsound.** Something
|
|
80
|
+
provably raises on every path, a branch is dead, a `case`
|
|
81
|
+
clause can never match, a local is written and never read, a
|
|
82
|
+
Hash literal repeats a key. `flow.unreachable-branch`,
|
|
83
|
+
`flow.always-truthy-condition` and `flow.unreachable-clause`
|
|
84
|
+
form the **reachability family** — each proves that a piece of
|
|
85
|
+
code cannot run. `unreachable-clause` is the newest member and
|
|
86
|
+
deliberately quieter than its siblings (`:info` under
|
|
87
|
+
`balanced`) while its corpus false-positive gate finishes;
|
|
88
|
+
bump it with `severity_overrides:` if you want it louder.
|
|
89
|
+
|
|
90
|
+
**`def.*` — a definition violates the contract it declares.**
|
|
91
|
+
A body whose return drifts from the declared RBS return, an
|
|
92
|
+
instance variable written with two disagreeing types, an
|
|
93
|
+
explicit-receiver call into a private method. The three
|
|
94
|
+
`def.override-*` rules are the Liskov Substitution Principle's
|
|
95
|
+
signature rule applied across a project-defined hierarchy
|
|
96
|
+
(superclass chain plus included and prepended modules, resolved
|
|
97
|
+
cross-file): returns may narrow, parameters may widen,
|
|
98
|
+
visibility may not shrink. They are the conceptual subject of
|
|
121
99
|
[appendix: Liskov substitution](appendix-liskov.md).
|
|
122
100
|
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
`off` drops the diagnostic from the result entirely — useful
|
|
167
|
-
when you want a profile-wide setting for most rules but
|
|
168
|
-
silence one specifically.
|
|
169
|
-
|
|
170
|
-
Family wildcards work in overrides too:
|
|
171
|
-
|
|
172
|
-
```yaml
|
|
173
|
-
severity_overrides:
|
|
174
|
-
call: warning # demote every call.* rule
|
|
175
|
-
dump: off # drop every dump.* rule
|
|
176
|
-
```
|
|
177
|
-
|
|
178
|
-
Per-rule entries beat family-wildcard entries:
|
|
179
|
-
|
|
180
|
-
```yaml
|
|
181
|
-
severity_overrides:
|
|
182
|
-
call: warning # every call.* → warning
|
|
183
|
-
call.undefined-method: error # except undefined-method, still error
|
|
184
|
-
```
|
|
185
|
-
|
|
186
|
-
YAML reserves the bareword `off`. If the stripped severity
|
|
187
|
-
seems not to apply, quote it: `"off"`. Same for `on`.
|
|
188
|
-
|
|
189
|
-
## In-source suppression
|
|
190
|
-
|
|
191
|
-
```ruby
|
|
192
|
-
"hello".no_such_method # rigor:disable call.undefined-method
|
|
193
|
-
```
|
|
194
|
-
|
|
195
|
-
The comment must be on the same line as the diagnostic. Use
|
|
196
|
-
the qualified rule, the family wildcard, or `all`:
|
|
197
|
-
|
|
198
|
-
```ruby
|
|
199
|
-
"hello".no_such_method # rigor:disable call
|
|
200
|
-
"hello".no_such_method # rigor:disable all
|
|
201
|
-
```
|
|
202
|
-
|
|
203
|
-
For multiline blocks, suppress at every line — Rigor does
|
|
204
|
-
not yet ship a `disable-block` syntax.
|
|
205
|
-
|
|
206
|
-
### File-scope suppression
|
|
207
|
-
|
|
208
|
-
When you need to silence a rule everywhere in a file —
|
|
209
|
-
typically a generated file, a fixture, or a vendored snippet
|
|
210
|
-
that triggers a known false positive — drop a single
|
|
211
|
-
`# rigor:disable-file` comment anywhere in the file:
|
|
212
|
-
|
|
213
|
-
```ruby
|
|
214
|
-
# rigor:disable-file call.undefined-method
|
|
215
|
-
|
|
216
|
-
# This whole file is generated; the analyzer's call surface
|
|
217
|
-
# is mismatched with the runtime layer for these stubs.
|
|
218
|
-
```
|
|
219
|
-
|
|
220
|
-
Convention is to put the comment near the top, but Rigor
|
|
221
|
-
scans every comment in the file so any placement works. The
|
|
222
|
-
same token forms apply: qualified rule, family wildcard, or
|
|
223
|
-
`all`. The line-scope `# rigor:disable` form continues to
|
|
224
|
-
work — the two compose, and any project-wide
|
|
225
|
-
`disable: [...]` in `.rigor.yml` also still applies.
|
|
226
|
-
|
|
227
|
-
## Project-wide suppression
|
|
228
|
-
|
|
229
|
-
```yaml
|
|
230
|
-
# .rigor.yml
|
|
231
|
-
disable:
|
|
232
|
-
- call.possible-nil-receiver
|
|
233
|
-
```
|
|
234
|
-
|
|
235
|
-
Drops the rule project-wide. Heavier hammer than
|
|
236
|
-
`severity_overrides: { call.possible-nil-receiver: off }` —
|
|
237
|
-
both work; the choice is stylistic.
|
|
238
|
-
|
|
239
|
-
## Baseline diffing for CI
|
|
240
|
-
|
|
241
|
-
When you adopt Rigor on an existing codebase, you usually
|
|
242
|
-
inherit a long tail of legitimate-but-pre-existing diagnostics
|
|
243
|
-
that nobody is going to fix today. The pragmatic move is to
|
|
244
|
-
**snapshot the current state as a baseline** and then have CI
|
|
245
|
-
fail only on *new* diagnostics introduced by a PR:
|
|
246
|
-
|
|
247
|
-
```sh
|
|
248
|
-
# Once: capture the current diagnostic surface.
|
|
249
|
-
rigor check --format=json > rigor.baseline.json
|
|
250
|
-
git add rigor.baseline.json
|
|
251
|
-
git commit
|
|
252
|
-
|
|
253
|
-
# Per PR: compare against the committed baseline.
|
|
254
|
-
rigor diff rigor.baseline.json
|
|
255
|
-
```
|
|
256
|
-
|
|
257
|
-
`rigor diff` prints `+ NEW` rows for each diagnostic that
|
|
258
|
-
wasn't in the baseline and `- FIXED` rows for each that has
|
|
259
|
-
been resolved since. The exit code is `1` when any new
|
|
260
|
-
diagnostic appears and `0` otherwise — so adding a new
|
|
261
|
-
violation fails CI, but the legacy diagnostics recorded in
|
|
262
|
-
the baseline don't.
|
|
263
|
-
|
|
264
|
-
When you fix a row in the baseline, regenerate it with the
|
|
265
|
-
same `rigor check --format=json > rigor.baseline.json` so
|
|
266
|
-
the project tightens monotonically over time. The
|
|
267
|
-
`--format=json` form of `rigor diff` itself is also
|
|
268
|
-
available for editor / dashboard integrations.
|
|
269
|
-
|
|
270
|
-
`rigor diff` is the lightweight, ad-hoc form — a JSON file you
|
|
271
|
-
diff by hand in a CI script. Most projects instead adopt the
|
|
272
|
-
**managed baseline**: `rigor baseline generate` writes a
|
|
273
|
-
`.rigor-baseline.yml`, you point at it with the `baseline:`
|
|
274
|
-
config key, and from then on `rigor check` itself exits clean
|
|
275
|
-
on recorded diagnostics and surfaces only new ones — no
|
|
276
|
-
separate diff step. That is the path the
|
|
277
|
-
[`rigor-project-init` skill](../manual/14-rails-quickstart.md)
|
|
278
|
-
sets up for you; see [Baselines](../manual/06-baseline.md) for
|
|
279
|
-
the full workflow ([ADR-22](../adr/22-baseline-and-project-onboarding.md)
|
|
280
|
-
for the design).
|
|
101
|
+
**`assert.*` and `dump.*` — the introspection helpers.**
|
|
102
|
+
`assert.type-mismatch` fires when an `assert_type("expected",
|
|
103
|
+
value)` call disagrees with the inferred type, so a snippet in
|
|
104
|
+
this handbook is a test of the engine as much as an
|
|
105
|
+
illustration. `dump.type` is not a problem report at all — it
|
|
106
|
+
is your probe during debugging: sprinkle `dump_type(value)`
|
|
107
|
+
through suspicious code, run `rigor check`, and read the
|
|
108
|
+
inferred types straight out of the diagnostic stream.
|
|
109
|
+
|
|
110
|
+
## Turning a diagnostic down
|
|
111
|
+
|
|
112
|
+
Rigor gives you five layers, and picking the right one is
|
|
113
|
+
mostly a question of *how much* you want to say:
|
|
114
|
+
|
|
115
|
+
1. **`severity_profile:`** — the project's overall stance.
|
|
116
|
+
`lenient` for a legacy codebase you are easing Rigor into,
|
|
117
|
+
`balanced` for everyday work, `strict` for a project with
|
|
118
|
+
no legacy noise.
|
|
119
|
+
2. **`severity_overrides:`** — one rule (or one family) at a
|
|
120
|
+
different severity from the rest of the profile. The right
|
|
121
|
+
layer when a rule is *useful but not blocking* for you.
|
|
122
|
+
3. **`disable:`** — the rule is off project-wide. Heavier than
|
|
123
|
+
an override to `off`; both work, and the choice is mostly
|
|
124
|
+
stylistic.
|
|
125
|
+
4. **`# rigor:disable` / `# rigor:disable-file`** — this line,
|
|
126
|
+
or this file. The right layer when the analyzer is wrong
|
|
127
|
+
*here* and right everywhere else. Prefer it to a
|
|
128
|
+
project-wide switch: it keeps the exception visible next to
|
|
129
|
+
the code that needed it, and it is the thing you later
|
|
130
|
+
promote into an `RBS::Extended` directive.
|
|
131
|
+
5. **A [baseline](../manual/06-baseline.md)** — the whole
|
|
132
|
+
existing backlog, recorded rather than hidden, so new
|
|
133
|
+
diagnostics still surface. This is the layer to reach for
|
|
134
|
+
on adoption day, and the one to reach for *instead of*
|
|
135
|
+
`disable:` when the rule is genuinely finding things you
|
|
136
|
+
have simply not fixed yet.
|
|
137
|
+
|
|
138
|
+
The exact syntax of all five — profile table, override
|
|
139
|
+
precedence, the three suppression forms, the baseline file and
|
|
140
|
+
its `rigor baseline` commands — is in
|
|
141
|
+
[manual — Diagnostics](../manual/04-diagnostics.md#severity-profiles)
|
|
142
|
+
and [manual — Baselines](../manual/06-baseline.md).
|
|
281
143
|
|
|
282
144
|
## Why a diagnostic might NOT fire when you expected one
|
|
283
145
|
|
|
@@ -357,6 +219,11 @@ The pragmatic loop on a project that just adopted Rigor:
|
|
|
357
219
|
`# rigor:disable` lines into `RBS::Extended` directives
|
|
358
220
|
so the analyzer learns the real contract.
|
|
359
221
|
|
|
222
|
+
On a codebase too large for step 2 in one sitting, record the
|
|
223
|
+
existing diagnostics as a
|
|
224
|
+
[baseline](../manual/06-baseline.md) first and run the loop
|
|
225
|
+
against what CI newly surfaces.
|
|
226
|
+
|
|
360
227
|
A clean `rigor check` run is the goal; a green CI badge says
|
|
361
228
|
"every diagnostic that fires is one we accept."
|
|
362
229
|
|
data/docs/handbook/09-plugins.md
CHANGED
|
@@ -9,9 +9,10 @@ It does **not** teach plugin *authoring*. That lives in
|
|
|
9
9
|
[`examples/`](../../examples/README.md) — six tutorial
|
|
10
10
|
walkthroughs, each spotlighting one extension surface.
|
|
11
11
|
Ready-to-install gems for real frameworks live in
|
|
12
|
-
[`plugins/`](../../plugins/README.md)
|
|
13
|
-
|
|
14
|
-
|
|
12
|
+
[`plugins/`](../../plugins/README.md), and activating one is
|
|
13
|
+
[manual — Using plugins](../manual/07-plugins.md). Read on to
|
|
14
|
+
decide whether you need a plugin; go to `examples/` once you
|
|
15
|
+
want to write one.
|
|
15
16
|
|
|
16
17
|
## When you reach for a plugin
|
|
17
18
|
|
|
@@ -51,151 +52,60 @@ compares the six worked examples on architectural axes
|
|
|
51
52
|
engine-collaboration via `Scope#type_of`, cross-plugin facts,
|
|
52
53
|
return-type contributions, …) and recommends a reading order.
|
|
53
54
|
|
|
54
|
-
##
|
|
55
|
+
## Two authoring paths
|
|
55
56
|
|
|
56
57
|
> Still here? Most readers should jump to
|
|
57
58
|
> [Should you write one?](#should-you-write-one) first — the
|
|
58
59
|
> answer is usually "no, RBS and `RBS::Extended` get you
|
|
59
|
-
> there."
|
|
60
|
-
|
|
61
|
-
The
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
which
|
|
107
|
-
|
|
108
|
-
## Macro / DSL expansion substrate (ADR-16)
|
|
109
|
-
|
|
110
|
-
A second authoring path was added on top of the hand-rolled
|
|
111
|
-
walker contract above: the **macro expansion substrate**
|
|
112
|
-
(ADR-16). For metaprogramming-heavy DSLs — Rails-style
|
|
113
|
-
`has_one_attached`, dry-struct's `attribute`, Devise's
|
|
114
|
-
`devise :strategy`, Sinatra's `get '/foo' do ... end` — the
|
|
115
|
-
substrate lets a plugin author **declare** the call shape
|
|
116
|
-
instead of walking the AST by hand. The plugin's body becomes
|
|
117
|
-
a single manifest entry; the substrate handles literal-symbol
|
|
118
|
-
extraction, name interpolation, registry lookup, and per-method
|
|
119
|
-
synthesis.
|
|
120
|
-
|
|
121
|
-
Four tier shapes are recognised. The
|
|
122
|
-
[per-library survey](../notes/20260515-macro-expansion-library-survey.md)
|
|
123
|
-
identifies which libraries fit each tier and which fall
|
|
124
|
-
outside the substrate's scope.
|
|
125
|
-
|
|
126
|
-
| Tier | Shape | Manifest declaration | Worked example |
|
|
127
|
-
| --- | --- | --- | --- |
|
|
128
|
-
| **A — block-as-method** | DSL call's block runs as an instance method on the receiver class (`Sinatra::Base#generate_method`) | `block_as_methods: [Macro::BlockAsMethod.new(receiver_constraint:, method_names:)]` | [`rigor-sinatra`](../../plugins/rigor-sinatra/) |
|
|
129
|
-
| **B — trait-inlining registry** | Class-level call enumerates symbols → bundled registry maps each to a module → substrate explodes the module's RBS methods onto the calling class | `trait_registries: [Macro::TraitRegistry.new(receiver_constraint:, method_name:, modules_by_symbol:, always_included:)]` | [`rigor-devise`](../../plugins/rigor-devise/) |
|
|
130
|
-
| **C — heredoc template** | Class-level call interpolates a literal symbol into a method-name template; substrate emits synthetic readers | `heredoc_templates: [Macro::HeredocTemplate.new(receiver_constraint:, method_name:, symbol_arg_position:, emit:)]` | [`rigor-dry-struct`](../../plugins/rigor-dry-struct/) |
|
|
131
|
-
|
|
132
|
-
The three Tier-A/B/C plugins above are each ~60–110 LoC of
|
|
133
|
-
**purely declarative** Ruby — no walker, no
|
|
134
|
-
`diagnostics_for_file`, no plugin-side state. The substrate's
|
|
135
|
-
pre-pass + dispatcher integration do the work.
|
|
136
|
-
|
|
137
|
-
### Concern re-targeting
|
|
138
|
-
|
|
139
|
-
`ActiveSupport::Concern.included do ... end` is a *deferred
|
|
140
|
-
class_eval*: any DSL calls inside the block fire on whoever
|
|
141
|
-
includes the concern, not on the concern module itself. The
|
|
142
|
-
substrate's scanner handles this re-targeting automatically.
|
|
143
|
-
For source like:
|
|
144
|
-
|
|
145
|
-
```ruby
|
|
146
|
-
module Auditable
|
|
147
|
-
extend ActiveSupport::Concern
|
|
148
|
-
included do
|
|
149
|
-
attribute :audited_at, Types::Time
|
|
150
|
-
end
|
|
151
|
-
end
|
|
152
|
-
|
|
153
|
-
class Address < Dry::Struct
|
|
154
|
-
include Auditable
|
|
155
|
-
attribute :city, Types::String
|
|
156
|
-
end
|
|
157
|
-
```
|
|
158
|
-
|
|
159
|
-
`Address` gets BOTH `city` (direct) AND `audited_at` (re-targeted
|
|
160
|
-
from `Auditable`) as synthetic readers. The same pattern works
|
|
161
|
-
for Tier B traits (Devise modules included via Concerns).
|
|
162
|
-
|
|
163
|
-
### Floor / ceiling
|
|
164
|
-
|
|
165
|
-
Per ADR-16 § WD13, the **floor** is that synthetic methods emit
|
|
166
|
-
by NAME so cross-file dispatch resolves (no more
|
|
167
|
-
`call.undefined-method`). The common cases also recover precise
|
|
168
|
-
return types: **Tier B** redispatches on the origin module's
|
|
169
|
-
authored RBS (a Devise `valid_password?` resolves to `bool`,
|
|
170
|
-
not `Dynamic[T]`), and **Tier C** resolves a plain class-name
|
|
171
|
-
return to its `Nominal`. What still degrades to `Dynamic[T]` is
|
|
172
|
-
the parameterised / utility-type-shaped Tier C return
|
|
173
|
-
(`Array[String]`, `Pick<T, K>`); routing those through the
|
|
174
|
-
[ADR-13](../adr/13-typenode-resolver-plugin.md) resolver chain
|
|
175
|
-
is the **ceiling**, demand-driven. The substrate never
|
|
176
|
-
*fabricates* precision per ADR-5 robustness.
|
|
177
|
-
|
|
178
|
-
### Choosing between the substrate and a hand-rolled walker
|
|
179
|
-
|
|
180
|
-
| If the DSL is… | Use the substrate | Use a hand-rolled walker |
|
|
181
|
-
| --- | --- | --- |
|
|
182
|
-
| `class-level call with literal symbol args + framework class_eval'd heredoc` | ✓ Tier C | — |
|
|
183
|
-
| `class-level call with literal symbol args + registry-driven module include` | ✓ Tier B | — |
|
|
184
|
-
| `class-level call with do…end block running as an instance method` | ✓ Tier A | — |
|
|
185
|
-
| `external Ruby files instance_eval'd under a declared self` | ✓ Tier D (contract only as of v0.1.x) | — |
|
|
186
|
-
| `domain DSL whose return type depends on argument shape` | — | `dynamic_return` ([`rigor-lisp-eval`](../../examples/rigor-lisp-eval/)) |
|
|
187
|
-
| `cross-file validation (collect declarations, then validate uses)` | — | Two-pass walker ([`rigor-statesman`](../../plugins/rigor-statesman/)) |
|
|
188
|
-
| `parsing an external project file (routes, schema, locale)` | — | `IoBoundary` + cache producer ([`rigor-routes`](../../examples/rigor-routes/)) |
|
|
189
|
-
| `schema-graph recorder (GraphQL-Ruby-style)` | — | Schema-resolution pass (no plugin authored yet) |
|
|
190
|
-
|
|
191
|
-
The substrate and the hand-rolled walker contract coexist —
|
|
192
|
-
a plugin can mix `manifest`-declared substrate entries with a
|
|
193
|
-
`diagnostics_for_file` walker. The
|
|
194
|
-
[`skills/rigor-plugin-author/SKILL.md`](../../skills/rigor-plugin-author/SKILL.md)
|
|
195
|
-
SKILL captures the decision flow in detail; the survey at
|
|
196
|
-
[`docs/notes/20260515-macro-expansion-library-survey.md`](../notes/20260515-macro-expansion-library-survey.md)
|
|
197
|
-
records which Ruby libraries the substrate covers and which
|
|
198
|
-
fall outside.
|
|
60
|
+
> there." What follows is for when it is "yes."
|
|
61
|
+
|
|
62
|
+
The decision that shapes everything else is *which* of the two
|
|
63
|
+
authoring paths your DSL falls into.
|
|
64
|
+
|
|
65
|
+
**Declare it.** If the DSL is a class-level call with literal
|
|
66
|
+
symbol arguments — a Rails-style `has_one_attached`, a
|
|
67
|
+
dry-struct `attribute`, a Devise `devise :strategy`, a Sinatra
|
|
68
|
+
`get "/foo" do … end` — the **macro-expansion substrate**
|
|
69
|
+
([ADR-16](../adr/16-macro-expansion.md)) already knows that
|
|
70
|
+
shape. You write a manifest entry describing the call, and the
|
|
71
|
+
substrate does the literal-symbol extraction, the name
|
|
72
|
+
interpolation and the per-method synthesis. The three bundled
|
|
73
|
+
plugins on this path are 60–110 lines of declarative Ruby with
|
|
74
|
+
no AST walking at all. The substrate also understands
|
|
75
|
+
`ActiveSupport::Concern`'s deferred `included do … end` block,
|
|
76
|
+
so a DSL call written inside a concern lands on the class that
|
|
77
|
+
includes it rather than on the concern.
|
|
78
|
+
|
|
79
|
+
**Walk it.** If the type depends on something the shape of the
|
|
80
|
+
call cannot tell you — argument *values* (`Lisp.eval` above), a
|
|
81
|
+
declaration made elsewhere in the project, or the contents of
|
|
82
|
+
an external file such as a route table or a schema dump — you
|
|
83
|
+
write a walker instead, and the plugin contract gives you the
|
|
84
|
+
hooks for it: a per-file emission pass, per-call-site
|
|
85
|
+
return-type and flow-narrowing contributions, sandboxed file
|
|
86
|
+
and HTTPS reads under a trust policy, cached producers for
|
|
87
|
+
expensive parses, and a cross-plugin fact store so one plugin's
|
|
88
|
+
parse feeds another plugin's checks.
|
|
89
|
+
|
|
90
|
+
The two paths coexist — one plugin can declare substrate
|
|
91
|
+
entries *and* walk files — and where you go next depends on
|
|
92
|
+
which of them you need:
|
|
93
|
+
|
|
94
|
+
- [`examples/README.md`](../../examples/README.md) — the six
|
|
95
|
+
walkthroughs, each spotlighting one contract surface, with a
|
|
96
|
+
map of which example demonstrates which one.
|
|
97
|
+
- [`docs/internal-spec/plugin.md`](../internal-spec/plugin.md)
|
|
98
|
+
— the binding plugin contract: manifest, hooks, services,
|
|
99
|
+
registry, load order. Its siblings
|
|
100
|
+
[`plugin-trust.md`](../internal-spec/plugin-trust.md) and
|
|
101
|
+
[`plugin-cache-producers.md`](../internal-spec/plugin-cache-producers.md)
|
|
102
|
+
cover the I/O and caching surfaces.
|
|
103
|
+
- [`docs/internal-spec/macro-substrate.md`](../internal-spec/macro-substrate.md)
|
|
104
|
+
— the substrate's tiers, the manifest field each one
|
|
105
|
+
declares, and how much return-type precision each recovers.
|
|
106
|
+
- [The macro-expansion library survey](../notes/20260515-macro-expansion-library-survey.md)
|
|
107
|
+
— which real Ruby libraries fit which tier, and which fall
|
|
108
|
+
outside the substrate entirely.
|
|
199
109
|
|
|
200
110
|
## Should you write one?
|
|
201
111
|
|
data/docs/handbook/README.md
CHANGED
|
@@ -56,9 +56,11 @@ up the flag, key, or command that *acts* on it.
|
|
|
56
56
|
how to nudge it through `.rbs` files and `%a{rigor:v1:…}`
|
|
57
57
|
directives.
|
|
58
58
|
8. [**Understanding errors**](08-understanding-errors.md) —
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
59
|
+
reading a diagnostic: what each rule family claims, why one
|
|
60
|
+
fires when you did not expect it (and stays silent when you
|
|
61
|
+
did), and which layer to reach for when you want it quieter.
|
|
62
|
+
The catalogue itself is
|
|
63
|
+
[manual — Diagnostics](../manual/04-diagnostics.md).
|
|
62
64
|
9. [**Plugins**](09-plugins.md) — when to author one,
|
|
63
65
|
pointer to the [examples/](../../examples/README.md)
|
|
64
66
|
landing page.
|