rigortype 0.4.5 → 0.4.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 +1 -1
- data/docs/handbook/11-sig-gen.md +11 -2
- data/docs/manual/02-cli-reference.md +46 -7
- data/docs/manual/03-configuration.md +2 -1
- data/docs/manual/04-diagnostics.md +1 -1
- data/docs/manual/10-mcp-server.md +9 -1
- data/docs/manual/12-caching.md +8 -1
- data/docs/manual/plugins/rigor-actionmailer.md +30 -0
- data/docs/manual/plugins/rigor-actionpack.md +30 -0
- data/docs/manual/plugins/rigor-devise.md +15 -0
- data/docs/manual/plugins/rigor-rspec.md +2 -1
- data/lib/rigor/analysis/buffer_binding.rb +14 -0
- data/lib/rigor/analysis/check_rules/inferred_param_guard.rb +30 -9
- data/lib/rigor/analysis/check_rules/ivar_write_collector.rb +142 -15
- data/lib/rigor/analysis/check_rules/lexical_method_sites.rb +4 -0
- data/lib/rigor/analysis/check_rules/rbs_receiver_surface.rb +66 -3
- data/lib/rigor/analysis/check_rules/rebinding_block_callees/dispatch.rb +112 -0
- data/lib/rigor/analysis/check_rules/rebinding_block_callees.rb +394 -0
- data/lib/rigor/analysis/check_rules/ruby_deprecations.rb +7 -0
- data/lib/rigor/analysis/check_rules/unreachable_clause_collector.rb +51 -0
- data/lib/rigor/analysis/check_rules.rb +93 -8
- data/lib/rigor/analysis/dependency_recorder.rb +11 -0
- data/lib/rigor/analysis/edit_replay.rb +67 -0
- data/lib/rigor/analysis/incremental.rb +37 -0
- data/lib/rigor/analysis/incremental_session.rb +175 -27
- data/lib/rigor/analysis/path_expansion.rb +41 -0
- data/lib/rigor/analysis/require_relative_closure.rb +100 -0
- data/lib/rigor/analysis/rule_catalog.rb +5 -1
- data/lib/rigor/analysis/run_disclosure_credit.rb +80 -0
- data/lib/rigor/analysis/run_disclosure_ledger.rb +155 -0
- data/lib/rigor/analysis/runner/buffer_pool_dispatcher.rb +6 -2
- data/lib/rigor/analysis/runner/diagnostic_aggregator.rb +11 -3
- data/lib/rigor/analysis/runner/pool_coordinator.rb +17 -4
- data/lib/rigor/analysis/runner/project_pre_passes.rb +45 -3
- data/lib/rigor/analysis/runner/scan_environment.rb +51 -0
- data/lib/rigor/analysis/runner.rb +156 -49
- data/lib/rigor/analysis/worker_session.rb +14 -2
- data/lib/rigor/cache/incremental_snapshot.rb +30 -7
- data/lib/rigor/cli/check_command.rb +93 -4
- data/lib/rigor/cli/diff_command.rb +2 -1
- data/lib/rigor/cli/lens_command.rb +5 -5
- data/lib/rigor/cli/mcp_command.rb +2 -1
- data/lib/rigor/cli/options.rb +20 -3
- data/lib/rigor/cli/plugins_command.rb +5 -0
- data/lib/rigor/cli/plugins_renderer.rb +24 -2
- data/lib/rigor/cli/show_bleedingedge_command.rb +2 -1
- data/lib/rigor/cli/sig_gen_command.rb +46 -7
- data/lib/rigor/cli/type_of_command.rb +2 -1
- data/lib/rigor/cli/type_of_project_scope.rb +27 -0
- data/lib/rigor/config_audit.rb +16 -5
- data/lib/rigor/configuration.rb +14 -4
- data/lib/rigor/environment/rbs_loader.rb +115 -6
- data/lib/rigor/environment/required_features.rb +136 -29
- data/lib/rigor/environment/ruby_library_roots.rb +122 -0
- data/lib/rigor/environment.rb +4 -2
- data/lib/rigor/inference/callee_mutation_chain.rb +115 -0
- data/lib/rigor/inference/deep_erasure.rb +138 -0
- data/lib/rigor/inference/def_return_typer.rb +159 -9
- data/lib/rigor/inference/definer_resolution.rb +17 -3
- data/lib/rigor/inference/error_info.rb +17 -3
- data/lib/rigor/inference/expression_typer.rb +62 -13
- data/lib/rigor/inference/global_write_census.rb +36 -1
- data/lib/rigor/inference/ivar_writer_census.rb +195 -0
- data/lib/rigor/inference/loop_value_record.rb +53 -0
- data/lib/rigor/inference/match_rebinding/frame.rb +19 -0
- data/lib/rigor/inference/method_dispatcher/constant_folding.rb +110 -9
- data/lib/rigor/inference/method_dispatcher/kernel_dispatch.rb +6 -2
- data/lib/rigor/inference/method_dispatcher/set_folding.rb +53 -20
- data/lib/rigor/inference/method_dispatcher.rb +46 -16
- data/lib/rigor/inference/module_function_state/spellings.rb +64 -0
- data/lib/rigor/inference/module_function_state.rb +12 -0
- data/lib/rigor/inference/mutation_widening.rb +5 -2
- data/lib/rigor/inference/narrowing.rb +66 -5
- data/lib/rigor/inference/object_mixins.rb +13 -1
- data/lib/rigor/inference/project_seed_tables.rb +2 -1
- data/lib/rigor/inference/project_yielder.rb +128 -0
- data/lib/rigor/inference/scope_indexer/certainty.rb +9 -0
- data/lib/rigor/inference/scope_indexer/declared_ivar_types.rb +64 -0
- data/lib/rigor/inference/scope_indexer/declared_param_writes.rb +162 -0
- data/lib/rigor/inference/scope_indexer/unseen_ivar_writers.rb +133 -0
- data/lib/rigor/inference/scope_indexer.rb +480 -103
- data/lib/rigor/inference/self_call_ivar_writes.rb +18 -6
- data/lib/rigor/inference/statement_evaluator.rb +658 -90
- data/lib/rigor/inference/synthetic_method_index.rb +23 -0
- data/lib/rigor/inference/synthetic_method_scanner/project_members.rb +130 -0
- data/lib/rigor/inference/synthetic_method_scanner.rb +72 -12
- data/lib/rigor/language_server/buffer_table.rb +6 -0
- data/lib/rigor/language_server/diagnostic_publisher.rb +20 -6
- data/lib/rigor/language_server/project_context.rb +59 -3
- data/lib/rigor/language_server/server.rb +5 -3
- data/lib/rigor/mcp/server.rb +9 -1
- data/lib/rigor/plugin/base.rb +26 -11
- data/lib/rigor/plugin/manifest.rb +29 -3
- data/lib/rigor/plugin/registry.rb +38 -0
- data/lib/rigor/protection/discovery_seed.rb +57 -2
- data/lib/rigor/rooted_glob.rb +10 -1
- data/lib/rigor/scope.rb +14 -5
- data/lib/rigor/sig_gen/generator.rb +308 -37
- data/lib/rigor/sig_gen/observation_collector.rb +4 -1
- data/lib/rigor/sig_gen/rbs_validity.rb +1 -1
- data/lib/rigor/sig_gen/renderer.rb +6 -3
- data/lib/rigor/sig_gen/type_elaborator.rb +21 -5
- data/lib/rigor/type/combinator.rb +4 -3
- data/lib/rigor/type/constant.rb +45 -3
- data/lib/rigor/type/hash_shape.rb +14 -9
- data/lib/rigor/type/struct_instance.rb +3 -2
- data/lib/rigor/version.rb +1 -1
- data/plugins/rigor-actionmailer/lib/rigor/plugin/actionmailer/mailer_discoverer.rb +60 -43
- data/plugins/rigor-actionmailer/lib/rigor/plugin/actionmailer.rb +20 -1
- data/plugins/rigor-actionpack/lib/rigor/plugin/actionpack.rb +13 -0
- data/plugins/rigor-activejob/lib/rigor/plugin/activejob.rb +4 -2
- data/plugins/rigor-devise/lib/rigor/plugin/devise.rb +5 -3
- data/plugins/rigor-dry-types/lib/rigor/plugin/dry_types.rb +6 -5
- data/plugins/rigor-rspec/lib/rigor/plugin/rspec/matcher_analyzer.rb +102 -11
- data/plugins/rigor-sidekiq/lib/rigor/plugin/sidekiq.rb +4 -2
- data/plugins/rigor-typelizer/lib/rigor/plugin/typelizer.rb +1 -1
- data/sig/rigor/analysis/baseline.rbs +3 -2
- data/sig/rigor/effects/file_collection.rbs +5 -6
- data/sig/rigor/effects/registry.rbs +8 -6
- data/sig/rigor/effects/summary.rbs +6 -5
- data/sig/rigor/environment.rbs +5 -3
- data/sig/rigor/inference/optimistic_origin.rbs +11 -0
- data/sig/rigor/inference.rbs +6 -0
- data/sig/rigor/plugin/additional_initializer.rbs +5 -5
- data/sig/rigor/plugin/effect_ancestry.rbs +6 -4
- data/sig/rigor/plugin/effect_attribution.rbs +25 -10
- data/sig/rigor/plugin/effect_edge.rbs +12 -5
- data/sig/rigor/plugin/effect_entry_points.rbs +8 -3
- data/sig/rigor/plugin/io_boundary.rbs +4 -4
- data/sig/rigor/plugin/manifest.rbs +16 -18
- data/sig/rigor/plugin/protocol_contract.rbs +10 -9
- data/sig/rigor/plugin/registry.rbs +1 -1
- data/sig/rigor/scope.rbs +31 -50
- data/sig/rigor/type.rbs +0 -6
- metadata +20 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 608dc6b03dfc9267330c24c6493f3f4ee2d025ac5ce01a044202828fc282e258
|
|
4
|
+
data.tar.gz: 395692680ea16ec6dc42d4187fc57c10ce00731eea4cd1d03176fcc2daa07c14
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: c7145aafeaf9fd22fe3759970c9b4eaafd8720796048641a387ec7a02d4e579689fde37540db686da9904b7d8419a6338b9db7caa38997d7184960c346253f5a
|
|
7
|
+
data.tar.gz: bff814da943de361c7c1e5bee99fd0e4847d16bb8a9bd3e36609e69246e8eb0e141c6c6e87b425720d709e2277195d9a14100a3018fef75670361fe2acd2160b
|
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.6`** (2026-10-23) — 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
|
@@ -538,8 +538,17 @@ overrides the method with the other one is not reported as
|
|
|
538
538
|
widening the return. A body that returns only `nil` keeps
|
|
539
539
|
the `untyped`: it is the shape of a template method a
|
|
540
540
|
subclass fills in, and `-> nil` would make every override
|
|
541
|
-
that returns a value a widening.
|
|
542
|
-
|
|
541
|
+
that returns a value a widening. A literal inside a tuple, a
|
|
542
|
+
record or a type argument is widened the same way while the
|
|
543
|
+
shape stays (`-> [1, "a"]` becomes `-> [Integer, String]`,
|
|
544
|
+
`-> { a: 1 }` becomes `-> { a: Integer }`); of the generic
|
|
545
|
+
types, only the arguments of `Array`, `Hash`, `Set`, `Range`
|
|
546
|
+
and `Enumerator` are widened. `nil` inside a union or a shape
|
|
547
|
+
and a computed `Float` are proposed as they are. When the
|
|
548
|
+
method also carries an inline `# @rbs` annotation for its
|
|
549
|
+
parameters only, a return holding a literal that would be
|
|
550
|
+
widened, nested or not, is not proposed at all and the
|
|
551
|
+
`untyped` stays. An `untyped` that
|
|
543
552
|
only the inline reader synthesizes for a `def` you did not
|
|
544
553
|
annotate keeps the literal, as does a method no `.rbs`
|
|
545
554
|
declares — the literal is still the strictest thing the body
|
|
@@ -28,15 +28,25 @@ rigor check [paths...]
|
|
|
28
28
|
`paths` are files or directories; when omitted, Rigor checks
|
|
29
29
|
the `paths:` list from the configuration file.
|
|
30
30
|
|
|
31
|
+
Only the files you name are reported on, but Rigor reads the
|
|
32
|
+
declarations in the configured `paths:` too, so a file resolves a
|
|
33
|
+
class or constant that another file declares. A file outside
|
|
34
|
+
`paths:` also reads the files it loads with a literal
|
|
35
|
+
`require_relative "..."`, and the files those load in turn. Their
|
|
36
|
+
classes, modules and constants resolve and their methods count as
|
|
37
|
+
defined, but Rigor does not infer what those methods return or
|
|
38
|
+
check their arity, since another file may redefine them.
|
|
39
|
+
|
|
31
40
|
| Option | Effect |
|
|
32
41
|
| --- | --- |
|
|
33
|
-
| `--config=PATH` | Use a specific config file instead of auto-discovery. |
|
|
42
|
+
| `--config=PATH` | Use a specific config file instead of auto-discovery. A path that does not exist is an error (exit 64), not a run with the defaults. |
|
|
34
43
|
| `--format=FORMAT` | Output format. Default `text`. Also `json` (structured stream), plus the CI-native renderings `sarif`, `github`, `gitlab`, `checkstyle`, `junit`, and `teamcity` — see [Running Rigor in CI](11-ci.md). |
|
|
35
44
|
| `--no-ci-detect` | Disable CI auto-detection — by default `text` output also emits the running CI's native annotations / hint (`RIGOR_CI_DETECT=0` does the same). See [Running Rigor in CI § auto-detection](11-ci.md). |
|
|
36
45
|
| `--explain` | Surface fail-soft fallback events as `info` diagnostics. |
|
|
37
46
|
| `--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
47
|
| `--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 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).
|
|
48
|
+
| `--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). It re-analyses half of an unchanged tree and serves the other half from cache, so it checks the cache-merge path but not that an edit re-checks every file it affects; `--verify-incremental-edits` adds that. |
|
|
49
|
+
| `--verify-incremental-edits=N` | With `--verify-incremental` (and implying it): after the check above, replay two edits on each of `N` files spread over the analysed list, through an editor buffer, never on disk: the file emptied, then the file reduced to its `class` / `module` declarations with every body dropped (skipped when that is the file itself, or holds no declaration). After each edit, the warm recheck of the baseline must equal a full analysis of the edited tree, or the run fails naming the edit and the file. Each replay costs a recheck and a full analysis. A replay the recheck declines (it would have been a full analysis) is counted in the OK line, and the run fails if every replay declined. Only these two removals are replayed, never an addition. Default 0. |
|
|
40
50
|
| `--clear-cache` | Delete the cache directory before running. |
|
|
41
51
|
| `--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
52
|
| `--[no-]stats` | Print a run summary (files, classes, memory, wall time) to stderr. Default on. |
|
|
@@ -60,7 +70,9 @@ that want the stricter reading.
|
|
|
60
70
|
### Editor mode scope
|
|
61
71
|
|
|
62
72
|
`--tmp-file` / `--instead-of` on their own analyse **only** the
|
|
63
|
-
buffer's file
|
|
73
|
+
buffer's file. It still reads the rest of the project's declarations
|
|
74
|
+
(the classes and methods other files define), as `rigor check FILE`
|
|
75
|
+
does, but it reports nothing about what the unsaved edit does to the
|
|
64
76
|
rest of the project.
|
|
65
77
|
|
|
66
78
|
Adding `--incremental` analyses the **whole project with the buffer
|
|
@@ -138,6 +150,19 @@ truncation explicit. `--trace` records fail-soft fallbacks,
|
|
|
138
150
|
after the rows of a line table in text output. The editor-mode
|
|
139
151
|
`--tmp-file` / `--instead-of` pair is accepted as on `check`.
|
|
140
152
|
|
|
153
|
+
A class or constant that another project file declares reads as
|
|
154
|
+
`rigor check` types it (`singleton(User)`) when a bare
|
|
155
|
+
`rigor check --incremental` has left a snapshot for the project:
|
|
156
|
+
`type-of` folds the cross-file declarations from that snapshot's
|
|
157
|
+
per-file bundles, re-reading any file whose bytes changed since,
|
|
158
|
+
and never writes it. With no reusable snapshot (none yet, or one a
|
|
159
|
+
change made unusable, for example a changed configuration, lockfile, `sig/`, `pre_eval:`
|
|
160
|
+
file or required feature, an engine upgrade, or a snapshot written
|
|
161
|
+
for other roots) the probe types the one file on its own, so such a name reads
|
|
162
|
+
`Dynamic[top]`, and it prints nothing about it. Only those
|
|
163
|
+
declarations are shared: the types `check` infers for an unannotated
|
|
164
|
+
parameter are not.
|
|
165
|
+
|
|
141
166
|
A template a plugin compiles into Ruby (an ERB view under
|
|
142
167
|
`rigor-actionpack`) is probed the way `rigor check` analyses
|
|
143
168
|
it: the compiled Ruby is typed under the view's declared
|
|
@@ -162,7 +187,9 @@ and is probed as plain Ruby, parse error included.
|
|
|
162
187
|
|
|
163
188
|
The four probe commands — `type-of`, `type-scan`, `trace` and
|
|
164
189
|
`annotate` — build their environment fresh on every invocation
|
|
165
|
-
and never read or write the persistent cache
|
|
190
|
+
and never read or write the persistent cache (apart from the
|
|
191
|
+
read-only snapshot lookup `type-of` makes for the project's
|
|
192
|
+
declarations, above). That is why none
|
|
166
193
|
of them takes `--no-cache`: the flag would have nothing to skip.
|
|
167
194
|
A probe therefore types against the environment `rigor check
|
|
168
195
|
--no-cache` analyses with. The environment a cached (default)
|
|
@@ -532,6 +559,12 @@ rigor sig-gen [paths]
|
|
|
532
559
|
| `--no-cache` | Do not read or write the analysis cache. Only effect collection uses it. |
|
|
533
560
|
| `--format=text\|json` | Output format. |
|
|
534
561
|
|
|
562
|
+
A `paths` argument that does not exist, or is neither a directory nor a `.rb` file, is reported the way
|
|
563
|
+
`rigor check` reports it: on stderr as `path:1:1: error: …`, or under `diagnostics` in `--print` and `--diff`
|
|
564
|
+
JSON output. When no argument is readable this exits `1`, under every mode, `--write` and `--check` included.
|
|
565
|
+
The configured default `paths:` count too, so `paths: [lib]` with no `lib/` directory exits `1`. A `--config`
|
|
566
|
+
that names no file, is a directory, or is empty is a usage error and exits `64`.
|
|
567
|
+
|
|
535
568
|
`--print`, `--diff`, `--write` and `--check` are mutually exclusive.
|
|
536
569
|
`--check` fails exactly when `--write` would create, change or
|
|
537
570
|
refuse a file, so a tighter return `--write` declines without
|
|
@@ -640,7 +673,7 @@ rigor triage [paths]
|
|
|
640
673
|
|
|
641
674
|
`--top=N` sets the hotspot count (default 10); `--hints-only`,
|
|
642
675
|
`--selectors-only`, and `--no-hints` select which sections print.
|
|
643
|
-
`triage` is advisory and
|
|
676
|
+
`triage` is advisory and exits `0` once it runs — it never gates a build; a usage error exits `64`.
|
|
644
677
|
|
|
645
678
|
By default the distribution, selectors, and hotspots sections count
|
|
646
679
|
only the **actionable** diagnostics (`error` + `warning`). `info`
|
|
@@ -946,6 +979,11 @@ loaded from (a `path:` line in text, a `"path"` key in JSON), so
|
|
|
946
979
|
if a stale installed gem is shadowing a newer checkout's bundled
|
|
947
980
|
plugin copy the mismatch is visible at a glance.
|
|
948
981
|
|
|
982
|
+
A plugin that declares `per_invocation_classes:` or
|
|
983
|
+
`per_invocation_non_action_macros:` ([ADR-125](../adr/125-per-invocation-classes.md))
|
|
984
|
+
also gets a line for each in text, and a key in JSON (an empty
|
|
985
|
+
array when it declares none).
|
|
986
|
+
|
|
949
987
|
```sh
|
|
950
988
|
rigor plugins [--format=text|json] [--strict] [--capabilities] [--config=PATH]
|
|
951
989
|
```
|
|
@@ -1268,8 +1306,9 @@ diagnostics about Rigor's own inference cutoffs and memory — see
|
|
|
1268
1306
|
| `64` | Usage error — unknown command, bad flag, malformed argument, or a value in `.rigor.yml` the loader cannot proceed on. |
|
|
1269
1307
|
| `70` | Internal fatal error — `SystemStackError` or `NoMemoryError` inside the analysis; Rigor could not finish the run, so its report on disk is empty rather than clean. |
|
|
1270
1308
|
|
|
1271
|
-
`rigor triage` is the exception: it is advisory and
|
|
1272
|
-
|
|
1309
|
+
`rigor triage` is the exception: it is advisory and exits `0`
|
|
1310
|
+
whenever it runs to completion. A usage error, such as a
|
|
1311
|
+
`--config` that names no file, still exits `64`.
|
|
1273
1312
|
|
|
1274
1313
|
A configuration mistake prints one `rigor:` line naming the key
|
|
1275
1314
|
and nothing else — no backtrace, on every command:
|
|
@@ -67,7 +67,7 @@ cache:
|
|
|
67
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. |
|
|
68
68
|
| `paths` | Array | `["lib"]` | Directories or files to analyse. |
|
|
69
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. |
|
|
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. |
|
|
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 in the config file's directory, or in the working directory when the config file's directory has neither or no config file is used; `[]` 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. |
|
|
71
71
|
| `includes` | Array | `[]` | Other config files to layer underneath this one. |
|
|
72
72
|
| `fold_platform_specific_paths` | Boolean | `false` | Resolve Ruby-version-conditional load paths when discovering sources. |
|
|
73
73
|
| `parameter_inference` | Boolean | `false` | Opt-in call-site parameter type inference on the `check` walk ([ADR-67](../adr/67-parameter-type-inference.md) WD6). When `true`, an undeclared `def` / `initialize` / setter parameter is typed to the union of its resolved call-site argument types, sharpening downstream ivar reads, folds, and protection coverage. Precision-additive only — the negative rules never fire against an inferred parameter. Cannot be combined with `--incremental`. |
|
|
@@ -106,6 +106,7 @@ rigor: disable: "call.undefined-methdo" is not a recognized rule id; the suppres
|
|
|
106
106
|
rigor: disable: "plugin_loader.load-error" names a run-level row, which `disable:` does not reach; use `severity_overrides:` or a baseline
|
|
107
107
|
rigor: disable: "plugin.shoulda-matchers.unknown-column" names no plugin `plugins:` configures; the suppression has no effect
|
|
108
108
|
rigor: severity_overrides: "flow.bogus" is not a recognized rule id; the override has no effect
|
|
109
|
+
rigor: severity_overrides: "undefined-method" is a legacy alias of `call.undefined-method`; the override has no effect, so write `call.undefined-method`
|
|
109
110
|
rigor: bundler.lockfile: "./missing/Gemfile.lock" does not exist
|
|
110
111
|
```
|
|
111
112
|
|
|
@@ -67,7 +67,7 @@ carries no `documentation_url`.
|
|
|
67
67
|
| <a id="rule-flow-return-in-ensure"></a>`flow.return-in-ensure` | An explicit `return` inside an `ensure` clause — it overrides the method's in-flight return value and silently swallows any in-flight exception. A `return` in a nested `def`, lambda, or `define_method` block inside the `ensure` does not fire (it exits that inner frame). | high |
|
|
68
68
|
| <a id="rule-flow-shadowed-rescue-clause"></a>`flow.shadowed-rescue-clause` | A `rescue` clause can never run because an earlier clause of the same chain already catches a superclass (or the same class) of every exception class it names. | high |
|
|
69
69
|
| <a id="rule-def-return-type-mismatch"></a>`def.return-type-mismatch` | The method body's result violates its declared RBS return type. | medium |
|
|
70
|
-
| <a id="rule-def-ivar-write-mismatch"></a>`def.ivar-write-mismatch` | An instance variable is written with a type disagreeing with its first write, unless its RBS declaration (`@x: T` or an `attr_*` member) admits both writes. | high |
|
|
70
|
+
| <a id="rule-def-ivar-write-mismatch"></a>`def.ivar-write-mismatch` | An instance variable is written with a type disagreeing with its first write, unless its RBS declaration (`@x: T` or an `attr_*` member) admits both writes. With `rigor-actionpack` or `rigor-actionmailer` loaded, writes in two different actions of a controller or mailer are not compared, since Rails runs one action per instance. A write in a private or protected helper, `initialize` or the same action still is, and so is one in a callback target or `helper_method` that a macro in the same file names. A public method named only by a macro in another file or a concern, or reached through a lambda callback, counts as an action, and so does an action another action calls. | high |
|
|
71
71
|
| <a id="rule-def-method-visibility-mismatch"></a>`def.method-visibility-mismatch` | An explicit-receiver call reaches a private method. | high |
|
|
72
72
|
| <a id="rule-def-override-visibility-reduced"></a>`def.override-visibility-reduced` | An override reduces the visibility it inherits from a project-defined ancestor. | high |
|
|
73
73
|
| <a id="rule-def-override-return-widened"></a>`def.override-return-widened` | An override's declared return type widens the inherited return (covariance). | high |
|
|
@@ -66,7 +66,9 @@ rigor mcp [--transport=stdio] [--config=PATH]
|
|
|
66
66
|
transport is queued for v2.
|
|
67
67
|
- `--config=PATH` — session-level default config path. Individual tool
|
|
68
68
|
calls may supply their own `config` argument, which takes precedence.
|
|
69
|
-
|
|
69
|
+
A path that names no file is an error, not a silent switch to the
|
|
70
|
+
defaults, for the session default and for a per-call `config` alike:
|
|
71
|
+
the call returns `isError: true`. Without either, `Configuration.discover` walks `.rigor.yml` /
|
|
70
72
|
`.rigor.dist.yml` from the working directory.
|
|
71
73
|
|
|
72
74
|
Exit codes: 0 (clean shutdown — stdin EOF), 64 (unknown
|
|
@@ -417,6 +419,12 @@ The key is absent under the default `untyped` policy.
|
|
|
417
419
|
Classifications: `new-file`, `new-method`, `tighter-return`,
|
|
418
420
|
`equivalent`, `skipped`.
|
|
419
421
|
|
|
422
|
+
A `paths` entry that is neither a directory nor a `.rb` file is reported
|
|
423
|
+
under a top-level `diagnostics` array, in the shape `rigor_check` uses
|
|
424
|
+
(`"message": "no such file or directory"`). It is a warning beside
|
|
425
|
+
paths that were read, and an error when none was. An empty `candidates`
|
|
426
|
+
list with no `diagnostics` means nothing was found to generate.
|
|
427
|
+
|
|
420
428
|
**Typical agent use:** call `rigor_sig_gen` to see which methods have
|
|
421
429
|
precise enough returns to be worth writing into `sig/`, then generate
|
|
422
430
|
and insert the RBS. The agent should NOT write signatures
|
data/docs/manual/12-caching.md
CHANGED
|
@@ -185,7 +185,14 @@ 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
187
|
fails if they disagree on a single diagnostic, or list the same
|
|
188
|
-
diagnostics in a different order.
|
|
188
|
+
diagnostics in a different order. That gate re-analyses an
|
|
189
|
+
unchanged tree, so it cannot see an edit that fails to re-check a
|
|
190
|
+
file it affects. `--verify-incremental-edits=N` also edits `N`
|
|
191
|
+
files one at a time, in memory and not on disk, and compares each
|
|
192
|
+
warm recheck with a full analysis of the edited tree. Each file is
|
|
193
|
+
edited twice: once emptied, and once reduced to its `class` /
|
|
194
|
+
`module` declarations with every body dropped. It replays only
|
|
195
|
+
those two removals, never an addition or any other edit.
|
|
189
196
|
|
|
190
197
|
Rigor is also precise about *what* an edit changed. Re-checking a
|
|
191
198
|
file that many others depend on — a base class, a widely-used
|
|
@@ -77,6 +77,36 @@ plugins:
|
|
|
77
77
|
`--fail-on=warning` run — regenerate with `rigor baseline
|
|
78
78
|
regenerate`.
|
|
79
79
|
|
|
80
|
+
## Instance variable writes across actions
|
|
81
|
+
|
|
82
|
+
The plugin tells the engine that a mailer runs one action per instance
|
|
83
|
+
([ADR-125](../../adr/125-per-invocation-classes.md)). Its manifest lists
|
|
84
|
+
`ActionMailer::Base` in `per_invocation_classes:`, and the callback macros
|
|
85
|
+
(`before_action` and the rest) and `helper_method` in
|
|
86
|
+
`per_invocation_non_action_macros:`, so a method those name is not an action.
|
|
87
|
+
|
|
88
|
+
So [`def.ivar-write-mismatch`](../04-diagnostics.md#rule-def-ivar-write-mismatch)
|
|
89
|
+
stays silent when `welcome` assigns `@items = [1, 2]` and `digest` assigns
|
|
90
|
+
`@items = { a: 1 }`. It still reports two divergent writes in the same action,
|
|
91
|
+
and a divergence that involves a private or protected helper, `initialize`, or a
|
|
92
|
+
callback target or `helper_method` that a macro in the same file names.
|
|
93
|
+
|
|
94
|
+
The callback macros include `before_deliver`, `after_deliver` and
|
|
95
|
+
`around_deliver`, which run on the action's own instance.
|
|
96
|
+
|
|
97
|
+
Two shapes stay silent because their macros are not listed: a public
|
|
98
|
+
method named by `rescue_from X, with: :handler` or by `layout :method_name`
|
|
99
|
+
counts as an action, as does one a lambda callback
|
|
100
|
+
(`before_action -> { load_roles }`) reaches ([#2320](https://github.com/rigortype/rigor/issues/2320) tracks the fix).
|
|
101
|
+
|
|
102
|
+
Visibility is read in source order, as Rails builds `action_methods` from the
|
|
103
|
+
public instance methods. A method that ends up protected or private is not an
|
|
104
|
+
action: it follows a bare `protected` or `private`, is named by `private :x` or
|
|
105
|
+
`protected :x`, or is defined inline (`private def x`). `public :x` or
|
|
106
|
+
`public def x` makes it one again. So it is no longer checked for a view
|
|
107
|
+
template, and calling it (`UserMailer.helper`) reports `unknown-action` at
|
|
108
|
+
error severity, as Rails raises `NoMethodError` for it.
|
|
109
|
+
|
|
80
110
|
## Plugin internals
|
|
81
111
|
|
|
82
112
|
The mailer/concern discoverer, the cached `:mailer_index` producer,
|
|
@@ -41,6 +41,36 @@ no-op rather than erroring.
|
|
|
41
41
|
| `plugin.actionpack.permit-call` | info | a `params.require(:m).permit(:key, …)` chain resolved to a known model; keys matched against its columns |
|
|
42
42
|
| `plugin.actionpack.unknown-permit-key` | error | a literal `permit(:key)` is a near-miss (edit distance ≤ 2) of a real column but not one — a likely typo (with a did-you-mean). A key nothing like any column (a legitimate virtual attribute) does not fire |
|
|
43
43
|
|
|
44
|
+
The plugin also tells the engine that a controller runs one action
|
|
45
|
+
per instance ([ADR-125](../../adr/125-per-invocation-classes.md)).
|
|
46
|
+
Its manifest lists `ActionController::Base` and
|
|
47
|
+
`ActionController::API` in `per_invocation_classes:`. It also lists
|
|
48
|
+
the callback macros (`before_action` and the rest) and
|
|
49
|
+
`helper_method` in `per_invocation_non_action_macros:`, so a method
|
|
50
|
+
those name is not an action.
|
|
51
|
+
|
|
52
|
+
So `def.ivar-write-mismatch` stays silent when `index` assigns
|
|
53
|
+
`@roles = Role.sorted.to_a` and `permissions` assigns
|
|
54
|
+
`@roles = Role.sorted`. It still reports a divergence that involves
|
|
55
|
+
a private or protected helper, `initialize`, or two writes in the
|
|
56
|
+
same action. It also still reports one that involves a callback
|
|
57
|
+
target or `helper_method`, when the macro naming it sits in the same
|
|
58
|
+
file as the method. Actions defined in a concern module and
|
|
59
|
+
`ActionController::Metal` controllers keep reporting too.
|
|
60
|
+
|
|
61
|
+
Two shapes stay silent because their macros are not listed: a public
|
|
62
|
+
method named by `rescue_from X, with: :handler` or by `layout :method_name`
|
|
63
|
+
counts as an action, as does one a lambda callback
|
|
64
|
+
(`before_action -> { load_roles }`) reaches ([#2320](https://github.com/rigortype/rigor/issues/2320) tracks the fix).
|
|
65
|
+
|
|
66
|
+
These shapes stay silent:
|
|
67
|
+
|
|
68
|
+
- a public method named only by a macro in another file or a
|
|
69
|
+
concern, such as `ApplicationController`;
|
|
70
|
+
- a public method a lambda callback reaches
|
|
71
|
+
(`before_action -> { load_roles }`);
|
|
72
|
+
- an action that calls another action.
|
|
73
|
+
|
|
44
74
|
Filter and render resolution honours nested-module controller
|
|
45
75
|
qualification (`module Admin; class WidgetsController` resolves
|
|
46
76
|
views under `admin/widgets/…`) and silences gem-shipped parent
|
|
@@ -48,6 +48,21 @@ whatever class includes the concern.
|
|
|
48
48
|
|
|
49
49
|
## Limitations
|
|
50
50
|
|
|
51
|
+
- **Needs RBS for the Devise modules.** The methods come from the
|
|
52
|
+
strategy modules' signatures (`Devise::Models::DatabaseAuthenticatable`
|
|
53
|
+
and the rest), which Rigor does not bundle and the `devise` gem does
|
|
54
|
+
not ship. Supply them through `signature_paths:` (or a
|
|
55
|
+
`rbs collection`); a strategy module with no signature contributes
|
|
56
|
+
nothing, and the model's `valid_password?` and friends stay as they
|
|
57
|
+
were. `plugins/rigor-devise/demo/sig/devise.rbs` is a minimal
|
|
58
|
+
example.
|
|
59
|
+
- **Your own definitions win.** A method the model defines itself
|
|
60
|
+
(`def`, `alias_method`, `attr_*`, `define_method`, `delegate`), or
|
|
61
|
+
that a module of your project it includes or prepends defines, is
|
|
62
|
+
left to that definition rather than typed from the Devise module.
|
|
63
|
+
This holds even for a module included before the `devise` call,
|
|
64
|
+
which Devise's module would override at runtime. Only the module's
|
|
65
|
+
own public methods are contributed.
|
|
51
66
|
- **Instance methods only.** Per-module `ClassMethods` (e.g.
|
|
52
67
|
`User.reset_password_by_token`) are not yet synthesised.
|
|
53
68
|
- **Controller helpers deferred.** `current_user` /
|
|
@@ -73,10 +73,11 @@ end
|
|
|
73
73
|
| `eq(literal)` / `eql(literal)` | meet with `Constant<literal>` (see below) |
|
|
74
74
|
| `be_a(T)` / `be_kind_of(T)` / `be_instance_of(T)` / `be_an_instance_of(T)` | meet with `T` |
|
|
75
75
|
| `be_nil` | narrow to `Constant<nil>` |
|
|
76
|
-
| `match(/regex/)` |
|
|
76
|
+
| `match(/regex/)` | keep the `String` and `Symbol` members, read an `Object`, `BasicObject` or module member as `String \| Symbol`, and remove `nil`, `true`, `false` and numeric members (unless the project defines `match`, `to_str` or `==` on them); unchanged when a member may be anything else |
|
|
77
77
|
| `not_to be_nil` / `to_not be_nil` | narrow away from `nil` |
|
|
78
78
|
| `not_to be_a(T)` / `not_to be_kind_of(T)` / `not_to be_instance_of(T)` / `not_to be_an_instance_of(T)` | narrow away from `T` |
|
|
79
79
|
| `not_to eq(literal)` / `not_to eql(literal)` (and `to_not`) | unchanged (see below) |
|
|
80
|
+
| `not_to match(/regex/)` (and `to_not`) | unchanged: a `String` that does not match passes |
|
|
80
81
|
|
|
81
82
|
Other matchers emit nothing. `be_truthy` is not narrowed, nor is
|
|
82
83
|
`expect { ... }.to raise_error(...)` (the block form), nor an
|
|
@@ -24,6 +24,20 @@ module Rigor
|
|
|
24
24
|
def display_path(path)
|
|
25
25
|
path == physical_path ? logical_path : path
|
|
26
26
|
end
|
|
27
|
+
|
|
28
|
+
# The file's identity for comparing two spellings of one path: its real path, resolved through its directory
|
|
29
|
+
# when the file is absent from disk (a new buffer's file), so a symlinked root or parent spells it as it spells
|
|
30
|
+
# an existing file. Two accepted limits: a missing name is compared as written, so a name that differs only in
|
|
31
|
+
# case is not matched; and a `..` in a missing path is resolved textually, not through the filesystem.
|
|
32
|
+
def self.real_path(path)
|
|
33
|
+
File.realpath(path)
|
|
34
|
+
rescue SystemCallError
|
|
35
|
+
absolute = File.absolute_path(path)
|
|
36
|
+
parent = File.dirname(absolute)
|
|
37
|
+
return absolute if parent == absolute
|
|
38
|
+
|
|
39
|
+
File.join(real_path(parent), File.basename(absolute))
|
|
40
|
+
end
|
|
27
41
|
end
|
|
28
42
|
end
|
|
29
43
|
end
|
|
@@ -21,6 +21,14 @@ module Rigor
|
|
|
21
21
|
# - the direct receiver / subject: `param.foo`, `case param`
|
|
22
22
|
# - an indexed element: `param[i] - x` (the `-` receiver is the `[]` call whose root is `param`)
|
|
23
23
|
# - any method chain: `param.foo.bar`, `param.foo(arg).baz`, `param.key?(:x)`
|
|
24
|
+
# - an array or hash literal holding the parameter (issue #2189): `[param].first.upcase`,
|
|
25
|
+
# `{a: param}[:a].upcase`. The engine reads such an element back precisely, so the read is the
|
|
26
|
+
# parameter's lower bound again; any other call on the literal declines too, the FP-safe direction.
|
|
27
|
+
# - an instance variable the same method wrote from such a value (issue #2189): `@x = param; @x.upcase`.
|
|
28
|
+
# This one IS a scope mark — {Inference::StatementEvaluator#eval_ivar_write} stamps `:inferred_param`
|
|
29
|
+
# under the ivar's `@`-prefixed name, which no local can spell — because the write and the read are
|
|
30
|
+
# separate statements. The mark is method-local: the class-ivar census that seeds a sibling method's
|
|
31
|
+
# ivars types the write's RHS without the inference seed, so a cross-method read is not a lower bound.
|
|
24
32
|
#
|
|
25
33
|
# Every value derived from an inferred parameter by dispatch is itself a lower bound, so firing against it
|
|
26
34
|
# is an FP by the same construction. No-op on a normal `check` run — `Scope#inferred_param?` is false
|
|
@@ -36,17 +44,30 @@ module Rigor
|
|
|
36
44
|
return false if node.nil? || depth > MAX_DEPTH
|
|
37
45
|
|
|
38
46
|
case node
|
|
39
|
-
when Prism::LocalVariableReadNode then scope.inferred_param?(node.name)
|
|
47
|
+
when Prism::LocalVariableReadNode, Prism::InstanceVariableReadNode then scope.inferred_param?(node.name)
|
|
40
48
|
when Prism::CallNode then rooted?(node.receiver, scope, depth + 1)
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
49
|
+
when Prism::ArrayNode, Prism::HashNode, Prism::KeywordHashNode, Prism::AssocNode
|
|
50
|
+
literal_rooted?(node, scope, depth + 1)
|
|
51
|
+
else combining_rooted?(node, scope, depth + 1)
|
|
52
|
+
end
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
# An array or hash literal is rooted when any element is: the engine reads the element back precisely.
|
|
56
|
+
def literal_rooted?(node, scope, depth)
|
|
57
|
+
parts = node.is_a?(Prism::AssocNode) ? [node.key, node.value] : node.elements
|
|
58
|
+
parts.any? { |part| rooted?(part, scope, depth) }
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
# Value-combining forms: the produced value is a union of the operands, so it is a lower bound when ANY
|
|
62
|
+
# operand is inferred-parameter-rooted (`cps[i] - x rescue -1`, `a && param.foo`, `param || b`). Declining
|
|
63
|
+
# on the whole expression is FP-safe by the same lower-bound argument.
|
|
64
|
+
def combining_rooted?(node, scope, depth)
|
|
65
|
+
case node
|
|
44
66
|
when Prism::RescueModifierNode
|
|
45
|
-
rooted?(node.expression, scope, depth
|
|
46
|
-
when Prism::AndNode, Prism::OrNode
|
|
47
|
-
|
|
48
|
-
when Prism::
|
|
49
|
-
when Prism::StatementsNode then rooted?(node.body.last, scope, depth + 1)
|
|
67
|
+
rooted?(node.expression, scope, depth) || rooted?(node.rescue_expression, scope, depth)
|
|
68
|
+
when Prism::AndNode, Prism::OrNode then rooted?(node.left, scope, depth) || rooted?(node.right, scope, depth)
|
|
69
|
+
when Prism::ParenthesesNode then rooted?(last_statement(node.body), scope, depth)
|
|
70
|
+
when Prism::StatementsNode then rooted?(node.body.last, scope, depth)
|
|
50
71
|
else false
|
|
51
72
|
end
|
|
52
73
|
end
|