rigortype 0.1.13 → 0.1.15
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 +18 -2
- data/lib/rigor/analysis/check_rules.rb +403 -5
- data/lib/rigor/analysis/diagnostic.rb +15 -3
- data/lib/rigor/analysis/rule_catalog.rb +80 -0
- data/lib/rigor/analysis/runner.rb +10 -0
- data/lib/rigor/cli/plugin_command.rb +245 -0
- data/lib/rigor/cli.rb +8 -0
- data/lib/rigor/configuration/severity_profile.rb +9 -0
- data/lib/rigor/environment/rbs_collection_discovery.rb +29 -7
- data/lib/rigor/environment/rbs_loader.rb +17 -0
- data/lib/rigor/environment.rb +13 -3
- data/lib/rigor/inference/scope_indexer.rb +59 -21
- data/lib/rigor/scope.rb +27 -1
- data/lib/rigor/triage/catalogue.rb +71 -5
- data/lib/rigor/version.rb +1 -1
- data/sig/rigor/environment.rbs +1 -0
- data/sig/rigor/scope.rbs +3 -0
- data/skills/rigor-plugin-author/SKILL.md +20 -0
- data/skills/rigor-plugin-author/references/01-plan-and-scaffold.md +59 -21
- data/skills/rigor-plugin-author/references/02-walker-and-types.md +64 -15
- data/skills/rigor-project-init/SKILL.md +72 -7
- data/skills/rigor-project-init/references/01-detect.md +42 -4
- data/skills/rigor-project-init/references/02-configure.md +17 -0
- data/skills/rigor-project-init/references/03-baseline-and-bugs.md +237 -22
- metadata +2 -1
|
@@ -49,8 +49,10 @@ Use the three sections like this:
|
|
|
49
49
|
| Hint `id` | Cause | Where this skill handles it |
|
|
50
50
|
| --- | --- | --- |
|
|
51
51
|
| `activesupport-core-ext` | ActiveSupport core-class monkey-patches not loaded. | Go back to Phase 3/4: add `rigor-activesupport-core-ext` to `plugins:` (it is an RBS-bundle plugin), re-run triage. This is a config gap, not a bug. |
|
|
52
|
-
| `gem-without-rbs` | A dependency ships no RBS. | Phase
|
|
53
|
-
| `project-monkey-patch` |
|
|
52
|
+
| `gem-without-rbs` | A dependency ships no RBS. | If `rbs_collection.lock.yaml` was present and Phase 1 installed the collection, re-run `rigor triage` — the hint may shrink or disappear. Otherwise: Phase 8 escalation — `bundle exec rbs collection install`, or `dependencies.source_inference:`, or open a Rigor issue. |
|
|
53
|
+
| `project-monkey-patch-known` | **High confidence.** The engine proved the called method *is* defined by a project file (a reopened core/stdlib/gem class) but is not applied cross-file. The hint **names the defining file(s)**. | Phase 7 escalation — copy the named file(s) straight into `pre_eval:`. No detective work needed; the diagnostic already found the source. |
|
|
54
|
+
| `project-monkey-patch` | An in-project monkey-patch / refinement Rigor did not see, inferred from the *spread* of the same method across ≥3 files (no proven def site). | Phase 7 escalation — find the defining file (grep for `def <method>` / `class <Receiver>`), register it via `pre_eval:`, or (if it is a DSL) write a project plugin. |
|
|
55
|
+
| `unresolved-toplevel` | Toplevel calls (outside any `def`/`class`/`module`) that resolve to nothing visible — usually a script relying on a monkey-patch or a `require`d helper Rigor did not walk (ADR-34). | Phase 7 escalation — if a project file defines these (toplevel `def`, or a patch on `Object`/`Kernel`), list it in `pre_eval:`. If nothing defines them, treat as genuine typos / missing requires (Phase 8). |
|
|
54
56
|
| `activerecord-relation-misinference` | An ActiveRecord relation inferred as `Array`. | Ensure `rigor-activerecord` is enabled (Phase 3). If it persists, it is an engine gap — open a Rigor issue. |
|
|
55
57
|
| `systemic-file-cluster` | One file × one rule, large count. | Acknowledge mode: a clean baseline bucket. Strict mode: a single fix may clear many — review that file first. |
|
|
56
58
|
| `genuine-bugs` | Low-count rules scattered across files. | **Phase 7** — these are the localised bugs Rigor caught. Review first, in both modes. Note: the hint groups all low-count rules regardless of severity — filter for `error` severity when prioritising actionable items. |
|
|
@@ -60,6 +62,80 @@ If triage flags `activesupport-core-ext` (or any config gap),
|
|
|
60
62
|
baseline and the real-bug review should both run against the
|
|
61
63
|
post-config diagnostic set, not the inflated one.
|
|
62
64
|
|
|
65
|
+
## Phase 6a — Pre-baseline cleanup
|
|
66
|
+
|
|
67
|
+
Before generating the baseline, **apply every quick fix that triage
|
|
68
|
+
has already diagnosed**. A smaller baseline is a better baseline: each
|
|
69
|
+
bucket it omits is a regression that will surface the moment it
|
|
70
|
+
appears, rather than hiding behind a ceiling that the monkey-patch
|
|
71
|
+
noise inflated.
|
|
72
|
+
|
|
73
|
+
Quick fixes that belong here (apply now, not as post-baseline
|
|
74
|
+
escalation):
|
|
75
|
+
|
|
76
|
+
### `project-monkey-patch` / `project-monkey-patch-known` → `pre_eval:`
|
|
77
|
+
|
|
78
|
+
When triage reports either hint, act before Phase 7:
|
|
79
|
+
|
|
80
|
+
1. **`project-monkey-patch-known`** — the hint's action line **names
|
|
81
|
+
the defining file(s)**. Copy them straight into `pre_eval:` in
|
|
82
|
+
`.rigor.dist.yml`:
|
|
83
|
+
|
|
84
|
+
```yaml
|
|
85
|
+
pre_eval:
|
|
86
|
+
- lib/core_ext/user_extensions.rb # exact path(s) named by the hint
|
|
87
|
+
```
|
|
88
|
+
|
|
89
|
+
2. **`project-monkey-patch`** (spread-based, no proven def site) —
|
|
90
|
+
find the defining file manually:
|
|
91
|
+
|
|
92
|
+
```sh
|
|
93
|
+
# Replace `current` / `User` with the method and receiver named in the hint
|
|
94
|
+
grep -rn "def self\.current\|cattr_accessor.*current" app/ lib/
|
|
95
|
+
grep -rn "def deliver_\|def find_by_" app/ lib/
|
|
96
|
+
```
|
|
97
|
+
|
|
98
|
+
Then add the file(s) to `pre_eval:`.
|
|
99
|
+
|
|
100
|
+
After adding `pre_eval:` entries, **re-run `rigor triage`** and note
|
|
101
|
+
the new total. If the count dropped by more than ~50 diagnostics,
|
|
102
|
+
the reduction is significant enough to justify this round; proceed
|
|
103
|
+
with the new, smaller count.
|
|
104
|
+
|
|
105
|
+
Repeat the loop (find → `pre_eval:` → re-triage) until the hint
|
|
106
|
+
disappears or the remaining count is stable. Then move to Phase 7.
|
|
107
|
+
|
|
108
|
+
> **If a cluster does NOT drop after you add its file to `pre_eval:`**,
|
|
109
|
+
> the methods are almost certainly **generated dynamically**
|
|
110
|
+
> (`define_method` with a computed name, `method_missing`, or a
|
|
111
|
+
> `class_eval` heredoc) — the pre-eval walker found no literal `def` to
|
|
112
|
+
> register. That is not a `pre_eval:` failure to retry; it is the signal
|
|
113
|
+
> that the durable fix is a **project-owned plugin**. Leave the file in
|
|
114
|
+
> `pre_eval:` only if it also defines literal methods; otherwise remove
|
|
115
|
+
> it and carry the cluster to Phase 8 § "Escalation path A", which
|
|
116
|
+
> hands off to the `rigor-plugin-author` skill.
|
|
117
|
+
|
|
118
|
+
> **Why `pre_eval:` reduces the count.** `pre_eval:` files are walked
|
|
119
|
+
> before per-file inference. Methods defined in them — including those
|
|
120
|
+
> added to existing classes via `class Foo; def bar; end; end` — become
|
|
121
|
+
> visible to every subsequent file analysis. The monkey-patched methods
|
|
122
|
+
> are no longer unknown, so `call.undefined-method` diagnostics that
|
|
123
|
+
> depended on them disappear.
|
|
124
|
+
|
|
125
|
+
### `gem-without-rbs` (if rbs collection was not yet installed)
|
|
126
|
+
|
|
127
|
+
If Phase 1 did not install the collection (project had no
|
|
128
|
+
`rbs_collection.lock.yaml`) and triage now reports `gem-without-rbs`,
|
|
129
|
+
this is the right moment to act before the baseline:
|
|
130
|
+
|
|
131
|
+
```sh
|
|
132
|
+
bundle exec rbs collection install # if rbs is in Gemfile
|
|
133
|
+
# or: rbs collection install
|
|
134
|
+
```
|
|
135
|
+
|
|
136
|
+
Re-run `rigor triage`. If the `gem-without-rbs` count drops,
|
|
137
|
+
re-generate the baseline against the new number.
|
|
138
|
+
|
|
63
139
|
## Phase 7 — Generate the baseline (acknowledge mode only)
|
|
64
140
|
|
|
65
141
|
**Strict mode skips this phase entirely.** A strict project has no
|
|
@@ -120,30 +196,160 @@ fine; the baseline is a starting envelope, not a verdict that the
|
|
|
120
196
|
bug is acceptable. Recommend the user run the `rigor-baseline-reduce`
|
|
121
197
|
skill next to work them down.
|
|
122
198
|
|
|
199
|
+
### Distinguishing sig quality false positives from real bugs
|
|
200
|
+
|
|
201
|
+
Some diagnostics in the `call.wrong-arity` and
|
|
202
|
+
`def.return-type-mismatch` families are caused by **incomplete or
|
|
203
|
+
incorrect `sig/` declarations**, not by real bugs in the project code.
|
|
204
|
+
Identify them before presenting findings to the user — a sig FP looks
|
|
205
|
+
alarming but needs a sig fix, not a code fix.
|
|
206
|
+
|
|
207
|
+
#### `call.wrong-arity` on `Struct.new(...)` subclasses
|
|
208
|
+
|
|
209
|
+
When a project defines a class as:
|
|
210
|
+
|
|
211
|
+
```ruby
|
|
212
|
+
MyStruct = Struct.new(:foo, :bar, :baz)
|
|
213
|
+
# or
|
|
214
|
+
class MyRecord < Struct.new(:id, :name)
|
|
215
|
+
```
|
|
216
|
+
|
|
217
|
+
and its `sig/` entry is an empty shell:
|
|
218
|
+
|
|
219
|
+
```rbs
|
|
220
|
+
class MyRecord
|
|
221
|
+
end
|
|
222
|
+
```
|
|
223
|
+
|
|
224
|
+
Rigor reads the empty sig and infers `initialize` takes 0 arguments
|
|
225
|
+
(the default). Any `MyRecord.new(x, y)` call then fires
|
|
226
|
+
`call.wrong-arity`. This is a **sig quality issue** — the sig is
|
|
227
|
+
missing the generated `initialize`.
|
|
228
|
+
|
|
229
|
+
To confirm: check the sig file for the class. If the sig has no
|
|
230
|
+
`initialize` def AND the Ruby source inherits from `Struct.new(...)`,
|
|
231
|
+
the diagnostic is a FP. Note it as a sig improvement task (add
|
|
232
|
+
`initialize` matching the Struct fields) rather than a code bug.
|
|
233
|
+
|
|
234
|
+
#### `def.return-type-mismatch` when the declared type is `bot`
|
|
235
|
+
|
|
236
|
+
A sig that declares `-> bot` (or `-> void` for a `raises`-only
|
|
237
|
+
method) says: "this method never returns normally". If Rigor infers
|
|
238
|
+
an actual return value, it fires `def.return-type-mismatch`.
|
|
239
|
+
|
|
240
|
+
Two interpretations:
|
|
241
|
+
|
|
242
|
+
| Sig says `-> bot` and … | Interpretation |
|
|
243
|
+
| --- | --- |
|
|
244
|
+
| the implementation has a missing `raise` on one branch | **Likely real bug** — the method was *intended* to always raise, but a code path escapes. High priority: review the branch. |
|
|
245
|
+
| the sig was written conservatively (e.g. auto-generated) and the method does sometimes return | **Sig quality issue** — the sig is too strict. Fix by loosening the return type in the sig. |
|
|
246
|
+
|
|
247
|
+
To distinguish: read the method body. If every branch ends in `raise`
|
|
248
|
+
or `exit` except one, the missing raise is likely a bug. If the
|
|
249
|
+
method has intentional return values but the sig says `bot`, it is a
|
|
250
|
+
sig issue.
|
|
251
|
+
|
|
252
|
+
#### `call.argument-type-mismatch` on regex capture variables (`$1`, `$~`)
|
|
253
|
+
|
|
254
|
+
Rigor infers `$1`, `$~`, and similar capture variables as
|
|
255
|
+
`String | nil` everywhere, even inside `gsub`/`match` blocks where
|
|
256
|
+
they are guaranteed non-nil by the match condition. Diagnostics of
|
|
257
|
+
the form:
|
|
258
|
+
|
|
259
|
+
```
|
|
260
|
+
expected String, got String | nil (on $1 / $~)
|
|
261
|
+
```
|
|
262
|
+
|
|
263
|
+
are **engine FPs** (ADR-24 WD3 / known limitation). Note them as
|
|
264
|
+
noise rather than surfacing them as bugs. They belong in the
|
|
265
|
+
baseline.
|
|
266
|
+
|
|
123
267
|
### Escalation path A — application-specific metaprogramming
|
|
124
268
|
|
|
125
|
-
If triage reports `project-monkey-patch`,
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
269
|
+
If triage reports `project-monkey-patch-known`, `project-monkey-patch`,
|
|
270
|
+
`unresolved-toplevel`, or a `call.undefined-method` cluster lands on
|
|
271
|
+
the project's own DSL / `define_method` factory / in-house macro,
|
|
272
|
+
Rigor cannot follow it by default.
|
|
273
|
+
|
|
274
|
+
**The decision that picks the fix is static-vs-dynamic.** Find the
|
|
275
|
+
defining file (named by the hint, or `grep -rn 'def <method>\|<Receiver>'
|
|
276
|
+
app/ lib/`) and look at *how* the method is defined:
|
|
277
|
+
|
|
278
|
+
| How the method is defined | Fix | Why |
|
|
279
|
+
| --- | --- | --- |
|
|
280
|
+
| **Literal `def foo` / `def self.foo`** in a project file (a plain reopen / monkey-patch, e.g. `lib/core_ext/string_extensions.rb`, `User.current`) | **`pre_eval:`** — copy the file into `pre_eval:` in `.rigor.dist.yml`. | Rigor walks `pre_eval:` files before per-file inference, so the literal method becomes visible everywhere. This is Phase 6a; do it now. |
|
|
281
|
+
| **Dynamically generated** — computed-name `define_method`, `method_missing`, or a `class_eval <<~RUBY … def #{name} … RUBY` heredoc template | **A project-owned Rigor plugin** (escalation, below). `pre_eval:` will *not* help. | The pre-eval walker finds no literal `def` to register, so the cluster survives `pre_eval:`. If you added the file to `pre_eval:` and re-triage shows the cluster unchanged, this is the case you are in. |
|
|
282
|
+
|
|
283
|
+
So the routine is: try `pre_eval:` for the static case in Phase 6a; if a
|
|
284
|
+
cluster **survives** because the methods are generated, that surviving
|
|
285
|
+
residue is the plugin signal.
|
|
286
|
+
|
|
287
|
+
#### The plugin handoff (the recommended next step)
|
|
288
|
+
|
|
289
|
+
A genuine generated DSL is **the project's own plugin to own** — Rigor
|
|
290
|
+
does not bundle per-application plugins for it. The textbook example is
|
|
291
|
+
Redmine's settings accessors:
|
|
292
|
+
|
|
293
|
+
```ruby
|
|
294
|
+
# app/models/setting.rb — generated, NOT a literal def
|
|
295
|
+
def self.define_setting(name, options = {})
|
|
296
|
+
# class_eval a heredoc that defines self.#{name} / #{name}? / #{name}=
|
|
297
|
+
end
|
|
298
|
+
# names come from config/settings.yml, iterated — not source literals
|
|
299
|
+
```
|
|
300
|
+
|
|
301
|
+
`Setting.app_title`, `Setting.default_language`, … are never written as
|
|
302
|
+
literal `def`s, so `pre_eval:` cannot see them; the durable fix is a
|
|
303
|
+
project plugin (it matches ADR-16's Tier C heredoc-template shape).
|
|
304
|
+
Rigor's stance: application-specific homegrown DSLs are out of scope for
|
|
305
|
+
the bundled substrate (ADR-16 § Audience) — the app authors the plugin
|
|
306
|
+
in their own repo.
|
|
307
|
+
|
|
308
|
+
> **Sizing the cluster — `rigor triage` may not isolate it.** Triage's
|
|
309
|
+
> `project-monkey-patch` hint groups by method spread and often lumps a
|
|
310
|
+
> generated-DSL cluster in with unrelated patches (or buries it), so the
|
|
311
|
+
> hint count is not the per-receiver size. To measure the actual cluster,
|
|
312
|
+
> drop to the raw stream and count by receiver:
|
|
313
|
+
>
|
|
314
|
+
> ```sh
|
|
315
|
+
> rigor check --format json | \
|
|
316
|
+
> ruby -rjson -e 'd=JSON.parse(File.read(0, encoding: "UTF-8").scrub); \
|
|
317
|
+
> puts d["diagnostics"].count { |x| x["message"].to_s.include?("for singleton(Setting)") }'
|
|
318
|
+
> # or quick-and-dirty: rigor check 2>&1 | grep -c "for singleton(Setting)"
|
|
319
|
+
> ```
|
|
320
|
+
>
|
|
321
|
+
> (Force UTF-8 + `.scrub` — diagnostic messages can carry non-ASCII from
|
|
322
|
+
> the project's own strings, e.g. i18n content.) Swap `Setting` for the
|
|
323
|
+
> receiver the def-site grep identified.
|
|
324
|
+
|
|
325
|
+
When you reach this point:
|
|
326
|
+
|
|
327
|
+
1. **Name the cluster and its generator** for the user — the count, the
|
|
328
|
+
receiver, and the defining method (e.g. *"~60 `call.undefined-method`
|
|
329
|
+
on `Setting.<name>`, generated by `Setting.define_setting`"*). Use the
|
|
330
|
+
raw-JSON count above, not the triage hint number.
|
|
331
|
+
2. Say plainly that the durable fix is a **project-owned plugin**, not a
|
|
332
|
+
baseline entry — the baseline only parenthesises the noise; the
|
|
333
|
+
plugin removes it and types the synthetic methods.
|
|
334
|
+
3. **Offer to launch the `rigor-plugin-author` skill**, and on the
|
|
335
|
+
user's confirmation, invoke it (Skill tool, `rigor-plugin-author`).
|
|
336
|
+
That skill scaffolds a standalone `rigor-<id>` gem or a
|
|
337
|
+
project-private plugin (loadable from the project's own `lib/`
|
|
338
|
+
without a gemspec) in the *user's* repo.
|
|
339
|
+
|
|
340
|
+
The offer is not mandatory — acknowledge mode may baseline the cluster
|
|
341
|
+
for now and author the plugin later. But make the next step explicit:
|
|
342
|
+
never leave a generated-DSL cluster in the baseline without telling the
|
|
343
|
+
user it has a real fix and what skill builds it.
|
|
139
344
|
|
|
140
345
|
### Escalation path B — an unsupported external gem
|
|
141
346
|
|
|
142
347
|
If triage reports `gem-without-rbs`, a dependency ships no type
|
|
143
348
|
information and Rigor has no built-in coverage. In order:
|
|
144
349
|
|
|
145
|
-
1. `rbs collection install`
|
|
146
|
-
|
|
350
|
+
1. `bundle exec rbs collection install` (bare `rbs collection install`
|
|
351
|
+
if `rbs` is not in the project's Gemfile) — pulls community RBS for
|
|
352
|
+
the gem if it exists. Re-run triage afterwards.
|
|
147
353
|
2. `dependencies.source_inference:` in `.rigor.dist.yml` — opt the
|
|
148
354
|
gem into Rigor inferring `Dynamic`-typed returns from its source.
|
|
149
355
|
3. If the gem is widely used and genuinely warrants first-class
|
|
@@ -162,7 +368,16 @@ the cause; the user decides whether to act now or defer.
|
|
|
162
368
|
`baseline:` line. Strict mode: neither.
|
|
163
369
|
- The user has seen the likely real bugs and knows the two escalation
|
|
164
370
|
paths.
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
371
|
+
- If a **dynamically-generated project DSL** was found, the user has
|
|
372
|
+
been told it needs a project-owned plugin and offered the
|
|
373
|
+
`rigor-plugin-author` handoff (path A) — not left silently in the
|
|
374
|
+
baseline.
|
|
375
|
+
|
|
376
|
+
Next sessions, by what the onboarding surfaced:
|
|
377
|
+
|
|
378
|
+
- **`rigor-plugin-author`** — when a generated project DSL survived
|
|
379
|
+
`pre_eval:` (escalation path A). This is the durable fix for the
|
|
380
|
+
cluster and the most impactful follow-up; offer it as the next step
|
|
381
|
+
before the user reaches for the baseline-reduce loop.
|
|
382
|
+
- **`rigor-baseline-reduce`** — acknowledge mode, to work the recorded
|
|
383
|
+
baseline down.
|
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: rigortype
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.1.
|
|
4
|
+
version: 0.1.15
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Rigor contributors
|
|
@@ -291,6 +291,7 @@ files:
|
|
|
291
291
|
- lib/rigor/cli/explain_command.rb
|
|
292
292
|
- lib/rigor/cli/lsp_command.rb
|
|
293
293
|
- lib/rigor/cli/mcp_command.rb
|
|
294
|
+
- lib/rigor/cli/plugin_command.rb
|
|
294
295
|
- lib/rigor/cli/plugins_command.rb
|
|
295
296
|
- lib/rigor/cli/plugins_renderer.rb
|
|
296
297
|
- lib/rigor/cli/prism_colorizer.rb
|