rigortype 0.3.4 → 0.3.6
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 +5 -5
- data/data/effects/core.yml +18 -1
- data/data/effects/registry.yml +31 -3
- data/data/gem_overlay/activesupport/core_ext.rbs +33 -0
- data/docs/handbook/02-everyday-types.md +1 -1
- data/docs/handbook/04-tuples-and-shapes.md +1 -1
- data/docs/handbook/08-understanding-errors.md +1 -1
- data/docs/handbook/09-plugins.md +2 -2
- data/docs/handbook/10-sorbet.md +1 -1
- data/docs/handbook/README.md +2 -2
- data/docs/handbook/appendix-go.md +1 -1
- data/docs/handbook/appendix-java-csharp.md +2 -2
- data/docs/handbook/appendix-mypy.md +1 -1
- data/docs/handbook/appendix-protocols-and-structural-typing.md +2 -2
- data/docs/handbook/appendix-rust.md +1 -1
- data/docs/handbook/appendix-type-theory.md +2 -2
- data/docs/handbook/appendix-typescript.md +4 -4
- data/docs/manual/02-cli-reference.md +73 -22
- data/docs/manual/03-configuration.md +19 -2
- data/docs/manual/04-diagnostics.md +15 -2
- data/docs/manual/08-skills.md +1 -1
- data/docs/manual/11-ci.md +38 -1
- data/docs/manual/12-caching.md +40 -1
- data/docs/manual/15-type-protection-coverage.md +1 -1
- data/docs/manual/16-rbs-extended-annotations.md +15 -2
- data/docs/manual/18-removing-dead-code.md +13 -4
- data/docs/manual/19-effect-labels.md +678 -0
- data/docs/manual/README.md +7 -2
- data/docs/manual/ci-templates/README.md +9 -0
- data/docs/manual/plugins/rigor-actionmailer.md +4 -4
- data/docs/manual/plugins/rigor-activejob.md +3 -3
- data/docs/manual/plugins/rigor-activerecord.md +4 -4
- data/docs/manual/plugins/rigor-rails-i18n.md +5 -5
- data/docs/manual/plugins/rigor-rspec.md +6 -2
- data/lib/rigor/analysis/diagnostic.rb +17 -6
- data/lib/rigor/analysis/effects_cache_probe.rb +132 -0
- data/lib/rigor/analysis/reachability/graph.rb +30 -11
- data/lib/rigor/analysis/reachability/plugin_roots.rb +11 -8
- data/lib/rigor/analysis/reachability/scan.rb +20 -4
- data/lib/rigor/analysis/reachability/scan_cache.rb +130 -0
- data/lib/rigor/analysis/rule_catalog.rb +10 -3
- data/lib/rigor/analysis/run_cache_key.rb +12 -0
- data/lib/rigor/analysis/run_cache_probe.rb +69 -1
- data/lib/rigor/analysis/runner/declaration_position.rb +8 -24
- data/lib/rigor/analysis/runner/effect_annotation_residual_pass.rb +28 -28
- data/lib/rigor/analysis/runner/effect_envelope_pass.rb +15 -9
- data/lib/rigor/analysis/runner/pool_coordinator.rb +25 -0
- data/lib/rigor/analysis/runner/run_snapshots.rb +7 -4
- data/lib/rigor/analysis/runner.rb +163 -31
- data/lib/rigor/analysis/worker_session.rb +3 -1
- data/lib/rigor/cache/file_digest.rb +20 -2
- data/lib/rigor/cli/check_command.rb +67 -48
- data/lib/rigor/cli/coverage_command.rb +5 -6
- data/lib/rigor/cli/doc_links.rb +100 -0
- data/lib/rigor/cli/docs_command.rb +32 -2
- data/lib/rigor/cli/effects_command.rb +149 -10
- data/lib/rigor/cli/effects_diff_renderer.rb +88 -14
- data/lib/rigor/cli/effects_explain_renderer.rb +6 -3
- data/lib/rigor/cli/effects_renderer.rb +41 -3
- data/lib/rigor/cli/effects_report.rb +116 -6
- data/lib/rigor/cli/effects_snapshot_command.rb +75 -11
- data/lib/rigor/cli/unused_command.rb +65 -16
- data/lib/rigor/cli.rb +33 -9
- data/lib/rigor/configuration.rb +37 -25
- data/lib/rigor/configuration_error.rb +20 -0
- data/lib/rigor/effects/collector.rb +38 -1
- data/lib/rigor/effects/definition_lines.rb +100 -0
- data/lib/rigor/effects/entry_points.rb +47 -1
- data/lib/rigor/effects/envelope_check.rb +18 -1
- data/lib/rigor/effects/file_collection.rb +18 -4
- data/lib/rigor/effects/framework_units.rb +68 -13
- data/lib/rigor/effects/inline_anchor.rb +134 -0
- data/lib/rigor/effects/liskov_check.rb +17 -8
- data/lib/rigor/effects/plugin_facts.rb +62 -13
- data/lib/rigor/effects/propagator.rb +79 -19
- data/lib/rigor/effects/registry.rb +10 -3
- data/lib/rigor/effects/scanner.rb +21 -9
- data/lib/rigor/effects/signature_sources.rb +29 -2
- data/lib/rigor/effects/snapshot.rb +74 -26
- data/lib/rigor/effects/snapshot_diff.rb +26 -3
- data/lib/rigor/effects/taint_cause.rb +1 -0
- data/lib/rigor/effects/unit_scan.rb +87 -12
- data/lib/rigor/inference/synthetic_method_scanner.rb +7 -0
- data/lib/rigor/plugin/base.rb +4 -0
- data/lib/rigor/plugin/box.rb +18 -2
- data/lib/rigor/plugin/effect_ancestry.rb +80 -0
- data/lib/rigor/plugin/manifest.rb +34 -10
- data/lib/rigor/plugin/registry.rb +10 -3
- data/lib/rigor/rbs_extended/envelope_scanner.rb +9 -0
- data/lib/rigor/rbs_extended.rb +22 -2
- data/lib/rigor/version.rb +1 -1
- data/plugins/rigor-activesupport-core-ext/sig/active_support/core_ext.rbs +17 -2
- data/plugins/rigor-devise/lib/rigor/plugin/devise.rb +27 -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.rb +13 -1
- data/skills/rigor-ci-setup/SKILL.md +2 -2
- data/skills/rigor-editor-setup/SKILL.md +2 -2
- data/skills/rigor-mcp-setup/SKILL.md +2 -2
- data/skills/rigor-monkeypatch-resolve/SKILL.md +1 -1
- data/skills/rigor-plugin-review/SKILL.md +3 -3
- metadata +10 -1
|
@@ -176,12 +176,15 @@ explicitly with `bundler.bundle_path:`, or supply signatures another way:
|
|
|
176
176
|
|
|
177
177
|
### Effect labels
|
|
178
178
|
|
|
179
|
+
The key reference is below; the workflow these keys serve — the vocabulary, the report, the committed
|
|
180
|
+
snapshot, the CI gate — is [Effect labels](19-effect-labels.md).
|
|
181
|
+
|
|
179
182
|
| Key | Type | Default | Meaning |
|
|
180
183
|
| --- | --- | --- | --- |
|
|
181
184
|
| `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
185
|
| `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
186
|
| `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). |
|
|
187
|
+
| `effects.snapshot.reach` | Array | `[]` | Entry points whose **transitive** footprint the snapshot records under `reach:`. The default is empty **deliberately**: Rigor could infer `[rails]` from your plugin list, but a snapshot is a record you agree to and review the diff of, and one whose contents moved because your plugin list moved would be the worse artefact. `rigor effects update` names the presets your plugins registered and leaves the same hint in the written file. 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); the error lists the presets your plugins did register, so it names the fix. |
|
|
185
188
|
| `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
189
|
| `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
190
|
| `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. |
|
|
@@ -215,7 +218,8 @@ effects:
|
|
|
215
218
|
reach: [rails]
|
|
216
219
|
```
|
|
217
220
|
|
|
218
|
-
`rails` — registered by
|
|
221
|
+
`rails` — registered by `rigor-railties`, which is a distinct plugin from the
|
|
222
|
+
[`rigor-rails`](plugins/rigor-rails.md) convenience grouping — stands for `app/controllers/**`,
|
|
219
223
|
`app/jobs/**`, `app/mailers/**` and `app/channels/**`: every way the outside world enters the
|
|
220
224
|
application. The component plugins also register the narrower `rails-controllers`, `rails-jobs`,
|
|
221
225
|
`rails-mailers` and `rails-channels` if you want one layer's footprint rather than all four. A preset is
|
|
@@ -254,6 +258,19 @@ When one method is a deliberate exception, write the narrower envelope on it in
|
|
|
254
258
|
and no `except:` key is needed. When a whole *kind* of effect is acceptable everywhere, name it in
|
|
255
259
|
`tolerated:` instead of loosening every stanza.
|
|
256
260
|
|
|
261
|
+
**One stanza can land hundreds of warnings**, one per (method, label) pair — on a mid-size Rails
|
|
262
|
+
application, `match: "app/helpers/**/*.rb"` with `effect: []` produced 343 across 18 files. That is
|
|
263
|
+
the layer telling you what it does rather than a misconfiguration, but it is not a work list either;
|
|
264
|
+
[Effect labels § Envelopes by convention](19-effect-labels.md) walks the recipe for working it down.
|
|
265
|
+
|
|
266
|
+
A stanza is checked against **proven** labels only, so on a Rails application it cannot fire on
|
|
267
|
+
`io.db.*`, `cache.*`, `telemetry`, `email.send`, `job.enqueue` or any `rails.*`: those come from a
|
|
268
|
+
plugin modelling a framework Rigor did not read, and a claim about unread code must not be able to
|
|
269
|
+
fail your build. The enforcement path for that half is the committed snapshot, whose `rigor effects
|
|
270
|
+
check` marks a declared-lane addition with `≤+` — [Effect labels § What a bound can and cannot
|
|
271
|
+
see](19-effect-labels.md#what-a-bound-can-and-cannot-see), and
|
|
272
|
+
[ADR-103](../adr/103-effect-labels.md) § WD17 for why.
|
|
273
|
+
|
|
257
274
|
If a layer is not ready for a bound yet, leave `envelopes:` out and start with the committed snapshot
|
|
258
275
|
([`rigor effects update`](02-cli-reference.md#the-effect-snapshot)) — it needs no declaration at all,
|
|
259
276
|
and stanzas are the second step, written once the record has told you what the layer actually does.
|
|
@@ -19,6 +19,18 @@ Every rule has a two-segment `family.rule` identifier:
|
|
|
19
19
|
| `assert` | `assert_type` checks. |
|
|
20
20
|
| `dump` | `dump_type` notices. |
|
|
21
21
|
|
|
22
|
+
Every diagnostic carries its identifier in brackets at the end of the
|
|
23
|
+
line, so the ID you need for `# rigor:disable`, for `disable:` and for
|
|
24
|
+
`severity_profile:` is the one you are already looking at:
|
|
25
|
+
|
|
26
|
+
```text
|
|
27
|
+
app/user.rb:11:3: error: undefined method `lenght' for "hello" [call.undefined-method]
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
A diagnostic no rule produced — a parse error, a path error, an
|
|
31
|
+
internal analyzer error — has nothing to suppress and carries no
|
|
32
|
+
bracket.
|
|
33
|
+
|
|
22
34
|
`rigor explain <rule>` prints the full catalogue entry for any
|
|
23
35
|
built-in rule ID; `rigor explain` with no argument lists them all.
|
|
24
36
|
|
|
@@ -27,7 +39,8 @@ built-in rule ID; `rigor explain` with no argument lists them all.
|
|
|
27
39
|
Each built-in rule has a stable per-rule anchor on this page
|
|
28
40
|
(`#rule-<family>-<name>`, dots written as dashes) — the
|
|
29
41
|
`documentation_url` field in `--format json` and `rigor explain`'s
|
|
30
|
-
`Documentation:` line both point here
|
|
42
|
+
`Documentation:` line both point here, at this chapter's published
|
|
43
|
+
copy on <https://rigor.typedduck.fail/manual/04-diagnostics/>. The `Evidence` column is
|
|
31
44
|
Rigor's confidence that a firing is a true positive (see
|
|
32
45
|
[Evidence tier](#evidence-tier) below). The one exception is
|
|
33
46
|
`rbs_extended.unsatisfied-conformance`, an `rbs_extended`-family rule
|
|
@@ -61,7 +74,7 @@ carries no `documentation_url`.
|
|
|
61
74
|
| <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
75
|
| <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
76
|
| <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. | — |
|
|
77
|
+
| <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. Reads both annotation lanes — `sig/*.rbs` and rbs-inline comments — except on a run that analyses no file at all (a warm `--incremental` with nothing changed), where only the `.rbs` lane is visible. | — |
|
|
65
78
|
| <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 |
|
|
66
79
|
| <a id="rule-suppression-empty"></a>`suppression.empty` | A `# rigor:disable[-file]` comment lists no rules, so it suppresses nothing. | high |
|
|
67
80
|
| <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 |
|
data/docs/manual/08-skills.md
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
Rigor bundles a set of **Agent Skills** — structured workflows an AI
|
|
4
4
|
coding agent (Claude Code and compatible tools) can run on your behalf.
|
|
5
|
-
They live in [`skills/`](../../skills
|
|
5
|
+
They live in [`skills/`](../../skills) and are auto-discovered when an
|
|
6
6
|
agent works inside a project that has Rigor available.
|
|
7
7
|
|
|
8
8
|
Skills are optional. Everything they do, you can do by hand with the
|
data/docs/manual/11-ci.md
CHANGED
|
@@ -73,7 +73,7 @@ platform reads to annotate the diff directly, instead of leaving them in
|
|
|
73
73
|
the job log ([ADR-51](../adr/51-ci-diagnostic-output-formats.md)). These
|
|
74
74
|
layer on the generic `--format json` stream; they add no new diagnostics,
|
|
75
75
|
only a platform-native rendering. Ready-to-copy template files live under
|
|
76
|
-
[`ci-templates/`](ci-templates
|
|
76
|
+
[`ci-templates/`](ci-templates).
|
|
77
77
|
|
|
78
78
|
### GitHub — inline annotations (the default)
|
|
79
79
|
|
|
@@ -188,6 +188,43 @@ The exit code is unchanged by `--format` — `0` when there are no errors,
|
|
|
188
188
|
`1` otherwise — so the job still gates the pipeline. `--format json`
|
|
189
189
|
remains available for any other tool that wants the raw diagnostic stream.
|
|
190
190
|
|
|
191
|
+
## Gating effect drift
|
|
192
|
+
|
|
193
|
+
If the project uses [effect labels](19-effect-labels.md), add a second
|
|
194
|
+
step beside `rigor check`. It answers a different question — "did this
|
|
195
|
+
branch change what the code *does*?" — and fails for different reasons,
|
|
196
|
+
so keep it as its own step and let the log say which one went red:
|
|
197
|
+
|
|
198
|
+
```yaml
|
|
199
|
+
- run: gem install rigortype
|
|
200
|
+
- run: rigor check
|
|
201
|
+
- run: rigor effects check
|
|
202
|
+
```
|
|
203
|
+
|
|
204
|
+
`rigor effects check` exits `0` when the committed
|
|
205
|
+
`.rigor-effects.yml` matches what Rigor just recomputed and `1`
|
|
206
|
+
otherwise — including when the file is missing from the checkout, which
|
|
207
|
+
is what catches the pull request that changed a method and forgot to
|
|
208
|
+
regenerate it. The author's fix is `rigor effects update` plus a commit;
|
|
209
|
+
the regenerated diff is what the reviewer reads.
|
|
210
|
+
|
|
211
|
+
A bot that wants to report *what this branch changes*, regardless of how
|
|
212
|
+
stale the default branch is, compares against the base instead of the
|
|
213
|
+
committed file:
|
|
214
|
+
|
|
215
|
+
```sh
|
|
216
|
+
rigor effects diff --baseline <(git show origin/main:.rigor-effects.yml)
|
|
217
|
+
```
|
|
218
|
+
|
|
219
|
+
`diff` prints the identical comparison and never gates, so it is safe as
|
|
220
|
+
a comment-posting step; `check` takes the same `--baseline`.
|
|
221
|
+
|
|
222
|
+
With an `effects:` block configured, the snapshot verbs share their
|
|
223
|
+
cached effect summaries with `rigor check`, so running both in one job
|
|
224
|
+
does not re-read the project twice. Exit codes, `--strict-tolerated`, the
|
|
225
|
+
`--no-tolerated-effects` audit switch and the review loop are covered in
|
|
226
|
+
[Effect labels § In CI](19-effect-labels.md).
|
|
227
|
+
|
|
191
228
|
## Pinning Rigor's version
|
|
192
229
|
|
|
193
230
|
The workflow above installs whatever `rigortype` is current at run
|
data/docs/manual/12-caching.md
CHANGED
|
@@ -49,6 +49,45 @@ The cache is also schema-versioned: after a Rigor upgrade that
|
|
|
49
49
|
changes the cache format, the stale cache is purged on the
|
|
50
50
|
first writable run.
|
|
51
51
|
|
|
52
|
+
## Effect summaries
|
|
53
|
+
|
|
54
|
+
[Effect labels](19-effect-labels.md) are cached alongside the rest,
|
|
55
|
+
but under their **own** identity: Rigor's effect vocabulary, its
|
|
56
|
+
built-in effect catalogue, and your `effects:` block. Two
|
|
57
|
+
consequences worth knowing:
|
|
58
|
+
|
|
59
|
+
- **Turning `effects:` on or off does not invalidate the entries
|
|
60
|
+
your `rigor check` already relies on.** The two identities are
|
|
61
|
+
separate, so adopting effect labels costs a first collection
|
|
62
|
+
pass and leaves the diagnostics cache alone. Upgrading to a
|
|
63
|
+
Rigor whose catalogue changed does the reverse — it re-reads
|
|
64
|
+
your effects without re-running your check.
|
|
65
|
+
- **The `rigor effects` verbs share those summaries with `rigor
|
|
66
|
+
check`.** In a job that runs both, the second command pays for
|
|
67
|
+
the propagation rather than for a second analysis.
|
|
68
|
+
|
|
69
|
+
The exception is a `rigor effects` run on a project with **no**
|
|
70
|
+
`effects:` block: it collects under an implicit empty block and
|
|
71
|
+
shares no cache with `rigor check`, because a run served from that
|
|
72
|
+
cache would have collected nothing.
|
|
73
|
+
|
|
74
|
+
**Everything the effect system produces survives a cache hit.** A
|
|
75
|
+
warm `rigor check` reports exactly the `effect.envelope-exceeded`
|
|
76
|
+
warnings a cold one does; there is no configuration in which the
|
|
77
|
+
effect checks fire on the first build of the day and go quiet
|
|
78
|
+
afterwards. The judgment itself is never stored — it is recomputed
|
|
79
|
+
each run from the summaries — so editing an envelope, a
|
|
80
|
+
`tolerated:` list or an `%a{pure}` annotation re-judges your code
|
|
81
|
+
on the next run even though no Ruby file changed, and a warning
|
|
82
|
+
can never outlive the stanza that justified it.
|
|
83
|
+
|
|
84
|
+
That costs speed in exactly one case. Declaring an envelope
|
|
85
|
+
anywhere opts the project out of the cache-hit fast path, because
|
|
86
|
+
Rigor has to load the analyzer to re-judge: a warm run takes about
|
|
87
|
+
a second on a mid-sized Rails application instead of about a
|
|
88
|
+
quarter of one. A project with `effects:` on and nothing declared
|
|
89
|
+
anywhere pays nothing.
|
|
90
|
+
|
|
52
91
|
## How a file is checked for changes
|
|
53
92
|
|
|
54
93
|
To decide whether a cached entry is still valid, Rigor needs to
|
|
@@ -98,7 +137,7 @@ CI detection that `auto` relies on.
|
|
|
98
137
|
| --- | --- |
|
|
99
138
|
| `rigor check --no-cache` | Run without reading or writing the persistent cache. |
|
|
100
139
|
| `rigor check --clear-cache` | Delete the cache directory, then run. |
|
|
101
|
-
| `rigor check --cache-stats` | Print the on-disk cache inventory when the run finishes. |
|
|
140
|
+
| `rigor check --cache-stats` | Print the on-disk cache inventory when the run finishes (on stderr under every `--format` but `text`, so the document on stdout stays parseable). |
|
|
102
141
|
| `rigor check --incremental` | Re-analyse only what changed; serve the rest from the incremental snapshot (see below). |
|
|
103
142
|
|
|
104
143
|
There is no config key to disable caching permanently — the
|
|
@@ -333,7 +333,7 @@ ones a type actually catches.
|
|
|
333
333
|
> [Configuration](03-configuration.md)). A project whose gems live in
|
|
334
334
|
> the active Ruby's default gem home — the common `rbenv` / `mise`
|
|
335
335
|
> case with no `--path` set — is invisible to the isolated analyzer by
|
|
336
|
-
> design ([ADR-27](
|
|
336
|
+
> design ([ADR-27](../adr/27-tool-distribution-model.md)):
|
|
337
337
|
> point Rigor at it with `bundler.bundle_path:`. Until you do, these
|
|
338
338
|
> holes keep the generic `engine_gap` cause instead of `add_rbs` —
|
|
339
339
|
> the label is missing, never wrong.
|
|
@@ -162,7 +162,7 @@ Every directive above describes what a method returns. Two
|
|
|
162
162
|
describe what it *does*: `%a{pure}` — rbs' own purity
|
|
163
163
|
annotation, read as "nothing at all" — and
|
|
164
164
|
`%a{rigor:v1:effect <labels>}`, a comma-separated list of bare
|
|
165
|
-
[effect labels](
|
|
165
|
+
[effect labels](19-effect-labels.md) the
|
|
166
166
|
method may not exceed. Both attach to a method or to a `class` /
|
|
167
167
|
`module`, where they distribute to that class's own methods
|
|
168
168
|
(nearest wins), and both tolerate mutating objects the method
|
|
@@ -210,7 +210,20 @@ on:
|
|
|
210
210
|
at the declaration.
|
|
211
211
|
- Without the block, one
|
|
212
212
|
[`effect.annotations-unchecked`](04-diagnostics.md#rule-effect-annotations-unchecked)
|
|
213
|
-
`:info` per run tells you the annotations are inert
|
|
213
|
+
`:info` per run tells you the annotations are inert — from either
|
|
214
|
+
lane, with one exception: a run that analyses **no file at all**
|
|
215
|
+
(a warm `rigor check --incremental` with nothing changed) has no
|
|
216
|
+
synthesised RBS to read, so it reports a `.rbs` annotation and not
|
|
217
|
+
an inline one. Any run that analyses something reports both.
|
|
218
|
+
|
|
219
|
+
Two practical notes. Annotating one method in a `.rbs` file forces you to
|
|
220
|
+
declare its whole signature — RBS has no way to annotate a method it does
|
|
221
|
+
not declare — while the rbs-inline form above does not, so prefer the
|
|
222
|
+
inline lane when the bound is all you want. And an envelope is checked
|
|
223
|
+
against the **proven** lane only: a method whose labels all sit in the
|
|
224
|
+
declared (`≤`) lane passes `%a{pure}` in silence.
|
|
225
|
+
[Effect labels](19-effect-labels.md) covers both, with the vocabulary
|
|
226
|
+
these annotations draw from.
|
|
214
227
|
|
|
215
228
|
## Higher-kinded type directives
|
|
216
229
|
|
|
@@ -140,7 +140,7 @@ Four sections, needing four different decisions:
|
|
|
140
140
|
|
|
141
141
|
| Section | What it means | What to do |
|
|
142
142
|
| --- | --- | --- |
|
|
143
|
-
| **Reachable only from test code** | Live test, no production caller | Work these first |
|
|
143
|
+
| **Reachable only from test code** | Live test, no production caller Rigor can see or suspect | Work these first |
|
|
144
144
|
| **Candidates** | Nothing reachable names it | Adjudicate — most are still live |
|
|
145
145
|
| **Cannot decide** | Something can name it at runtime | Read the reason; do not delete from here |
|
|
146
146
|
| **Namespace-only** | A module wrapping live code | Excluded from candidates; count only |
|
|
@@ -209,14 +209,23 @@ skip most of a list quickly:
|
|
|
209
209
|
|
|
210
210
|
### Cannot decide: read the reason, do not delete
|
|
211
211
|
|
|
212
|
-
These were demoted
|
|
213
|
-
|
|
212
|
+
These were demoted because something can name the class at runtime —
|
|
213
|
+
out of `candidates`, or out of **reachable only from test code**. Each
|
|
214
|
+
row says what:
|
|
214
215
|
|
|
215
216
|
```
|
|
216
217
|
1 Handlers::Alpha lib/handlers.rb:2
|
|
217
218
|
constantize on an interpolated string (lib/dispatch.rb:14)
|
|
218
219
|
```
|
|
219
220
|
|
|
221
|
+
A row demoted out of the test-only section is the second kind, and it
|
|
222
|
+
is the one worth knowing about: a class your specs reference and a
|
|
223
|
+
data file also names — a job in `config/recurring.yml`, a class named
|
|
224
|
+
from a YAML setting — is not a dead production path, because the
|
|
225
|
+
configuration may well be what drives it. The test-only section makes
|
|
226
|
+
a claim about production, so a row Rigor holds evidence against
|
|
227
|
+
belongs here instead, with the file named.
|
|
228
|
+
|
|
220
229
|
`"Foo".constantize` names `Foo` exactly, so it counts as an ordinary
|
|
221
230
|
reference and never reaches this section. `"Foo::#{key}".constantize`
|
|
222
231
|
can only bound the namespace, so everything under `Foo` is demoted. A
|
|
@@ -322,5 +331,5 @@ Running it as a **reporting** job is fine, and is the intended way to
|
|
|
322
331
|
notice the drift described above: run it on a schedule, publish the
|
|
323
332
|
JSON, and let a person read the trend.
|
|
324
333
|
|
|
325
|
-
[adr-102]:
|
|
334
|
+
[adr-102]: ../adr/102-unused-code-reachability-report.md
|
|
326
335
|
[issue-363]: https://github.com/rigortype/rigor/issues/363
|