rigortype 0.3.3 → 0.3.4
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 +11 -24
- data/data/builtins/ruby_core/enumerable.yml +4 -8
- data/data/builtins/ruby_core/exception.yml +6 -9
- data/data/builtins/ruby_core/hash.yml +13 -20
- data/data/builtins/ruby_core/io.yml +6 -9
- data/data/builtins/ruby_core/numeric.yml +2 -3
- data/data/builtins/ruby_core/proc.yml +4 -8
- data/data/builtins/ruby_core/re.yml +6 -9
- data/data/builtins/ruby_core/set.yml +2 -3
- data/data/builtins/ruby_core/string.yml +4 -7
- data/data/builtins/ruby_core/struct.yml +0 -6
- data/data/builtins/ruby_core/time.yml +3 -6
- data/data/effects/core.yml +843 -0
- data/data/effects/registry.yml +67 -0
- data/docs/handbook/07-rbs-and-extended.md +106 -5
- data/docs/manual/02-cli-reference.md +311 -1
- data/docs/manual/03-configuration.md +84 -1
- data/docs/manual/04-diagnostics.md +6 -2
- data/docs/manual/16-rbs-extended-annotations.md +77 -0
- data/docs/manual/18-removing-dead-code.md +326 -0
- data/docs/manual/README.md +5 -0
- data/docs/manual/plugins/rigor-actionmailer.md +5 -0
- data/docs/manual/plugins/rigor-activejob.md +47 -0
- data/docs/manual/plugins/rigor-factorybot.md +16 -0
- data/docs/manual/plugins/rigor-pundit.md +26 -0
- data/docs/manual/plugins/rigor-rails-routes.md +41 -2
- data/docs/manual/plugins/rigor-rspec-rails.md +5 -0
- data/docs/manual/plugins/rigor-rspec.md +8 -0
- data/docs/manual/plugins/rigor-sidekiq.md +54 -0
- data/lib/rigor/analysis/check_rules/rule_ids.rb +28 -2
- data/lib/rigor/analysis/incremental_session.rb +140 -6
- data/lib/rigor/analysis/reachability/graph.rb +244 -0
- data/lib/rigor/analysis/reachability/plugin_roots.rb +164 -0
- data/lib/rigor/analysis/reachability/project_files.rb +58 -0
- data/lib/rigor/analysis/reachability/scan.rb +241 -0
- data/lib/rigor/analysis/reachability/signature_scan.rb +134 -0
- data/lib/rigor/analysis/rule_catalog.rb +158 -0
- data/lib/rigor/analysis/run_cache_key.rb +16 -0
- data/lib/rigor/analysis/runner/declaration_position.rb +54 -0
- data/lib/rigor/analysis/runner/effect_annotation_residual_pass.rb +108 -0
- data/lib/rigor/analysis/runner/effect_envelope_pass.rb +358 -0
- data/lib/rigor/analysis/runner/envelope_messages.rb +61 -0
- data/lib/rigor/analysis/runner/pool_coordinator.rb +68 -10
- data/lib/rigor/analysis/runner.rb +420 -10
- data/lib/rigor/analysis/worker_session.rb +58 -2
- data/lib/rigor/bleeding_edge.rb +22 -0
- data/lib/rigor/cache/incremental_snapshot.rb +24 -5
- data/lib/rigor/cli/check_command.rb +13 -1
- data/lib/rigor/cli/check_runner_factory.rb +4 -1
- data/lib/rigor/cli/effects_command.rb +131 -0
- data/lib/rigor/cli/effects_diff_renderer.rb +123 -0
- data/lib/rigor/cli/effects_explain_renderer.rb +68 -0
- data/lib/rigor/cli/effects_renderer.rb +54 -0
- data/lib/rigor/cli/effects_report.rb +53 -0
- data/lib/rigor/cli/effects_snapshot_command.rb +256 -0
- data/lib/rigor/cli/unused_command.rb +288 -0
- data/lib/rigor/cli.rb +16 -0
- data/lib/rigor/configuration/severity_profile.rb +23 -0
- data/lib/rigor/configuration.rb +296 -14
- data/lib/rigor/effects/attribution.rb +76 -0
- data/lib/rigor/effects/catalog.rb +275 -0
- data/lib/rigor/effects/collector.rb +182 -0
- data/lib/rigor/effects/config_envelopes.rb +185 -0
- data/lib/rigor/effects/discharge.rb +69 -0
- data/lib/rigor/effects/effect_table.rb +92 -0
- data/lib/rigor/effects/entry_points.rb +93 -0
- data/lib/rigor/effects/envelope.rb +86 -0
- data/lib/rigor/effects/envelope_check.rb +172 -0
- data/lib/rigor/effects/envelope_index.rb +157 -0
- data/lib/rigor/effects/file_collection.rb +160 -0
- data/lib/rigor/effects/framework_units.rb +167 -0
- data/lib/rigor/effects/identity.rb +104 -0
- data/lib/rigor/effects/label.rb +77 -0
- data/lib/rigor/effects/label_intent.rb +73 -0
- data/lib/rigor/effects/label_set.rb +136 -0
- data/lib/rigor/effects/liskov_check.rb +167 -0
- data/lib/rigor/effects/local_ownership.rb +132 -0
- data/lib/rigor/effects/method_key.rb +40 -0
- data/lib/rigor/effects/mutation_classifier.rb +92 -0
- data/lib/rigor/effects/narrowing.rb +202 -0
- data/lib/rigor/effects/origin.rb +66 -0
- data/lib/rigor/effects/path_finder.rb +89 -0
- data/lib/rigor/effects/plugin_facts.rb +335 -0
- data/lib/rigor/effects/propagator.rb +275 -0
- data/lib/rigor/effects/registry.rb +193 -0
- data/lib/rigor/effects/scanner.rb +290 -0
- data/lib/rigor/effects/signature_sources.rb +74 -0
- data/lib/rigor/effects/snapshot.rb +380 -0
- data/lib/rigor/effects/snapshot_diff.rb +265 -0
- data/lib/rigor/effects/summary.rb +154 -0
- data/lib/rigor/effects/taint_cause.rb +38 -0
- data/lib/rigor/effects/unit_scan.rb +572 -0
- data/lib/rigor/effects/unknown_label_check.rb +86 -0
- data/lib/rigor/effects/unknown_label_report.rb +59 -0
- data/lib/rigor/effects.rb +47 -0
- data/lib/rigor/environment/rbs_loader.rb +34 -0
- data/lib/rigor/environment.rb +8 -1
- data/lib/rigor/flow_contribution/element.rb +1 -0
- data/lib/rigor/flow_contribution/merge_result.rb +5 -3
- data/lib/rigor/flow_contribution/merger.rb +16 -1
- data/lib/rigor/flow_contribution.rb +20 -4
- data/lib/rigor/inference/expression_typer.rb +18 -4
- data/lib/rigor/inference/pre_eval_constants.rb +189 -0
- data/lib/rigor/inference/scope_indexer.rb +15 -3
- data/lib/rigor/plugin/base.rb +24 -0
- data/lib/rigor/plugin/effect_attribution.rb +208 -0
- data/lib/rigor/plugin/effect_edge.rb +101 -0
- data/lib/rigor/plugin/effect_entry_points.rb +51 -0
- data/lib/rigor/plugin/first_party.rb +57 -0
- data/lib/rigor/plugin/isolation.rb +4 -1
- data/lib/rigor/plugin/manifest.rb +103 -3
- data/lib/rigor/plugin/registry.rb +48 -0
- data/lib/rigor/plugin.rb +1 -0
- data/lib/rigor/rbs_extended/envelope_scanner.rb +160 -0
- data/lib/rigor/rbs_extended.rb +168 -0
- data/lib/rigor/reflection.rb +131 -30
- data/lib/rigor/version.rb +1 -1
- data/lib/rigor.rb +1 -0
- data/plugins/rigor-actioncable/lib/rigor/plugin/actioncable/effects.rb +90 -0
- data/plugins/rigor-actioncable/lib/rigor/plugin/actioncable.rb +7 -1
- data/plugins/rigor-actionmailer/lib/rigor/plugin/actionmailer/effects.rb +85 -0
- data/plugins/rigor-actionmailer/lib/rigor/plugin/actionmailer.rb +8 -1
- data/plugins/rigor-actionpack/lib/rigor/plugin/actionpack/effects.rb +152 -0
- data/plugins/rigor-actionpack/lib/rigor/plugin/actionpack.rb +9 -1
- data/plugins/rigor-activejob/lib/rigor/plugin/activejob/effects.rb +156 -0
- data/plugins/rigor-activejob/lib/rigor/plugin/activejob/recurring_scan.rb +113 -0
- data/plugins/rigor-activejob/lib/rigor/plugin/activejob.rb +68 -3
- data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord/effects.rb +186 -0
- data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord.rb +12 -1
- data/plugins/rigor-activerecord/sig/active_record/relation.rbs +145 -0
- data/plugins/rigor-activestorage/lib/rigor/plugin/activestorage/effects.rb +77 -0
- data/plugins/rigor-activestorage/lib/rigor/plugin/activestorage.rb +6 -1
- data/plugins/rigor-activesupport-core-ext/lib/rigor/plugin/activesupport_core_ext/effects.rb +143 -0
- data/plugins/rigor-activesupport-core-ext/lib/rigor/plugin/activesupport_core_ext.rb +9 -1
- data/plugins/rigor-activesupport-core-ext/sig/active_support/core_ext.rbs +385 -28
- data/plugins/rigor-factorybot/lib/rigor/plugin/factorybot.rb +30 -2
- data/plugins/rigor-pundit/lib/rigor/plugin/pundit/authorization_scan.rb +151 -0
- data/plugins/rigor-pundit/lib/rigor/plugin/pundit.rb +46 -3
- data/plugins/rigor-rails/lib/rigor-rails.rb +1 -0
- data/plugins/rigor-rails-i18n/lib/rigor/plugin/rails_i18n/effects.rb +47 -0
- data/plugins/rigor-rails-i18n/lib/rigor/plugin/rails_i18n.rb +7 -1
- data/plugins/rigor-rails-routes/lib/rigor/plugin/rails_routes/acronyms.rb +81 -0
- data/plugins/rigor-rails-routes/lib/rigor/plugin/rails_routes/helper_table.rb +13 -1
- data/plugins/rigor-rails-routes/lib/rigor/plugin/rails_routes/routes_parser.rb +202 -15
- data/plugins/rigor-rails-routes/lib/rigor/plugin/rails_routes.rb +43 -3
- data/plugins/rigor-railties/lib/rigor/plugin/railties/effects.rb +171 -0
- data/plugins/rigor-railties/lib/rigor/plugin/railties.rb +62 -0
- data/plugins/rigor-railties/lib/rigor-railties.rb +3 -0
- data/plugins/rigor-sidekiq/lib/rigor/plugin/sidekiq/schedule_scan.rb +122 -0
- data/plugins/rigor-sidekiq/lib/rigor/plugin/sidekiq.rb +56 -3
- data/sig/rigor/analysis/fact_store.rbs +1 -0
- data/sig/rigor/inference.rbs +2 -0
- data/sig/rigor/rbs_extended.rbs +6 -0
- data/sig/rigor.rbs +17 -1
- data/skills/rigor-unused-adjudicate/SKILL.md +90 -0
- metadata +79 -1
|
@@ -20,6 +20,27 @@ def read_name: () -> String
|
|
|
20
20
|
|
|
21
21
|
The plain `() -> String` stays the compatibility contract; the
|
|
22
22
|
annotation tells Rigor the return is a non-empty string.
|
|
23
|
+
|
|
24
|
+
You may also write any of them **in a `.rb` file**, as an
|
|
25
|
+
rbs-inline `# @rbs %a{…}` comment — `%a{}` is rbs-inline's own
|
|
26
|
+
upstream grammar, and the annotation reaches Rigor on the same
|
|
27
|
+
path the generated signature does:
|
|
28
|
+
|
|
29
|
+
```rb
|
|
30
|
+
# rbs_inline: enabled
|
|
31
|
+
|
|
32
|
+
class Reader
|
|
33
|
+
# @rbs %a{rigor:v1:return: non-empty-string}
|
|
34
|
+
# @rbs return: String
|
|
35
|
+
def read_name = "x"
|
|
36
|
+
end
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
This needs the `rbs-inline` library installed; Rigor ingests
|
|
40
|
+
inline annotations by default when it is
|
|
41
|
+
([ADR-93](../adr/93-default-rbs-inline-ingestion.md)). There is
|
|
42
|
+
no Rigor-only comment dialect: `# rigor:` comments remain
|
|
43
|
+
suppression-only.
|
|
23
44
|
This page is the *operational* reference — the directives you can
|
|
24
45
|
write and their syntax. For the normative rules (conflict
|
|
25
46
|
handling, merging, provenance) see
|
|
@@ -135,6 +156,62 @@ directives on one class combine like an intersection of
|
|
|
135
156
|
interfaces. The directive is purely additive — a class that
|
|
136
157
|
already satisfies the interface type-checks with or without it.
|
|
137
158
|
|
|
159
|
+
## Effect envelopes — bounding what a method *does*
|
|
160
|
+
|
|
161
|
+
Every directive above describes what a method returns. Two
|
|
162
|
+
describe what it *does*: `%a{pure}` — rbs' own purity
|
|
163
|
+
annotation, read as "nothing at all" — and
|
|
164
|
+
`%a{rigor:v1:effect <labels>}`, a comma-separated list of bare
|
|
165
|
+
[effect labels](../type-specification/effect-labels.md) the
|
|
166
|
+
method may not exceed. Both attach to a method or to a `class` /
|
|
167
|
+
`module`, where they distribute to that class's own methods
|
|
168
|
+
(nearest wins), and both tolerate mutating objects the method
|
|
169
|
+
itself allocated and never let out:
|
|
170
|
+
|
|
171
|
+
```rbs
|
|
172
|
+
class UserRepository
|
|
173
|
+
%a{rigor:v1:effect io.db, nondet.time}
|
|
174
|
+
def find: (Integer) -> User
|
|
175
|
+
|
|
176
|
+
%a{pure}
|
|
177
|
+
def slug: (String) -> String
|
|
178
|
+
end
|
|
179
|
+
```
|
|
180
|
+
|
|
181
|
+
The same two work as rbs-inline comments in a `.rb` file:
|
|
182
|
+
|
|
183
|
+
```rb
|
|
184
|
+
# rbs_inline: enabled
|
|
185
|
+
|
|
186
|
+
class UserRepository
|
|
187
|
+
# @rbs %a{rigor:v1:effect io.db}
|
|
188
|
+
# @rbs id: Integer
|
|
189
|
+
# @rbs return: User
|
|
190
|
+
def find(id) = User.find(id)
|
|
191
|
+
|
|
192
|
+
# @rbs %a{pure}
|
|
193
|
+
# @rbs return: String
|
|
194
|
+
def slug(s) = s.strip.downcase
|
|
195
|
+
end
|
|
196
|
+
```
|
|
197
|
+
|
|
198
|
+
Three things follow, and all three need an `effects:` block in
|
|
199
|
+
`.rigor.yml` — an annotation alone never turns effect collection
|
|
200
|
+
on:
|
|
201
|
+
|
|
202
|
+
- A method whose proven effects escape its bound fires
|
|
203
|
+
[`effect.envelope-exceeded`](04-diagnostics.md#rule-effect-envelope-exceeded),
|
|
204
|
+
positioned at the Ruby `def`.
|
|
205
|
+
- A label the registry does not recognise makes the **whole
|
|
206
|
+
annotation** read as unbounded — a typo can never manufacture
|
|
207
|
+
a finding — and, where the spelling is evidently meant to be a
|
|
208
|
+
label, says so as
|
|
209
|
+
[`effect.unknown-label`](04-diagnostics.md#rule-effect-unknown-label)
|
|
210
|
+
at the declaration.
|
|
211
|
+
- Without the block, one
|
|
212
|
+
[`effect.annotations-unchecked`](04-diagnostics.md#rule-effect-annotations-unchecked)
|
|
213
|
+
`:info` per run tells you the annotations are inert.
|
|
214
|
+
|
|
138
215
|
## Higher-kinded type directives
|
|
139
216
|
|
|
140
217
|
Two declaration-level directives register and define the
|
|
@@ -0,0 +1,326 @@
|
|
|
1
|
+
# Removing dead code with `rigor unused`
|
|
2
|
+
|
|
3
|
+
This chapter is for the person handed an old codebase and asked what
|
|
4
|
+
can go. `rigor unused` turns "which of these 1,500 classes is dead?"
|
|
5
|
+
into a finite list of specific questions. It does not answer them.
|
|
6
|
+
|
|
7
|
+
The command reference — every flag, every output section — is in
|
|
8
|
+
[CLI reference](02-cli-reference.md#rigor-unused). This chapter is the
|
|
9
|
+
workflow around it.
|
|
10
|
+
|
|
11
|
+
**Before you start** you need a working `rigor check` setup: a config
|
|
12
|
+
file with `paths:`, and the plugins for your framework enabled. See
|
|
13
|
+
[Configuration](03-configuration.md) and [Using plugins](07-plugins.md).
|
|
14
|
+
`rigor unused` reads the same config and loads the same `plugins:`, and
|
|
15
|
+
the plugin half is not optional — the section on
|
|
16
|
+
[roots](#roots-are-the-lever) explains why a Rails app without
|
|
17
|
+
`rigor-rails-routes` produces a report that is mostly noise.
|
|
18
|
+
|
|
19
|
+
## What it can and cannot tell you
|
|
20
|
+
|
|
21
|
+
`rigor unused` answers one question exactly: **which project classes
|
|
22
|
+
and modules does no reachable code name?** That is not the same
|
|
23
|
+
question as "what is dead", and the gap between them is the whole
|
|
24
|
+
subject of this chapter.
|
|
25
|
+
|
|
26
|
+
On one mid-sized Rails application, every surviving candidate was
|
|
27
|
+
hand-checked against the whole repository: **4 of 57 were genuinely
|
|
28
|
+
dead.** The other 53 were live code reached by means the analysis
|
|
29
|
+
cannot see.
|
|
30
|
+
|
|
31
|
+
Treat that ratio as an order of magnitude, not a constant. It is one
|
|
32
|
+
adjudication of one codebase; your rate depends on how much of your
|
|
33
|
+
framework has plugin coverage. What generalises is the shape: most
|
|
34
|
+
rows will be live code, so the output is a review queue and not a work
|
|
35
|
+
list. That is also why this ships as its own command rather than a
|
|
36
|
+
`rigor check` diagnostic — see [ADR-102][adr-102].
|
|
37
|
+
|
|
38
|
+
Three limits are worth knowing before you plan any work:
|
|
39
|
+
|
|
40
|
+
- **Naming is not calling.** A class whose constant is named once and
|
|
41
|
+
whose every method is dead counts as fully reachable. This is a
|
|
42
|
+
constant-level report; unused *methods* are not reported at all.
|
|
43
|
+
- **It under-reports, by an unknown amount.** An over-claiming root
|
|
44
|
+
source removes rows silently — see
|
|
45
|
+
[watching for drift](#running-it-as-a-campaign). There is no recall
|
|
46
|
+
figure, and no way to measure one short of adjudicating classes the
|
|
47
|
+
report never showed you.
|
|
48
|
+
- **Value constants** (`TIMEOUT = 30`) are omitted entirely; they do
|
|
49
|
+
not resolve across files, so every one would be a false row.
|
|
50
|
+
**Reopened gem or stdlib classes** are never candidates — reopening
|
|
51
|
+
`String` in an initializer declares it in your project, but it is
|
|
52
|
+
not yours to call unused.
|
|
53
|
+
|
|
54
|
+
## The first run
|
|
55
|
+
|
|
56
|
+
```sh
|
|
57
|
+
rigor unused
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
It analyses the whole project every time; there is no incremental
|
|
61
|
+
mode, because reachability computed over a subset is not sound. Budget
|
|
62
|
+
roughly the cost of a full `rigor check`: about 14 seconds for 1,300
|
|
63
|
+
files, about 4 minutes for 11,000.
|
|
64
|
+
|
|
65
|
+
The summary tells you whether the rest of the report is worth reading:
|
|
66
|
+
|
|
67
|
+
```
|
|
68
|
+
Reachability
|
|
69
|
+
declared (project-owned): 1497
|
|
70
|
+
roots: 428 (312 from plugins, 3 matched no declaration)
|
|
71
|
+
reachable: 1417
|
|
72
|
+
candidates: 45
|
|
73
|
+
reachable only from tests: 164
|
|
74
|
+
cannot decide: 25
|
|
75
|
+
namespace-only (excluded): 10
|
|
76
|
+
```
|
|
77
|
+
|
|
78
|
+
`reachable`, `candidates`, `cannot decide` and `namespace-only`
|
|
79
|
+
partition `declared` — they sum to 1497. **`reachable only from tests`
|
|
80
|
+
is a subset of `reachable`**, not a fifth bucket, so do not add it in.
|
|
81
|
+
|
|
82
|
+
Read `roots` first. A root is a declaration something outside your code
|
|
83
|
+
reaches — an entry point, a route, a framework convention.
|
|
84
|
+
Reachability is computed *from* roots, so a thin root set inflates
|
|
85
|
+
everything below it. The pathological case is a Rails application with
|
|
86
|
+
`0 from plugins`: nothing in Ruby source names a controller, so every
|
|
87
|
+
controller you own appears dead. There is no published healthy ratio
|
|
88
|
+
to compare against; what you are checking is whether the number is
|
|
89
|
+
plausible for your framework, and `0 from plugins` on a framework app
|
|
90
|
+
never is.
|
|
91
|
+
|
|
92
|
+
## Getting the number down
|
|
93
|
+
|
|
94
|
+
Two things change the count. Only one of them helps.
|
|
95
|
+
|
|
96
|
+
### Roots are the lever
|
|
97
|
+
|
|
98
|
+
Wiring up root sources took one application's candidate list from 129
|
|
99
|
+
to 57, and another's from 278 to 45 — **56% and 84%**.
|
|
100
|
+
|
|
101
|
+
**Your plugins**, first and most of it. `rigor-rails-routes` reads
|
|
102
|
+
`config/routes.rb` statically and names every controller it dispatches
|
|
103
|
+
to, including routes written under a conditional; it follows
|
|
104
|
+
`draw(:admin)` into `config/routes/*.rb`. `rigor-pundit` publishes the
|
|
105
|
+
policies your `authorize` calls actually name. Mounted engines, and
|
|
106
|
+
Devise/Doorkeeper controller remappings, are not covered — those
|
|
107
|
+
controllers will still appear as candidates. If your framework has no
|
|
108
|
+
Rigor plugin at all, its entry points supply nothing, and that is a
|
|
109
|
+
gap in coverage rather than a finding about your code.
|
|
110
|
+
|
|
111
|
+
**Entry points you declare**, for anything no plugin reaches:
|
|
112
|
+
|
|
113
|
+
```sh
|
|
114
|
+
rigor unused --entry-point='lib/cli.rb' --entry-point='lib/workers/**/*.rb'
|
|
115
|
+
```
|
|
116
|
+
|
|
117
|
+
Repeatable. Each glob is matched against every declaration's path, and
|
|
118
|
+
roots every declaration in a matching file.
|
|
119
|
+
|
|
120
|
+
**File-level references**, free. Code that runs on load — an
|
|
121
|
+
initializer naming a class, a `.rake` task — roots what it names. This
|
|
122
|
+
happens whether or not those directories are in `paths:`, because
|
|
123
|
+
references are harvested from the whole project while declarations come
|
|
124
|
+
only from `paths:`.
|
|
125
|
+
|
|
126
|
+
### Widening `paths:` is not the lever
|
|
127
|
+
|
|
128
|
+
The obvious first move is to analyse more of the project. Measured on
|
|
129
|
+
three applications, adding `config/` to `paths:` moved the candidate
|
|
130
|
+
count by **+0.7%, −0.8% and +3.4%** — nothing, in two directions.
|
|
131
|
+
|
|
132
|
+
The reason follows from the asymmetry above: references already came
|
|
133
|
+
from the whole project, so widening `paths:` adds *declarations* to
|
|
134
|
+
explain without adding references that explain them. Analyse `config/`
|
|
135
|
+
if you want it type-checked. It will not shorten this list.
|
|
136
|
+
|
|
137
|
+
## Working the list
|
|
138
|
+
|
|
139
|
+
Four sections, needing four different decisions:
|
|
140
|
+
|
|
141
|
+
| Section | What it means | What to do |
|
|
142
|
+
| --- | --- | --- |
|
|
143
|
+
| **Reachable only from test code** | Live test, no production caller | Work these first |
|
|
144
|
+
| **Candidates** | Nothing reachable names it | Adjudicate — most are still live |
|
|
145
|
+
| **Cannot decide** | Something can name it at runtime | Read the reason; do not delete from here |
|
|
146
|
+
| **Namespace-only** | A module wrapping live code | Excluded from candidates; count only |
|
|
147
|
+
|
|
148
|
+
Work them in that order — which is not the order you *read* the
|
|
149
|
+
summary in, where `roots` comes first because it tells you whether to
|
|
150
|
+
trust the rest.
|
|
151
|
+
|
|
152
|
+
### Test-only rows first
|
|
153
|
+
|
|
154
|
+
Not because the section is smaller — in the sample above it is 164
|
|
155
|
+
rows against 45 candidates — but because each row carries its own
|
|
156
|
+
evidence. A class with a passing spec and no production caller is
|
|
157
|
+
either dead or reached by a mechanism worth writing down, and either
|
|
158
|
+
way you can settle it from the spec file. On one application this
|
|
159
|
+
section held 22 policy classes whose only remaining caller was
|
|
160
|
+
`spec/policies/`. Those 22 were not adjudicated, so treat the section
|
|
161
|
+
as high-signal rather than high-yield; the yield is unmeasured.
|
|
162
|
+
|
|
163
|
+
### Candidates: sort by how conventional the class is
|
|
164
|
+
|
|
165
|
+
A candidate row is its name and declaration site:
|
|
166
|
+
|
|
167
|
+
```
|
|
168
|
+
Candidates — nothing reachable references these (45)
|
|
169
|
+
1 Api::V1::Timelines::TopicController app/controllers/api/v1/timelines/topic_controller.rb:3
|
|
170
|
+
```
|
|
171
|
+
|
|
172
|
+
The 53 false positives in the adjudicated run fell into a few recurring
|
|
173
|
+
shapes, and 28 of them were the first one. Recognising these lets you
|
|
174
|
+
skip most of a list quickly:
|
|
175
|
+
|
|
176
|
+
- **Framework naming conventions** — a helper module paired to a
|
|
177
|
+
controller by name, a Rails generator, a `ClassMethods` module
|
|
178
|
+
auto-extended by `ActiveSupport::Concern`, a decorator a gem applies
|
|
179
|
+
to a model by name, a join model reached through
|
|
180
|
+
`has_many :speakers_talks`, or a custom validator derived from an
|
|
181
|
+
option key (`validates :start_date, date: true` resolves the name
|
|
182
|
+
`DateValidator`). Nothing writes the name; the framework derives it.
|
|
183
|
+
Two of these clear whole clusters at once, so check them before
|
|
184
|
+
working through rows one at a time: **`include_all_helpers` defaults
|
|
185
|
+
to true**, so unless the app disables it every `app/helpers/*Helper`
|
|
186
|
+
is live; and a decorator gem's convention covers `app/decorators`
|
|
187
|
+
wholesale.
|
|
188
|
+
- **Named as a string in configuration** — a recurring-job schedule
|
|
189
|
+
(`config/recurring.yml`, `config/schedule.yml`, a `sidekiq.yml`
|
|
190
|
+
scheduler block), a queue definition, a class name in a settings
|
|
191
|
+
file. The scheduler resolves it by name, so no Ruby code has to, and
|
|
192
|
+
a scheduled job commonly has no `perform_later` call anywhere in the
|
|
193
|
+
repository. Check that the file is actually loaded — a `Procfile` or
|
|
194
|
+
deploy config passing `--recurring_schedule_file` is the confirming
|
|
195
|
+
evidence.
|
|
196
|
+
- **Called from a view** — `app/views/**/*.erb` is not Ruby and is not
|
|
197
|
+
analysed, so a helper used only from templates has no caller the
|
|
198
|
+
report can see. Grep the view tree before believing any helper row.
|
|
199
|
+
- **Self-registering classes** — a class whose body calls a
|
|
200
|
+
registration DSL (`add "link"`, `register :thing`). The evidence of
|
|
201
|
+
use is inside the class, not outside it.
|
|
202
|
+
- **Published extension points** — a base class subclassed only from
|
|
203
|
+
outside your repository. Dead from within, load-bearing without.
|
|
204
|
+
- **Scaffolding for a feature you never used** — `rails new` generates
|
|
205
|
+
`ApplicationCable::Connection` and `ApplicationCable::Channel`
|
|
206
|
+
whether or not you add a channel. If `app/channels` holds nothing
|
|
207
|
+
else, the honest finding is that the feature is unused, not the
|
|
208
|
+
class. Decide about the feature.
|
|
209
|
+
|
|
210
|
+
### Cannot decide: read the reason, do not delete
|
|
211
|
+
|
|
212
|
+
These were demoted out of `candidates` because something can name the
|
|
213
|
+
class at runtime. Each row says what:
|
|
214
|
+
|
|
215
|
+
```
|
|
216
|
+
1 Handlers::Alpha lib/handlers.rb:2
|
|
217
|
+
constantize on an interpolated string (lib/dispatch.rb:14)
|
|
218
|
+
```
|
|
219
|
+
|
|
220
|
+
`"Foo".constantize` names `Foo` exactly, so it counts as an ordinary
|
|
221
|
+
reference and never reaches this section. `"Foo::#{key}".constantize`
|
|
222
|
+
can only bound the namespace, so everything under `Foo` is demoted. A
|
|
223
|
+
class name appearing as a string in a `.yml`, `.json` or template file
|
|
224
|
+
demotes the same way — weaker evidence than a constant reference, and
|
|
225
|
+
neither proof of use nor grounds to call it dead.
|
|
226
|
+
|
|
227
|
+
A large section here is telling you that dynamic dispatch is
|
|
228
|
+
load-bearing in this codebase, which is the honest reason a deletion
|
|
229
|
+
campaign stops where it does.
|
|
230
|
+
|
|
231
|
+
## Before you delete
|
|
232
|
+
|
|
233
|
+
A candidate row is the start of a case. Confirm each of these:
|
|
234
|
+
|
|
235
|
+
1. **Search for spellings the report cannot match.** Do not re-grep
|
|
236
|
+
the fully-qualified name — a row would not be in `candidates` if
|
|
237
|
+
the FQN appeared anywhere the report reads. Grep instead for the
|
|
238
|
+
forms it cannot: the snake_cased autoload path
|
|
239
|
+
(`admin/reports_controller`), the demodulized leaf, any name
|
|
240
|
+
assembled by interpolation, and the view tree.
|
|
241
|
+
2. **Check it is not reached by convention** — the shapes above.
|
|
242
|
+
3. **Check nothing outside the repository references it.** For a gem,
|
|
243
|
+
a library, or an app with third-party extensions, "unused" means
|
|
244
|
+
"unused by callers I can see", and your public API is exactly what
|
|
245
|
+
you cannot see.
|
|
246
|
+
4. **Read the git history** — but check it is real first. A class
|
|
247
|
+
added recently and never wired up is unfinished work rather than
|
|
248
|
+
dead code. In a shallow clone, or one whose history is all
|
|
249
|
+
dependency bumps, the "last change" date is an artifact and proves
|
|
250
|
+
nothing; say so instead of quoting a bot commit as the file's age.
|
|
251
|
+
5. **Delete its tests with it.** A spec that must survive the class it
|
|
252
|
+
tests is testing nothing.
|
|
253
|
+
|
|
254
|
+
**Empty is not the same as dead.** A module the framework includes
|
|
255
|
+
everywhere but which declares no methods is live by every measure this
|
|
256
|
+
report uses, and still worth deleting — as generator residue, not as
|
|
257
|
+
dead code. Say which of the two you found; they justify different
|
|
258
|
+
amounts of care from a reviewer.
|
|
259
|
+
|
|
260
|
+
### Making the case to someone else
|
|
261
|
+
|
|
262
|
+
The useful artifacts are the candidate row with its declaration site,
|
|
263
|
+
the result of step 1, and the date of the file's last substantive
|
|
264
|
+
change.
|
|
265
|
+
|
|
266
|
+
State the precision figure yourself rather than waiting to be asked. A
|
|
267
|
+
reviewer who knows the report is mostly false positives, and that you
|
|
268
|
+
checked this row by hand, has more reason to trust the specific claim —
|
|
269
|
+
not less. Be equally direct that the tool under-reports: it will not
|
|
270
|
+
have shown you everything.
|
|
271
|
+
|
|
272
|
+
## Running it as a campaign
|
|
273
|
+
|
|
274
|
+
**Re-run after configuring, not after deleting.** The list moves far
|
|
275
|
+
more when you add a root source than when you remove a class.
|
|
276
|
+
|
|
277
|
+
**Watch `matched no declaration`.** This counts plugin-supplied roots
|
|
278
|
+
naming something your project does not declare:
|
|
279
|
+
|
|
280
|
+
```
|
|
281
|
+
roots: 428 (312 from plugins, 3 matched no declaration)
|
|
282
|
+
```
|
|
283
|
+
|
|
284
|
+
A few are normal — framework classes such as `Rails::HealthController`
|
|
285
|
+
are correctly named and correctly not yours. There is no threshold to
|
|
286
|
+
quote; what matters is the trend, because **an over-claiming root
|
|
287
|
+
source silently hides dead code**. Under-supply leaves rows on the list
|
|
288
|
+
where you can see them; over-supply removes them where you cannot.
|
|
289
|
+
|
|
290
|
+
**Expect drift in one direction.** New code adds candidates; new routes
|
|
291
|
+
and entry points remove them. If the count climbs over months with
|
|
292
|
+
nobody adding dead code, suspect a plugin stopped loading before you
|
|
293
|
+
suspect the codebase.
|
|
294
|
+
|
|
295
|
+
To track any of this you have to keep the numbers yourself — commit the
|
|
296
|
+
`--format json` output, or record the summary line. There is no
|
|
297
|
+
baseline file for this command.
|
|
298
|
+
|
|
299
|
+
### If your project ships generated RBS
|
|
300
|
+
|
|
301
|
+
Signature files both declare and reference, and only the references
|
|
302
|
+
count. `class Talk < ApplicationRecord` in `sig/` references
|
|
303
|
+
`ApplicationRecord`; the `Talk` it declares is not a reference to
|
|
304
|
+
`Talk`. If it were, every class with a generated signature would root
|
|
305
|
+
itself and the report would be empty of exactly the rows you want.
|
|
306
|
+
|
|
307
|
+
That is worth knowing because it was wrong until recently, and the
|
|
308
|
+
symptom was silent: on one application 48 of 101 roots came from `sig/`
|
|
309
|
+
and eleven rows never appeared. If you are on a release before this was
|
|
310
|
+
fixed ([issue #363][issue-363]), compare against a run with
|
|
311
|
+
`signature_paths:` emptied — a large gap between the two is the tell.
|
|
312
|
+
|
|
313
|
+
## Automating it
|
|
314
|
+
|
|
315
|
+
**Do not gate a build on this.** The command always exits 0 when it
|
|
316
|
+
runs, precisely so that it cannot become a check; the only non-zero
|
|
317
|
+
exit is a usage error, such as passing `--incremental`. A threshold on
|
|
318
|
+
the candidate count would fire on your own tooling as often as on your
|
|
319
|
+
code — the count climbing usually means a plugin stopped loading.
|
|
320
|
+
|
|
321
|
+
Running it as a **reporting** job is fine, and is the intended way to
|
|
322
|
+
notice the drift described above: run it on a schedule, publish the
|
|
323
|
+
JSON, and let a person read the trend.
|
|
324
|
+
|
|
325
|
+
[adr-102]: https://github.com/rigortype/rigor/blob/master/docs/adr/102-unused-code-reachability-report.md
|
|
326
|
+
[issue-363]: https://github.com/rigortype/rigor/issues/363
|
data/docs/manual/README.md
CHANGED
|
@@ -61,6 +61,11 @@ flag, key, or command that *acts* on it.
|
|
|
61
61
|
and your tests into one safety net (`rigor coverage
|
|
62
62
|
--protection [--mutation --with-tests --include-dynamic]`).
|
|
63
63
|
|
|
64
|
+
18. [Removing dead code](18-removing-dead-code.md) — working
|
|
65
|
+
`rigor unused` as a campaign on an old codebase: why the list
|
|
66
|
+
starts long, which lever actually shortens it, what to confirm
|
|
67
|
+
before deleting, and how to make the case to a reviewer.
|
|
68
|
+
|
|
64
69
|
### Integration and operations
|
|
65
70
|
|
|
66
71
|
9. [Editor integration](09-editor-integration.md) — wiring
|
|
@@ -64,6 +64,11 @@ plugins:
|
|
|
64
64
|
- A brand-new view file does not invalidate the cached index until
|
|
65
65
|
something the mailer file touches changes (the read-tracking
|
|
66
66
|
trade-off).
|
|
67
|
+
- **No roots for [`rigor unused`](../02-cli-reference.md#rigor-unused).**
|
|
68
|
+
`MyMailer.welcome(...)` names the mailer as an ordinary constant,
|
|
69
|
+
which the report already records. Rooting every class under
|
|
70
|
+
`app/mailers` instead would mark a mailer nothing sends reachable
|
|
71
|
+
forever on no evidence.
|
|
67
72
|
|
|
68
73
|
## Plugin internals
|
|
69
74
|
|
|
@@ -36,8 +36,47 @@ plugins:
|
|
|
36
36
|
config:
|
|
37
37
|
job_search_paths: ["app/jobs"] # default
|
|
38
38
|
job_base_classes: ["ApplicationJob", "ActiveJob::Base"] # default
|
|
39
|
+
recurring_paths: ["config/recurring.yml"] # default
|
|
39
40
|
```
|
|
40
41
|
|
|
42
|
+
`recurring_paths` are the schedule *files* — not directories —
|
|
43
|
+
behind the reachability roots below. The default is where Solid
|
|
44
|
+
Queue's recurring schedule conventionally lives; list your own path
|
|
45
|
+
if you keep it elsewhere.
|
|
46
|
+
|
|
47
|
+
## Job roots for `rigor unused`
|
|
48
|
+
|
|
49
|
+
Solid Queue is the Active Job backend Rails ships by default from
|
|
50
|
+
8.0, and a **recurring task names its job by string**:
|
|
51
|
+
|
|
52
|
+
```yaml
|
|
53
|
+
# config/recurring.yml
|
|
54
|
+
production:
|
|
55
|
+
send_reminder:
|
|
56
|
+
class: "SendReminderJob"
|
|
57
|
+
schedule: "*/3 * * * *"
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
That job runs every three minutes and there is no `perform_later`
|
|
61
|
+
for it anywhere, so the constant scan sees nothing and
|
|
62
|
+
[`rigor unused`](../02-cli-reference.md#rigor-unused) reports live
|
|
63
|
+
production code as possibly dead. This plugin supplies the jobs your
|
|
64
|
+
recurring schedule names, so they drop out of the candidate list.
|
|
65
|
+
|
|
66
|
+
**Every environment block is read**, not just `production:` — a job
|
|
67
|
+
scheduled in `staging:` is still live code. A flat, environment-less
|
|
68
|
+
document works too.
|
|
69
|
+
|
|
70
|
+
Only `class:` is read. A `command:` entry is inline Ruby
|
|
71
|
+
(`command: "SomeModel.cleanup"`), and parsing a constant out of an
|
|
72
|
+
arbitrary snippet would root a class on a guess, so it supplies
|
|
73
|
+
nothing. A `class:` naming a job the plugin never discovered is
|
|
74
|
+
dropped rather than published: a typo costs you a root instead of
|
|
75
|
+
quietly hiding a dead job.
|
|
76
|
+
|
|
77
|
+
The schedule is read with `YAML.safe_load`. Nothing boots Rails and
|
|
78
|
+
nothing loads Solid Queue.
|
|
79
|
+
|
|
41
80
|
## Limitations
|
|
42
81
|
|
|
43
82
|
- **Direct-superclass match only.** `class WelcomeJob < BaseJob`
|
|
@@ -48,6 +87,14 @@ plugins:
|
|
|
48
87
|
- **Positional arity only.** Required keyword arguments are
|
|
49
88
|
recorded by the discoverer but not yet validated at the call
|
|
50
89
|
site.
|
|
90
|
+
- **Roots only from a recurring schedule.** A job is rooted for
|
|
91
|
+
`rigor unused` when `config/recurring.yml` names it, and never for
|
|
92
|
+
existing under `app/jobs`: `MyJob.perform_later` names the job as
|
|
93
|
+
an ordinary constant the report already records, and rooting every
|
|
94
|
+
discovered job would mark an orphaned one reachable forever on no
|
|
95
|
+
evidence. A schedule loaded from Ruby rather than from a file under
|
|
96
|
+
`recurring_paths` supplies nothing, by the same "read only what is
|
|
97
|
+
written" rule.
|
|
51
98
|
|
|
52
99
|
## Plugin internals
|
|
53
100
|
|
|
@@ -54,6 +54,22 @@ plugins:
|
|
|
54
54
|
# Minitest projects: ["test/factories"]
|
|
55
55
|
```
|
|
56
56
|
|
|
57
|
+
## Factory references for `rigor unused`
|
|
58
|
+
|
|
59
|
+
`factory :user, class: "Admin::User"` names a class as a string, and
|
|
60
|
+
a bare `factory :user` relies on FactoryBot constantizing the factory
|
|
61
|
+
name. Neither is a constant reference, so
|
|
62
|
+
[`rigor unused`](../02-cli-reference.md#rigor-unused) cannot see them
|
|
63
|
+
and would report a factoried model as dead.
|
|
64
|
+
|
|
65
|
+
This plugin supplies those model classes — as **test-role
|
|
66
|
+
references**, not as roots. A class reached only through a factory
|
|
67
|
+
therefore leaves the candidate list and appears under **Reachable
|
|
68
|
+
only from test code** instead. That is deliberate: a model kept alive
|
|
69
|
+
by its factory and its spec and nothing else is dead production code
|
|
70
|
+
with a live test, and rooting factories would have hidden exactly
|
|
71
|
+
that finding.
|
|
72
|
+
|
|
57
73
|
## Limitations
|
|
58
74
|
|
|
59
75
|
- **Literal arguments only** — `FactoryBot.create(name)` with a
|
|
@@ -48,8 +48,34 @@ plugins:
|
|
|
48
48
|
config:
|
|
49
49
|
policy_search_paths: ["app/policies"] # default
|
|
50
50
|
policy_base_classes: ["ApplicationPolicy"] # default
|
|
51
|
+
authorization_call_paths: ["app/controllers"] # default
|
|
51
52
|
```
|
|
52
53
|
|
|
54
|
+
`authorization_call_paths` is where the plugin looks for the
|
|
55
|
+
`authorize` / `policy` / `policy_scope` calls behind the reachability
|
|
56
|
+
roots below. Widen it if you also authorize from `app/graphql` or
|
|
57
|
+
`app/services`; it defaults narrow because the walk is a parse per
|
|
58
|
+
file on every run.
|
|
59
|
+
|
|
60
|
+
## Policy roots for `rigor unused`
|
|
61
|
+
|
|
62
|
+
`authorize @post` runs `PostPolicy#update?`, and the name
|
|
63
|
+
`PostPolicy` is never written down. So without help,
|
|
64
|
+
[`rigor unused`](../02-cli-reference.md#rigor-unused) reports every
|
|
65
|
+
policy in your app as possibly dead. This plugin supplies the
|
|
66
|
+
policies your code actually authorizes against, so they drop out of
|
|
67
|
+
the candidate list.
|
|
68
|
+
|
|
69
|
+
It publishes the policies **an authorization call names**, not every
|
|
70
|
+
class under `policy_search_paths`. A policy nothing authorizes
|
|
71
|
+
against stays in the report, which is the answer you wanted to see:
|
|
72
|
+
a root source that claims more than it can show hides real dead code
|
|
73
|
+
without telling you.
|
|
74
|
+
|
|
75
|
+
A policy the plugin cannot attribute to a call — a namespaced
|
|
76
|
+
`Admin::PostPolicy`, a record argument it cannot read — stays a
|
|
77
|
+
candidate rather than being guessed at.
|
|
78
|
+
|
|
53
79
|
## Limitations
|
|
54
80
|
|
|
55
81
|
- **Direct-superclass match only.** `class AdminPostPolicy <
|
|
@@ -83,6 +83,44 @@ The parsed helper table is published as the `:helper_table`
|
|
|
83
83
|
cross-plugin fact (ADR-9), which `rigor-actionpack` consumes to
|
|
84
84
|
validate helper calls inside controllers.
|
|
85
85
|
|
|
86
|
+
### Controller roots for `rigor unused`
|
|
87
|
+
|
|
88
|
+
The same parse also yields the **controller classes** your routes
|
|
89
|
+
dispatch to, published as the `:reachability_roots` fact that
|
|
90
|
+
[`rigor unused`](../02-cli-reference.md#rigor-unused) seeds its
|
|
91
|
+
reachability report with.
|
|
92
|
+
|
|
93
|
+
This matters because Rails reaches a controller by *name* at request
|
|
94
|
+
time. Nothing in your code references `Admin::UsersController`, so
|
|
95
|
+
without route knowledge every controller in the app reads as
|
|
96
|
+
unreferenced — on Mastodon that was 233 spurious candidates.
|
|
97
|
+
|
|
98
|
+
Controller names are composed the way Rails composes them:
|
|
99
|
+
`resources :users` inside `namespace :admin` is
|
|
100
|
+
`Admin::UsersController`; a singular `resource :profile` is served by
|
|
101
|
+
the plural `ProfilesController`; `scope module:` and a `module:`
|
|
102
|
+
option on a resource extend the namespace; `to: "posts#index"`,
|
|
103
|
+
`controller:`, and the hashrocket `get "help" => "help#show"` form
|
|
104
|
+
are all read. Acronyms declared in
|
|
105
|
+
`config/initializers/inflections.rb` are applied, so
|
|
106
|
+
`scope module: :activitypub` resolves to `ActivityPub::…`.
|
|
107
|
+
|
|
108
|
+
Two properties follow from parsing rather than booting, and are the
|
|
109
|
+
reason to prefer this over reading a running app's route table:
|
|
110
|
+
|
|
111
|
+
- **No Rails runtime is loaded.**
|
|
112
|
+
- **A route under a conditional is still a root.**
|
|
113
|
+
`get "/beta", to: "beta#index" if ENV["ENABLE_BETA"]` does not
|
|
114
|
+
appear in a booted app's route table unless the condition happened
|
|
115
|
+
to hold; both branches are visible to a static read.
|
|
116
|
+
|
|
117
|
+
**Not covered as roots** (deliberately, rather than half-covered):
|
|
118
|
+
controllers remapped by `devise_for … controllers:`, by
|
|
119
|
+
`use_doorkeeper … controllers:`, or served through a mounted Grape
|
|
120
|
+
API. Those mappings name controllers the plugin does not resolve, so
|
|
121
|
+
they may appear as `rigor unused` candidates even though they are
|
|
122
|
+
routed. Helper-name recognition for all three is unaffected.
|
|
123
|
+
|
|
86
124
|
## Limitations
|
|
87
125
|
|
|
88
126
|
- **Statically unfoldable route definitions.** Helpers produced by
|
|
@@ -92,9 +130,10 @@ validate helper calls inside controllers.
|
|
|
92
130
|
`unknown-helper`. Record those in a baseline, or
|
|
93
131
|
`# rigor:disable` the line.
|
|
94
132
|
- **Project-custom inflections** declared in
|
|
95
|
-
`config/initializers/inflections.rb` are not yet ingested
|
|
133
|
+
`config/initializers/inflections.rb` are not yet fully ingested
|
|
96
134
|
(ADR-39 slice 3); the standard ActiveSupport inflections are
|
|
97
|
-
covered.
|
|
135
|
+
covered, and `inflect.acronym` declarations are read for
|
|
136
|
+
controller-class composition only.
|
|
98
137
|
|
|
99
138
|
## Plugin internals
|
|
100
139
|
|
|
@@ -62,6 +62,11 @@ The plugin has no configuration knobs.
|
|
|
62
62
|
/ `have_received` (overlap engine constant / undefined-method rules).
|
|
63
63
|
- **Literal arguments only** — a status passed via a variable or method
|
|
64
64
|
call is not statically checkable, so it's accepted silently.
|
|
65
|
+
- **No roots for [`rigor unused`](../02-cli-reference.md#rigor-unused)**,
|
|
66
|
+
for the reason [`rigor-rspec`](rigor-rspec.md#limitations) gives: a
|
|
67
|
+
spec's constant reference is already recorded with the `test` role,
|
|
68
|
+
and rooting it would delete the report's "reachable only from tests"
|
|
69
|
+
answer.
|
|
65
70
|
|
|
66
71
|
## Plugin internals
|
|
67
72
|
|
|
@@ -67,6 +67,14 @@ to enable project-wide alongside non-spec files.
|
|
|
67
67
|
not flagged.
|
|
68
68
|
- Constant validation (`RSpec.describe SomeClass`) is the
|
|
69
69
|
engine's job, not this plugin's.
|
|
70
|
+
- **No roots for [`rigor unused`](../02-cli-reference.md#rigor-unused).**
|
|
71
|
+
`RSpec.describe User` is already recorded as a reference, stamped
|
|
72
|
+
with the `test` role because it came from a spec. Publishing spec
|
|
73
|
+
references as roots would strip that role, promote every
|
|
74
|
+
spec-referenced class to production-reachable, and delete the
|
|
75
|
+
report's **Reachable only from test code** section — the row that
|
|
76
|
+
tells you a class is dead production code with a live test. The
|
|
77
|
+
ordinary handling is the correct one.
|
|
70
78
|
|
|
71
79
|
## Related plugins
|
|
72
80
|
|