rigortype 0.2.9 → 0.3.0
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 +2 -2
- data/docs/handbook/03-narrowing.md +2 -1
- data/docs/handbook/04-tuples-and-shapes.md +7 -3
- data/docs/handbook/06-classes.md +18 -9
- data/docs/handbook/08-understanding-errors.md +1 -0
- data/docs/handbook/11-sig-gen.md +11 -1
- data/docs/handbook/appendix-mypy.md +4 -3
- data/docs/install.md +14 -6
- data/docs/manual/01-installation.md +98 -4
- data/docs/manual/02-cli-reference.md +49 -12
- data/docs/manual/03-configuration.md +34 -2
- data/docs/manual/04-diagnostics.md +23 -0
- data/docs/manual/07-plugins.md +4 -4
- data/docs/manual/09-editor-integration.md +4 -4
- data/docs/manual/11-ci.md +51 -0
- data/docs/manual/12-caching.md +65 -0
- data/lib/rigor/analysis/check_rules/always_truthy_condition_collector.rb +9 -1
- data/lib/rigor/analysis/check_rules/dead_assignment_collector.rb +5 -3
- data/lib/rigor/analysis/check_rules/duplicate_hash_key_collector.rb +128 -0
- data/lib/rigor/analysis/check_rules/inferred_param_guard.rb +60 -0
- data/lib/rigor/analysis/check_rules/ivar_write_collector.rb +3 -2
- data/lib/rigor/analysis/check_rules/return_in_ensure_collector.rb +117 -0
- data/lib/rigor/analysis/check_rules/rule_ids.rb +125 -0
- data/lib/rigor/analysis/check_rules/rule_walk.rb +2 -1
- data/lib/rigor/analysis/check_rules/self_closedness_scanner.rb +2 -1
- data/lib/rigor/analysis/check_rules/shadowed_rescue_collector.rb +283 -0
- data/lib/rigor/analysis/check_rules/unreachable_clause_collector.rb +10 -1
- data/lib/rigor/analysis/check_rules/void_value_use_collector.rb +93 -0
- data/lib/rigor/analysis/check_rules.rb +557 -95
- data/lib/rigor/analysis/dependency_recorder.rb +93 -9
- data/lib/rigor/analysis/dependency_source_inference/walker.rb +2 -1
- data/lib/rigor/analysis/incremental_session.rb +456 -51
- data/lib/rigor/analysis/path_expansion.rb +42 -0
- data/lib/rigor/analysis/plugin_fact_fingerprint.rb +188 -0
- data/lib/rigor/analysis/rule_catalog.rb +226 -1
- data/lib/rigor/analysis/run_cache_key.rb +68 -0
- data/lib/rigor/analysis/run_cache_probe.rb +72 -0
- data/lib/rigor/analysis/runner/diagnostic_aggregator.rb +181 -28
- data/lib/rigor/analysis/runner/pool_coordinator.rb +77 -15
- data/lib/rigor/analysis/runner/project_pre_passes.rb +95 -49
- data/lib/rigor/analysis/runner/run_snapshots.rb +9 -2
- data/lib/rigor/analysis/runner.rb +343 -68
- data/lib/rigor/analysis/severity_stamp.rb +43 -0
- data/lib/rigor/analysis/worker_session.rb +30 -2
- data/lib/rigor/bleeding_edge.rb +37 -8
- data/lib/rigor/builtins/regex_refinement.rb +36 -1
- data/lib/rigor/cache/descriptor.rb +88 -28
- data/lib/rigor/cache/file_digest.rb +158 -0
- data/lib/rigor/cache/incremental_snapshot.rb +41 -5
- data/lib/rigor/cache/rbs_descriptor.rb +56 -10
- data/lib/rigor/cache/store.rb +33 -3
- data/lib/rigor/ci_detector.rb +90 -0
- data/lib/rigor/cli/annotate_command.rb +15 -9
- data/lib/rigor/cli/baseline_command.rb +4 -1
- data/lib/rigor/cli/check_command.rb +212 -27
- data/lib/rigor/cli/coverage_command.rb +6 -1
- data/lib/rigor/cli/docs_command.rb +0 -30
- data/lib/rigor/cli/doctor_command.rb +163 -23
- data/lib/rigor/cli/lsp_command.rb +5 -0
- data/lib/rigor/cli/mcp_command.rb +5 -0
- data/lib/rigor/cli/plugins_command.rb +36 -6
- data/lib/rigor/cli/plugins_renderer.rb +44 -12
- data/lib/rigor/cli/probe_environment.rb +85 -0
- data/lib/rigor/cli/sig_gen_command.rb +36 -9
- data/lib/rigor/cli/skill_command.rb +1 -32
- data/lib/rigor/cli/trace_command.rb +5 -6
- data/lib/rigor/cli/type_of_command.rb +9 -8
- data/lib/rigor/cli/type_scan_command.rb +8 -8
- data/lib/rigor/cli.rb +15 -4
- data/lib/rigor/config_audit.rb +48 -2
- data/lib/rigor/configuration/severity_profile.rb +31 -0
- data/lib/rigor/configuration.rb +156 -8
- data/lib/rigor/environment/default_libraries.rb +35 -0
- data/lib/rigor/environment/rbs_loader.rb +164 -5
- data/lib/rigor/environment.rb +19 -27
- data/lib/rigor/inference/budget_trace.rb +77 -2
- data/lib/rigor/inference/closure_escape_analyzer.rb +14 -1
- data/lib/rigor/inference/def_handle.rb +23 -0
- data/lib/rigor/inference/def_node_resolver.rb +90 -0
- data/lib/rigor/inference/def_return_typer.rb +2 -1
- data/lib/rigor/inference/expression_typer.rb +361 -113
- data/lib/rigor/inference/method_dispatcher/data_folding.rb +30 -5
- data/lib/rigor/inference/method_dispatcher/kernel_dispatch.rb +200 -0
- data/lib/rigor/inference/method_dispatcher/literal_string_folding.rb +22 -16
- data/lib/rigor/inference/method_dispatcher/member_shape_projection.rb +33 -0
- data/lib/rigor/inference/method_dispatcher/overload_selector.rb +11 -0
- data/lib/rigor/inference/method_dispatcher/rbs_dispatch.rb +42 -9
- data/lib/rigor/inference/method_dispatcher/receiver_affinity.rb +5 -0
- data/lib/rigor/inference/method_dispatcher/shape_dispatch.rb +28 -35
- data/lib/rigor/inference/method_dispatcher/struct_folding.rb +40 -8
- data/lib/rigor/inference/method_dispatcher.rb +57 -1
- data/lib/rigor/inference/mutation_widening.rb +42 -8
- data/lib/rigor/inference/narrowing.rb +127 -17
- data/lib/rigor/inference/parameter_inference_collector.rb +15 -0
- data/lib/rigor/inference/project_patched_scanner.rb +2 -1
- data/lib/rigor/inference/rbs_type_translator.rb +8 -1
- data/lib/rigor/inference/scope_indexer.rb +451 -36
- data/lib/rigor/inference/statement_evaluator.rb +64 -15
- data/lib/rigor/inference/struct_fold_safety.rb +48 -11
- data/lib/rigor/inference/synthetic_method_scanner.rb +15 -14
- data/lib/rigor/inference/void_origin.rb +25 -0
- data/lib/rigor/inference/void_tail_summary.rb +220 -0
- data/lib/rigor/language_server/completion_provider.rb +2 -1
- data/lib/rigor/language_server/document_symbol_provider.rb +2 -1
- data/lib/rigor/language_server/folding_range_provider.rb +2 -1
- data/lib/rigor/language_server/selection_range_provider.rb +2 -1
- data/lib/rigor/language_server/signature_help_provider.rb +2 -1
- data/lib/rigor/plugin/base.rb +9 -22
- data/lib/rigor/plugin/inflector.rb +12 -3
- data/lib/rigor/plugin/io_boundary.rb +5 -1
- data/lib/rigor/plugin/isolation.rb +81 -11
- data/lib/rigor/plugin/load_error.rb +10 -1
- data/lib/rigor/plugin/loader.rb +96 -14
- data/lib/rigor/plugin/node_rule_walk.rb +5 -3
- data/lib/rigor/plugin/registry.rb +32 -23
- data/lib/rigor/plugin.rb +26 -0
- data/lib/rigor/protection/mutator.rb +3 -2
- data/lib/rigor/reflection.rb +64 -0
- data/lib/rigor/runtime/jit.rb +128 -0
- data/lib/rigor/scope/discovery_index.rb +12 -2
- data/lib/rigor/scope.rb +122 -28
- data/lib/rigor/sig_gen/classification.rb +5 -1
- data/lib/rigor/sig_gen/generator.rb +38 -6
- data/lib/rigor/sig_gen/observation_collector.rb +4 -3
- data/lib/rigor/sig_gen/rbs_validity.rb +44 -0
- data/lib/rigor/sig_gen/renderer.rb +10 -0
- data/lib/rigor/sig_gen/write_result.rb +9 -4
- data/lib/rigor/sig_gen/writer.rb +24 -2
- data/lib/rigor/source/node_children.rb +116 -0
- data/lib/rigor/source/node_locator.rb +3 -1
- data/lib/rigor/source/node_walker.rb +4 -2
- data/lib/rigor/source.rb +1 -0
- data/lib/rigor/type/difference.rb +28 -24
- data/lib/rigor/type/hash_shape.rb +34 -10
- data/lib/rigor/version.rb +1 -1
- data/lib/rigortype.rb +24 -0
- data/plugins/rigor-actioncable/lib/rigor/plugin/actioncable/channel_discoverer.rb +4 -2
- data/plugins/rigor-actionmailer/lib/rigor/plugin/actionmailer/mailer_discoverer.rb +6 -4
- data/plugins/rigor-actionpack/lib/rigor/plugin/actionpack/controller_discoverer.rb +5 -3
- data/plugins/rigor-activejob/lib/rigor/plugin/activejob/job_discoverer.rb +4 -2
- data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord/analyzer.rb +3 -1
- data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord/model_discoverer.rb +4 -2
- data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord/schema_parser.rb +4 -2
- data/plugins/rigor-activestorage/lib/rigor/plugin/activestorage/analyzer.rb +3 -1
- data/plugins/rigor-activestorage/lib/rigor/plugin/activestorage/attachment_discoverer.rb +4 -2
- data/plugins/rigor-dry-types/lib/rigor/plugin/dry_types.rb +38 -6
- data/plugins/rigor-factorybot/lib/rigor/plugin/factorybot/factory_discoverer.rb +3 -1
- data/plugins/rigor-hanami/lib/rigor/plugin/hanami/action_checker.rb +3 -1
- data/plugins/rigor-mangrove/lib/rigor/plugin/mangrove.rb +10 -0
- data/plugins/rigor-minitest/lib/rigor/plugin/minitest.rb +11 -0
- data/plugins/rigor-pundit/lib/rigor/plugin/pundit/policy_discoverer.rb +3 -1
- data/plugins/rigor-rails/lib/rigor-rails.rb +11 -12
- data/plugins/rigor-rails-routes/lib/rigor/plugin/rails_routes/grape_api_discoverer.rb +3 -1
- data/plugins/rigor-rails-routes/lib/rigor/plugin/rails_routes/helper_discoverer.rb +3 -1
- data/plugins/rigor-rails-routes/lib/rigor/plugin/rails_routes/routes_parser.rb +7 -5
- data/plugins/rigor-rbs-inline/lib/rigor/plugin/rbs_inline.rb +83 -9
- data/plugins/rigor-rbs-inline/lib/rigor-rbs-inline.rb +9 -5
- data/plugins/rigor-rspec/lib/rigor/plugin/rspec/let_scope_index.rb +3 -1
- data/plugins/rigor-rspec/lib/rigor/plugin/rspec/scope_walker.rb +3 -1
- data/plugins/rigor-rspec/lib/rigor/plugin/rspec.rb +11 -0
- data/plugins/rigor-rspec-rails/lib/rigor/plugin/rspec_rails/http_status_codes.rb +4 -1
- data/plugins/rigor-sidekiq/lib/rigor/plugin/sidekiq/worker_discoverer.rb +4 -2
- data/plugins/rigor-sorbet/lib/rigor/plugin/sorbet/catalog_walker.rb +3 -1
- data/plugins/rigor-sorbet/lib/rigor/plugin/sorbet.rb +74 -14
- data/sig/prism_node_children.rbs +9 -0
- data/sig/rigor/inference.rbs +7 -0
- data/sig/rigor/plugin/base.rbs +2 -3
- data/sig/rigor/reflection.rbs +1 -0
- data/sig/rigor/scope.rbs +12 -1
- data/skills/rigor-ci-setup/SKILL.md +10 -0
- data/skills/rigor-editor-setup/SKILL.md +11 -0
- data/skills/rigor-next-steps/SKILL.md +23 -2
- data/skills/rigor-plugin-author/SKILL.md +3 -3
- data/skills/rigor-plugin-author/references/02-walker-and-types.md +2 -2
- data/skills/rigor-plugin-review/SKILL.md +2 -2
- data/skills/rigor-plugin-review/references/01-best-practices-checklist.md +5 -4
- data/skills/rigor-project-init/SKILL.md +29 -3
- data/skills/rigor-project-init/references/01-detect.md +1 -1
- data/skills/rigor-project-init/references/02-configure.md +39 -5
- data/skills/rigor-project-init/references/03-baseline-and-bugs.md +20 -0
- data/skills/rigor-project-init/references/05-jit-performance.md +125 -0
- metadata +42 -24
- data/lib/rigor/cli/ci_detector.rb +0 -89
data/docs/manual/11-ci.md
CHANGED
|
@@ -246,6 +246,57 @@ updates:
|
|
|
246
246
|
extra files — but Dependabot does not see a version inside a `run:`
|
|
247
247
|
step, so updates to the pin are manual.
|
|
248
248
|
|
|
249
|
+
## Persisting the analysis cache across runs
|
|
250
|
+
|
|
251
|
+
By default each CI run starts cold and re-analyses everything. The
|
|
252
|
+
templates leave it that way on purpose — cold runs are simple,
|
|
253
|
+
deterministic, and fast enough for most projects. When the Rigor
|
|
254
|
+
job's runtime starts to matter (a large codebase, many plugins),
|
|
255
|
+
persist [the analysis cache](12-caching.md) between runs.
|
|
256
|
+
|
|
257
|
+
**Restore and save the cache directory** with your CI's cache
|
|
258
|
+
facility, keyed on anything that changes the analysis wholesale
|
|
259
|
+
(the Rigor version / lockfile). That is the whole setup: Rigor
|
|
260
|
+
detects CI and switches its cache freshness check from stat
|
|
261
|
+
metadata (which a fresh checkout regenerates, and which would
|
|
262
|
+
otherwise force parts of the restored cache to recompute every
|
|
263
|
+
run) to content hashes, which are identical across checkouts.
|
|
264
|
+
Details: [How a file is checked for changes](12-caching.md#how-a-file-is-checked-for-changes).
|
|
265
|
+
|
|
266
|
+
GitHub Actions:
|
|
267
|
+
|
|
268
|
+
```yaml
|
|
269
|
+
- uses: actions/cache@v4
|
|
270
|
+
with:
|
|
271
|
+
path: .rigor/cache
|
|
272
|
+
key: rigor-cache-${{ runner.os }}-${{ hashFiles('.github/rigor/Gemfile.lock') }}-${{ github.sha }}
|
|
273
|
+
restore-keys: rigor-cache-${{ runner.os }}-${{ hashFiles('.github/rigor/Gemfile.lock') }}-
|
|
274
|
+
- run: bundle exec rigor check
|
|
275
|
+
```
|
|
276
|
+
|
|
277
|
+
GitLab CI:
|
|
278
|
+
|
|
279
|
+
```yaml
|
|
280
|
+
rigor:
|
|
281
|
+
cache:
|
|
282
|
+
key: rigor-cache
|
|
283
|
+
paths:
|
|
284
|
+
- .rigor/cache
|
|
285
|
+
```
|
|
286
|
+
|
|
287
|
+
Two refinements:
|
|
288
|
+
|
|
289
|
+
- A **self-hosted runner that reuses its workspace** keeps stable
|
|
290
|
+
stat metadata, so the faster stat check works there — opt back
|
|
291
|
+
into it with `cache.validation: stat`.
|
|
292
|
+
- On Rigor versions before the `auto` default, set
|
|
293
|
+
`RIGOR_STRICT_VALIDATION: "1"` on the job explicitly.
|
|
294
|
+
|
|
295
|
+
The cache is content-keyed and self-invalidating — a stale or
|
|
296
|
+
corrupt restore degrades to a recompute, never a wrong result. If
|
|
297
|
+
you would rather guarantee every run is independent, pass
|
|
298
|
+
`--no-cache` instead and skip this section.
|
|
299
|
+
|
|
249
300
|
## Container image
|
|
250
301
|
|
|
251
302
|
A standalone image is published to GHCR with Ruby 4.0 and Rigor
|
data/docs/manual/12-caching.md
CHANGED
|
@@ -38,6 +38,49 @@ The cache is also schema-versioned: after a Rigor upgrade that
|
|
|
38
38
|
changes the cache format, the stale cache is purged on the
|
|
39
39
|
first writable run.
|
|
40
40
|
|
|
41
|
+
## How a file is checked for changes
|
|
42
|
+
|
|
43
|
+
To decide whether a cached entry is still valid, Rigor needs to
|
|
44
|
+
know which of your files changed since the entry was written. By
|
|
45
|
+
default it checks each file's **stat metadata** first — size,
|
|
46
|
+
nanosecond modification and change timestamps, and inode — and
|
|
47
|
+
only re-hashes a file's content when that metadata moved. On a
|
|
48
|
+
large project an unchanged run then reads *zero* content bytes to
|
|
49
|
+
validate the cache, instead of re-hashing every file.
|
|
50
|
+
|
|
51
|
+
The content hash stays the sole authority on whether a file
|
|
52
|
+
actually changed: the stat check only decides whether the hash
|
|
53
|
+
needs recomputing. A file that was merely `touch`ed (new
|
|
54
|
+
timestamp, identical content) is re-hashed once and correctly
|
|
55
|
+
found unchanged. Editing a file always moves its timestamps, so
|
|
56
|
+
an edit is never missed.
|
|
57
|
+
|
|
58
|
+
The stat check is skipped where it cannot work: the default
|
|
59
|
+
setting is `validation: auto`, which behaves as `stat` on your
|
|
60
|
+
machine and switches to hashing every file (`digest`) when a CI
|
|
61
|
+
environment is detected. CI is the common case of untrustworthy
|
|
62
|
+
stat metadata — a fresh checkout regenerates every timestamp and
|
|
63
|
+
inode, so a cache restored across CI runs (for example with
|
|
64
|
+
`actions/cache`) never passes the stat check, and parts of it
|
|
65
|
+
(plugin watch-glob entries, which are stat-signature only) would
|
|
66
|
+
be recomputed on every run. Content hashes are identical across
|
|
67
|
+
checkouts, so under `digest` the restored cache simply hits.
|
|
68
|
+
|
|
69
|
+
The setting can be forced either way:
|
|
70
|
+
|
|
71
|
+
```yaml
|
|
72
|
+
cache:
|
|
73
|
+
validation: digest # hash always — for any filesystem whose
|
|
74
|
+
# timestamps or inodes cannot be trusted
|
|
75
|
+
# validation: stat # stat always — e.g. a self-hosted CI
|
|
76
|
+
# runner that reuses its workspace, where
|
|
77
|
+
# stat metadata IS stable across runs
|
|
78
|
+
```
|
|
79
|
+
|
|
80
|
+
For a single run, `RIGOR_STRICT_VALIDATION=1` forces `digest`
|
|
81
|
+
and wins over the config key; `RIGOR_CI_DETECT=0` turns off the
|
|
82
|
+
CI detection that `auto` relies on.
|
|
83
|
+
|
|
41
84
|
## Controlling the cache
|
|
42
85
|
|
|
43
86
|
| Flag | Effect |
|
|
@@ -90,6 +133,28 @@ gate, `rigor check --verify-incremental`, asserts this on every
|
|
|
90
133
|
build: it runs the incremental analyzer and a full analysis and
|
|
91
134
|
fails if they disagree on a single diagnostic.
|
|
92
135
|
|
|
136
|
+
Rigor is also precise about *what* an edit changed. Re-checking a
|
|
137
|
+
file that many others depend on — a base class, a widely-used
|
|
138
|
+
concern, a shared utility — used to re-check every dependent.
|
|
139
|
+
Now, if your edit did not change the file's declaration shape
|
|
140
|
+
(the signatures of its methods, its superclass and includes) or
|
|
141
|
+
the types its methods return, the files that only depend on those
|
|
142
|
+
things are left untouched: a comment, a formatting change, or an
|
|
143
|
+
internal refactor that preserves every method's return type stops
|
|
144
|
+
at the edited file. An edit that *does* change a return type or a
|
|
145
|
+
signature still propagates to the dependents that consume it, so
|
|
146
|
+
the result stays identical to a full run.
|
|
147
|
+
|
|
148
|
+
Types contributed by plugins (a Sorbet signature, an
|
|
149
|
+
ActiveRecord column type, a dry-types alias) are validated too:
|
|
150
|
+
if a plugin's cross-file contributions change between runs, the
|
|
151
|
+
snapshot is dropped and the next run is a full one, so a plugin
|
|
152
|
+
edit can never leave a dependent stale.
|
|
153
|
+
|
|
154
|
+
An `--incremental` re-check honours `--workers=N` (and
|
|
155
|
+
`RIGOR_RACTOR_WORKERS` / `parallel.workers:`), analysing the
|
|
156
|
+
affected files in parallel just as a full run does.
|
|
157
|
+
|
|
93
158
|
The snapshot lives under the cache directory (`.rigor/cache`)
|
|
94
159
|
and is keyed by a fingerprint of your configuration, your locked
|
|
95
160
|
gems, your project's own `sig/` RBS, and the Rigor version.
|
|
@@ -2,6 +2,9 @@
|
|
|
2
2
|
|
|
3
3
|
require "prism"
|
|
4
4
|
|
|
5
|
+
require_relative "../../source/node_children"
|
|
6
|
+
require_relative "inferred_param_guard"
|
|
7
|
+
|
|
5
8
|
module Rigor
|
|
6
9
|
module Analysis
|
|
7
10
|
module CheckRules
|
|
@@ -81,7 +84,7 @@ module Rigor
|
|
|
81
84
|
visit(node) if conditional_node?(node) && !in_loop_or_block
|
|
82
85
|
|
|
83
86
|
child_in_loop_or_block = in_loop_or_block || enters_loop_or_block?(node)
|
|
84
|
-
node.
|
|
87
|
+
node.rigor_each_child { |child| walk(child, in_loop_or_block: child_in_loop_or_block) }
|
|
85
88
|
end
|
|
86
89
|
|
|
87
90
|
def conditional_node?(node)
|
|
@@ -100,6 +103,11 @@ module Rigor
|
|
|
100
103
|
scope = @scope_index[node]
|
|
101
104
|
return if scope.nil?
|
|
102
105
|
|
|
106
|
+
# ADR-67 WD6b — a predicate rooted at an inferred parameter (`opts.key?(:x)`) folds to a constant only
|
|
107
|
+
# because the seed's lower-bound type pinned it; a wider real caller would not. Declining preserves
|
|
108
|
+
# the precision-additive contract.
|
|
109
|
+
return if InferredParamGuard.rooted?(predicate, scope)
|
|
110
|
+
|
|
103
111
|
predicate_type = scope.type_of(predicate)
|
|
104
112
|
return unless predicate_type.is_a?(Type::Constant)
|
|
105
113
|
|
|
@@ -2,6 +2,8 @@
|
|
|
2
2
|
|
|
3
3
|
require "prism"
|
|
4
4
|
|
|
5
|
+
require_relative "../../source/node_children"
|
|
6
|
+
|
|
5
7
|
module Rigor
|
|
6
8
|
module Analysis
|
|
7
9
|
module CheckRules
|
|
@@ -65,7 +67,7 @@ module Rigor
|
|
|
65
67
|
return unless node.is_a?(Prism::Node)
|
|
66
68
|
|
|
67
69
|
collect_def_assignments(node) if node.is_a?(Prism::DefNode)
|
|
68
|
-
node.
|
|
70
|
+
node.rigor_each_child { |child| walk_for_def_nodes(child) }
|
|
69
71
|
end
|
|
70
72
|
|
|
71
73
|
def collect_def_assignments(def_node)
|
|
@@ -92,7 +94,7 @@ module Rigor
|
|
|
92
94
|
# / similar shapes don't trip the rule.
|
|
93
95
|
accumulator << node.name if reading_assignment?(node)
|
|
94
96
|
|
|
95
|
-
node.
|
|
97
|
+
node.rigor_each_child { |child| gather_read_names(child, accumulator) }
|
|
96
98
|
accumulator
|
|
97
99
|
end
|
|
98
100
|
|
|
@@ -110,7 +112,7 @@ module Rigor
|
|
|
110
112
|
# outer walker visits them separately.
|
|
111
113
|
return accumulator if node.is_a?(Prism::DefNode) && !accumulator.last.equal?(node)
|
|
112
114
|
|
|
113
|
-
node.
|
|
115
|
+
node.rigor_each_child { |child| gather_write_nodes(child, accumulator) }
|
|
114
116
|
accumulator
|
|
115
117
|
end
|
|
116
118
|
|
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "prism"
|
|
4
|
+
|
|
5
|
+
require_relative "../../source/node_children"
|
|
6
|
+
|
|
7
|
+
module Rigor
|
|
8
|
+
module Analysis
|
|
9
|
+
module CheckRules
|
|
10
|
+
# Walks a parse tree and collects every Hash-literal entry whose LITERAL key repeats an earlier
|
|
11
|
+
# literal key in the same literal. Ruby's runtime keeps the LAST entry silently (the interpreter
|
|
12
|
+
# itself only warns under `-w`), so the earlier value is dead — almost always a copy-paste or merge
|
|
13
|
+
# leftover. Modeled on PHPStan's `DuplicateKeysInLiteralArraysRule`.
|
|
14
|
+
#
|
|
15
|
+
# The collector is the read-side companion to the `flow.duplicate-hash-key` rule. The envelope is
|
|
16
|
+
# purely syntactic and deliberately narrow so it cannot fire on working code:
|
|
17
|
+
#
|
|
18
|
+
# - Only keys whose VALUE is pinned at parse time participate: symbols (including the `key:`
|
|
19
|
+
# shorthand and the `:key =>` hashrocket spelling of the same symbol), plain non-interpolated
|
|
20
|
+
# strings, integers, floats, and the `true` / `false` / `nil` literals. Any other key form —
|
|
21
|
+
# interpolated strings/symbols, constants, method calls, locals — is never compared and never
|
|
22
|
+
# fires.
|
|
23
|
+
# - Key spaces are tagged by literal kind, matching `Hash`'s `eql?`-based lookup: `:a` and `"a"`
|
|
24
|
+
# are different keys, and `1` and `1.0` are different keys (`1.eql?(1.0)` is false), so Integer
|
|
25
|
+
# and Float literals are never cross-compared.
|
|
26
|
+
# - A `**splat` element is itself never a key, and it does NOT rescue a literal duplicate around
|
|
27
|
+
# it: in `{ a: 1, **h, a: 2 }` the second `a:` overwrites whatever the earlier entries (literal
|
|
28
|
+
# or splatted) contributed, so the pair still collides and still fires.
|
|
29
|
+
# - Both `Prism::HashNode` (braced literals) and `Prism::KeywordHashNode` (bare keyword arguments,
|
|
30
|
+
# `m(a: 1, a: 2)` — Prism parses these with only a `-w`-level warning, same as braced literals)
|
|
31
|
+
# are scanned. Each literal is scanned independently; nested Hash literals are their own scope.
|
|
32
|
+
class DuplicateHashKeyCollector
|
|
33
|
+
# ADR-53 Track B — the node classes the shared {RuleWalk} dispatches to this collector. Prism node
|
|
34
|
+
# classes are leaves, so class-equality dispatch matches the legacy walk's `is_a?` gate. The
|
|
35
|
+
# collector needs no context gate: it is purely syntactic and fires wherever the full DFS reaches
|
|
36
|
+
# a Hash literal, loops and blocks included.
|
|
37
|
+
NODE_CLASSES = [Prism::HashNode, Prism::KeywordHashNode].freeze
|
|
38
|
+
|
|
39
|
+
# Returns `Array<{hash_node:, key_node:, first_key_node:, key_label:}>` — one entry per LATER
|
|
40
|
+
# occurrence of a repeated literal key (`key_node` is the repeat the diagnostic points at;
|
|
41
|
+
# `first_key_node` is the earliest occurrence its message references). Empty when the tree has no
|
|
42
|
+
# qualifying duplicates.
|
|
43
|
+
def initialize(scope_index)
|
|
44
|
+
@scope_index = scope_index
|
|
45
|
+
@results = []
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
# Legacy single-collector walk — kept as the oracle the ADR-53 Track B equivalence harness
|
|
49
|
+
# compares {RuleWalk} against.
|
|
50
|
+
def collect(root)
|
|
51
|
+
walk_for_hash_nodes(root)
|
|
52
|
+
@results.freeze
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
# {RuleWalk} entry point: the legacy walk's per-literal logic, invoked at every Hash literal under
|
|
56
|
+
# the shared traversal contract. The `context` is unused — this collector processes all literals.
|
|
57
|
+
def visit(hash_node, _context = nil)
|
|
58
|
+
collect_duplicate_keys(hash_node)
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
# The accumulated result, frozen the same way `#collect` returns it — used by {RuleWalk}-driven
|
|
62
|
+
# callers after the walk completes.
|
|
63
|
+
def results
|
|
64
|
+
@results.freeze
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
private
|
|
68
|
+
|
|
69
|
+
def walk_for_hash_nodes(node)
|
|
70
|
+
return unless node.is_a?(Prism::Node)
|
|
71
|
+
|
|
72
|
+
collect_duplicate_keys(node) if node.is_a?(Prism::HashNode) || node.is_a?(Prism::KeywordHashNode)
|
|
73
|
+
node.rigor_each_child { |child| walk_for_hash_nodes(child) }
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
def collect_duplicate_keys(hash_node)
|
|
77
|
+
seen = {}
|
|
78
|
+
hash_node.elements.each do |element|
|
|
79
|
+
# `**splat` elements (AssocSplatNode) contribute no literal key; they are skipped WITHOUT
|
|
80
|
+
# resetting `seen` — a splat between two identical literal keys does not rescue the collision.
|
|
81
|
+
next unless element.is_a?(Prism::AssocNode)
|
|
82
|
+
|
|
83
|
+
key = literal_key(element.key)
|
|
84
|
+
next if key.nil?
|
|
85
|
+
|
|
86
|
+
first = seen[key]
|
|
87
|
+
if first
|
|
88
|
+
@results << {
|
|
89
|
+
hash_node: hash_node,
|
|
90
|
+
key_node: element.key,
|
|
91
|
+
first_key_node: first,
|
|
92
|
+
key_label: key_label(element.key)
|
|
93
|
+
}
|
|
94
|
+
else
|
|
95
|
+
seen[key] = element.key
|
|
96
|
+
end
|
|
97
|
+
end
|
|
98
|
+
end
|
|
99
|
+
|
|
100
|
+
# Maps a key node to a comparable `[kind, value]` tag, or nil for any non-value-pinned key form.
|
|
101
|
+
# The kind tag keeps the literal spaces separate (`:a` ≠ `"a"`, `1` ≠ `1.0`); within a kind the
|
|
102
|
+
# parsed Ruby value is compared under Hash's own `eql?`/`hash` semantics, exactly reproducing the
|
|
103
|
+
# runtime collision.
|
|
104
|
+
def literal_key(key_node)
|
|
105
|
+
case key_node
|
|
106
|
+
when Prism::SymbolNode then [:symbol, key_node.unescaped]
|
|
107
|
+
when Prism::StringNode then [:string, key_node.unescaped]
|
|
108
|
+
when Prism::IntegerNode then [:integer, key_node.value]
|
|
109
|
+
when Prism::FloatNode then [:float, key_node.value]
|
|
110
|
+
when Prism::TrueNode then [:true_literal]
|
|
111
|
+
when Prism::FalseNode then [:false_literal]
|
|
112
|
+
when Prism::NilNode then [:nil_literal]
|
|
113
|
+
end
|
|
114
|
+
end
|
|
115
|
+
|
|
116
|
+
# Renders the key for the diagnostic message in its canonical Ruby spelling, so the `a:` shorthand
|
|
117
|
+
# and `:a =>` hashrocket occurrences of one symbol read as the same key.
|
|
118
|
+
def key_label(key_node)
|
|
119
|
+
case key_node
|
|
120
|
+
when Prism::SymbolNode then ":#{key_node.unescaped}"
|
|
121
|
+
when Prism::StringNode then key_node.unescaped.inspect
|
|
122
|
+
else key_node.slice
|
|
123
|
+
end
|
|
124
|
+
end
|
|
125
|
+
end
|
|
126
|
+
end
|
|
127
|
+
end
|
|
128
|
+
end
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "prism"
|
|
4
|
+
|
|
5
|
+
module Rigor
|
|
6
|
+
module Analysis
|
|
7
|
+
module CheckRules
|
|
8
|
+
# ADR-67 WD6b — the shared "is this node rooted at an inferred parameter?" predicate.
|
|
9
|
+
#
|
|
10
|
+
# When the `parameter_inference:` gate seeds an undeclared parameter from its call-site union, that type
|
|
11
|
+
# is a *lower bound* (only the resolved call sites contribute; an unseen / dynamic caller could pass any
|
|
12
|
+
# wider value). A diagnostic decided against a lower bound is a false positive by construction — the same
|
|
13
|
+
# reasoning that keeps ADR-67 WD1 non-negotiable at the parameter boundary. So every negative in-body
|
|
14
|
+
# rule must decline when the value it reasons about is (transitively) an inferred parameter.
|
|
15
|
+
#
|
|
16
|
+
# `rooted?` walks a receiver / argument / condition-subject node down to its syntactic root and reports
|
|
17
|
+
# whether that root is a pristine inferred-parameter local. It is purely syntactic — a bounded AST walk
|
|
18
|
+
# over `.receiver` — not a value-flow provenance channel (the mark stays on the direct parameter local
|
|
19
|
+
# in the scope; this reads the chain root at the firing site). It covers:
|
|
20
|
+
#
|
|
21
|
+
# - the direct receiver / subject: `param.foo`, `case param`
|
|
22
|
+
# - an indexed element: `param[i] - x` (the `-` receiver is the `[]` call whose root is `param`)
|
|
23
|
+
# - any method chain: `param.foo.bar`, `param.foo(arg).baz`, `param.key?(:x)`
|
|
24
|
+
#
|
|
25
|
+
# Every value derived from an inferred parameter by dispatch is itself a lower bound, so firing against it
|
|
26
|
+
# is an FP by the same construction. No-op on a normal `check` run — `Scope#inferred_param?` is false
|
|
27
|
+
# unless the gate seeded the table.
|
|
28
|
+
module InferredParamGuard
|
|
29
|
+
module_function
|
|
30
|
+
|
|
31
|
+
# A defensive depth cap against a pathological receiver chain (the walk is otherwise linear in chain
|
|
32
|
+
# length, which is tiny in practice).
|
|
33
|
+
MAX_DEPTH = 64
|
|
34
|
+
|
|
35
|
+
def rooted?(node, scope, depth = 0)
|
|
36
|
+
return false if node.nil? || depth > MAX_DEPTH
|
|
37
|
+
|
|
38
|
+
case node
|
|
39
|
+
when Prism::LocalVariableReadNode then scope.inferred_param?(node.name)
|
|
40
|
+
when Prism::CallNode then rooted?(node.receiver, scope, depth + 1)
|
|
41
|
+
# Value-combining forms: the produced value is a union of the operands, so it is a lower bound when
|
|
42
|
+
# ANY operand is inferred-parameter-rooted (`cps[i] - x rescue -1`, `a && param.foo`, `param || b`).
|
|
43
|
+
# Declining on the whole expression is FP-safe by the same lower-bound argument.
|
|
44
|
+
when Prism::RescueModifierNode
|
|
45
|
+
rooted?(node.expression, scope, depth + 1) || rooted?(node.rescue_expression, scope, depth + 1)
|
|
46
|
+
when Prism::AndNode, Prism::OrNode
|
|
47
|
+
rooted?(node.left, scope, depth + 1) || rooted?(node.right, scope, depth + 1)
|
|
48
|
+
when Prism::ParenthesesNode then rooted?(last_statement(node.body), scope, depth + 1)
|
|
49
|
+
when Prism::StatementsNode then rooted?(node.body.last, scope, depth + 1)
|
|
50
|
+
else false
|
|
51
|
+
end
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
def last_statement(body)
|
|
55
|
+
body.is_a?(Prism::StatementsNode) ? body.body.last : body
|
|
56
|
+
end
|
|
57
|
+
end
|
|
58
|
+
end
|
|
59
|
+
end
|
|
60
|
+
end
|
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
require "prism"
|
|
4
4
|
|
|
5
5
|
require_relative "../../source/constant_path"
|
|
6
|
+
require_relative "../../source/node_children"
|
|
6
7
|
|
|
7
8
|
module Rigor
|
|
8
9
|
module Analysis
|
|
@@ -78,7 +79,7 @@ module Rigor
|
|
|
78
79
|
return
|
|
79
80
|
end
|
|
80
81
|
|
|
81
|
-
node.
|
|
82
|
+
node.rigor_each_child { |child| walk(child, qualified_prefix) }
|
|
82
83
|
end
|
|
83
84
|
|
|
84
85
|
def collect_def_writes(def_node, qualified_prefix)
|
|
@@ -95,7 +96,7 @@ module Rigor
|
|
|
95
96
|
record_write(node, class_name) if node.is_a?(Prism::InstanceVariableWriteNode)
|
|
96
97
|
return if BARRIER_NODES.any? { |klass| node.is_a?(klass) }
|
|
97
98
|
|
|
98
|
-
node.
|
|
99
|
+
node.rigor_each_child { |child| gather_writes(child, class_name) }
|
|
99
100
|
end
|
|
100
101
|
|
|
101
102
|
def record_write(node, class_name)
|
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "prism"
|
|
4
|
+
|
|
5
|
+
require_relative "../../source/node_children"
|
|
6
|
+
|
|
7
|
+
module Rigor
|
|
8
|
+
module Analysis
|
|
9
|
+
module CheckRules
|
|
10
|
+
# Walks a parse tree and collects every explicit `ReturnNode` that sits lexically inside the `ensure`
|
|
11
|
+
# clause of a `begin` / `def` / `class` body. In Ruby, `return` inside `ensure` silently discards
|
|
12
|
+
# both the method's in-flight return value and any in-flight exception — the classic footgun RuboCop
|
|
13
|
+
# ships as `Lint/EnsureReturn` and PHPStan as `OverwrittenExitPointByFinallyRule`.
|
|
14
|
+
#
|
|
15
|
+
# The check is purely syntactic (no type engine involved) with a frame-aware envelope so it only
|
|
16
|
+
# flags a `return` that actually belongs to the frame whose `ensure` it sits in:
|
|
17
|
+
#
|
|
18
|
+
# - A `return` inside a nested `def`, a lambda (`-> {}` / `lambda {}`), or a `define_method` block
|
|
19
|
+
# within the ensure body is skipped — that `return` exits the inner frame, not the one whose
|
|
20
|
+
# in-flight exception the ensure guards.
|
|
21
|
+
# - A `return` inside a *plain* block (`each do ... return ... end`) within the ensure body DOES
|
|
22
|
+
# belong to the enclosing method and fires.
|
|
23
|
+
# - A nested `begin ... ensure ... end` *inside* an ensure body is not descended into at its own
|
|
24
|
+
# `ensure` clause — that inner `EnsureNode` is visited separately (every `BeginNode` is), so each
|
|
25
|
+
# offending `return` is collected exactly once.
|
|
26
|
+
class ReturnInEnsureCollector
|
|
27
|
+
# ADR-53 Track B — the node classes the shared {RuleWalk} dispatches to this collector. Prism
|
|
28
|
+
# represents every construct that can carry an `ensure` clause — `begin ... end`, a `def` body with
|
|
29
|
+
# `rescue` / `ensure`, a class / module body with them — as a `BeginNode`, so one dispatch class
|
|
30
|
+
# covers all the syntactic hosts. No context gates: the footgun is frame-relative, not
|
|
31
|
+
# position-relative, so a `begin/ensure` inside a loop, block, or lambda is checked the same way.
|
|
32
|
+
NODE_CLASSES = [Prism::BeginNode].freeze
|
|
33
|
+
|
|
34
|
+
# Receiver-less calls whose attached block opens a new return frame: a `return` inside their block
|
|
35
|
+
# exits the lambda / defined method, not the method whose `ensure` we are scanning. `proc` is
|
|
36
|
+
# deliberately absent — `return` inside a `Proc` block returns from the enclosing method, so it
|
|
37
|
+
# stays in scope for the rule.
|
|
38
|
+
FRAME_BARRIER_CALL_NAMES = %i[lambda define_method].freeze
|
|
39
|
+
|
|
40
|
+
def initialize(scope_index)
|
|
41
|
+
@scope_index = scope_index
|
|
42
|
+
@results = []
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
# Legacy single-collector walk — the oracle the ADR-53 Track B equivalence harness compares
|
|
46
|
+
# {RuleWalk} against.
|
|
47
|
+
def collect(root)
|
|
48
|
+
walk_for_begin_nodes(root)
|
|
49
|
+
@results.freeze
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
# {RuleWalk} entry point: invoked at every `BeginNode` the shared traversal reaches. The `context`
|
|
53
|
+
# is unused — this collector checks every ensure clause.
|
|
54
|
+
def visit(begin_node, _context = nil)
|
|
55
|
+
collect_ensure_returns(begin_node)
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
# Returns `Array<{return_node:}>` — one entry per offending `return`, frozen the same way
|
|
59
|
+
# `#collect` returns it.
|
|
60
|
+
def results
|
|
61
|
+
@results.freeze
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
private
|
|
65
|
+
|
|
66
|
+
def walk_for_begin_nodes(node)
|
|
67
|
+
return unless node.is_a?(Prism::Node)
|
|
68
|
+
|
|
69
|
+
collect_ensure_returns(node) if node.is_a?(Prism::BeginNode)
|
|
70
|
+
node.rigor_each_child { |child| walk_for_begin_nodes(child) }
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
def collect_ensure_returns(begin_node)
|
|
74
|
+
ensure_clause = begin_node.ensure_clause
|
|
75
|
+
return if ensure_clause.nil?
|
|
76
|
+
|
|
77
|
+
gather_returns(ensure_clause.statements)
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
def gather_returns(node)
|
|
81
|
+
return unless node.is_a?(Prism::Node)
|
|
82
|
+
|
|
83
|
+
case node
|
|
84
|
+
when Prism::ReturnNode
|
|
85
|
+
@results << { return_node: node }
|
|
86
|
+
when Prism::DefNode, Prism::LambdaNode, Prism::EnsureNode
|
|
87
|
+
# Def / lambda open a new return frame — a `return` below them exits the nested frame, not the
|
|
88
|
+
# one whose ensure we are scanning. A nested `begin/ensure`'s own EnsureNode is skipped for a
|
|
89
|
+
# different reason: its BeginNode is visited separately, which collects those returns once —
|
|
90
|
+
# descending here would double-collect them.
|
|
91
|
+
return
|
|
92
|
+
when Prism::CallNode
|
|
93
|
+
return gather_returns_around_barrier_block(node) if frame_barrier_call?(node)
|
|
94
|
+
end
|
|
95
|
+
|
|
96
|
+
node.rigor_each_child { |child| gather_returns(child) }
|
|
97
|
+
end
|
|
98
|
+
|
|
99
|
+
# `lambda { ... }` / `define_method(:x) { ... }`: the receiver and arguments stay in the current
|
|
100
|
+
# frame (and may themselves contain an offending `return`), only the attached block opens a new
|
|
101
|
+
# one — descend into everything except the block.
|
|
102
|
+
def gather_returns_around_barrier_block(call_node)
|
|
103
|
+
barrier_block = call_node.block
|
|
104
|
+
call_node.rigor_each_child do |child|
|
|
105
|
+
gather_returns(child) unless child.equal?(barrier_block)
|
|
106
|
+
end
|
|
107
|
+
end
|
|
108
|
+
|
|
109
|
+
def frame_barrier_call?(call_node)
|
|
110
|
+
call_node.receiver.nil? &&
|
|
111
|
+
FRAME_BARRIER_CALL_NAMES.include?(call_node.name) &&
|
|
112
|
+
call_node.block.is_a?(Prism::BlockNode)
|
|
113
|
+
end
|
|
114
|
+
end
|
|
115
|
+
end
|
|
116
|
+
end
|
|
117
|
+
end
|
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Rigor
|
|
4
|
+
module Analysis
|
|
5
|
+
module CheckRules
|
|
6
|
+
# Canonical identifiers for each rule. Per ADR-8 § "Diagnostic ID family hierarchy", rule names are
|
|
7
|
+
# `family.rule-name` two-segment strings; the families group diagnostics by where they originate
|
|
8
|
+
# (`call.*` for call-site rules, `flow.*` for flow-analysis proofs, `assert.*` for runtime-assertion
|
|
9
|
+
# rules, `dump.*` for debug helpers, `def.*` for method-definition rules). Used by the configuration
|
|
10
|
+
# `disable:` list and the in-source `# rigor:disable <rule>` suppression comment system; new rules MUST
|
|
11
|
+
# register here so user configuration can refer to them.
|
|
12
|
+
#
|
|
13
|
+
# ADR-87 WD4 — this pure-data constant table is split out of the (engine-heavy) `check_rules.rb` so
|
|
14
|
+
# {Analysis::RuleCatalog} — which the CLI's JSON `evidence_tier` / `documentation_url` enrichment reads
|
|
15
|
+
# — can require it WITHOUT loading the inference engine. The boot-slimming hit path stays engine-free
|
|
16
|
+
# even when it formats JSON.
|
|
17
|
+
RULE_UNDEFINED_METHOD = "call.undefined-method"
|
|
18
|
+
RULE_SELF_UNDEFINED_METHOD = "call.self-undefined-method"
|
|
19
|
+
RULE_UNRESOLVED_TOPLEVEL = "call.unresolved-toplevel"
|
|
20
|
+
RULE_WRONG_ARITY = "call.wrong-arity"
|
|
21
|
+
RULE_ARGUMENT_TYPE = "call.argument-type-mismatch"
|
|
22
|
+
RULE_NIL_RECEIVER = "call.possible-nil-receiver"
|
|
23
|
+
RULE_RAISE_NON_EXCEPTION = "call.raise-non-exception"
|
|
24
|
+
RULE_DUMP_TYPE = "dump.type"
|
|
25
|
+
RULE_ASSERT_TYPE = "assert.type-mismatch"
|
|
26
|
+
RULE_ALWAYS_RAISES = "flow.always-raises"
|
|
27
|
+
RULE_UNREACHABLE_BRANCH = "flow.unreachable-branch"
|
|
28
|
+
RULE_RETURN_TYPE = "def.return-type-mismatch"
|
|
29
|
+
RULE_VISIBILITY_MISMATCH = "def.method-visibility-mismatch"
|
|
30
|
+
RULE_OVERRIDE_VISIBILITY_REDUCED = "def.override-visibility-reduced"
|
|
31
|
+
RULE_OVERRIDE_RETURN_WIDENED = "def.override-return-widened"
|
|
32
|
+
RULE_OVERRIDE_PARAM_NARROWED = "def.override-param-narrowed"
|
|
33
|
+
RULE_IVAR_WRITE_MISMATCH = "def.ivar-write-mismatch"
|
|
34
|
+
RULE_DEAD_ASSIGNMENT = "flow.dead-assignment"
|
|
35
|
+
RULE_ALWAYS_TRUTHY_CONDITION = "flow.always-truthy-condition"
|
|
36
|
+
RULE_UNREACHABLE_CLAUSE = "flow.unreachable-clause"
|
|
37
|
+
RULE_DUPLICATE_HASH_KEY = "flow.duplicate-hash-key"
|
|
38
|
+
RULE_RETURN_IN_ENSURE = "flow.return-in-ensure"
|
|
39
|
+
RULE_SHADOWED_RESCUE_CLAUSE = "flow.shadowed-rescue-clause"
|
|
40
|
+
RULE_SUPPRESSION_UNKNOWN_RULE = "suppression.unknown-rule"
|
|
41
|
+
RULE_SUPPRESSION_EMPTY = "suppression.empty"
|
|
42
|
+
RULE_SUPPRESSION_UNKNOWN_MARKER = "suppression.unknown-marker"
|
|
43
|
+
# ADR-100 — the first `static.value-use.*` id: a value recovered from an author-declared `-> void`
|
|
44
|
+
# return, used in value context. Authored `:warning`, resolved `:off` by every profile and promoted to
|
|
45
|
+
# `:warning` only by the `use-of-void-value` bleeding-edge feature.
|
|
46
|
+
RULE_VALUE_USE_VOID = "static.value-use.void"
|
|
47
|
+
|
|
48
|
+
ALL_RULES = [
|
|
49
|
+
RULE_UNDEFINED_METHOD,
|
|
50
|
+
RULE_SELF_UNDEFINED_METHOD,
|
|
51
|
+
RULE_UNRESOLVED_TOPLEVEL,
|
|
52
|
+
RULE_WRONG_ARITY,
|
|
53
|
+
RULE_ARGUMENT_TYPE,
|
|
54
|
+
RULE_NIL_RECEIVER,
|
|
55
|
+
RULE_RAISE_NON_EXCEPTION,
|
|
56
|
+
RULE_DUMP_TYPE,
|
|
57
|
+
RULE_ASSERT_TYPE,
|
|
58
|
+
RULE_ALWAYS_RAISES,
|
|
59
|
+
RULE_UNREACHABLE_BRANCH,
|
|
60
|
+
RULE_DEAD_ASSIGNMENT,
|
|
61
|
+
RULE_ALWAYS_TRUTHY_CONDITION,
|
|
62
|
+
RULE_UNREACHABLE_CLAUSE,
|
|
63
|
+
RULE_DUPLICATE_HASH_KEY,
|
|
64
|
+
RULE_RETURN_IN_ENSURE,
|
|
65
|
+
RULE_SHADOWED_RESCUE_CLAUSE,
|
|
66
|
+
RULE_RETURN_TYPE,
|
|
67
|
+
RULE_VISIBILITY_MISMATCH,
|
|
68
|
+
RULE_OVERRIDE_VISIBILITY_REDUCED,
|
|
69
|
+
RULE_OVERRIDE_RETURN_WIDENED,
|
|
70
|
+
RULE_OVERRIDE_PARAM_NARROWED,
|
|
71
|
+
RULE_IVAR_WRITE_MISMATCH,
|
|
72
|
+
RULE_SUPPRESSION_UNKNOWN_RULE,
|
|
73
|
+
RULE_SUPPRESSION_EMPTY,
|
|
74
|
+
RULE_SUPPRESSION_UNKNOWN_MARKER,
|
|
75
|
+
RULE_VALUE_USE_VOID
|
|
76
|
+
].freeze
|
|
77
|
+
|
|
78
|
+
# Backward-compat alias table (ADR-8 § "Backward compatibility"). Existing user code with
|
|
79
|
+
# `# rigor:disable undefined-method` / `disable: [undefined-method]` keeps working — the legacy
|
|
80
|
+
# unprefixed identifiers map to their canonical `family.rule-name` form here. Removing the aliases is a
|
|
81
|
+
# future ADR once user code has migrated; until then, both spellings resolve identically.
|
|
82
|
+
LEGACY_RULE_ALIASES = {
|
|
83
|
+
"undefined-method" => RULE_UNDEFINED_METHOD,
|
|
84
|
+
"self-undefined-method" => RULE_SELF_UNDEFINED_METHOD,
|
|
85
|
+
"wrong-arity" => RULE_WRONG_ARITY,
|
|
86
|
+
"argument-type-mismatch" => RULE_ARGUMENT_TYPE,
|
|
87
|
+
"possible-nil-receiver" => RULE_NIL_RECEIVER,
|
|
88
|
+
"raise-non-exception" => RULE_RAISE_NON_EXCEPTION,
|
|
89
|
+
"dump-type" => RULE_DUMP_TYPE,
|
|
90
|
+
"assert-type" => RULE_ASSERT_TYPE,
|
|
91
|
+
"always-raises" => RULE_ALWAYS_RAISES,
|
|
92
|
+
"unreachable-branch" => RULE_UNREACHABLE_BRANCH,
|
|
93
|
+
"method-visibility-mismatch" => RULE_VISIBILITY_MISMATCH,
|
|
94
|
+
"ivar-write-mismatch" => RULE_IVAR_WRITE_MISMATCH,
|
|
95
|
+
"dead-assignment" => RULE_DEAD_ASSIGNMENT,
|
|
96
|
+
"always-truthy-condition" => RULE_ALWAYS_TRUTHY_CONDITION,
|
|
97
|
+
"unreachable-clause" => RULE_UNREACHABLE_CLAUSE,
|
|
98
|
+
"duplicate-hash-key" => RULE_DUPLICATE_HASH_KEY,
|
|
99
|
+
"return-in-ensure" => RULE_RETURN_IN_ENSURE,
|
|
100
|
+
"shadowed-rescue-clause" => RULE_SHADOWED_RESCUE_CLAUSE
|
|
101
|
+
}.freeze
|
|
102
|
+
|
|
103
|
+
# Family wildcard — a `<family>` token in a suppression comment or `disable:` list disables every rule
|
|
104
|
+
# whose canonical id starts with `<family>.`. Per ADR-8 § "1".
|
|
105
|
+
RULE_FAMILIES = %w[call flow assert dump def suppression static].freeze
|
|
106
|
+
|
|
107
|
+
# Families of diagnostics the engine emits OUTSIDE the CheckRules catalogue (aggregator-level and
|
|
108
|
+
# reporter-level diagnostics such as `rbs_extended.unsatisfied-conformance`,
|
|
109
|
+
# `dynamic.dependency-source.*`, `rbs.coverage.*`, `pre-eval.parse-error`), plus the `plugin.` prefix
|
|
110
|
+
# reserved for plugin-produced identifiers. `suppression.unknown-rule` treats a dotted token whose
|
|
111
|
+
# first segment appears here as KNOWN and stays silent: these ids are legitimate suppression /
|
|
112
|
+
# `severity_overrides:` vocabulary the light rule-id table cannot enumerate (plugins load dynamically;
|
|
113
|
+
# aggregator ids live in the engine-heavy runner), so under-warning is the FP-safe direction.
|
|
114
|
+
NON_CHECK_DIAGNOSTIC_FAMILIES = %w[rbs_extended dynamic rbs pre-eval plugin].freeze
|
|
115
|
+
|
|
116
|
+
# Bare (dot-less) diagnostic ids the engine emits outside the catalogue (see the `rule:` literals in
|
|
117
|
+
# `Analysis::Runner` / `Runner::DiagnosticAggregator`). A token equal to one of these is treated as
|
|
118
|
+
# known by `suppression.unknown-rule` even though it carries no family prefix; extend the list when
|
|
119
|
+
# the runner grows a new bare id.
|
|
120
|
+
NON_CHECK_DIAGNOSTIC_IDS = %w[
|
|
121
|
+
configuration-error load-error pool-degraded runtime-error source-rbs-synthesis-failed
|
|
122
|
+
].freeze
|
|
123
|
+
end
|
|
124
|
+
end
|
|
125
|
+
end
|