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
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
# Effect-label registry — the shared vocabulary of ADR-103 WD2, normative in
|
|
2
|
+
# `docs/type-specification/effect-labels.md`.
|
|
3
|
+
#
|
|
4
|
+
# Hand-written, NOT generated: every row is a deliberate vocabulary decision. The generated
|
|
5
|
+
# `data/builtins/ruby_core/*.yml` `purity:` facet answers fold-safety, not effects, and is never
|
|
6
|
+
# read as a label source (ADR-103 WD3).
|
|
7
|
+
#
|
|
8
|
+
# `vocabulary` bumps only on a rename or a removal, never on a leaf addition — adding a leaf can
|
|
9
|
+
# never change what a recognised bound admits, because subsumption is a segment-aware prefix test.
|
|
10
|
+
# A renamed or removed spelling is recorded in `retired:` so a snapshot written by an older Rigor
|
|
11
|
+
# still reads.
|
|
12
|
+
vocabulary: 1
|
|
13
|
+
|
|
14
|
+
labels:
|
|
15
|
+
# Steins v1, verbatim (rigortype/steins `docs/type-specification/effects.md`). Shared vocabulary:
|
|
16
|
+
# a policy naming these labels must read the same against a PHP service and a Rails app.
|
|
17
|
+
- exit
|
|
18
|
+
- ffi
|
|
19
|
+
- global.read
|
|
20
|
+
- global.write
|
|
21
|
+
- io
|
|
22
|
+
- io.db
|
|
23
|
+
- io.fs
|
|
24
|
+
- io.fs.read
|
|
25
|
+
- io.fs.write
|
|
26
|
+
- io.input
|
|
27
|
+
- io.ipc
|
|
28
|
+
- io.net
|
|
29
|
+
- io.net.http
|
|
30
|
+
- io.output
|
|
31
|
+
- io.output.buffer
|
|
32
|
+
- io.output.header
|
|
33
|
+
- io.output.stdout
|
|
34
|
+
- io.output.stderr
|
|
35
|
+
- io.process
|
|
36
|
+
- io.signal
|
|
37
|
+
- mutate
|
|
38
|
+
- mutate.local
|
|
39
|
+
- nondet
|
|
40
|
+
- nondet.random
|
|
41
|
+
- nondet.time
|
|
42
|
+
|
|
43
|
+
# Ruby leaves under `mutate` (Steins ADR-0055's names; ADR-103 WD14). `mutate.self` is self's
|
|
44
|
+
# state, `mutate.instance` a receiver that is neither self nor frame-owned, `mutate.static`
|
|
45
|
+
# class-level state. Unknown ownership taints rather than producing a bare `mutate`.
|
|
46
|
+
- mutate.self
|
|
47
|
+
- mutate.instance
|
|
48
|
+
- mutate.static
|
|
49
|
+
|
|
50
|
+
# Proposed shared core leaves, to raise with Steins: a `SELECT` is a read whichever language
|
|
51
|
+
# issued it, an `INSERT` a write, `BEGIN`/`COMMIT` neither.
|
|
52
|
+
- io.db.read
|
|
53
|
+
- io.db.write
|
|
54
|
+
- io.db.transaction
|
|
55
|
+
|
|
56
|
+
# Application-meaning roots, small and shared. These are the labels a policy actually names
|
|
57
|
+
# ("presenters do not enqueue jobs") and the ones `tolerated:` grips, so they must spell the same
|
|
58
|
+
# in Steins and Rigor.
|
|
59
|
+
- telemetry
|
|
60
|
+
- email.send
|
|
61
|
+
- job.enqueue
|
|
62
|
+
- cache.read
|
|
63
|
+
- cache.write
|
|
64
|
+
|
|
65
|
+
# Retired spellings: `old.label: [replacement, ...]`. Empty at vocabulary 1 — nothing has been
|
|
66
|
+
# renamed or removed yet.
|
|
67
|
+
retired: {}
|
|
@@ -165,6 +165,79 @@ so because you added it. The stacking and diagnostic semantics
|
|
|
165
165
|
are in
|
|
166
166
|
[manual — `conforms-to`](../manual/16-rbs-extended-annotations.md#conforms-to--a-checked-structural-contract).
|
|
167
167
|
|
|
168
|
+
## Bounding what a method *does* — effect envelopes
|
|
169
|
+
|
|
170
|
+
Every directive so far describes what a method returns. One
|
|
171
|
+
describes what it *does*:
|
|
172
|
+
|
|
173
|
+
```rbs
|
|
174
|
+
class UserRepository
|
|
175
|
+
%a{rigor:v1:effect io.db}
|
|
176
|
+
def find: (Integer) -> User
|
|
177
|
+
|
|
178
|
+
%a{pure}
|
|
179
|
+
def slug: (String) -> String
|
|
180
|
+
end
|
|
181
|
+
```
|
|
182
|
+
|
|
183
|
+
`%a{rigor:v1:effect io.db}` says "this method may touch the
|
|
184
|
+
database, and nothing else the vocabulary names". `%a{pure}`
|
|
185
|
+
says "nothing at all" — it is rbs' own purity annotation, the
|
|
186
|
+
one Steep already reads, so Rigor honours the spelling that
|
|
187
|
+
exists rather than inventing a synonym. Both tolerate mutating
|
|
188
|
+
objects the method itself allocated and never let out, so a
|
|
189
|
+
`%a{pure}` method may still build and fill a local array.
|
|
190
|
+
|
|
191
|
+
The payload is a comma-separated list of bare labels
|
|
192
|
+
(`%a{rigor:v1:effect io.db, nondet.time}`), from the vocabulary
|
|
193
|
+
in
|
|
194
|
+
[effect-labels.md](../type-specification/effect-labels.md).
|
|
195
|
+
Write the annotation on a `class` or `module` declaration
|
|
196
|
+
instead and it applies to every method of that class —
|
|
197
|
+
reopenings and `attr_writer`-generated methods included, but
|
|
198
|
+
never to subclasses. A method that carries its own envelope
|
|
199
|
+
keeps it; nearest wins.
|
|
200
|
+
|
|
201
|
+
The bound covers the method's whole *code*, including what it
|
|
202
|
+
calls. A `find` declared `io.db` that reaches an HTTP request
|
|
203
|
+
through a helper exceeds its envelope, and Rigor says so — on
|
|
204
|
+
the Ruby `def`, naming the route it took:
|
|
205
|
+
|
|
206
|
+
```text
|
|
207
|
+
lib/user_repository.rb:12: warning: Method UserRepository#find
|
|
208
|
+
performs io.net.http (Net::HTTP.get via PaymentGateway#charge),
|
|
209
|
+
but is declared %a{rigor:v1:effect io.db} at sig/repo.rbs:3,
|
|
210
|
+
so io.net.http exceeds the envelope.
|
|
211
|
+
```
|
|
212
|
+
|
|
213
|
+
Two things it will not do. It never reports an effect it only
|
|
214
|
+
*suspects*: a call Rigor could not resolve makes the summary
|
|
215
|
+
read "and possibly more", and possibly is not a finding. And
|
|
216
|
+
none of this happens unless you asked for it — the check needs
|
|
217
|
+
an `effects:` block in `.rigor.yml`, so an `%a{pure}` already
|
|
218
|
+
sitting in your signatures for Steep's benefit stays inert
|
|
219
|
+
until you opt in. Set `effects.check: false` to keep the
|
|
220
|
+
`rigor effects` report and its snapshot while silencing the
|
|
221
|
+
diagnostic. Inert is not the same as unmentioned: annotations
|
|
222
|
+
with no `effects:` block earn one
|
|
223
|
+
[`effect.annotations-unchecked`](../manual/04-diagnostics.md#rule-effect-annotations-unchecked)
|
|
224
|
+
`:info` per run, so a bound nobody checks never goes unnoticed.
|
|
225
|
+
|
|
226
|
+
Misspell a label and the annotation does not narrow to the part
|
|
227
|
+
Rigor recognised — the **whole** tag reads as unbounded, so a
|
|
228
|
+
typo can never turn into findings on correct code. That would
|
|
229
|
+
be a silent loss of a contract you thought you had, so where
|
|
230
|
+
the spelling is evidently meant to be a label Rigor says so:
|
|
231
|
+
[`effect.unknown-label`](../manual/04-diagnostics.md#rule-effect-unknown-label),
|
|
232
|
+
at the line you wrote it on, naming the nearest real label.
|
|
233
|
+
A word that resembles nothing in the vocabulary stays silent —
|
|
234
|
+
you may be opening a root of your own.
|
|
235
|
+
|
|
236
|
+
The whole feature — the label vocabulary, the committed effect
|
|
237
|
+
snapshot, and `rigor effects` itself — is
|
|
238
|
+
[ADR-103](../adr/103-effect-labels.md); start with
|
|
239
|
+
[`rigor effects`](../manual/02-cli-reference.md#rigor-effects).
|
|
240
|
+
|
|
168
241
|
## Worked example: an assertion gate
|
|
169
242
|
|
|
170
243
|
```rbs
|
|
@@ -299,11 +372,39 @@ class Slug
|
|
|
299
372
|
end
|
|
300
373
|
```
|
|
301
374
|
|
|
302
|
-
You
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
375
|
+
You can also write them **in a `.rb` file**, as rbs-inline
|
|
376
|
+
`# @rbs %a{…}` comments:
|
|
377
|
+
|
|
378
|
+
```rb
|
|
379
|
+
# rbs_inline: enabled
|
|
380
|
+
|
|
381
|
+
class Slug
|
|
382
|
+
# @rbs %a{rigor:v1:return: non-empty-string}
|
|
383
|
+
# @rbs id: String
|
|
384
|
+
# @rbs return: String
|
|
385
|
+
def normalise(id) = id.strip
|
|
386
|
+
end
|
|
387
|
+
```
|
|
388
|
+
|
|
389
|
+
`%a{}` is *rbs-inline's own* grammar, not a Rigor dialect, and
|
|
390
|
+
the annotation reaches Rigor through the ordinary path: the
|
|
391
|
+
rbs-inline writer copies it verbatim onto the signature it
|
|
392
|
+
generates, and that signature joins the same RBS environment
|
|
393
|
+
your `sig/` tree lands in. So this is not a per-directive
|
|
394
|
+
feature — every `RBS::Extended` directive is read from the same
|
|
395
|
+
annotation object whichever buffer it arrived in — the effect
|
|
396
|
+
envelopes above (`%a{pure}`, `%a{rigor:v1:effect …}`) included.
|
|
397
|
+
Reading them requires the rbs-inline library, which Rigor
|
|
398
|
+
ingests by default when it is installed
|
|
399
|
+
([ADR-93](../adr/93-default-rbs-inline-ingestion.md)).
|
|
400
|
+
|
|
401
|
+
What Rigor does **not** offer is a Rigor-only comment dialect —
|
|
402
|
+
there is no `# rigor:effect` directive and no file pragma. The
|
|
403
|
+
`# rigor:` comment family stays suppression-only (`disable`,
|
|
404
|
+
`disable-file`). Application code never has to carry
|
|
405
|
+
Rigor-specific syntax ([ADR-0](../adr/0-concept.md)); an
|
|
406
|
+
upstream annotation form you may use if you want one is a
|
|
407
|
+
different thing from a requirement.
|
|
307
408
|
|
|
308
409
|
## Inline RBS in Ruby source — the `rigor-rbs-inline` plugin
|
|
309
410
|
|
|
@@ -48,6 +48,7 @@ the `paths:` list from the configuration file.
|
|
|
48
48
|
| `--treat-all-as-inline-rbs` | Force-load `rigor-rbs-inline` with `require_magic_comment: false`, so every analysed file is treated as inline-RBS without the `# rbs_inline: enabled` comment (ADR-32). |
|
|
49
49
|
| `--bleeding-edge[=ids]` | Adopt the bleeding-edge overlay for this run, overriding the configured [`bleeding_edge:`](03-configuration.md) selection (ADR-50 § WD2). Bare adopts every queued feature; `--bleeding-edge=a,b` adopts only the named feature ids. Inspect it with [`rigor show-bleedingedge`](#rigor-show-bleedingedge). |
|
|
50
50
|
| `--no-bleeding-edge` | Ignore any configured `bleeding_edge:` selection for this run (adopt none). |
|
|
51
|
+
| `--no-tolerated-effects` | Check effect envelopes as if [`effects.tolerated:`](03-configuration.md) were empty — the audit switch for your discharge policy (ADR-103). Judgment only: the run, what it collects and its cache entry are identical either way, so this never costs a re-analysis. |
|
|
51
52
|
| `--tmp-file=PATH --instead-of=PATH` | Editor mode: analyse `PATH` using the buffer in `--tmp-file`. Both required together. Alone, only the buffer's own file produces diagnostics; add `--incremental` for whole-project scope (see below). |
|
|
52
53
|
|
|
53
54
|
Exit `0` when no error-severity diagnostics remain, `1` when
|
|
@@ -162,6 +163,208 @@ rigor type-scan PATH...
|
|
|
162
163
|
unrecognized-node ratio exceeds `RATIO`, and `--format=text|json`
|
|
163
164
|
selects the output format.
|
|
164
165
|
|
|
166
|
+
## `rigor effects`
|
|
167
|
+
|
|
168
|
+
Report what each method *does* — its effect labels — rather
|
|
169
|
+
than what it returns, and manage the committed **effect
|
|
170
|
+
snapshot** that gates drift. Opt-in and observational: nothing
|
|
171
|
+
here emits a diagnostic or changes `rigor check`'s output, and
|
|
172
|
+
only `rigor effects check` ever exits non-zero.
|
|
173
|
+
|
|
174
|
+
```sh
|
|
175
|
+
rigor effects [PATH...] # the report
|
|
176
|
+
rigor effects {update,check,diff,explain} # the snapshot
|
|
177
|
+
```
|
|
178
|
+
|
|
179
|
+
With no paths the report analyses the configured `paths:`. It
|
|
180
|
+
runs with effect collection enabled even when your `.rigor.yml`
|
|
181
|
+
carries no `effects:` block, so you can try it before
|
|
182
|
+
configuring anything; such an ad-hoc run shares no cache with
|
|
183
|
+
`rigor check`, because a run served from that cache would have
|
|
184
|
+
collected nothing.
|
|
185
|
+
|
|
186
|
+
Each line is one method, sorted by key:
|
|
187
|
+
|
|
188
|
+
```
|
|
189
|
+
Tracer::Reporter#report: [io.output.stdout, nondet.time]
|
|
190
|
+
Tracer::Gateway#fetch: [] …?
|
|
191
|
+
dynamic-receiver (external_gem_without_rbs)
|
|
192
|
+
```
|
|
193
|
+
|
|
194
|
+
The labels are the **transitive** footprint — the method's own
|
|
195
|
+
plus every project method it reaches. A ` …?` suffix means the
|
|
196
|
+
list is not exhaustive: some call could not be resolved, so the
|
|
197
|
+
reading is "these effects, and possibly more". The indented
|
|
198
|
+
lines say why. A taint is never a finding.
|
|
199
|
+
|
|
200
|
+
A ` ≤ [...]` clause after the labels is the **declared** lane:
|
|
201
|
+
what a source Rigor trusts but did not verify *claims* the
|
|
202
|
+
method does, today the `effects.attribution:` table you wrote
|
|
203
|
+
for gem methods it cannot see. It is printed apart from the
|
|
204
|
+
proven labels and never folded in among them, because the two
|
|
205
|
+
answer different questions. It follows call edges exactly as
|
|
206
|
+
the proven labels do, so a controller two hops above an
|
|
207
|
+
attributed gem call carries the claim rather than only a
|
|
208
|
+
"possibly more"; a declared label the proven list already
|
|
209
|
+
covers is not printed twice.
|
|
210
|
+
|
|
211
|
+
```
|
|
212
|
+
Gateways::Client#fetch: [] ≤ [io.net.http] …?
|
|
213
|
+
plugin-attribution (Acme::Http.get)
|
|
214
|
+
```
|
|
215
|
+
|
|
216
|
+
A method is omitted when it is exhaustive and proves nothing
|
|
217
|
+
beyond `mutate.local` — mutation of objects its own frame
|
|
218
|
+
allocated and never let out, which every effect envelope
|
|
219
|
+
tolerates. `--full` lists every method instead.
|
|
220
|
+
|
|
221
|
+
`--format=text|json` selects the output format; the JSON
|
|
222
|
+
payload additionally carries each method's *direct* summary
|
|
223
|
+
broken down per origin. `--config=PATH` picks a config file.
|
|
224
|
+
`--no-tolerated-effects` is accepted for symmetry with the
|
|
225
|
+
subcommands and does nothing here: the report is an
|
|
226
|
+
observation, and observations are undischarged.
|
|
227
|
+
|
|
228
|
+
What is collected and how it propagates is
|
|
229
|
+
[the effect-summaries internal spec](../internal-spec/effect-summaries.md);
|
|
230
|
+
the label vocabulary is
|
|
231
|
+
[the effect-labels specification](../type-specification/effect-labels.md).
|
|
232
|
+
|
|
233
|
+
### The effect snapshot
|
|
234
|
+
|
|
235
|
+
Four subcommands manage a committed record of the effects
|
|
236
|
+
Rigor observed — `.rigor-effects.yml`, the effect equivalent
|
|
237
|
+
of `db/schema.rb`:
|
|
238
|
+
|
|
239
|
+
```sh
|
|
240
|
+
rigor effects update # write the snapshot; commit it
|
|
241
|
+
rigor effects check # 0 fresh, 1 drift — the CI gate
|
|
242
|
+
rigor effects diff # the same comparison, never gating
|
|
243
|
+
rigor effects explain # why an entry point reaches a label
|
|
244
|
+
```
|
|
245
|
+
|
|
246
|
+
Unlike the report, the subcommands take no paths: a snapshot
|
|
247
|
+
records the whole project, and one written over a subset would
|
|
248
|
+
read as a project where every other method vanished. They all
|
|
249
|
+
accept `--config=PATH`, `--format=text|json` and `--full`;
|
|
250
|
+
`check`, `diff` and `explain` additionally accept
|
|
251
|
+
`--baseline=PATH` (compare against a file other than the
|
|
252
|
+
configured one — `--baseline <(git show
|
|
253
|
+
origin/main:.rigor-effects.yml)` in a bot),
|
|
254
|
+
`--strict-tolerated` and `--no-tolerated-effects`.
|
|
255
|
+
|
|
256
|
+
The file records two tables. `methods:` holds each method's
|
|
257
|
+
**direct** summary — what its own body does, block literals
|
|
258
|
+
and catalogued callees included, but not what the project
|
|
259
|
+
methods it calls do. That is deliberate: an entry moves only
|
|
260
|
+
when its own lines changed, so the diff stays attributable to
|
|
261
|
+
the pull request that caused it. `reach:` holds the
|
|
262
|
+
**transitive** footprint at the entry points
|
|
263
|
+
`effects.snapshot.reach:` names, where a leaf change is
|
|
264
|
+
supposed to fan out — the fan-out is the blast radius.
|
|
265
|
+
|
|
266
|
+
```yaml
|
|
267
|
+
# .rigor-effects.yml — generated by `rigor effects update`. Commit it; review its diff.
|
|
268
|
+
schema: 1
|
|
269
|
+
rigor: "0.3.3"
|
|
270
|
+
vocabulary: 1
|
|
271
|
+
config_digest: "9ec82bfc…"
|
|
272
|
+
methods:
|
|
273
|
+
"PaymentGateway#charge":
|
|
274
|
+
effects: ["io.net.http", "telemetry"]
|
|
275
|
+
"Reports::Nightly#perform":
|
|
276
|
+
effects: ["io.db.read"]
|
|
277
|
+
exhaustive: false
|
|
278
|
+
unresolved: ["dynamic-send"]
|
|
279
|
+
reach:
|
|
280
|
+
"OrdersController#create":
|
|
281
|
+
effects: ["io.db.read", "io.net.http", "job.enqueue"]
|
|
282
|
+
```
|
|
283
|
+
|
|
284
|
+
A method is left out when it is exhaustive and proves nothing
|
|
285
|
+
beyond `mutate.local`, when its summary is a synthesised
|
|
286
|
+
accessor's, and when it carries no label in either lane — a
|
|
287
|
+
row that would say only "not exhaustive, and here is why" is
|
|
288
|
+
something `rigor effects` and `rigor effects explain` answer
|
|
289
|
+
better than a committed record. `--full` records everything.
|
|
290
|
+
The header carries
|
|
291
|
+
the Rigor and vocabulary versions and a digest of your
|
|
292
|
+
`effects:` block, so an upgrade or a policy edit shows up as a
|
|
293
|
+
*regeneration event* rather than as silent reinterpretation.
|
|
294
|
+
|
|
295
|
+
Under `methods:` the declared lane is what that method's own
|
|
296
|
+
body claims; under `reach:` it is the transitive claim, like
|
|
297
|
+
the proven labels beside it.
|
|
298
|
+
|
|
299
|
+
`check` prints one line per difference: `+ label` / `- label`
|
|
300
|
+
for the proven lane, `≤+` / `≤-` for the declared one,
|
|
301
|
+
`materialised` when a declared label became proven,
|
|
302
|
+
`exhaustive → not` when someone introduced a call Rigor cannot
|
|
303
|
+
follow, and `+symbol` / `-symbol` for methods that appeared or
|
|
304
|
+
vanished (a rename is one of each, counted in the footer). A
|
|
305
|
+
removal read off a summary that is no longer exhaustive is
|
|
306
|
+
printed hedged — "possibly more" cannot prove an absence.
|
|
307
|
+
|
|
308
|
+
`effects.snapshot.gate:` decides what fails. `symmetric` (the
|
|
309
|
+
default) fails on any drift: a job that stopped enqueueing is
|
|
310
|
+
news too. `additions` is the ratchet — only growth fails.
|
|
311
|
+
`effects.tolerated:` is applied at judgment time, never while
|
|
312
|
+
writing: a difference confined to tolerated labels is printed
|
|
313
|
+
under a `tolerated:` heading and does not fail the gate unless
|
|
314
|
+
you pass `--strict-tolerated`. `--no-tolerated-effects` judges
|
|
315
|
+
as if the list were empty; on `update` it changes nothing,
|
|
316
|
+
because the record itself is undischarged.
|
|
317
|
+
|
|
318
|
+
Discharge works **per origin**, not per label. `Logger#info`
|
|
319
|
+
carries `io` and `telemetry` together, so `tolerated:
|
|
320
|
+
[telemetry]` frees the `io` that came with the logging — and
|
|
321
|
+
leaves an `io.fs.read` from a `File.read` two lines down
|
|
322
|
+
exactly where it was. An added label is discharged only when
|
|
323
|
+
every origin that introduces it is discharged.
|
|
324
|
+
|
|
325
|
+
### Reviewing effect drift
|
|
326
|
+
|
|
327
|
+
Day one, run `rigor effects update` and commit the result. The
|
|
328
|
+
diff you are committing is the team's first map: which
|
|
329
|
+
controllers reach the network, which jobs write, which
|
|
330
|
+
presenters query.
|
|
331
|
+
|
|
332
|
+
Add `rigor effects check` to CI. From then on a pull request
|
|
333
|
+
that changes what the code *does* fails it with the reason
|
|
334
|
+
spelled out:
|
|
335
|
+
|
|
336
|
+
```
|
|
337
|
+
Effect drift against .rigor-effects.yml:
|
|
338
|
+
|
|
339
|
+
methods:
|
|
340
|
+
PaymentGateway#charge + io.net.http
|
|
341
|
+
|
|
342
|
+
reach:
|
|
343
|
+
OrdersController#create + io.net.http
|
|
344
|
+
|
|
345
|
+
Run `rigor effects update` and commit the result if this change is intended.
|
|
346
|
+
```
|
|
347
|
+
|
|
348
|
+
The author runs `rigor effects explain` to see the route —
|
|
349
|
+
|
|
350
|
+
```
|
|
351
|
+
reach:
|
|
352
|
+
OrdersController#create → OrderService#place → PaymentGateway#charge → Net::HTTP.get [io.net.http]
|
|
353
|
+
```
|
|
354
|
+
|
|
355
|
+
— then runs `rigor effects update` and commits the regenerated
|
|
356
|
+
file. **Intent is expressed by committing the regenerated
|
|
357
|
+
snapshot**, not by annotating the code; the reviewer reads the
|
|
358
|
+
two-line diff alongside the code change and either nods or
|
|
359
|
+
pushes back. A bundle update that moves effects with no code
|
|
360
|
+
diff of its own works the same way, and is exactly the case
|
|
361
|
+
worth seeing.
|
|
362
|
+
|
|
363
|
+
None of this is a diagnostic. `rigor check`'s output and exit
|
|
364
|
+
code are identical whether or not you use the snapshot, and
|
|
365
|
+
drift is never a finding: whether it *matters* is the
|
|
366
|
+
reviewer's judgment, which is what makes it a review artefact.
|
|
367
|
+
|
|
165
368
|
## `rigor explain`
|
|
166
369
|
|
|
167
370
|
Print the catalogue entry for a diagnostic rule, or list every
|
|
@@ -300,6 +503,112 @@ rigor triage --format json | jq '[.selectors[] | select(.receiver == "String")]'
|
|
|
300
503
|
The same `receiver_type` / `method_name` fields ride on each
|
|
301
504
|
diagnostic of `rigor check --format json`, for per-site (rather than
|
|
302
505
|
aggregated) grouping.
|
|
506
|
+
## `rigor unused`
|
|
507
|
+
|
|
508
|
+
Report project constants that nothing reachable references — a
|
|
509
|
+
starting point for dead-code removal.
|
|
510
|
+
|
|
511
|
+
```sh
|
|
512
|
+
rigor unused [paths] --entry-point='lib/cli.rb'
|
|
513
|
+
```
|
|
514
|
+
|
|
515
|
+
**Read the output as a review queue, not a defect list.** On a
|
|
516
|
+
hand-adjudicated corpus target only **7% of the rows were genuinely
|
|
517
|
+
unused**; the rest were reachable by means static analysis cannot
|
|
518
|
+
see. That is why this is a separate command and never a `rigor check`
|
|
519
|
+
diagnostic — see
|
|
520
|
+
[ADR-102](https://github.com/rigortype/rigor/blob/master/docs/adr/102-unused-code-reachability-report.md).
|
|
521
|
+
|
|
522
|
+
Reachability is computed from **roots**, not by counting references,
|
|
523
|
+
so a cluster of classes that only reference each other is still
|
|
524
|
+
reported. Roots are the declarations in files matching
|
|
525
|
+
`--entry-point=GLOB` (repeatable), anything referenced at file level
|
|
526
|
+
by non-test code, and anything the project's **plugins** contribute.
|
|
527
|
+
|
|
528
|
+
Plugin-supplied roots are where framework knowledge enters. A Rails
|
|
529
|
+
controller is reached by name at request time, so nothing in the
|
|
530
|
+
project references it and a reference index cannot tell a live
|
|
531
|
+
controller from a dead one. `rigor-rails-routes` closes that by
|
|
532
|
+
reading `config/routes.rb` statically and naming every controller it
|
|
533
|
+
dispatches to — no Rails boot, and a route written under a
|
|
534
|
+
conditional (`get "/beta", to: "beta#index" if ENV["BETA"]`) is
|
|
535
|
+
visible where a booted app's route table would not show it. On two
|
|
536
|
+
Rails corpus targets this removed 56 % and 84 % of the candidate list.
|
|
537
|
+
|
|
538
|
+
`rigor unused` loads the same `plugins:` your `rigor check` run uses,
|
|
539
|
+
and prints how many roots came from them:
|
|
540
|
+
|
|
541
|
+
```
|
|
542
|
+
roots: 404 (288 from plugins, 0 matched no declaration)
|
|
543
|
+
```
|
|
544
|
+
|
|
545
|
+
`matched no declaration` is the number a plugin claimed that this
|
|
546
|
+
project does not declare — normally framework classes such as
|
|
547
|
+
`Rails::HealthController`. A number climbing away from zero means a
|
|
548
|
+
root source has drifted out of step with the code, which matters
|
|
549
|
+
because an over-claiming root source silently *hides* dead code. A
|
|
550
|
+
framework Rigor has no plugin for supplies no roots, so its
|
|
551
|
+
controllers still read as candidates.
|
|
552
|
+
|
|
553
|
+
`rigor-pundit` supplies the second kind of root: a policy class is
|
|
554
|
+
reached as `PostPolicy` from `authorize @post`, a name that appears
|
|
555
|
+
nowhere in the source. It publishes the policies your authorization
|
|
556
|
+
calls actually name — not every class under `app/policies`, because a
|
|
557
|
+
file's location is not evidence that anything authorizes against it.
|
|
558
|
+
|
|
559
|
+
`rigor-sidekiq` shows how narrow a root source has to be to be worth
|
|
560
|
+
having. A worker named as `class: "NightlyReportWorker"` in a cron
|
|
561
|
+
schedule is enqueued from YAML, so its name appears nowhere in the
|
|
562
|
+
code and it reads as dead — that name becomes a root. The queue list
|
|
563
|
+
in the same `sidekiq.yml` does not: a queue name is not a class name,
|
|
564
|
+
and inflecting one into a worker name would root a class on a naming
|
|
565
|
+
coincidence.
|
|
566
|
+
|
|
567
|
+
A plugin can also contribute a **reference** rather than a root, and
|
|
568
|
+
`rigor-factorybot` is why the distinction exists. `factory :user,
|
|
569
|
+
class: "Admin::User"` names a class as a string the scan cannot see,
|
|
570
|
+
so it is real evidence of use — but a factory lives in the test tree.
|
|
571
|
+
Supplied as a reference carrying the `test` role, the class leaves
|
|
572
|
+
the candidate list and appears under *Reachable only from test code*;
|
|
573
|
+
supplied as a root it would have been promoted to
|
|
574
|
+
production-reachable, and the more interesting finding would have
|
|
575
|
+
disappeared.
|
|
576
|
+
|
|
577
|
+
Most bundled plugins deliberately contribute nothing.
|
|
578
|
+
`MyJob.perform_later`, `MyMailer.welcome`, `MyWorker.perform_async`
|
|
579
|
+
and `RSpec.describe User` all write the class name as an ordinary
|
|
580
|
+
constant, which the report already records — and for the spec case,
|
|
581
|
+
records with the `test` role that makes the section above possible.
|
|
582
|
+
Each plugin's page says which choice it made and why.
|
|
583
|
+
|
|
584
|
+
Three things the report separates rather than merges:
|
|
585
|
+
|
|
586
|
+
- **Reachable only from test code** gets its own section — a class
|
|
587
|
+
used solely by its own spec is dead production code with a live
|
|
588
|
+
test, which is a more actionable finding than either bucket alone.
|
|
589
|
+
- **Constants something can name at runtime** are demoted to a
|
|
590
|
+
`cannot decide` section with the reason, never claimed as unused.
|
|
591
|
+
`"Foo".constantize` names `Foo` exactly, so it counts as an ordinary
|
|
592
|
+
reference; `"Foo::#{key}".constantize` instead marks everything
|
|
593
|
+
under `Foo` undecidable. A class name appearing as a string in a
|
|
594
|
+
`.yml`, `.json`, or template file demotes it the same way — that is
|
|
595
|
+
weaker evidence than a constant reference, so it is neither proof of
|
|
596
|
+
use nor grounds to call it dead.
|
|
597
|
+
- **Namespace modules** wrapping live code are excluded and counted,
|
|
598
|
+
because nothing references an intermediate namespace by itself. A
|
|
599
|
+
namespace whose contents are *all* unreachable is still reported.
|
|
600
|
+
|
|
601
|
+
Only class and module constants are reported at all; value constants
|
|
602
|
+
are omitted because they do not resolve across files.
|
|
603
|
+
|
|
604
|
+
References are harvested from a wider file set than the analysed
|
|
605
|
+
paths — `.rake` tasks, `config/`, specs and the project's own `sig/`
|
|
606
|
+
all count as references — because a constant used only from a Rake
|
|
607
|
+
task is not dead.
|
|
608
|
+
|
|
609
|
+
`--format json` emits the same data; `--limit=N` truncates the
|
|
610
|
+
printed lists. `--incremental` is refused: reachability is only sound
|
|
611
|
+
over a whole-project run.
|
|
303
612
|
|
|
304
613
|
## `rigor coverage`
|
|
305
614
|
|
|
@@ -654,6 +963,7 @@ Queued today:
|
|
|
654
963
|
| `use-of-void-value` | severity | Using a value recovered from an author-declared `-> void` return in value context is reported as `static.value-use.void` (`warning`). |
|
|
655
964
|
| `discovery-seeded-mutation-sites` | behaviour | [`rigor coverage --protection --mutation`](15-type-protection-coverage.md) measures against the same cross-file project discovery Tier 1 already uses — both when picking the sites and when deciding whether a breakage was caught — so a call on a project class declared in a *sibling* file is measured instead of dropped, and a breakage there can actually be caught. **Adds sites to the denominator, so the reported effectiveness ratio moves** — check it against any `--threshold` you pin in CI before adopting. |
|
|
656
965
|
| `dependent-closure-kill-oracle` | behaviour | [`rigor coverage --protection --mutation`](15-type-protection-coverage.md) counts a breakage as caught when the diagnostic appears anywhere in the mutated file **or the files that depend on it**, instead of in the mutated file alone — so changing what a method returns counts as caught when the error lands in its callers. Can only **add** kills, so the ratio moves up or not at all; it costs about a third more wall time per mutant, and a ratio measured under it is not comparable with one measured without it. |
|
|
966
|
+
| `effects-on-by-default` | behaviour | A project whose `.rigor.yml` carries no [`effects:`](03-configuration.md#effect-labels) key at all is treated as if it had written `effects: {}` — [effect collection](03-configuration.md#effect-labels), the `rigor effects` verbs' cache sharing, and `effects.check` all turn on with every sub-key at its default. Writing `effects: false` explicitly still opts out. **Scheduled to graduate at v0.4.0** ([ADR-103](../adr/103-effect-labels.md) § WD15) rather than at the next major — an owner ruling specific to this feature, ahead of the general v1.0.0 majors-only cadence below. |
|
|
657
967
|
|
|
658
968
|
Once a feature **graduates** — it becomes the default at a major
|
|
659
969
|
([ADR-50](../adr/50-release-engineering-and-stability-strategy.md) § WD7)
|
|
@@ -758,7 +1068,7 @@ diagnostics about Rigor's own inference cutoffs and memory — see
|
|
|
758
1068
|
| Code | Meaning |
|
|
759
1069
|
| --- | --- |
|
|
760
1070
|
| `0` | Success — no error-severity diagnostics. |
|
|
761
|
-
| `1` | Diagnostics found, or a per-command failure (parse error, missing file, new diagnostics on `diff`). |
|
|
1071
|
+
| `1` | Diagnostics found, or a per-command failure (parse error, missing file, new diagnostics on `diff`, effect drift on `effects check`). |
|
|
762
1072
|
| `64` | Usage error — unknown command, bad flag, malformed argument. |
|
|
763
1073
|
|
|
764
1074
|
`rigor triage` is the exception: it is advisory and always
|
|
@@ -70,7 +70,7 @@ cache:
|
|
|
70
70
|
| --- | --- | --- | --- |
|
|
71
71
|
| `libraries` | Array | `[]` | Standard-library / gem names whose bundled RBS to load. |
|
|
72
72
|
| `signature_paths` | Array | `nil` | Extra directories of `.rbs` files. Relative entries resolve against the config file's directory. |
|
|
73
|
-
| `pre_eval` | Array | `[]` | Files (or globs) walked before per-file analysis, to register project monkey-patches. |
|
|
73
|
+
| `pre_eval` | Array | `[]` | Files (or globs) walked before per-file analysis, to register project monkey-patches and publish their top-level constants project-wide. |
|
|
74
74
|
| `plugins` | Array | `[]` | Plugins to activate — see [Using plugins](07-plugins.md). |
|
|
75
75
|
|
|
76
76
|
### Config validation warnings
|
|
@@ -174,6 +174,89 @@ explicitly with `bundler.bundle_path:`, or supply signatures another way:
|
|
|
174
174
|
| `plugins_io.allowed_paths` | Array | `[]` | Filesystem paths plugins may read. |
|
|
175
175
|
| `plugins_io.allowed_url_hosts` | Array | `[]` | URL hosts plugins may fetch from when `network: allowlist`. |
|
|
176
176
|
|
|
177
|
+
### Effect labels
|
|
178
|
+
|
|
179
|
+
| Key | Type | Default | Meaning |
|
|
180
|
+
| --- | --- | --- | --- |
|
|
181
|
+
| `effects` | Hash | absent | **Opt-in to effect labels ([ADR-103](../adr/103-effect-labels.md)).** The *presence* of this block is the switch — `effects: {}` enables collection with every sub-key at its default, and leaving it out keeps `rigor check` byte-identical and free. Nothing else turns collection on: an `%a{pure}` or `%a{rigor:v1:effect …}` annotation in your RBS does not, because an annotation must not silently make every run more expensive — such a project gets one `effect.annotations-unchecked` `:info` per run instead, saying the annotations are inert. `rigor effects` runs under an implicit empty block when the key is absent, so you can try the report before configuring anything. Effect summaries are cached under their own identity (Rigor's effect vocabulary, its built-in catalogue and this block), so `rigor effects` after `rigor check` in the same job is a cache hit plus the propagation, and turning this block on or off does not invalidate your diagnostics cache. The sub-keys are below. `views` is declared in the schema and reserved: accepted and **not yet read**. See [`rigor effects`](02-cli-reference.md#rigor-effects). |
|
|
182
|
+
| `effects.check` | Boolean | `true` | Whether the envelopes you declared — `%a{pure}` and `%a{rigor:v1:effect …}` in RBS, and the `effects.envelopes:` stanzas below — are checked against what Rigor proved, surfacing `effect.envelope-exceeded` and, for a label the vocabulary does not recognise, `effect.unknown-label`. Set it to `false` to keep the report and the snapshot while silencing both. Never on without an `effects:` block. |
|
|
183
|
+
| `effects.snapshot.path` | String | `.rigor-effects.yml` | Where `rigor effects update` writes the committed record. |
|
|
184
|
+
| `effects.snapshot.reach` | Array | `[]` | Entry points whose **transitive** footprint the snapshot records under `reach:`. Each entry is a project-relative file glob (the `unused --entry-point` semantics — `**` is the only way across a directory boundary) or the name of an entry-point **preset** a plugin registered. On a Rails app, `reach: [rails]` is the one you want — see below. A name nothing registered is an error when the snapshot is built (not when the configuration loads, because the plugins that name presets load *from* that configuration). |
|
|
185
|
+
| `effects.snapshot.gate` | String | `symmetric` | What `rigor effects check` treats as drift. `symmetric` fails on any difference — a job that *stopped* enqueueing is news too; `additions` is the growth-only ratchet. |
|
|
186
|
+
| `effects.labels` | Array | `[]` | Effect labels **your project** registers, layered over Rigor's shipped vocabulary. A project may open any root (`acme.cache`) — listing the label here is the vouching act. Once registered, a label is usable in every other key below and stops being reported as unknown. Malformed spellings are a load error. |
|
|
187
|
+
| `effects.attribution` | Hash | `{}` | What a call into code Rigor cannot see *does*, keyed by method: `{"Net::HTTP.get": [io.net.http], "Logger#info": [telemetry]}`. Keys are method keys — `Owner#instance_method` or `Owner.singleton_method` — and anything else is a load error. The labels land in the caller's **declared** lane and never in the proven one, so an attribution can never make a diagnostic fire; the call still counts as unresolved, because you told Rigor what that code does and Rigor did not read it. Use it for gems nobody has written a plugin for. |
|
|
188
|
+
| `effects.envelopes` | Array | `[]` | Effect envelopes by **convention**, so a whole architectural layer is bounded by one stanza instead of a per-method annotation. Each entry names exactly one of `match:` (a project-relative path glob over the files a class is defined in) or `namespace:` (a constant glob: `*` is one segment, `**` is one or more), plus `effect:` — the labels the selected classes may perform, or `[]` for pure. Nearest wins: a per-method annotation beats a class-level one, which beats a stanza; among stanzas the **first** match wins. See the example below. |
|
|
189
|
+
| `effects.tolerated` | Array | `[]` | Labels your project has decided not to act on. Applied when a bound or a difference is **judged**, never when a record is written, and **per origin**: `Logger#info` carries `io` and `telemetry` together, so `tolerated: [telemetry]` frees the `io` that came with the logging and leaves an `io.fs.read` from a `File.read` in the same method exactly where it was. `rigor check --no-tolerated-effects` (and the same flag on `rigor effects check`) re-judges as if the list were empty — the audit switch for the policy. |
|
|
190
|
+
|
|
191
|
+
Want the block on without writing it? The
|
|
192
|
+
[`effects-on-by-default`](02-cli-reference.md#rigor-show-bleedingedge)
|
|
193
|
+
bleeding-edge feature (`bleeding_edge: [effects-on-by-default]`) makes a
|
|
194
|
+
config with no `effects:` key at all behave as `effects: {}` — collection,
|
|
195
|
+
`effects.check`, and everything else on this page all turn on at their
|
|
196
|
+
defaults. It only fills an *absence*: write `effects: false` and you stay
|
|
197
|
+
opted out regardless, and any `effects:` block you do write is left exactly
|
|
198
|
+
as written. It previews what becomes the default at **v0.4.0**
|
|
199
|
+
([ADR-103](../adr/103-effect-labels.md) § WD15).
|
|
200
|
+
|
|
201
|
+
#### Entry-point presets
|
|
202
|
+
|
|
203
|
+
`reach:` asks "whose footprint should the record cover", and on a framework the honest answer is a fact
|
|
204
|
+
about the framework rather than about your code. So the plugin that models it names the set, and you
|
|
205
|
+
adopt it:
|
|
206
|
+
|
|
207
|
+
```yaml
|
|
208
|
+
plugins:
|
|
209
|
+
- rigor-railties
|
|
210
|
+
- rigor-activerecord
|
|
211
|
+
- rigor-actionpack
|
|
212
|
+
|
|
213
|
+
effects:
|
|
214
|
+
snapshot:
|
|
215
|
+
reach: [rails]
|
|
216
|
+
```
|
|
217
|
+
|
|
218
|
+
`rails` — registered by [`rigor-railties`](plugins/rigor-rails.md) — stands for `app/controllers/**`,
|
|
219
|
+
`app/jobs/**`, `app/mailers/**` and `app/channels/**`: every way the outside world enters the
|
|
220
|
+
application. The component plugins also register the narrower `rails-controllers`, `rails-jobs`,
|
|
221
|
+
`rails-mailers` and `rails-channels` if you want one layer's footprint rather than all four. A preset is
|
|
222
|
+
just a name for globs; mixing the two in one list is fine.
|
|
223
|
+
|
|
224
|
+
Listing the plugin is what registers its preset, so `reach: [rails]` without `rigor-railties` in
|
|
225
|
+
`plugins:` is an error saying so.
|
|
226
|
+
|
|
227
|
+
#### Envelopes by convention
|
|
228
|
+
|
|
229
|
+
The `envelopes:` list is the surface that pays on day one, before you have written any RBS:
|
|
230
|
+
|
|
231
|
+
```yaml
|
|
232
|
+
effects:
|
|
233
|
+
envelopes:
|
|
234
|
+
- match: "app/presenters/**/*.rb" # presenters render; they do not query
|
|
235
|
+
effect: []
|
|
236
|
+
- namespace: "Policies::*" # Policies::Edit, not Policies::Admin::Edit
|
|
237
|
+
effect: [mutate.local]
|
|
238
|
+
- match: "app/jobs/**/*.rb"
|
|
239
|
+
effect: [io]
|
|
240
|
+
tolerated: [telemetry]
|
|
241
|
+
```
|
|
242
|
+
|
|
243
|
+
A stanza attaches its bound to every method of every class it selects, exactly as an annotation on the
|
|
244
|
+
class would. A method that exceeds it gets one `effect.envelope-exceeded` at its `def`, naming the
|
|
245
|
+
stanza it broke:
|
|
246
|
+
|
|
247
|
+
```
|
|
248
|
+
app/presenters/user_presenter.rb:14:1: warning: Method Presenters::User#render performs io.fs.read
|
|
249
|
+
(File.read), but is declared effect: [] at .rigor.yml effects.envelopes[0], so io.fs.read exceeds
|
|
250
|
+
the envelope. [effect.envelope-exceeded]
|
|
251
|
+
```
|
|
252
|
+
|
|
253
|
+
When one method is a deliberate exception, write the narrower envelope on it in RBS — nearest wins,
|
|
254
|
+
and no `except:` key is needed. When a whole *kind* of effect is acceptable everywhere, name it in
|
|
255
|
+
`tolerated:` instead of loosening every stanza.
|
|
256
|
+
|
|
257
|
+
If a layer is not ready for a bound yet, leave `envelopes:` out and start with the committed snapshot
|
|
258
|
+
([`rigor effects update`](02-cli-reference.md#the-effect-snapshot)) — it needs no declaration at all,
|
|
259
|
+
and stanzas are the second step, written once the record has told you what the layer actually does.
|
|
177
260
|
### Reserved for other implementations
|
|
178
261
|
|
|
179
262
|
| Key | Type | Default | Meaning |
|
|
@@ -58,6 +58,10 @@ carries no `documentation_url`.
|
|
|
58
58
|
| <a id="rule-def-override-return-widened"></a>`def.override-return-widened` | An override's declared return type widens the inherited return (covariance). | high |
|
|
59
59
|
| <a id="rule-def-override-param-narrowed"></a>`def.override-param-narrowed` | An override narrows an inherited parameter type (contravariance). | high |
|
|
60
60
|
| <a id="rule-static-value-use-void"></a>`static.value-use.void` | A value recovered from an author-declared `-> void` return is used in value context (an assignment right-hand side, a call receiver, or a call argument). Off by default; reaches a run only through the `use-of-void-value` bleeding-edge feature (ADR-100). A bare-statement `void` call and a legitimate `top` value both stay silent. | high |
|
|
61
|
+
| <a id="rule-effect-envelope-exceeded"></a>`effect.envelope-exceeded` | A method performs an effect its declared envelope does not admit — its proven effect labels (its own body plus everything it calls) are not covered by the `%a{pure}` or `%a{rigor:v1:effect …}` bound written on it or on its class. Opt-in twice over: it needs an `effects:` block in `.rigor.yml` and an envelope you wrote. Positioned at the Ruby `def`. Unproven ("and possibly more") effects never fire, and `mutate.local` is tolerated by every envelope. | high |
|
|
62
|
+
| <a id="rule-effect-liskov-widened"></a>`effect.liskov-widened` | An override escapes the envelope written on the method it overrides. A `PgRepo` is usable wherever a `Repo` is, so a `%a{rigor:v1:effect io.db}` on `Repo#find` binds `PgRepo#find` too: an implementation may be purer than the bound it inherits, never less pure. Either what the override *does* exceeds the inherited bound, or the envelope the override *declares for itself* is wider than it. Both sides must be authored — nothing fires unless someone wrote an envelope on the ancestor — and only subclassing counts, not `include`. Positioned at the override's `def`. Needs an `effects:` block. | high |
|
|
63
|
+
| <a id="rule-effect-unknown-label"></a>`effect.unknown-label` | An effect declaration names a label the registry does not know — a typo in an envelope (`%a{rigor:v1:effect io.bd}`), or a member of `effects.tolerated:`. The whole tag then reads as unbounded, so the declaration quietly stops doing anything; this says so. Positioned at the declaration: the `.rbs` line, the `.rb` line for an rbs-inline annotation, or `.rigor.yml` for a config value. `# rigor:disable` comments are not read out of `.rbs` or `.rigor.yml`, so use `disable:` or the baseline there. Only fires where the spelling is evidently meant to be a label (close to a known one, next to a known one, dotted, or retired) — a word nothing resembles stays silent, because you may be opening your own vocabulary. Needs an `effects:` block. | high |
|
|
64
|
+
| <a id="rule-effect-annotations-unchecked"></a>`effect.annotations-unchecked` | Your signatures carry `%a{pure}` / `%a{rigor:v1:effect …}` but `.rigor.yml` has no `effects:` block, so nothing checks them. One `:info` per run, positioned at the first annotation. An annotation never turns effect collection on by itself — that would make one line in one file more expensive for every run — so this is how it tells you instead. Add `effects: {}` to opt in, or `disable:` it to keep the annotations documentary. | — |
|
|
61
65
|
| <a id="rule-suppression-unknown-rule"></a>`suppression.unknown-rule` | A `# rigor:disable[-file]` comment names a rule that does not exist (typically a typo), so the suppression silently does nothing. `plugin.`-prefixed tokens are never flagged. | high |
|
|
62
66
|
| <a id="rule-suppression-empty"></a>`suppression.empty` | A `# rigor:disable[-file]` comment lists no rules, so it suppresses nothing. | high |
|
|
63
67
|
| <a id="rule-suppression-unknown-marker"></a>`suppression.unknown-marker` | A comment uses a suppression marker Rigor does not recognise — typically the RuboCop reflex `# rigor:disable-next-line <rule>` or `# rigor:enable <rule>`. Rigor's only markers are `# rigor:disable <rules>` (suppresses on its own line) and `# rigor:disable-file <rules>`, so the comment suppresses nothing. | high |
|
|
@@ -104,8 +108,8 @@ are:
|
|
|
104
108
|
|
|
105
109
|
| Severity | Rules |
|
|
106
110
|
| --- | --- |
|
|
107
|
-
| `warning` | `call.unresolved-toplevel`, `def.ivar-write-mismatch`, `def.return-type-mismatch`, `def.override-visibility-reduced`, `def.override-return-widened`, `def.override-param-narrowed`, `flow.unreachable-branch`, `flow.always-truthy-condition`, `flow.dead-assignment`, `flow.duplicate-hash-key`, `flow.return-in-ensure`, `flow.shadowed-rescue-clause`, `suppression.unknown-rule`, `suppression.empty`, `suppression.unknown-marker` |
|
|
108
|
-
| `info` | `flow.unreachable-clause`, `dump.type` |
|
|
111
|
+
| `warning` | `call.unresolved-toplevel`, `def.ivar-write-mismatch`, `def.return-type-mismatch`, `def.override-visibility-reduced`, `def.override-return-widened`, `def.override-param-narrowed`, `flow.unreachable-branch`, `flow.always-truthy-condition`, `flow.dead-assignment`, `flow.duplicate-hash-key`, `flow.return-in-ensure`, `flow.shadowed-rescue-clause`, `suppression.unknown-rule`, `suppression.empty`, `suppression.unknown-marker`, `effect.envelope-exceeded`, `effect.liskov-widened` |
|
|
112
|
+
| `info` | `flow.unreachable-clause`, `dump.type`, `effect.unknown-label`, `effect.annotations-unchecked` |
|
|
109
113
|
| `off` | `call.self-undefined-method`, `static.value-use.void` |
|
|
110
114
|
|
|
111
115
|
Everything else emits as `error`. For one rule under all three
|