rigortype 0.4.2 → 0.4.3
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/docs/handbook/11-sig-gen.md +12 -5
- data/docs/manual/02-cli-reference.md +19 -1
- data/docs/manual/03-configuration.md +6 -4
- data/docs/manual/05-inspecting-types.md +50 -0
- data/docs/manual/12-caching.md +18 -1
- data/docs/manual/14-rails-quickstart.md +2 -2
- data/docs/manual/18-removing-dead-code.md +16 -9
- data/lib/rigor/analysis/check_rules/rule_ids.rb +7 -0
- data/lib/rigor/analysis/check_rules.rb +23 -22
- data/lib/rigor/analysis/effects_cache_probe.rb +1 -1
- data/lib/rigor/analysis/incremental_run_slot.rb +3 -3
- data/lib/rigor/analysis/incremental_session.rb +89 -22
- data/lib/rigor/analysis/path_expansion.rb +62 -6
- data/lib/rigor/analysis/plugin_fact_fingerprint.rb +124 -37
- data/lib/rigor/analysis/reachability/graph.rb +119 -15
- data/lib/rigor/analysis/reachability/scan.rb +332 -173
- data/lib/rigor/analysis/reachability/scan_cache.rb +4 -2
- data/lib/rigor/analysis/run_cache_probe.rb +1 -1
- data/lib/rigor/analysis/run_stats.rb +28 -6
- data/lib/rigor/analysis/runner/diagnostic_aggregator.rb +26 -1
- data/lib/rigor/analysis/runner/pool_coordinator.rb +28 -9
- data/lib/rigor/analysis/runner/project_pre_passes.rb +21 -1
- data/lib/rigor/analysis/runner.rb +29 -11
- data/lib/rigor/analysis/worker_session.rb +1 -1
- data/lib/rigor/cache/engine_source.rb +21 -7
- data/lib/rigor/cache/incremental_snapshot.rb +36 -8
- data/lib/rigor/cli/check_command.rb +138 -39
- data/lib/rigor/cli/docs_command.rb +5 -4
- data/lib/rigor/cli/lens_command.rb +144 -0
- data/lib/rigor/cli/lsp_command.rb +5 -2
- data/lib/rigor/cli/skill_command.rb +3 -3
- data/lib/rigor/cli/skill_describe.rb +1 -1
- data/lib/rigor/cli/type_of_command.rb +2 -1
- data/lib/rigor/cli/unused_command.rb +157 -24
- data/lib/rigor/cli.rb +30 -4
- data/lib/rigor/configuration.rb +15 -0
- data/lib/rigor/environment/bundle_sig_discovery.rb +11 -7
- data/lib/rigor/environment/installed_gem_set.rb +38 -11
- data/lib/rigor/environment/lockfile_resolver.rb +37 -6
- data/lib/rigor/environment/missing_gem_constant_index.rb +22 -7
- data/lib/rigor/environment/rbs_loader.rb +33 -0
- data/lib/rigor/environment/required_features.rb +4 -2
- data/lib/rigor/environment.rb +12 -5
- data/lib/rigor/inference/builtins/cruby_definers.rb +22 -5
- data/lib/rigor/inference/captured_locals.rb +43 -3
- data/lib/rigor/inference/expression_typer.rb +58 -25
- data/lib/rigor/inference/in_effect_refinements/scope_reads.rb +21 -17
- data/lib/rigor/inference/match_rebinding.rb +1 -1
- data/lib/rigor/inference/method_dispatcher/argument_nodes.rb +25 -0
- data/lib/rigor/inference/method_dispatcher/kernel_dispatch.rb +3 -2
- data/lib/rigor/inference/method_dispatcher/match_data_folding.rb +2 -2
- data/lib/rigor/inference/method_dispatcher/member_shape_projection.rb +3 -3
- data/lib/rigor/inference/method_dispatcher/optional_count.rb +63 -0
- data/lib/rigor/inference/method_dispatcher/rbs_dispatch.rb +4 -16
- data/lib/rigor/inference/method_dispatcher/struct_folding.rb +4 -7
- data/lib/rigor/inference/method_dispatcher/struct_materialization.rb +8 -26
- data/lib/rigor/inference/method_dispatcher/unit_denominator_rational.rb +67 -0
- data/lib/rigor/inference/method_dispatcher.rb +52 -3
- data/lib/rigor/inference/project_patched_definedness_guard.rb +86 -0
- data/lib/rigor/inference/project_patched_methods.rb +2 -2
- data/lib/rigor/inference/project_patched_scanner.rb +38 -24
- data/lib/rigor/inference/refined_dispatch/class_object.rb +350 -0
- data/lib/rigor/inference/refined_dispatch/own_method.rb +112 -40
- data/lib/rigor/inference/refined_dispatch.rb +62 -14
- data/lib/rigor/inference/repeated_or_writes.rb +2 -0
- data/lib/rigor/inference/scope_indexer.rb +140 -5
- data/lib/rigor/inference/statement_evaluator.rb +223 -31
- data/lib/rigor/inference/untyped_rejoin.rb +75 -0
- data/lib/rigor/language_server/project_context.rb +56 -5
- data/lib/rigor/lens/anchor.rb +115 -0
- data/lib/rigor/lens/contract_reader.rb +118 -0
- data/lib/rigor/lens/document.rb +113 -0
- data/lib/rigor/lens/file_reader.rb +433 -0
- data/lib/rigor/lens/members.rb +153 -0
- data/lib/rigor/lens/parameters.rb +91 -0
- data/lib/rigor/lens/plugins.rb +40 -0
- data/lib/rigor/lens/run.rb +280 -0
- data/lib/rigor/lens/slot.rb +79 -0
- data/lib/rigor/lens.rb +40 -0
- data/lib/rigor/project_environment.rb +20 -1
- data/lib/rigor/protection/mutation_cache.rb +2 -1
- data/lib/rigor/scope.rb +22 -11
- data/lib/rigor/sig_gen/generator.rb +45 -5
- data/lib/rigor/version.rb +1 -1
- data/plugins/rigor-active-model-serializers/lib/rigor/plugin/active_model_serializers/serializer_discoverer.rb +29 -3
- data/plugins/rigor-active-model-serializers/lib/rigor/plugin/active_model_serializers.rb +5 -1
- data/sig/rigor/scope.rbs +3 -0
- data/sig/rigor.rbs +6 -3
- data/skills/rigor-project-init/references/02-configure.md +2 -2
- metadata +18 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: b20cef50f4d076f1afefb8c20563cd21a95a56b01bf4db3257371adfe7c64d5d
|
|
4
|
+
data.tar.gz: 9c4f5f8825106ece96fb2ac00dc1ad4ef43f302368f1a12f87521c725530b924
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 4418a4a75a22d27038d0607caa5a79723470e68f9662da1aec0b79e8056a1b157f78cf85f83cb51f646aa22d2634b0d851bb9f0132a548f5104acdee2372d1d2
|
|
7
|
+
data.tar.gz: 026710fb13eebddf98776d6a9a3fb8e1738a7cc5fdd77c326afa0adb680c0760462d9e41b4aff57d1c41d1878fc05c41a8658be7b69ed4f265709aa27f6fc825
|
data/README.md
CHANGED
|
@@ -232,7 +232,7 @@ rigor docs --list # list every bundled page
|
|
|
232
232
|
|
|
233
233
|
## Status
|
|
234
234
|
|
|
235
|
-
Current release: **`v0.4.
|
|
235
|
+
Current release: **`v0.4.3`** (2026-10-15) — on the
|
|
236
236
|
`0.4.x` line, continuing the evaluation era opened by `v0.2.0`, the first
|
|
237
237
|
publicly-announced (general / evaluation) release. The line publishes
|
|
238
238
|
an enumerated [compatibility surface](docs/compatibility.md) as a
|
data/docs/handbook/11-sig-gen.md
CHANGED
|
@@ -188,11 +188,11 @@ changes which calls the signature accepts:
|
|
|
188
188
|
$ rigor sig-gen --diff lib/c.rb
|
|
189
189
|
--- lib/c.rb: C#m
|
|
190
190
|
- def m: (Integer x) -> untyped
|
|
191
|
-
+ def m: (Integer x) ->
|
|
191
|
+
+ def m: (Integer x) -> Float
|
|
192
192
|
|
|
193
193
|
--- lib/c.rb: C#n
|
|
194
194
|
- def n: (?) -> untyped
|
|
195
|
-
+ def n: (?) ->
|
|
195
|
+
+ def n: (?) -> Float
|
|
196
196
|
```
|
|
197
197
|
|
|
198
198
|
The `-` line is the declaration the proposal replaces. A method
|
|
@@ -523,9 +523,16 @@ Neither does a declaration the body proves as a literal.
|
|
|
523
523
|
-> String` whose body is the string `"bot"`: the declared
|
|
524
524
|
type is your abstraction over that body, and pinning the
|
|
525
525
|
literal would rewrite a contract every sibling class shares.
|
|
526
|
-
A
|
|
527
|
-
|
|
528
|
-
|
|
526
|
+
A declared `untyped` that you wrote in `sig/` is treated
|
|
527
|
+
the same way for a value literal: an Integer, String or
|
|
528
|
+
Symbol the body proves is widened to its class (`-> 1`
|
|
529
|
+
becomes `-> Integer`, `-> ("S" | nil)` becomes
|
|
530
|
+
`-> (String | nil)`), while `nil`, `true`, `false` and a
|
|
531
|
+
computed `Float` are proposed as they are. An `untyped` that
|
|
532
|
+
only the inline reader synthesizes for a `def` you did not
|
|
533
|
+
annotate keeps the literal, as does a method no `.rbs`
|
|
534
|
+
declares — the literal is still the strictest thing the body
|
|
535
|
+
proves, and that is what gets written.
|
|
529
536
|
|
|
530
537
|
A comment rather than a `%a{…}` annotation, for three
|
|
531
538
|
reasons: it stays out of the `rigor:v1:` directive namespace
|
|
@@ -36,7 +36,7 @@ the `paths:` list from the configuration file.
|
|
|
36
36
|
| `--explain` | Surface fail-soft fallback events as `info` diagnostics. |
|
|
37
37
|
| `--no-cache` | Skip the persistent cache for this run. With `--incremental` it is a cold full analysis: the incremental snapshot and the run-result cache are neither read nor written, so the next plain `--incremental` run still uses the snapshot it had before. |
|
|
38
38
|
| `--incremental` | Re-analyse only the files changed since the last run plus the files that depend on them, serving the rest from a cross-process disk snapshot (ADR-46). Diagnostics are identical to a full run; a config / gem / version change (or a file added or removed) transparently forces a full re-analysis. A run with nothing changed is answered from the cache without starting the analyzer. See [Caching](12-caching.md). |
|
|
39
|
-
| `--verify-incremental` | Acceptance gate: run the incremental analyzer against a full `--no-cache` run and assert the diagnostics
|
|
39
|
+
| `--verify-incremental` | Acceptance gate: run the incremental analyzer against a full `--no-cache` run and assert the diagnostics agree as a set and in the same order, then exit (0 on match, 1 on mismatch: the differing diagnostics for a set mismatch, or the first index where the order diverges for a same-set, different-order mismatch). Used in CI to guarantee `--incremental` never serves a stale result. |
|
|
40
40
|
| `--clear-cache` | Delete the cache directory before running. |
|
|
41
41
|
| `--cache-stats` | Print the on-disk cache inventory when finished — on stdout under `--format text`, on stderr under every other format, so machine-readable output stays a parseable document. `--clear-cache`'s and `--verify-incremental`'s notes follow the same rule. |
|
|
42
42
|
| `--[no-]stats` | Print a run summary (files, classes, memory, wall time) to stderr. Default on. |
|
|
@@ -171,6 +171,24 @@ the two builds against each other — but if you are chasing a
|
|
|
171
171
|
disagreement between a probe and a `check` run, comparing
|
|
172
172
|
against `rigor check --no-cache` removes that variable.
|
|
173
173
|
|
|
174
|
+
## `rigor lens`
|
|
175
|
+
|
|
176
|
+
Map the declarations of one or more files with their types and
|
|
177
|
+
type provenance (experimental). See
|
|
178
|
+
[Inspecting inferred types](05-inspecting-types.md).
|
|
179
|
+
|
|
180
|
+
```sh
|
|
181
|
+
rigor lens [--format=text|json] [--config=PATH] [--workers=N] FILE...
|
|
182
|
+
```
|
|
183
|
+
|
|
184
|
+
A lens runs the same incremental analysis as
|
|
185
|
+
`rigor check --incremental` and reads and writes its cache.
|
|
186
|
+
`--workers` applies to a cold run. `--format=json` writes one
|
|
187
|
+
`lens/v0` document ([specification](../internal-spec/lens.md)).
|
|
188
|
+
Exit `0` when at least one file has a result, `1` when none
|
|
189
|
+
does, `64` on a usage error; diagnostics in the code never
|
|
190
|
+
change the exit status.
|
|
191
|
+
|
|
174
192
|
## `rigor trace`
|
|
175
193
|
|
|
176
194
|
Replay HOW the engine typed a file, step by step, as a
|
|
@@ -19,8 +19,10 @@ To *layer* configs rather than replace, a config file can name
|
|
|
19
19
|
a base with `includes:` (recursive). `--config=PATH` bypasses
|
|
20
20
|
discovery entirely.
|
|
21
21
|
|
|
22
|
-
|
|
23
|
-
|
|
22
|
+
Relative paths in a config file resolve against that file's own directory, with three exceptions:
|
|
23
|
+
`bundler.bundle_path:`, `bundler.lockfile:` and `rbs_collection.lockfile:` resolve against the working directory
|
|
24
|
+
`rigor` runs from. They match only when you run `rigor` from the directory that holds the config file, which is
|
|
25
|
+
the usual case; with `--config path/to/.rigor.yml` from elsewhere, give these three as absolute paths.
|
|
24
26
|
A leading `~/` (or a bare `~`) in a path setting expands to your home directory; `~name` is an ordinary
|
|
25
27
|
directory called `~name`, not another user's home. This covers `paths:`, `signature_paths:`, `test_paths:`,
|
|
26
28
|
`pre_eval:`, `plugins_io.allowed_paths:`, `includes:`, `bundler.bundle_path:`, `bundler.lockfile:` and
|
|
@@ -64,7 +66,7 @@ cache:
|
|
|
64
66
|
| --- | --- | --- | --- |
|
|
65
67
|
| `target_ruby` | String | `"4.0"` | The lowest Ruby version *your* project runs on — `"X.Y"`, `"X.Y.Z"`, or `"latest"`. Independent of the Ruby Rigor itself runs on. Every value selects the syntax Rigor parses. A version you set explicitly also turns on the Ruby-deprecation rules for that version ([`call.deprecated-ruby2-keywords`](04-diagnostics.md#rule-call-deprecated-ruby2-keywords) from `"4.1"`); the default and `"latest"` never do, which is why `rigor init` writes the key commented out. |
|
|
66
68
|
| `paths` | Array | `["lib"]` | Directories or files to analyse. |
|
|
67
|
-
| `exclude` | Array | `[]` | Glob patterns to skip. `vendor/bundle`, `.bundle`, and `node_modules` are always excluded. |
|
|
69
|
+
| `exclude` | Array | `[]` | Glob patterns to skip. A relative pattern is matched against the file's path relative to the directory of the config file in use (the working directory when no config file is used), so it excludes the same files whether the path came from `paths:` or from a command-line argument, and it never matches on a directory above that base (a file outside it is seen as `../…`, so `../shared/lib/gen/**` reaches a sibling directory that `paths:` names). A leading `./` is ignored, and a pattern starting with `.` (such as `.*`) also matches files outside the base, which appear as `../…`. A symlink to a file is matched only under its own name: the realpath pass resolves directories, not files. `*` and `**` also cross `/`, so `lib/*.rb` matches nested files too. An absolute pattern is matched against the absolute path. Patterns from an `includes:` file resolve against the top-level config's directory, not the included file's. `vendor/bundle`, `.bundle`, and `node_modules` are always excluded. |
|
|
68
70
|
| `test_paths` | Array | `nil` | The project's test roots: the directories (or files) holding its tests. Relative entries resolve against the config file's directory. Unset auto-detects whichever of `spec/` and `test/` exist; `[]` declares none. `rigor sig-gen --params=observed` reads call sites there to type parameters, and names on stderr a declared root that does not exist. Test roots are not analysed unless `paths:` also lists them, and changing them invalidates no cache. |
|
|
69
71
|
| `includes` | Array | `[]` | Other config files to layer underneath this one. |
|
|
70
72
|
| `fold_platform_specific_paths` | Boolean | `false` | Resolve Ruby-version-conditional load paths when discovering sources. |
|
|
@@ -76,7 +78,7 @@ cache:
|
|
|
76
78
|
| --- | --- | --- | --- |
|
|
77
79
|
| `libraries` | Array | `[]` | Standard-library / gem names whose bundled RBS to load. |
|
|
78
80
|
| `signature_paths` | Array | `nil` | Extra directories of `.rbs` files. Relative entries resolve against the config file's directory. |
|
|
79
|
-
| `pre_eval` | Array | `[]` | Files (or globs) walked before per-file analysis, to register project monkey-patches and publish their top-level constants project-wide. |
|
|
81
|
+
| `pre_eval` | Array | `[]` | Files (or globs) walked before per-file analysis, to register project monkey-patches and publish their top-level constants project-wide. The walker does not follow `require` / `require_relative`, so list the files in load order: a method declared in two files takes the last-listed one, and a file that requires a later-listed one is still read in list order. |
|
|
80
82
|
| `plugins` | Array | `[]` | Plugins to activate — see [Using plugins](07-plugins.md). |
|
|
81
83
|
|
|
82
84
|
### Signature generation
|
|
@@ -89,6 +89,14 @@ array. Line queries add `line_enumerations` metadata with the
|
|
|
89
89
|
shown and total expression counts. An exact position is the same
|
|
90
90
|
query the editor integration answers on hover.
|
|
91
91
|
|
|
92
|
+
Inside a `rescue` or `ensure` clause, `type-of` and hover can
|
|
93
|
+
disagree with `rigor check`. `check` reads a variable that the
|
|
94
|
+
`begin` body assigns as untyped there, because the clause may run
|
|
95
|
+
after the body raised partway through. `type-of` shows the value
|
|
96
|
+
the variable had before the `begin`. In
|
|
97
|
+
`conn = nil; begin; conn = open_conn; work; rescue; conn.close; end`,
|
|
98
|
+
`type-of` reports `conn` as `nil` and `check` reports nothing.
|
|
99
|
+
|
|
92
100
|
## `rigor trace` — watch the inference happen
|
|
93
101
|
|
|
94
102
|
Where `annotate` and `type-of` show the *answer*, `rigor trace
|
|
@@ -110,11 +118,53 @@ typer visits; the default keeps only the three teachable event
|
|
|
110
118
|
kinds. The JSON stream is stable enough to build course
|
|
111
119
|
material or figures from.
|
|
112
120
|
|
|
121
|
+
## `rigor lens` — a file's declarations and where their types come from
|
|
122
|
+
|
|
123
|
+
`rigor lens` lists every class, module, method and attribute a
|
|
124
|
+
file declares, with the type each one has and where that type
|
|
125
|
+
came from: `sig/`, an inline `#:` / `@rbs` comment, a plugin, a
|
|
126
|
+
library, or Rigor's inference from the body. It is experimental:
|
|
127
|
+
the command and its `lens/v0` JSON may still change.
|
|
128
|
+
|
|
129
|
+
```sh
|
|
130
|
+
rigor lens lib/shop.rb # text, for reading
|
|
131
|
+
rigor lens --format=json lib/*.rb # one JSON document
|
|
132
|
+
```
|
|
133
|
+
|
|
134
|
+
```text
|
|
135
|
+
lib/shop.rb xxh3-64:845f7426b6acfe12 (2 omitted)
|
|
136
|
+
4:7c90 class Shop
|
|
137
|
+
8:e37e Shop#label: (::Integer n) -> ::String [contract inline]
|
|
138
|
+
14:ce93 Shop#total: () -> Integer [single_site inferred]
|
|
139
|
+
16:182b Shop#fetch [contested; also lib/legacy_shop.rb:2]
|
|
140
|
+
omitted constant Shop::LIMIT (line 5)
|
|
141
|
+
```
|
|
142
|
+
|
|
143
|
+
The answer is the one `rigor check` reaches. A lens runs the
|
|
144
|
+
same incremental analysis as `rigor check --incremental` and
|
|
145
|
+
shares its cache, so the first run costs a full check and later
|
|
146
|
+
runs reuse it. The diagnostics it reports for a file are the
|
|
147
|
+
ones `check` reports.
|
|
148
|
+
|
|
149
|
+
- A method Ruby could take from more than one definition (the
|
|
150
|
+
same method defined in two files) is `contested`. Rigor does
|
|
151
|
+
not guess which one runs, so it prints no signature for it.
|
|
152
|
+
- A declaration this version does not model yet (a constant, an
|
|
153
|
+
`alias`, a `define_method`) is listed as omitted, never
|
|
154
|
+
dropped.
|
|
155
|
+
- `LINE:HASH` is an anchor: it changes when the declaration or
|
|
156
|
+
the type comments above it change, and not when prose
|
|
157
|
+
comments do.
|
|
158
|
+
|
|
159
|
+
The JSON document is specified in
|
|
160
|
+
[the lens output contract](../internal-spec/lens.md).
|
|
161
|
+
|
|
113
162
|
## Which to reach for
|
|
114
163
|
|
|
115
164
|
| You want… | Use |
|
|
116
165
|
| --- | --- |
|
|
117
166
|
| One expression's type, from the shell | `rigor type-of` |
|
|
167
|
+
| A file's declarations, their types and where each came from | `rigor lens` |
|
|
118
168
|
| Every line of a file surveyed | `rigor annotate` |
|
|
119
169
|
| The derivation replayed step by step | `rigor trace` |
|
|
120
170
|
| A type printed mid-analysis, in context | `dump_type` |
|
data/docs/manual/12-caching.md
CHANGED
|
@@ -184,7 +184,8 @@ file, which other files its analysis read from, so it knows
|
|
|
184
184
|
exactly which files an edit can affect. A continuous-integration
|
|
185
185
|
gate, `rigor check --verify-incremental`, asserts this on every
|
|
186
186
|
build: it runs the incremental analyzer and a full analysis and
|
|
187
|
-
fails if they disagree on a single diagnostic
|
|
187
|
+
fails if they disagree on a single diagnostic, or list the same
|
|
188
|
+
diagnostics in a different order.
|
|
188
189
|
|
|
189
190
|
Rigor is also precise about *what* an edit changed. Re-checking a
|
|
190
191
|
file that many others depend on — a base class, a widely-used
|
|
@@ -259,6 +260,22 @@ version change does, or `--clear-cache`. So the shortcut can stay
|
|
|
259
260
|
off indefinitely; run `rigor check --incremental --clear-cache`
|
|
260
261
|
once to turn it back on.
|
|
261
262
|
|
|
263
|
+
|
|
264
|
+
A warm `--incremental` run says on stderr how far the edit spread,
|
|
265
|
+
for example `rigor: --incremental warm — re-analysed 3 of 1295 files
|
|
266
|
+
(1 changed), reused the rest`. The first count is the files the
|
|
267
|
+
recheck re-analysed, out of M, the analysed set: every project file
|
|
268
|
+
the run covers, or only the subset under a path argument such as
|
|
269
|
+
`rigor check --incremental lib`. The re-analysed files are M minus
|
|
270
|
+
those served from the snapshot. They include files that a change
|
|
271
|
+
reached without editing them, such as a file whose inferred
|
|
272
|
+
parameter types moved (ADR-67), not only the edited files and their
|
|
273
|
+
dependents. The parenthesis lists `changed`, and `added` or
|
|
274
|
+
`removed` when there are some. View templates from a plugin are not
|
|
275
|
+
in M. The recheck re-runs every template on every warm run, so they
|
|
276
|
+
are counted after the file clause, as `plus 2 view templates`. An
|
|
277
|
+
unchanged run reads `re-analysed 0 of 1295 files, reused all`.
|
|
278
|
+
|
|
262
279
|
`--incremental` is most useful for fast local re-checks and CI
|
|
263
280
|
on a changed branch.
|
|
264
281
|
|
|
@@ -116,8 +116,9 @@ gap in coverage rather than a finding about your code.
|
|
|
116
116
|
rigor unused --entry-point='lib/cli.rb' --entry-point='lib/workers/**/*.rb'
|
|
117
117
|
```
|
|
118
118
|
|
|
119
|
-
Repeatable. Each glob is matched against every declaration's path
|
|
120
|
-
|
|
119
|
+
Repeatable. Each glob is matched against every declaration's path as
|
|
120
|
+
written, relative to the project root, and relative to the directory
|
|
121
|
+
you run in, and roots every declaration in a matching file.
|
|
121
122
|
|
|
122
123
|
**File-level references**, free. Code that runs on load — an
|
|
123
124
|
initializer naming a class, a `.rake` task — roots what it names. This
|
|
@@ -162,9 +163,12 @@ section held 22 policy classes whose only remaining caller was
|
|
|
162
163
|
`spec/policies/`. Those 22 were not adjudicated, so treat the section
|
|
163
164
|
as high-signal rather than high-yield; the yield is unmeasured.
|
|
164
165
|
|
|
165
|
-
"Test code" is decided by path, relative to the directory
|
|
166
|
-
|
|
167
|
-
|
|
166
|
+
"Test code" is decided by path, relative to the project root: the directory of the `.rigor.yml` in
|
|
167
|
+
use, or the directory you run `rigor unused` in when there is none or when the config sits below it
|
|
168
|
+
(`--config=ci/rigor.yml` from the project root). Running it from a subdirectory with `--config` reads
|
|
169
|
+
the same project, and references are read from the directory you run it in as well. A file counts
|
|
170
|
+
as test code when its path as written below either directory says so. The test paths are `spec/` and `test/` anywhere below the root,
|
|
171
|
+
`*_spec.rb` and `*_test.rb`, and the end-to-end trees `qa/`, `e2e/` and `features/` at the root. Tooling directories such as
|
|
168
172
|
`rubocop/`, `keeps/`, `tooling/` and `scripts/` count as production, so a class used only by them is
|
|
169
173
|
rooted rather than listed: deleting it would break the tooling.
|
|
170
174
|
|
|
@@ -257,10 +261,13 @@ can only bound the namespace, so everything under `Foo` is demoted;
|
|
|
257
261
|
`"V#{version}"` is the start of a name, so `V1`, `V2_0` and everything
|
|
258
262
|
under them are. `const_get` looks the name up from its receiver, so
|
|
259
263
|
`const_get("V#{version}")` inside `class Migration` demotes
|
|
260
|
-
`Migration::V1` — and the
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
same
|
|
264
|
+
`Migration::V1` — and the `V*` names under `Migration`'s superclasses
|
|
265
|
+
and mixins and at the top level as well, unless the call passes `false`
|
|
266
|
+
as its second argument. A literal `const_get("Handler")` is a reference
|
|
267
|
+
looked up in the same order, so it names `Migration::Handler` when
|
|
268
|
+
that exists rather than a top-level `Handler`. A name built into a
|
|
269
|
+
local variable first (`name = "V#{version}"; const_get(name)`) is read
|
|
270
|
+
the same way. A
|
|
264
271
|
class name appearing as a string in a `.yml`, `.json` or template file
|
|
265
272
|
demotes the same way — weaker evidence than a constant reference, and
|
|
266
273
|
neither proof of use nor grounds to call it dead.
|
|
@@ -156,6 +156,13 @@ module Rigor
|
|
|
156
156
|
# first segment appears here as KNOWN and stays silent: these ids are legitimate suppression /
|
|
157
157
|
# `severity_overrides:` vocabulary the light rule-id table cannot enumerate (plugins load dynamically;
|
|
158
158
|
# aggregator ids live in the engine-heavy runner), so under-warning is the FP-safe direction.
|
|
159
|
+
# The `pre-eval.*` ids (ADR-17 WD6) the bare family wildcard `pre-eval` expands to. Emitted by the
|
|
160
|
+
# `pre_eval:` pre-pass, outside the CheckRules catalogue, so the family cannot ride `RULE_FAMILIES`
|
|
161
|
+
# (which also drives the catalogue and the config audit over `ALL_RULES`).
|
|
162
|
+
PRE_EVAL_RULES = %w[
|
|
163
|
+
pre-eval.parse-error pre-eval.file-not-found pre-eval.duplicate-declaration
|
|
164
|
+
].freeze
|
|
165
|
+
|
|
159
166
|
NON_CHECK_DIAGNOSTIC_FAMILIES = %w[rbs_extended dynamic rbs pre-eval plugin plugin_trust].freeze
|
|
160
167
|
|
|
161
168
|
# Bare (dot-less) diagnostic ids the engine emits outside the catalogue (see the `rule:` literals in
|
|
@@ -87,6 +87,7 @@ module Rigor
|
|
|
87
87
|
def self.resolve_rule_token(token)
|
|
88
88
|
return nil if token == "all"
|
|
89
89
|
return [LEGACY_RULE_ALIASES.fetch(token)] if LEGACY_RULE_ALIASES.key?(token)
|
|
90
|
+
return PRE_EVAL_RULES if token == "pre-eval"
|
|
90
91
|
return ALL_RULES.select { |r| r.start_with?("#{token}.") } if RULE_FAMILIES.include?(token)
|
|
91
92
|
|
|
92
93
|
ALL_RULES.include?(token) ? [token] : []
|
|
@@ -654,7 +655,7 @@ module Rigor
|
|
|
654
655
|
# `dynamic.*`, `rbs.*`, `pre-eval.*`, and any `plugin.`-prefixed id — plugins load dynamically, so
|
|
655
656
|
# their rule vocabulary cannot be enumerated here and under-warning is the FP-safe direction).
|
|
656
657
|
def known_suppression_token?(token)
|
|
657
|
-
return true if token
|
|
658
|
+
return true if %w[all pre-eval].include?(token)
|
|
658
659
|
return true if ALL_RULES.include?(token) || LEGACY_RULE_ALIASES.key?(token) ||
|
|
659
660
|
RULE_FAMILIES.include?(token) || NON_CHECK_DIAGNOSTIC_IDS.include?(token)
|
|
660
661
|
|
|
@@ -863,29 +864,23 @@ module Rigor
|
|
|
863
864
|
|
|
864
865
|
# Is a refinement of `call_node`'s method name into `class_name` (or an ancestor) in effect at the call?
|
|
865
866
|
# Issue #1120 asks it for a method the class lacks. The two argument rules ask
|
|
866
|
-
# {#refined_redefinition_in_effect?} instead (issue #1740)
|
|
867
|
-
#
|
|
868
|
-
#
|
|
869
|
-
#
|
|
870
|
-
#
|
|
871
|
-
#
|
|
867
|
+
# {#refined_redefinition_in_effect?} instead (issue #1740). Typing the call from the refine body is #1664.
|
|
868
|
+
# Issue #1752 — a class-object receiver (`kind` `:singleton`) is reached by a refinement of any class or module
|
|
869
|
+
# on its lookup: `Class`, `Module`, `Object`, `Kernel`, `BasicObject`, or a module it extends. A refined
|
|
870
|
+
# singleton class (`refine X.singleton_class`) names no constant target, so its rows are class-unknown, which
|
|
871
|
+
# reach every receiver. Issue #1673 — the answer is derived from the file's in-effect refinements
|
|
872
|
+
# ({Inference::InEffectRefinements}), the list the typer reads, with each `using` expanded through its
|
|
873
|
+
# module's includes (issue #1671).
|
|
872
874
|
#
|
|
873
875
|
# ADR-121 WD7 — a module in effect whose refinements Rigor cannot read
|
|
874
876
|
# ({Inference::InEffectRefinements.opaque_module?}: a gem's, one from outside the analysed paths, one with an
|
|
875
877
|
# unreadable `refine`) may refine any class, a class object's included, so the call declines whatever its
|
|
876
|
-
# receiver. Issue #1796
|
|
878
|
+
# receiver. Issue #1796.
|
|
877
879
|
def refined_method_in_effect?(class_name, call_node, scope, kind, lexical_sites)
|
|
878
880
|
return false if lexical_sites.nil?
|
|
879
881
|
return true if opaque_refinement_in_effect?(call_node, scope, lexical_sites)
|
|
880
882
|
|
|
881
|
-
modules =
|
|
882
|
-
if kind == :instance
|
|
883
|
-
Inference::InEffectRefinements.refining_modules(scope, class_name, call_node.name)
|
|
884
|
-
else
|
|
885
|
-
# ADR-121 WD7 — a row whose class the walk could not name (`refine(k)` with `k` a singleton class)
|
|
886
|
-
# reaches a class object too.
|
|
887
|
-
Inference::InEffectRefinements.class_unknown_refining_modules(scope, call_node.name)
|
|
888
|
-
end
|
|
883
|
+
modules = Inference::InEffectRefinements.refining_modules(scope, class_name, call_node.name, kind)
|
|
889
884
|
!modules.nil? &&
|
|
890
885
|
lexical_sites.refinements.refinement_active?(call_node.location.start_offset, modules,
|
|
891
886
|
scope.declared_refinements) do |name|
|
|
@@ -902,8 +897,8 @@ module Rigor
|
|
|
902
897
|
#
|
|
903
898
|
# A call with nothing in effect is checked. Otherwise it reports only from positive knowledge: the list
|
|
904
899
|
# carries no unknown marker and no opaque module, and {Inference::RefinedDispatch::OwnMethod} proves the
|
|
905
|
-
# receiver's own definer answers first.
|
|
906
|
-
#
|
|
900
|
+
# receiver's own definer answers first. Issue #1752 — a class-object receiver (`kind` `:singleton`) walks its
|
|
901
|
+
# singleton levels: `Integer.sqrt(1, 2)` under `refine Object do def sqrt(a, b)` runs `Integer.sqrt` and raises.
|
|
907
902
|
def refined_redefinition_in_effect?(class_name, call_node, scope, kind, lexical_sites)
|
|
908
903
|
return false if lexical_sites.nil?
|
|
909
904
|
|
|
@@ -911,9 +906,8 @@ module Rigor
|
|
|
911
906
|
return false if list.empty?
|
|
912
907
|
return true if list.include?(Inference::InEffectRefinements::UNKNOWN)
|
|
913
908
|
return true if opaque_refinement_in_effect?(call_node, scope, lexical_sites)
|
|
914
|
-
return refined_method_in_effect?(class_name, call_node, scope, kind, lexical_sites) unless kind == :instance
|
|
915
909
|
|
|
916
|
-
!Inference::RefinedDispatch::OwnMethod.answers?(scope, class_name, call_node.name, list)
|
|
910
|
+
!Inference::RefinedDispatch::OwnMethod.answers?(scope, class_name, call_node.name, list, kind: kind)
|
|
917
911
|
end
|
|
918
912
|
|
|
919
913
|
# The in-effect list at the call as the typer reads it: the lexical activations, each `using` expanded,
|
|
@@ -2093,7 +2087,13 @@ module Rigor
|
|
|
2093
2087
|
# arm restores the polarity without touching the shared helper:
|
|
2094
2088
|
# `String | nil` keeps firing, and so does `Dynamic | String | nil` —
|
|
2095
2089
|
# the nameless arm stays permissive inside the all-arms check, which is
|
|
2096
|
-
# only about the arms' method surface.
|
|
2090
|
+
# only about the arms' method surface. That union no longer arises from
|
|
2091
|
+
# a merge re-joining the narrowed edges of one untyped local, which
|
|
2092
|
+
# binds it back to `Dynamic[top]` (issue #1840,
|
|
2093
|
+
# `Inference::UntypedRejoin`); it still does when the program writes
|
|
2094
|
+
# the `nil` (`x = nil if c`) and no `nil?` edge strips it before the
|
|
2095
|
+
# re-joining merge, or when the arms are not a narrowing of one
|
|
2096
|
+
# untyped value.
|
|
2097
2097
|
def union_has_nameable_non_nil_arm?(union, scope)
|
|
2098
2098
|
union.members.any? do |m|
|
|
2099
2099
|
next false if nil_member?(m)
|
|
@@ -2195,7 +2195,8 @@ module Rigor
|
|
|
2195
2195
|
# ADR-121 WD7 — a refinement in effect (or one Rigor cannot read) may define the name on one arm's class.
|
|
2196
2196
|
def refined_on_some_arm?(members, call_node, scope, lexical_sites)
|
|
2197
2197
|
members.any? do |member|
|
|
2198
|
-
|
|
2198
|
+
kind = member.is_a?(Type::Singleton) ? :singleton : :instance
|
|
2199
|
+
refined_method_in_effect?(concrete_class_name(member), call_node, scope, kind, lexical_sites)
|
|
2199
2200
|
end
|
|
2200
2201
|
end
|
|
2201
2202
|
|
|
@@ -80,7 +80,7 @@ module Rigor
|
|
|
80
80
|
private
|
|
81
81
|
|
|
82
82
|
def effects_descriptor(paths)
|
|
83
|
-
files = PathExpansion.ruby_files(paths, @configuration.exclude_patterns)
|
|
83
|
+
files = PathExpansion.ruby_files(paths, @configuration.exclude_patterns, base: @configuration.exclude_base)
|
|
84
84
|
base = RunCacheKey.descriptor(
|
|
85
85
|
configuration: @configuration, files: files, explain: false,
|
|
86
86
|
rbs_config_entries: RunCacheKey.libraries_config_entries(@configuration)
|
|
@@ -42,8 +42,8 @@ module Rigor
|
|
|
42
42
|
# plugin's identity and the engine's (key slots), so a project whose plugins claim template globs is served
|
|
43
43
|
# too. The ADR-87 probe misses on one: the plain slot's key carries the `template-units` slot.
|
|
44
44
|
#
|
|
45
|
-
# `explain:` is always false: the session
|
|
46
|
-
# `--explain`.
|
|
45
|
+
# `explain:` is always false: the session writes no slot for an `--explain` run (#1533), and the CLI declines
|
|
46
|
+
# the probe under `--explain`.
|
|
47
47
|
module IncrementalRunSlot
|
|
48
48
|
module_function
|
|
49
49
|
|
|
@@ -115,7 +115,7 @@ module Rigor
|
|
|
115
115
|
def serve(configuration:, cache_root:, paths:)
|
|
116
116
|
return nil if configuration.effects_enabled?
|
|
117
117
|
|
|
118
|
-
files = PathExpansion.ruby_files(paths, configuration.exclude_patterns)
|
|
118
|
+
files = PathExpansion.ruby_files(paths, configuration.exclude_patterns, base: configuration.exclude_base)
|
|
119
119
|
slot_key = key(configuration: configuration, target: Target.new(files: files, roots: paths))
|
|
120
120
|
return nil if slot_key.nil?
|
|
121
121
|
|