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
|
@@ -22,6 +22,7 @@ module Rigor
|
|
|
22
22
|
module_function
|
|
23
23
|
|
|
24
24
|
UNDEFINED_METHOD_RULE = "call.undefined-method"
|
|
25
|
+
UNRESOLVED_TOPLEVEL_RULE = "call.unresolved-toplevel"
|
|
25
26
|
|
|
26
27
|
# `undefined method `foo' for <receiver>`
|
|
27
28
|
UNDEF_METHOD = /\Aundefined method [`'"]([^`'"]+)['"`] for (.+)\z/
|
|
@@ -104,9 +105,19 @@ module Rigor
|
|
|
104
105
|
# monkey-patch): a known AR method on `Array[...]` deserves
|
|
105
106
|
# the precise relation-misinference hint, not the generic
|
|
106
107
|
# "project core-ext" guess H2 would otherwise claim it for.
|
|
108
|
+
#
|
|
109
|
+
# H2K (known project patch) runs before the generic H2: the
|
|
110
|
+
# engine has already proved the defining site via the
|
|
111
|
+
# `project_definition_site` field (ADR-17), so those
|
|
112
|
+
# diagnostics get the high-confidence file-naming hint rather
|
|
113
|
+
# than the spread-based guess. H7 (unresolved toplevel) runs
|
|
114
|
+
# before the systemic / genuine-bug catch-alls so toplevel
|
|
115
|
+
# resolution misses route to `pre_eval:` (ADR-34) instead of
|
|
116
|
+
# reading as scattered bugs.
|
|
107
117
|
def recognisers
|
|
108
118
|
%i[h1_activesupport h4_ar_relation h3_gem_without_rbs
|
|
109
|
-
h2_monkey_patch
|
|
119
|
+
h2k_known_project_patch h2_monkey_patch h7_unresolved_toplevel
|
|
120
|
+
h5_systemic_cluster h6_genuine_bugs]
|
|
110
121
|
end
|
|
111
122
|
|
|
112
123
|
# --- H1 — likely ActiveSupport core_ext --------------------
|
|
@@ -127,6 +138,31 @@ module Rigor
|
|
|
127
138
|
), matched]
|
|
128
139
|
end
|
|
129
140
|
|
|
141
|
+
# --- H2K — known project monkey-patch (engine-proven) ------
|
|
142
|
+
# ADR-17 / WD3 slice 4: the `call.undefined-method` rule sets
|
|
143
|
+
# `project_definition_site` when the project itself defines the
|
|
144
|
+
# called method on the receiver class somewhere in the file set
|
|
145
|
+
# (a reopened core/stdlib/gem class the dispatcher does not
|
|
146
|
+
# apply cross-file). That is direct evidence — not a spread
|
|
147
|
+
# heuristic — so this recogniser is `:likely` and names the
|
|
148
|
+
# defining files outright. It runs before the generic H2.
|
|
149
|
+
def h2k_known_project_patch(pool)
|
|
150
|
+
matched = pool.select(&:project_definition_site)
|
|
151
|
+
return nil if matched.empty?
|
|
152
|
+
|
|
153
|
+
files = matched.map { |d| d.project_definition_site.sub(/:\d+\z/, "") }
|
|
154
|
+
.uniq.sort
|
|
155
|
+
[Hint.new(
|
|
156
|
+
id: "project-monkey-patch-known", confidence: :likely,
|
|
157
|
+
diagnostic_count: matched.size,
|
|
158
|
+
summary: "#{matched.size} undefined-method site(s) resolve to project " \
|
|
159
|
+
"definitions in #{files.first(3).join(', ')} — reopened core/" \
|
|
160
|
+
"stdlib/gem classes Rigor does not apply cross-file",
|
|
161
|
+
action: "List #{files.size == 1 ? 'this file' : 'these files'} in " \
|
|
162
|
+
"`.rigor.yml`'s `pre_eval:` (ADR-17): #{files.join(', ')}"
|
|
163
|
+
), matched]
|
|
164
|
+
end
|
|
165
|
+
|
|
130
166
|
# --- H2 — likely a project monkey-patch / refinement -------
|
|
131
167
|
def h2_monkey_patch(pool)
|
|
132
168
|
groups = undefined_method_groups(pool).select do |(_method, _recv), diags|
|
|
@@ -179,6 +215,30 @@ module Rigor
|
|
|
179
215
|
), matched]
|
|
180
216
|
end
|
|
181
217
|
|
|
218
|
+
# --- H7 — unresolved toplevel implicit-self calls ----------
|
|
219
|
+
# ADR-34: `call.unresolved-toplevel` fires on a toplevel
|
|
220
|
+
# implicit-self call (no receiver, outside any def / class /
|
|
221
|
+
# module) that resolves against no visible contributor. The
|
|
222
|
+
# canonical opt-out is `pre_eval:` — the file is usually a
|
|
223
|
+
# script relying on methods defined by a monkey-patch or a
|
|
224
|
+
# required helper Rigor did not walk. Grouped, not per-site,
|
|
225
|
+
# so the report names the cluster once.
|
|
226
|
+
def h7_unresolved_toplevel(pool)
|
|
227
|
+
matched = pool.select { |d| rule_of(d) == UNRESOLVED_TOPLEVEL_RULE }
|
|
228
|
+
return nil if matched.empty?
|
|
229
|
+
|
|
230
|
+
files = matched.map(&:path).uniq.sort
|
|
231
|
+
[Hint.new(
|
|
232
|
+
id: "unresolved-toplevel", confidence: :possible,
|
|
233
|
+
diagnostic_count: matched.size,
|
|
234
|
+
summary: "#{matched.size} toplevel call(s) resolve to nothing visible " \
|
|
235
|
+
"across #{files.size} file(s) (#{top_methods(matched, parser: :toplevel)})",
|
|
236
|
+
action: "If a monkey-patch or required helper defines these, list its " \
|
|
237
|
+
"file in `.rigor.yml`'s `pre_eval:` (ADR-17); otherwise they may " \
|
|
238
|
+
"be genuine typos or missing requires."
|
|
239
|
+
), matched]
|
|
240
|
+
end
|
|
241
|
+
|
|
182
242
|
# --- H5 — systemic single-file cluster ---------------------
|
|
183
243
|
def h5_systemic_cluster(pool)
|
|
184
244
|
bucket = pool.group_by { |d| [d.path, rule_of(d)] }
|
|
@@ -282,10 +342,16 @@ module Rigor
|
|
|
282
342
|
groups.keys.first(3).map { |method, recv| "`#{method}` on #{recv}" }.join(", ")
|
|
283
343
|
end
|
|
284
344
|
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
345
|
+
# `parser: :undefined_method` (default) reads the method from
|
|
346
|
+
# the parsed `undefined-method` shape; `parser: :toplevel`
|
|
347
|
+
# reads the structured `method_name` field directly (the
|
|
348
|
+
# `unresolved-toplevel` rule carries no receiver to parse).
|
|
349
|
+
def top_methods(diagnostics, limit: 5, parser: :undefined_method)
|
|
350
|
+
names = diagnostics.filter_map do |d|
|
|
351
|
+
parser == :toplevel ? d.method_name : parse_undefined_method(d)&.fetch(:method)
|
|
352
|
+
end
|
|
353
|
+
names.tally.sort_by { |method, count| [-count, method] }
|
|
354
|
+
.first(limit).map { |method, count| "#{method}×#{count}" }.join(" ")
|
|
289
355
|
end
|
|
290
356
|
|
|
291
357
|
def rule_of(diag)
|
data/lib/rigor/version.rb
CHANGED
data/sig/rigor/environment.rbs
CHANGED
|
@@ -51,6 +51,7 @@ module Rigor
|
|
|
51
51
|
def self.reset_default!: () -> void
|
|
52
52
|
def self.build_env_for: (libraries: Array[String], signature_paths: Array[String | _ToPath]) -> untyped
|
|
53
53
|
def self.vendored_gem_sig_paths: () -> Array[Pathname]
|
|
54
|
+
def self.vendored_gem_names: () -> Array[String]
|
|
54
55
|
|
|
55
56
|
def initialize: (?libraries: Array[String], ?signature_paths: Array[String | _ToPath], ?cache_store: untyped?) -> void
|
|
56
57
|
def class_known?: (String | Symbol name) -> bool
|
data/sig/rigor/scope.rbs
CHANGED
|
@@ -15,6 +15,7 @@ module Rigor
|
|
|
15
15
|
attr_reader in_source_constants: Hash[String, Type::t]
|
|
16
16
|
attr_reader discovered_methods: Hash[String, Hash[Symbol, Symbol]]
|
|
17
17
|
attr_reader discovered_def_nodes: Hash[String, Hash[Symbol, untyped]]
|
|
18
|
+
attr_reader discovered_def_sources: Hash[String, Hash[Symbol, String]]
|
|
18
19
|
attr_reader discovered_method_visibilities: Hash[String, Hash[Symbol, Symbol]]
|
|
19
20
|
attr_reader discovered_superclasses: Hash[String, String]
|
|
20
21
|
attr_reader discovered_includes: Hash[String, Array[String]]
|
|
@@ -56,7 +57,9 @@ module Rigor
|
|
|
56
57
|
def with_discovered_methods: (Hash[String, Hash[Symbol, Symbol]] table) -> Scope
|
|
57
58
|
def discovered_method?: (String | Symbol class_name, String | Symbol method_name, Symbol kind) -> bool
|
|
58
59
|
def with_discovered_def_nodes: (Hash[String, Hash[Symbol, untyped]] table) -> Scope
|
|
60
|
+
def with_discovered_def_sources: (Hash[String, Hash[Symbol, String]] table) -> Scope
|
|
59
61
|
def user_def_for: (String | Symbol class_name, String | Symbol method_name) -> untyped?
|
|
62
|
+
def user_def_site_for: (String | Symbol class_name, String | Symbol method_name) -> String?
|
|
60
63
|
def top_level_def_for: (String | Symbol method_name) -> untyped?
|
|
61
64
|
def toplevel?: () -> bool
|
|
62
65
|
def with_discovered_method_visibilities: (Hash[String, Hash[Symbol, Symbol]] table) -> Scope
|
|
@@ -43,6 +43,26 @@ release as potentially contract-changing:
|
|
|
43
43
|
Tell the user this up front. A plugin written today is a preview
|
|
44
44
|
artefact, valuable but not yet on a stable foundation.
|
|
45
45
|
|
|
46
|
+
## Read a real plugin — `rigor plugin`
|
|
47
|
+
|
|
48
|
+
You do not have to learn the `Rigor::Plugin::Base` surface from this
|
|
49
|
+
prose alone. Because Rigor is installed on disk (`mise` / `gem
|
|
50
|
+
install`), every plugin bundled in the toolchain is readable source.
|
|
51
|
+
Use it as a worked-example library throughout this skill:
|
|
52
|
+
|
|
53
|
+
```sh
|
|
54
|
+
rigor plugin list # all bundled + example plugins, with paths
|
|
55
|
+
rigor plugin print rigor-activesupport-core-ext # a plugin's main source, inline
|
|
56
|
+
rigor plugin path rigor-units # the dir, to browse with your file tool
|
|
57
|
+
rigor plugin root # gem root + public API (lib/rigor/plugin.rb)
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
`rigor plugin` (singular) browses the toolchain's plugins; `rigor
|
|
61
|
+
plugins` (plural) reports your own `.rigor.yml` activation — different
|
|
62
|
+
commands. When a step below is thinner than you need, read a shipped
|
|
63
|
+
plugin that does the same thing. (Paths are local to where `rigor`
|
|
64
|
+
runs — see the command's own note about containers.)
|
|
65
|
+
|
|
46
66
|
## Phase 0 — Standalone gem or project-private?
|
|
47
67
|
|
|
48
68
|
Decide where the plugin lives before scaffolding anything.
|
|
@@ -63,9 +63,57 @@ on it directly.
|
|
|
63
63
|
## Project-private layout
|
|
64
64
|
|
|
65
65
|
A project-private plugin lives inside the application repo and is
|
|
66
|
-
never published.
|
|
66
|
+
never published. Rigor activates it by `require "rigor-<id>"`, so the
|
|
67
|
+
plugin's `lib/` must be on the **load path of the Ruby process that
|
|
68
|
+
runs `rigor`**. *Which* mechanism puts it there depends entirely on
|
|
69
|
+
**how `rigor` is installed** — and getting this wrong is the most
|
|
70
|
+
common project-private activation failure (`could not load plugin gem
|
|
71
|
+
"rigor-<id>"`).
|
|
72
|
+
|
|
73
|
+
> **First answer this: how does `rigor` run?** Per the
|
|
74
|
+
> `rigor-project-init` workflow and the manual's installation chapter,
|
|
75
|
+
> the recommended install is **standalone** (`mise` / `gem install`) —
|
|
76
|
+
> and crucially **`rigortype` is NOT in your app's `Gemfile`**. A
|
|
77
|
+
> standalone `rigor` does **not** load your project's bundle, so a
|
|
78
|
+
> `path:`-gem in the `Gemfile` + `bundle install` puts the plugin on
|
|
79
|
+
> the *bundle's* load path, which the standalone `rigor` never reads.
|
|
80
|
+
> The path-gem route below works **only** if you deliberately run
|
|
81
|
+
> `rigor` from a bundle that includes `rigortype` (an advanced / CI
|
|
82
|
+
> setup). For the default standalone install, use `RUBYLIB`.
|
|
83
|
+
|
|
84
|
+
### Recommended for a standalone (mise / gem install) `rigor` — `RUBYLIB`
|
|
85
|
+
|
|
86
|
+
Drop the plugin under the app and put its `lib/` (or its root, if the
|
|
87
|
+
entry file sits at the top) on `RUBYLIB` as an **absolute path**:
|
|
67
88
|
|
|
68
|
-
|
|
89
|
+
```text
|
|
90
|
+
your-app/rigor-ext/rigor-myapp.rb # requires the plugin class
|
|
91
|
+
```
|
|
92
|
+
|
|
93
|
+
```sh
|
|
94
|
+
# Absolute path — a relative RUBYLIB is resolved against the process
|
|
95
|
+
# CWD and frequently does not match; use $(pwd).
|
|
96
|
+
RUBYLIB="$(pwd)/rigor-ext" rigor check
|
|
97
|
+
```
|
|
98
|
+
|
|
99
|
+
Ruby adds `RUBYLIB` entries to `$LOAD_PATH` at startup, so the
|
|
100
|
+
standalone `rigor` binary finds `require "rigor-myapp"`. Set it on
|
|
101
|
+
every invocation (CI included); a `Rakefile` task or a shell alias
|
|
102
|
+
keeps it ergonomic.
|
|
103
|
+
|
|
104
|
+
> **Pitfall — do not wrap this in `bundle exec`.** `bundle exec`
|
|
105
|
+
> rebuilds `$LOAD_PATH` from the bundle and drops `RUBYLIB` entries, so
|
|
106
|
+
> `RUBYLIB=… bundle exec rigor` fails to find the plugin. If for some
|
|
107
|
+
> reason you must run `rigor` through `bundle exec` (or any wrapper
|
|
108
|
+
> that resets the load path), pass the directory as a Ruby flag
|
|
109
|
+
> instead — `RUBYOPT="-I$(pwd)/rigor-ext" rigor check` — which Bundler
|
|
110
|
+
> preserves.
|
|
111
|
+
|
|
112
|
+
Verify activation with `rigor plugins` (plural): the entry should show
|
|
113
|
+
`[OK ]` with `load-error: 0`. If it shows `[ERR] could not load plugin
|
|
114
|
+
gem`, the load path is the problem, not the plugin code.
|
|
115
|
+
|
|
116
|
+
### Advanced (only when `rigor` runs from a bundle) — a path gem
|
|
69
117
|
|
|
70
118
|
Keep the plugin in a subdirectory with its own gemspec, and reference
|
|
71
119
|
it from the app's `Gemfile` by path:
|
|
@@ -87,25 +135,15 @@ gem "rigortype", "~> 0.1.0"
|
|
|
87
135
|
gem "rigor-myapp", path: "rigor-plugin"
|
|
88
136
|
```
|
|
89
137
|
|
|
90
|
-
`bundle install`
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
```text
|
|
100
|
-
your-app/rigor-ext/rigor-myapp.rb # requires the plugin class
|
|
101
|
-
```
|
|
102
|
-
|
|
103
|
-
```sh
|
|
104
|
-
RUBYLIB=rigor-ext rigor check
|
|
105
|
-
```
|
|
106
|
-
|
|
107
|
-
Workable, but the `RUBYLIB` has to be set on every invocation (CI
|
|
108
|
-
included). Prefer the path gem unless the plugin is a throwaway.
|
|
138
|
+
`bundle install` puts `rigor-myapp` on the bundle's load path — but
|
|
139
|
+
this only helps if you then **run `rigor` through that bundle**
|
|
140
|
+
(`bundle exec rigor`, or a bundle whose `bin/` is on `PATH`). It also
|
|
141
|
+
means `rigortype` *is* in this `Gemfile`, which the `rigor-project-init`
|
|
142
|
+
workflow recommends against for the analyzer-as-tool install. So treat
|
|
143
|
+
this route as the **CI / isolated-bundle** option, not the default. It
|
|
144
|
+
keeps the plugin versioned, testable, and trivially promotable to a
|
|
145
|
+
real gem later — choose it when your `rigor` already runs from a
|
|
146
|
+
bundle; otherwise use `RUBYLIB` above.
|
|
109
147
|
|
|
110
148
|
## The plugin class skeleton
|
|
111
149
|
|
|
@@ -89,6 +89,21 @@ type.
|
|
|
89
89
|
|
|
90
90
|
## Optional — contribute a return type with `flow_contribution_for`
|
|
91
91
|
|
|
92
|
+
> **Critical — this hook does NOT make a method "defined", so it does
|
|
93
|
+
> NOT suppress `call.undefined-method`.** Method *existence* and call
|
|
94
|
+
> *type* are two independent checks. `flow_contribution_for` sharpens
|
|
95
|
+
> the type of a call the analyzer has **already resolved to a real
|
|
96
|
+
> method** (turning a `Dynamic` return into something precise). It is
|
|
97
|
+
> never consulted for a receiver/method the analyzer cannot find — that
|
|
98
|
+
> fires `call.undefined-method` first, and a flow contribution does
|
|
99
|
+
> nothing to silence it. **If your goal is to kill a
|
|
100
|
+
> `call.undefined-method` cluster on a DSL-generated method (the common
|
|
101
|
+
> reason `rigor-project-init` hands off to this skill), the fix is to
|
|
102
|
+
> make the method *exist* in Rigor's view — ship RBS declaring it (see
|
|
103
|
+
> "Shipping RBS for the DSL" below), not a flow contribution.** Reach
|
|
104
|
+
> for `flow_contribution_for` only when the call already resolves and
|
|
105
|
+
> you want a *better return type*.
|
|
106
|
+
|
|
92
107
|
A plugin can do more than emit diagnostics: it can *supply* the
|
|
93
108
|
inferred return type for a call site the core analyzer would
|
|
94
109
|
otherwise type as `Dynamic`. Implement `flow_contribution_for`:
|
|
@@ -128,23 +143,57 @@ most likely to shift before v0.2.0. Implement it only if the plugin
|
|
|
128
143
|
genuinely needs to sharpen call-site types; a diagnostics-only
|
|
129
144
|
plugin can skip it entirely.
|
|
130
145
|
|
|
131
|
-
## Shipping RBS for the DSL
|
|
146
|
+
## Shipping RBS for the DSL — the way to suppress `call.undefined-method`
|
|
132
147
|
|
|
133
148
|
If the DSL introduces methods or classes that Rigor cannot see (a
|
|
134
|
-
`Money` class defined by metaprogramming,
|
|
135
|
-
`
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
149
|
+
`Money` class defined by metaprogramming, `Setting.<name>` accessors a
|
|
150
|
+
`class_eval` heredoc generates, methods mixed into `Numeric`), give
|
|
151
|
+
Rigor RBS declaring them so *core* inference — not just your plugin —
|
|
152
|
+
treats them as **defined**. This is what removes the
|
|
153
|
+
`call.undefined-method` diagnostics on those methods; nothing else
|
|
154
|
+
(not `diagnostics_for_file`, not `flow_contribution_for`) makes a
|
|
155
|
+
method exist in Rigor's view.
|
|
156
|
+
|
|
157
|
+
Two ways to wire the RBS, depending on how the plugin is packaged:
|
|
158
|
+
|
|
159
|
+
1. **A packaged gem plugin** — declare `signature_paths:` in the
|
|
160
|
+
**plugin manifest** (resolved relative to the plugin's gem root, per
|
|
161
|
+
ADR-25), so activating the plugin contributes its RBS with no
|
|
162
|
+
user-side wiring. This is how the bundled RBS-bundle plugins work —
|
|
163
|
+
read one as a worked example:
|
|
164
|
+
|
|
165
|
+
```sh
|
|
166
|
+
rigor plugin print rigor-activesupport-core-ext # see manifest + sig/ layout
|
|
167
|
+
rigor plugin path rigor-activesupport-core-ext # then browse its sig/ dir
|
|
168
|
+
```
|
|
169
|
+
|
|
170
|
+
2. **A project-private plugin** — the manifest field still works, but
|
|
171
|
+
the simplest reliable route is to ship the `.rbs` under the plugin's
|
|
172
|
+
own `sig/` and add **that path** to the *consuming project's*
|
|
173
|
+
`.rigor.yml`:
|
|
174
|
+
|
|
175
|
+
```yaml
|
|
176
|
+
signature_paths:
|
|
177
|
+
- rigor-ext/sig # the project-private plugin's RBS directory
|
|
178
|
+
```
|
|
179
|
+
|
|
180
|
+
If the DSL's method names are generated from a data file (Redmine's
|
|
181
|
+
`Setting` names come from `config/settings.yml`), generate the `.rbs`
|
|
182
|
+
from that same source — a small script that reads the data file and
|
|
183
|
+
emits one declaration per name keeps the RBS in sync with the DSL.
|
|
184
|
+
|
|
185
|
+
RBS covers what the *shape* of the DSL is (which methods exist, their
|
|
186
|
+
signatures); the plugin walker covers the *dynamic* parts RBS cannot
|
|
187
|
+
express (a value computed from a literal argument, a dimensional rule).
|
|
188
|
+
They compose — many plugins ship both.
|
|
189
|
+
|
|
190
|
+
> **Browse a real plugin instead of guessing the API.** Because Rigor
|
|
191
|
+
> is installed on disk (`mise` / `gem install`), every bundled plugin's
|
|
192
|
+
> source is readable as a worked example. `rigor plugin list` shows
|
|
193
|
+
> them all with absolute paths; `rigor plugin print <name>` inlines a
|
|
194
|
+
> plugin's main source; `rigor plugin root` points at the engine source
|
|
195
|
+
> and the public `Rigor::Plugin::Base` API. When the prose here is
|
|
196
|
+
> thinner than you need, read a shipped plugin that does the same thing.
|
|
148
197
|
|
|
149
198
|
## Output of this module
|
|
150
199
|
|
|
@@ -92,8 +92,10 @@ Below that, either mode is reasonable — ask.
|
|
|
92
92
|
| 4 | Write `.rigor.dist.yml` — severity profile follows the mode. Verify activation with `rigor plugins`. | [`references/02-configure.md`](references/02-configure.md) |
|
|
93
93
|
| 5 | Generate initial RBS sigs; uplift attr_reader precision with `--params=observed`. | [`references/04-sig-uplift.md`](references/04-sig-uplift.md) |
|
|
94
94
|
| 6 | Run `rigor triage --format json` to diagnose the diagnostic stream. | [`references/03-baseline-and-bugs.md`](references/03-baseline-and-bugs.md) |
|
|
95
|
+
| 6a | **Pre-baseline cleanup** — apply quick fixes that triage diagnosed (`pre_eval:` for monkey-patch hints, `rbs collection install` if still needed). Re-run triage; repeat until the count is stable. | [`references/03-baseline-and-bugs.md`](references/03-baseline-and-bugs.md) § "Phase 6a" |
|
|
95
96
|
| 7 | Acknowledge mode only — generate the baseline and wire `baseline:`. | [`references/03-baseline-and-bugs.md`](references/03-baseline-and-bugs.md) |
|
|
96
|
-
| 8 | Surface likely real bugs;
|
|
97
|
+
| 8 | Surface likely real bugs; distinguish sig quality FPs from real bugs; offer escalation paths. | [`references/03-baseline-and-bugs.md`](references/03-baseline-and-bugs.md) |
|
|
98
|
+
| 9 | Confirm the generated files with the user — what each is, and whether to commit it. | (this file — § "Final step") |
|
|
97
99
|
|
|
98
100
|
Load each reference when you reach its phase. Phases run in order;
|
|
99
101
|
the only branch is Phase 7 (acknowledge mode runs it, strict mode
|
|
@@ -107,7 +109,7 @@ committed `sig/` directory.
|
|
|
107
109
|
| 1 | [`references/01-detect.md`](references/01-detect.md) | **Phases 1 + 3.** Gemfile / Gemfile.lock walk → framework family. The plugin-recommendation table (Rails / dry-rb / Sinatra / RSpec / plain Ruby). RBS-collection presence check. |
|
|
108
110
|
| 2 | [`references/02-configure.md`](references/02-configure.md) | **Phase 4.** Severity-profile choice tied to the mode. The `.rigor.dist.yml` template and every key it uses. The `.rigor.dist.yml` vs `.rigor.yml` convention. |
|
|
109
111
|
| 3 | [`references/04-sig-uplift.md`](references/04-sig-uplift.md) | **Phase 5.** `rigor sig-gen --write` baseline. `rigor sig-gen --params=observed --write` attr_reader precision uplift. Handling residual `untyped` methods. Committing `sig/`. |
|
|
110
|
-
| 4 | [`references/03-baseline-and-bugs.md`](references/03-baseline-and-bugs.md) | **Phases 6–8.** `rigor triage` as the diagnosis layer. `rigor baseline generate` + wiring `baseline:`. Surfacing likely real bugs. The two escalation paths — write a project plugin, or open a Rigor issue. |
|
|
112
|
+
| 4 | [`references/03-baseline-and-bugs.md`](references/03-baseline-and-bugs.md) | **Phases 6–8.** `rigor triage` as the diagnosis layer. Phase 6a pre-baseline cleanup loop (`pre_eval:` for monkey-patch hints, `rbs collection install`). `rigor baseline generate` + wiring `baseline:`. Surfacing likely real bugs; sig quality FP recognition (Struct `call.wrong-arity`, `-> bot` return-type-mismatch, regex-capture `$1` FPs). The two escalation paths — write a project plugin, or open a Rigor issue. |
|
|
111
113
|
|
|
112
114
|
## Escalation paths (Phase 7 preview)
|
|
113
115
|
|
|
@@ -115,10 +117,23 @@ Some diagnostic clusters are neither a quick fix nor honest baseline
|
|
|
115
117
|
material. Two of them have a dedicated answer this skill hands off to:
|
|
116
118
|
|
|
117
119
|
- **Application-specific metaprogramming** — a project DSL,
|
|
118
|
-
`define_method` factory,
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
120
|
+
`define_method` factory, `method_missing` accessor, or `class_eval`
|
|
121
|
+
heredoc generator that Rigor cannot follow produces a cluster of
|
|
122
|
+
`call.undefined-method` (or `unresolved-toplevel`). **First decide
|
|
123
|
+
whether `pre_eval:` can fix it** — it resolves only methods written
|
|
124
|
+
as *literal* `def` / `def self.` in a project file. When the methods
|
|
125
|
+
are **generated dynamically** (computed `define_method` names,
|
|
126
|
+
`method_missing`, a `class_eval <<~RUBY … def #{name} … RUBY`
|
|
127
|
+
template), `pre_eval:` walks the file and finds no literal method to
|
|
128
|
+
register, so the cluster *survives*. That residue is the signal that
|
|
129
|
+
the durable fix is a **project-private Rigor plugin** which teaches
|
|
130
|
+
Rigor the DSL's shape. **This is the recommended next step**, and
|
|
131
|
+
Rigor does **not** ship per-application plugins for it — a Redmine
|
|
132
|
+
app's `Setting.define_setting` accessors, an in-house
|
|
133
|
+
`acts_as_*`, etc. are the *project's* plugin to own (ADR-16 §
|
|
134
|
+
Audience: application-specific homegrown DSLs are out of scope for
|
|
135
|
+
the bundled substrate). Surface it and offer to launch the
|
|
136
|
+
**`rigor-plugin-author`** skill (see § "Next step" below).
|
|
122
137
|
- **An external gem Rigor does not understand** — a dependency ships
|
|
123
138
|
no RBS and Rigor has no built-in coverage for it. Try
|
|
124
139
|
`rbs collection install` first; if that gem genuinely needs Rigor
|
|
@@ -126,4 +141,54 @@ material. Two of them have a dedicated answer this skill hands off to:
|
|
|
126
141
|
<https://github.com/rigortype/rigor/issues>.
|
|
127
142
|
|
|
128
143
|
Neither is a Phase 7 obligation — they are options to *offer* the
|
|
129
|
-
user when the triage report points at one of these causes.
|
|
144
|
+
user when the triage report points at one of these causes. The
|
|
145
|
+
project-DSL handoff is detailed in
|
|
146
|
+
[`references/03-baseline-and-bugs.md`](references/03-baseline-and-bugs.md)
|
|
147
|
+
§ "Escalation path A".
|
|
148
|
+
|
|
149
|
+
## Next step — hand off to plugin authoring when a project DSL remains
|
|
150
|
+
|
|
151
|
+
Onboarding's job ends at a committed config + (acknowledge mode) a
|
|
152
|
+
baseline. But if Phase 6a/8 found a **dynamically-generated project
|
|
153
|
+
DSL** that `pre_eval:` could not resolve (a `define_method` factory,
|
|
154
|
+
`method_missing`, or a `class_eval` heredoc generator), the onboarding
|
|
155
|
+
is not *complete* until the user knows the durable fix — a
|
|
156
|
+
**project-owned Rigor plugin**, which Rigor does not bundle per app.
|
|
157
|
+
|
|
158
|
+
Before the final file-confirmation step, when such a cluster exists:
|
|
159
|
+
name it and its generator, say plainly that the fix is a project plugin
|
|
160
|
+
(not a baseline entry), and **offer to launch the `rigor-plugin-author`
|
|
161
|
+
skill** — on the user's confirmation, invoke it (Skill tool). The full
|
|
162
|
+
detection-and-handoff recipe is
|
|
163
|
+
[`references/03-baseline-and-bugs.md`](references/03-baseline-and-bugs.md)
|
|
164
|
+
§ "Escalation path A". The offer is not automatic — the user may
|
|
165
|
+
baseline the cluster now and author the plugin later — but never leave
|
|
166
|
+
a generated-DSL cluster in the baseline without naming its real fix.
|
|
167
|
+
|
|
168
|
+
## Final step — confirm the generated files with the user
|
|
169
|
+
|
|
170
|
+
Onboarding scatters new files across the project root. Before
|
|
171
|
+
finishing, present the user with a short message that names **each
|
|
172
|
+
file the workflow produced**, says what it is, and recommends
|
|
173
|
+
whether to commit it — so the team shares one consistent setup
|
|
174
|
+
rather than each developer reinventing it. Do not silently leave the
|
|
175
|
+
files for the user to discover.
|
|
176
|
+
|
|
177
|
+
Run `git status --short` first; only describe files that actually
|
|
178
|
+
exist (e.g. `sig/` exists only if Phase 5 ran; `.rigor-baseline.yml`
|
|
179
|
+
only in acknowledge mode). For each, give the commit recommendation:
|
|
180
|
+
|
|
181
|
+
| File | What it is | Commit? |
|
|
182
|
+
| --- | --- | --- |
|
|
183
|
+
| `.rigor.dist.yml` | The shared project config (Phase 4) — `target_ruby`, `paths:`, `plugins:`, `severity_profile:`, and the `baseline:` pointer. The single source of truth every contributor's `rigor check` reads. | **Yes** — it is the shared config; sharing it is the whole point. |
|
|
184
|
+
| `.rigor-baseline.yml` | Acknowledge mode only (Phase 7) — the snapshot of today's known diagnostics. Doubles as a record of project state; without it each developer's baseline diverges and the regression guard means different things per machine. | **Yes** — commit it; it documents project state and pins the regression envelope. |
|
|
185
|
+
| `sig/` | RBS skeletons from `rigor sig-gen` (Phase 5), if that phase ran. A first-class project artefact — it sharpens inference for everyone. | **Yes** — commit it (see [`references/04-sig-uplift.md`](references/04-sig-uplift.md) § "Commit the sig/ directory"). |
|
|
186
|
+
| `.rigor/` (contains `cache/`) | The per-file analysis cache `rigor check` writes to speed up re-runs. Regenerable and machine-local. | **No** — add `.rigor/` to `.gitignore`. |
|
|
187
|
+
| `.rigor.yml` | Optional per-developer local override (not written by this skill). Takes precedence over `.rigor.dist.yml`; used to opt out locally (e.g. run without the baseline). | **No** — gitignore it if a developer creates one. |
|
|
188
|
+
|
|
189
|
+
Recommend the two concrete actions and **ask before doing them**
|
|
190
|
+
(both touch the user's repo): (1) add `.rigor/` — and `.rigor.yml`
|
|
191
|
+
if present — to `.gitignore`; (2) commit `.rigor.dist.yml`,
|
|
192
|
+
`.rigor-baseline.yml`, and `sig/` as the shared Rigor setup. Per the
|
|
193
|
+
git-safety default, do not commit on the user's behalf until they
|
|
194
|
+
confirm.
|
|
@@ -35,13 +35,51 @@ Also note per-gem markers that have their own plugin: `devise`,
|
|
|
35
35
|
`rbs_collection.auto_detect: true` (the default).
|
|
36
36
|
- **Lockfile present, `.gem_rbs_collection/` absent** → the lockfile
|
|
37
37
|
was generated but the gems were never downloaded. Rigor loads the
|
|
38
|
-
lockfile but finds no RBS files.
|
|
39
|
-
reports `gem-without-rbs` hints for gems that appear in
|
|
40
|
-
`rbs_collection.yaml`, run `rbs collection install` first and
|
|
41
|
-
re-run triage.
|
|
38
|
+
lockfile but finds no RBS files. **Act now — see below.**
|
|
42
39
|
- **Both absent** → note it; Phase 6's triage may recommend
|
|
43
40
|
`rbs collection install` if `gem-without-rbs` hints appear.
|
|
44
41
|
|
|
42
|
+
### RBS collection — install if absent
|
|
43
|
+
|
|
44
|
+
When `rbs_collection.lock.yaml` is present **and** `.gem_rbs_collection/`
|
|
45
|
+
is absent, the collection was configured (e.g. by Steep or `rbs_rails`)
|
|
46
|
+
but never installed. Installing it now — before writing the config or
|
|
47
|
+
running triage — gives Rigor community RBS for dozens of gems and avoids
|
|
48
|
+
a triage → config-fix → re-triage cycle.
|
|
49
|
+
|
|
50
|
+
Offer to run:
|
|
51
|
+
|
|
52
|
+
```sh
|
|
53
|
+
bundle exec rbs collection install
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
Use `bundle exec` when `rbs` appears in `Gemfile` or `Gemfile.lock`
|
|
57
|
+
(the common case — Steep / rbs_rails workflows put it there). If `rbs`
|
|
58
|
+
is not in the Gemfile, use the bare `rbs collection install` instead.
|
|
59
|
+
|
|
60
|
+
Ask the user for permission before running, since this installs files
|
|
61
|
+
into `.gem_rbs_collection/` (a generated directory that belongs in
|
|
62
|
+
`.gitignore`). Only proceed with their confirmation.
|
|
63
|
+
|
|
64
|
+
After installation, verify with:
|
|
65
|
+
|
|
66
|
+
```sh
|
|
67
|
+
ls .gem_rbs_collection/
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
The directory should contain RBS gem subdirectories. Continue to
|
|
71
|
+
Phase 2 — the collection will be auto-detected by Rigor at analysis
|
|
72
|
+
time.
|
|
73
|
+
|
|
74
|
+
**Note: RBS collision after install.** Some gems (e.g. `cgi`, `logger`,
|
|
75
|
+
`base64`) were extracted from Ruby's stdlib into standalone gems from
|
|
76
|
+
Ruby 3.3 onwards. When these appear in both `.gem_rbs_collection/` and
|
|
77
|
+
Rigor's bundled stdlib, `rigor triage` may print a
|
|
78
|
+
`RBS::DuplicatedDeclarationError`. If this happens, note the error and
|
|
79
|
+
continue — plugin-based diagnostics are unaffected. File a Rigor issue
|
|
80
|
+
at <https://github.com/rigortype/rigor/issues> so the engine can
|
|
81
|
+
deduplicate stdlib gems from the collection automatically.
|
|
82
|
+
|
|
45
83
|
### Path scope
|
|
46
84
|
|
|
47
85
|
Note the conventional source roots so Phase 4 can set `paths:`:
|
|
@@ -18,6 +18,23 @@ config as `.rigor.dist.yml` lets a contributor opt out locally (for
|
|
|
18
18
|
example, run without the baseline) without touching the committed
|
|
19
19
|
file.
|
|
20
20
|
|
|
21
|
+
## Selecting `target_ruby`
|
|
22
|
+
|
|
23
|
+
Read the project's declared Ruby version from `.ruby-version`, the
|
|
24
|
+
`Gemfile` `ruby "…"` line, or `.tool-versions`. Use the **minimum**
|
|
25
|
+
version of the declared range (e.g. `>= 3.2.0, < 3.5.0` → `3.2`).
|
|
26
|
+
|
|
27
|
+
Then apply this decision table — **do not ask the user**; act
|
|
28
|
+
automatically and emit the message described:
|
|
29
|
+
|
|
30
|
+
| Declared minimum | `target_ruby` to write | Action |
|
|
31
|
+
| --- | --- | --- |
|
|
32
|
+
| 4.x | `"4.0"` (or exact patch) | Write as-is. |
|
|
33
|
+
| 3.3 – 3.x | `"3.3"` (or exact minor) | Write as-is. |
|
|
34
|
+
| 3.0 – 3.2 | `"3.3"` | Write `"3.3"` automatically. Emit: *"Project targets Ruby X.Y, but Prism (the parser bundled with rigortype) supports Ruby 3.3 as its minimum. Setting `target_ruby: \"3.3\"` — syntax parsing is compatible across 3.0–3.3, but RBS type definitions in `.gem_rbs_collection/` or community libraries may be authored for 3.3+ and could produce type-level false positives on APIs that changed between 3.0 and 3.3."* |
|
|
35
|
+
| 2.x | — | **Stop.** Emit: *"Project targets Ruby 2.x. Rigor's bundled Prism parser does not support Ruby 2.x syntax (removed keyword argument syntax, pattern matching differences). Rigor cannot reliably analyse this project. Consider upgrading the project to Ruby 3.3+ before onboarding Rigor."* Do not write a config; do not continue the skill. |
|
|
36
|
+
| Unknown / not declared | `"4.0"` (Rigor's default) | Write as-is; note that the default was used. |
|
|
37
|
+
|
|
21
38
|
## Severity profile — follows the mode
|
|
22
39
|
|
|
23
40
|
The `severity_profile:` key re-stamps every rule's severity. Set it
|