rigortype 0.3.3 → 0.3.5
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 +860 -0
- data/data/effects/registry.yml +95 -0
- data/docs/handbook/07-rbs-and-extended.md +106 -5
- data/docs/manual/02-cli-reference.md +359 -2
- data/docs/manual/03-configuration.md +101 -1
- data/docs/manual/04-diagnostics.md +8 -3
- data/docs/manual/11-ci.md +37 -0
- data/docs/manual/12-caching.md +39 -0
- data/docs/manual/16-rbs-extended-annotations.md +90 -0
- data/docs/manual/18-removing-dead-code.md +326 -0
- data/docs/manual/19-effect-labels.md +671 -0
- data/docs/manual/README.md +10 -0
- data/docs/manual/ci-templates/README.md +9 -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 +168 -3
- data/lib/rigor/analysis/run_cache_key.rb +16 -0
- data/lib/rigor/analysis/run_cache_probe.rb +69 -1
- data/lib/rigor/analysis/runner/declaration_position.rb +38 -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 +93 -10
- data/lib/rigor/analysis/runner/run_snapshots.rb +7 -4
- data/lib/rigor/analysis/runner.rb +429 -10
- data/lib/rigor/analysis/worker_session.rb +60 -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 +245 -0
- data/lib/rigor/cli/effects_diff_renderer.rb +127 -0
- data/lib/rigor/cli/effects_explain_renderer.rb +68 -0
- data/lib/rigor/cli/effects_renderer.rb +92 -0
- data/lib/rigor/cli/effects_report.rb +163 -0
- data/lib/rigor/cli/effects_snapshot_command.rb +275 -0
- data/lib/rigor/cli/unused_command.rb +288 -0
- data/lib/rigor/cli.rb +28 -1
- data/lib/rigor/configuration/severity_profile.rb +23 -0
- data/lib/rigor/configuration.rb +322 -28
- data/lib/rigor/configuration_error.rb +20 -0
- data/lib/rigor/effects/attribution.rb +76 -0
- data/lib/rigor/effects/catalog.rb +275 -0
- data/lib/rigor/effects/collector.rb +219 -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 +139 -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 +174 -0
- data/lib/rigor/effects/framework_units.rb +222 -0
- data/lib/rigor/effects/identity.rb +104 -0
- data/lib/rigor/effects/inline_anchor.rb +134 -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 +384 -0
- data/lib/rigor/effects/propagator.rb +335 -0
- data/lib/rigor/effects/registry.rb +200 -0
- data/lib/rigor/effects/scanner.rb +302 -0
- data/lib/rigor/effects/signature_sources.rb +90 -0
- data/lib/rigor/effects/snapshot.rb +396 -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 +39 -0
- data/lib/rigor/effects/unit_scan.rb +647 -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 +28 -0
- data/lib/rigor/plugin/box.rb +18 -2
- data/lib/rigor/plugin/effect_ancestry.rb +80 -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 +127 -3
- data/lib/rigor/plugin/registry.rb +55 -0
- data/lib/rigor/plugin.rb +1 -0
- data/lib/rigor/rbs_extended/envelope_scanner.rb +160 -0
- data/lib/rigor/rbs_extended.rb +188 -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 +401 -29
- data/plugins/rigor-devise/lib/rigor/plugin/devise.rb +27 -0
- 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-rbs-inline/lib/rigor/plugin/rbs_inline.rb +56 -1
- data/plugins/rigor-sidekiq/lib/rigor/plugin/sidekiq/effects.rb +75 -0
- data/plugins/rigor-sidekiq/lib/rigor/plugin/sidekiq/schedule_scan.rb +122 -0
- data/plugins/rigor-sidekiq/lib/rigor/plugin/sidekiq.rb +68 -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 +84 -1
|
@@ -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
|