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.
Files changed (171) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +1 -1
  3. data/data/builtins/ruby_core/array.yml +11 -24
  4. data/data/builtins/ruby_core/enumerable.yml +4 -8
  5. data/data/builtins/ruby_core/exception.yml +6 -9
  6. data/data/builtins/ruby_core/hash.yml +13 -20
  7. data/data/builtins/ruby_core/io.yml +6 -9
  8. data/data/builtins/ruby_core/numeric.yml +2 -3
  9. data/data/builtins/ruby_core/proc.yml +4 -8
  10. data/data/builtins/ruby_core/re.yml +6 -9
  11. data/data/builtins/ruby_core/set.yml +2 -3
  12. data/data/builtins/ruby_core/string.yml +4 -7
  13. data/data/builtins/ruby_core/struct.yml +0 -6
  14. data/data/builtins/ruby_core/time.yml +3 -6
  15. data/data/effects/core.yml +860 -0
  16. data/data/effects/registry.yml +95 -0
  17. data/docs/handbook/07-rbs-and-extended.md +106 -5
  18. data/docs/manual/02-cli-reference.md +359 -2
  19. data/docs/manual/03-configuration.md +101 -1
  20. data/docs/manual/04-diagnostics.md +8 -3
  21. data/docs/manual/11-ci.md +37 -0
  22. data/docs/manual/12-caching.md +39 -0
  23. data/docs/manual/16-rbs-extended-annotations.md +90 -0
  24. data/docs/manual/18-removing-dead-code.md +326 -0
  25. data/docs/manual/19-effect-labels.md +671 -0
  26. data/docs/manual/README.md +10 -0
  27. data/docs/manual/ci-templates/README.md +9 -0
  28. data/docs/manual/plugins/rigor-actionmailer.md +5 -0
  29. data/docs/manual/plugins/rigor-activejob.md +47 -0
  30. data/docs/manual/plugins/rigor-factorybot.md +16 -0
  31. data/docs/manual/plugins/rigor-pundit.md +26 -0
  32. data/docs/manual/plugins/rigor-rails-routes.md +41 -2
  33. data/docs/manual/plugins/rigor-rspec-rails.md +5 -0
  34. data/docs/manual/plugins/rigor-rspec.md +8 -0
  35. data/docs/manual/plugins/rigor-sidekiq.md +54 -0
  36. data/lib/rigor/analysis/check_rules/rule_ids.rb +28 -2
  37. data/lib/rigor/analysis/incremental_session.rb +140 -6
  38. data/lib/rigor/analysis/reachability/graph.rb +244 -0
  39. data/lib/rigor/analysis/reachability/plugin_roots.rb +164 -0
  40. data/lib/rigor/analysis/reachability/project_files.rb +58 -0
  41. data/lib/rigor/analysis/reachability/scan.rb +241 -0
  42. data/lib/rigor/analysis/reachability/signature_scan.rb +134 -0
  43. data/lib/rigor/analysis/rule_catalog.rb +168 -3
  44. data/lib/rigor/analysis/run_cache_key.rb +16 -0
  45. data/lib/rigor/analysis/run_cache_probe.rb +69 -1
  46. data/lib/rigor/analysis/runner/declaration_position.rb +38 -0
  47. data/lib/rigor/analysis/runner/effect_annotation_residual_pass.rb +108 -0
  48. data/lib/rigor/analysis/runner/effect_envelope_pass.rb +358 -0
  49. data/lib/rigor/analysis/runner/envelope_messages.rb +61 -0
  50. data/lib/rigor/analysis/runner/pool_coordinator.rb +93 -10
  51. data/lib/rigor/analysis/runner/run_snapshots.rb +7 -4
  52. data/lib/rigor/analysis/runner.rb +429 -10
  53. data/lib/rigor/analysis/worker_session.rb +60 -2
  54. data/lib/rigor/bleeding_edge.rb +22 -0
  55. data/lib/rigor/cache/incremental_snapshot.rb +24 -5
  56. data/lib/rigor/cli/check_command.rb +13 -1
  57. data/lib/rigor/cli/check_runner_factory.rb +4 -1
  58. data/lib/rigor/cli/effects_command.rb +245 -0
  59. data/lib/rigor/cli/effects_diff_renderer.rb +127 -0
  60. data/lib/rigor/cli/effects_explain_renderer.rb +68 -0
  61. data/lib/rigor/cli/effects_renderer.rb +92 -0
  62. data/lib/rigor/cli/effects_report.rb +163 -0
  63. data/lib/rigor/cli/effects_snapshot_command.rb +275 -0
  64. data/lib/rigor/cli/unused_command.rb +288 -0
  65. data/lib/rigor/cli.rb +28 -1
  66. data/lib/rigor/configuration/severity_profile.rb +23 -0
  67. data/lib/rigor/configuration.rb +322 -28
  68. data/lib/rigor/configuration_error.rb +20 -0
  69. data/lib/rigor/effects/attribution.rb +76 -0
  70. data/lib/rigor/effects/catalog.rb +275 -0
  71. data/lib/rigor/effects/collector.rb +219 -0
  72. data/lib/rigor/effects/config_envelopes.rb +185 -0
  73. data/lib/rigor/effects/discharge.rb +69 -0
  74. data/lib/rigor/effects/effect_table.rb +92 -0
  75. data/lib/rigor/effects/entry_points.rb +139 -0
  76. data/lib/rigor/effects/envelope.rb +86 -0
  77. data/lib/rigor/effects/envelope_check.rb +172 -0
  78. data/lib/rigor/effects/envelope_index.rb +157 -0
  79. data/lib/rigor/effects/file_collection.rb +174 -0
  80. data/lib/rigor/effects/framework_units.rb +222 -0
  81. data/lib/rigor/effects/identity.rb +104 -0
  82. data/lib/rigor/effects/inline_anchor.rb +134 -0
  83. data/lib/rigor/effects/label.rb +77 -0
  84. data/lib/rigor/effects/label_intent.rb +73 -0
  85. data/lib/rigor/effects/label_set.rb +136 -0
  86. data/lib/rigor/effects/liskov_check.rb +167 -0
  87. data/lib/rigor/effects/local_ownership.rb +132 -0
  88. data/lib/rigor/effects/method_key.rb +40 -0
  89. data/lib/rigor/effects/mutation_classifier.rb +92 -0
  90. data/lib/rigor/effects/narrowing.rb +202 -0
  91. data/lib/rigor/effects/origin.rb +66 -0
  92. data/lib/rigor/effects/path_finder.rb +89 -0
  93. data/lib/rigor/effects/plugin_facts.rb +384 -0
  94. data/lib/rigor/effects/propagator.rb +335 -0
  95. data/lib/rigor/effects/registry.rb +200 -0
  96. data/lib/rigor/effects/scanner.rb +302 -0
  97. data/lib/rigor/effects/signature_sources.rb +90 -0
  98. data/lib/rigor/effects/snapshot.rb +396 -0
  99. data/lib/rigor/effects/snapshot_diff.rb +265 -0
  100. data/lib/rigor/effects/summary.rb +154 -0
  101. data/lib/rigor/effects/taint_cause.rb +39 -0
  102. data/lib/rigor/effects/unit_scan.rb +647 -0
  103. data/lib/rigor/effects/unknown_label_check.rb +86 -0
  104. data/lib/rigor/effects/unknown_label_report.rb +59 -0
  105. data/lib/rigor/effects.rb +47 -0
  106. data/lib/rigor/environment/rbs_loader.rb +34 -0
  107. data/lib/rigor/environment.rb +8 -1
  108. data/lib/rigor/flow_contribution/element.rb +1 -0
  109. data/lib/rigor/flow_contribution/merge_result.rb +5 -3
  110. data/lib/rigor/flow_contribution/merger.rb +16 -1
  111. data/lib/rigor/flow_contribution.rb +20 -4
  112. data/lib/rigor/inference/expression_typer.rb +18 -4
  113. data/lib/rigor/inference/pre_eval_constants.rb +189 -0
  114. data/lib/rigor/inference/scope_indexer.rb +15 -3
  115. data/lib/rigor/plugin/base.rb +28 -0
  116. data/lib/rigor/plugin/box.rb +18 -2
  117. data/lib/rigor/plugin/effect_ancestry.rb +80 -0
  118. data/lib/rigor/plugin/effect_attribution.rb +208 -0
  119. data/lib/rigor/plugin/effect_edge.rb +101 -0
  120. data/lib/rigor/plugin/effect_entry_points.rb +51 -0
  121. data/lib/rigor/plugin/first_party.rb +57 -0
  122. data/lib/rigor/plugin/isolation.rb +4 -1
  123. data/lib/rigor/plugin/manifest.rb +127 -3
  124. data/lib/rigor/plugin/registry.rb +55 -0
  125. data/lib/rigor/plugin.rb +1 -0
  126. data/lib/rigor/rbs_extended/envelope_scanner.rb +160 -0
  127. data/lib/rigor/rbs_extended.rb +188 -0
  128. data/lib/rigor/reflection.rb +131 -30
  129. data/lib/rigor/version.rb +1 -1
  130. data/lib/rigor.rb +1 -0
  131. data/plugins/rigor-actioncable/lib/rigor/plugin/actioncable/effects.rb +90 -0
  132. data/plugins/rigor-actioncable/lib/rigor/plugin/actioncable.rb +7 -1
  133. data/plugins/rigor-actionmailer/lib/rigor/plugin/actionmailer/effects.rb +85 -0
  134. data/plugins/rigor-actionmailer/lib/rigor/plugin/actionmailer.rb +8 -1
  135. data/plugins/rigor-actionpack/lib/rigor/plugin/actionpack/effects.rb +152 -0
  136. data/plugins/rigor-actionpack/lib/rigor/plugin/actionpack.rb +9 -1
  137. data/plugins/rigor-activejob/lib/rigor/plugin/activejob/effects.rb +156 -0
  138. data/plugins/rigor-activejob/lib/rigor/plugin/activejob/recurring_scan.rb +113 -0
  139. data/plugins/rigor-activejob/lib/rigor/plugin/activejob.rb +68 -3
  140. data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord/effects.rb +186 -0
  141. data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord.rb +12 -1
  142. data/plugins/rigor-activerecord/sig/active_record/relation.rbs +145 -0
  143. data/plugins/rigor-activestorage/lib/rigor/plugin/activestorage/effects.rb +77 -0
  144. data/plugins/rigor-activestorage/lib/rigor/plugin/activestorage.rb +6 -1
  145. data/plugins/rigor-activesupport-core-ext/lib/rigor/plugin/activesupport_core_ext/effects.rb +143 -0
  146. data/plugins/rigor-activesupport-core-ext/lib/rigor/plugin/activesupport_core_ext.rb +9 -1
  147. data/plugins/rigor-activesupport-core-ext/sig/active_support/core_ext.rbs +401 -29
  148. data/plugins/rigor-devise/lib/rigor/plugin/devise.rb +27 -0
  149. data/plugins/rigor-factorybot/lib/rigor/plugin/factorybot.rb +30 -2
  150. data/plugins/rigor-pundit/lib/rigor/plugin/pundit/authorization_scan.rb +151 -0
  151. data/plugins/rigor-pundit/lib/rigor/plugin/pundit.rb +46 -3
  152. data/plugins/rigor-rails/lib/rigor-rails.rb +1 -0
  153. data/plugins/rigor-rails-i18n/lib/rigor/plugin/rails_i18n/effects.rb +47 -0
  154. data/plugins/rigor-rails-i18n/lib/rigor/plugin/rails_i18n.rb +7 -1
  155. data/plugins/rigor-rails-routes/lib/rigor/plugin/rails_routes/acronyms.rb +81 -0
  156. data/plugins/rigor-rails-routes/lib/rigor/plugin/rails_routes/helper_table.rb +13 -1
  157. data/plugins/rigor-rails-routes/lib/rigor/plugin/rails_routes/routes_parser.rb +202 -15
  158. data/plugins/rigor-rails-routes/lib/rigor/plugin/rails_routes.rb +43 -3
  159. data/plugins/rigor-railties/lib/rigor/plugin/railties/effects.rb +171 -0
  160. data/plugins/rigor-railties/lib/rigor/plugin/railties.rb +62 -0
  161. data/plugins/rigor-railties/lib/rigor-railties.rb +3 -0
  162. data/plugins/rigor-rbs-inline/lib/rigor/plugin/rbs_inline.rb +56 -1
  163. data/plugins/rigor-sidekiq/lib/rigor/plugin/sidekiq/effects.rb +75 -0
  164. data/plugins/rigor-sidekiq/lib/rigor/plugin/sidekiq/schedule_scan.rb +122 -0
  165. data/plugins/rigor-sidekiq/lib/rigor/plugin/sidekiq.rb +68 -3
  166. data/sig/rigor/analysis/fact_store.rbs +1 -0
  167. data/sig/rigor/inference.rbs +2 -0
  168. data/sig/rigor/rbs_extended.rbs +6 -0
  169. data/sig/rigor.rbs +17 -1
  170. data/skills/rigor-unused-adjudicate/SKILL.md +90 -0
  171. 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