rigortype 0.2.9 → 0.3.1
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/data/builtins/ruby_core/array.yml +416 -392
- data/data/builtins/ruby_core/file.yml +42 -42
- data/data/builtins/ruby_core/hash.yml +302 -302
- data/data/builtins/ruby_core/io.yml +191 -191
- data/data/builtins/ruby_core/numeric.yml +321 -366
- data/data/builtins/ruby_core/proc.yml +124 -124
- data/data/builtins/ruby_core/range.yml +21 -21
- data/data/builtins/ruby_core/rational.yml +39 -39
- data/data/builtins/ruby_core/re.yml +65 -65
- data/data/builtins/ruby_core/set.yml +106 -106
- data/data/builtins/ruby_core/struct.yml +14 -14
- data/data/core_overlay/string_scanner.rbs +6 -5
- data/docs/handbook/01-getting-started.md +22 -34
- data/docs/handbook/03-narrowing.md +2 -1
- data/docs/handbook/04-tuples-and-shapes.md +7 -3
- data/docs/handbook/06-classes.md +19 -10
- data/docs/handbook/07-rbs-and-extended.md +76 -101
- data/docs/handbook/08-understanding-errors.md +114 -246
- data/docs/handbook/09-plugins.md +54 -144
- data/docs/handbook/11-sig-gen.md +11 -1
- data/docs/handbook/README.md +5 -3
- data/docs/handbook/appendix-liskov.md +4 -2
- data/docs/handbook/appendix-mypy.md +4 -3
- data/docs/handbook/appendix-phpstan.md +14 -7
- data/docs/handbook/appendix-steep.md +4 -2
- data/docs/handbook/appendix-type-theory.md +3 -1
- data/docs/install.md +14 -6
- data/docs/manual/01-installation.md +98 -4
- data/docs/manual/02-cli-reference.md +81 -12
- data/docs/manual/03-configuration.md +34 -2
- data/docs/manual/04-diagnostics.md +59 -4
- data/docs/manual/06-baseline.md +35 -1
- data/docs/manual/07-plugins.md +4 -4
- data/docs/manual/08-skills.md +6 -1
- data/docs/manual/09-editor-integration.md +7 -6
- data/docs/manual/11-ci.md +51 -0
- data/docs/manual/12-caching.md +65 -0
- data/docs/manual/plugins/rigor-actioncable.md +32 -0
- data/docs/manual/plugins/rigor-devise.md +4 -2
- 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 +112 -0
- data/lib/rigor/analysis/check_rules.rb +581 -98
- 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 +78 -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 +344 -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_cache_producer.rb +11 -1
- data/lib/rigor/cache/rbs_descriptor.rb +56 -10
- data/lib/rigor/cache/rbs_environment_marshal_patch.rb +38 -0
- data/lib/rigor/cache/store.rb +132 -27
- 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 +223 -32
- data/lib/rigor/cli/check_invocation.rb +84 -0
- 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 +169 -31
- 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 +22 -33
- data/lib/rigor/cli/skill_deep_probe.rb +172 -0
- data/lib/rigor/cli/skill_describe.rb +75 -9
- 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 +36 -0
- data/lib/rigor/environment/rbs_loader.rb +164 -5
- data/lib/rigor/environment.rb +29 -28
- 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 +17 -1
- 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/buffer_resolution.rb +6 -3
- data/lib/rigor/language_server/buffer_table.rb +46 -6
- data/lib/rigor/language_server/completion_provider.rb +2 -1
- data/lib/rigor/language_server/diagnostic_publisher.rb +4 -0
- 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/incremental_sync.rb +159 -0
- data/lib/rigor/language_server/selection_range_provider.rb +2 -1
- data/lib/rigor/language_server/server.rb +19 -9
- data/lib/rigor/language_server/signature_help_provider.rb +2 -1
- data/lib/rigor/language_server.rb +1 -0
- data/lib/rigor/plugin/base.rb +38 -24
- 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 +207 -49
- 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-actioncable/lib/rigor/plugin/actioncable.rb +51 -1
- 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/cache.rbs +6 -0
- data/sig/rigor/inference/void_origin.rbs +18 -0
- data/sig/rigor/inference.rbs +7 -0
- data/sig/rigor/plugin/base.rbs +6 -6
- 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 +48 -26
- data/lib/rigor/cli/ci_detector.rb +0 -89
|
@@ -41,18 +41,26 @@ carries no `documentation_url`.
|
|
|
41
41
|
| <a id="rule-call-wrong-arity"></a>`call.wrong-arity` | The positional-argument count matches no signature. | high |
|
|
42
42
|
| <a id="rule-call-argument-type-mismatch"></a>`call.argument-type-mismatch` | An argument's type provably violates the parameter contract. | high |
|
|
43
43
|
| <a id="rule-call-possible-nil-receiver"></a>`call.possible-nil-receiver` | The receiver is `T \| nil` and the method is not defined on `NilClass`. | high |
|
|
44
|
+
| <a id="rule-call-raise-non-exception"></a>`call.raise-non-exception` | A `raise` / `fail` argument's concrete type is provably not an Exception class, an Exception instance, a String, or an object defining `#exception` — a runtime `TypeError`. | high |
|
|
44
45
|
| <a id="rule-call-unresolved-toplevel"></a>`call.unresolved-toplevel` | A top-level implicit-self call resolves against no same-file `def`, `pre_eval:` patch, or `Kernel` / `Object` method. | low |
|
|
45
46
|
| <a id="rule-flow-always-raises"></a>`flow.always-raises` | The expression provably raises on every reachable path. | high |
|
|
46
47
|
| <a id="rule-flow-unreachable-branch"></a>`flow.unreachable-branch` | An `if` / `unless` / ternary branch is statically dead. | high |
|
|
47
48
|
| <a id="rule-flow-always-truthy-condition"></a>`flow.always-truthy-condition` | A condition is provably always truthy or always falsey. | medium |
|
|
48
49
|
| <a id="rule-flow-dead-assignment"></a>`flow.dead-assignment` | A local is written but never read in the same method. | medium |
|
|
49
50
|
| <a id="rule-flow-unreachable-clause"></a>`flow.unreachable-clause` | A `case`/`when` or `case`/`in` clause is statically dead — its subject type is disjoint with the pattern, or a prior clause already exhausted the subject. | medium |
|
|
51
|
+
| <a id="rule-flow-duplicate-hash-key"></a>`flow.duplicate-hash-key` | A Hash literal repeats a literal key (symbol, plain string, integer, float, `true`/`false`/`nil`) — the last entry silently overwrites the earlier one at runtime. Literal keys only; symbol vs string and `1` vs `1.0` never collide, and interpolated / constant / computed keys are never compared. A `**splat` between two identical literal keys does not rescue the pair. | high |
|
|
52
|
+
| <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 |
|
|
53
|
+
| <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 |
|
|
50
54
|
| <a id="rule-def-return-type-mismatch"></a>`def.return-type-mismatch` | The method body's result violates its declared RBS return type. | medium |
|
|
51
55
|
| <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. | high |
|
|
52
56
|
| <a id="rule-def-method-visibility-mismatch"></a>`def.method-visibility-mismatch` | An explicit-receiver call reaches a private method. | high |
|
|
53
57
|
| <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 |
|
|
54
58
|
| <a id="rule-def-override-return-widened"></a>`def.override-return-widened` | An override's declared return type widens the inherited return (covariance). | high |
|
|
55
59
|
| <a id="rule-def-override-param-narrowed"></a>`def.override-param-narrowed` | An override narrows an inherited parameter type (contravariance). | high |
|
|
60
|
+
| <a id="rule-static-value-use-void"></a>`static.value-use.void` | A value recovered from an author-declared `-> void` return is used in value context (an assignment right-hand side, a call receiver, or a call argument). Off by default; reaches a run only through the `use-of-void-value` bleeding-edge feature (ADR-100). A bare-statement `void` call and a legitimate `top` value both stay silent. | high |
|
|
61
|
+
| <a id="rule-suppression-unknown-rule"></a>`suppression.unknown-rule` | A `# rigor:disable[-file]` comment names a rule that does not exist (typically a typo), so the suppression silently does nothing. `plugin.`-prefixed tokens are never flagged. | high |
|
|
62
|
+
| <a id="rule-suppression-empty"></a>`suppression.empty` | A `# rigor:disable[-file]` comment lists no rules, so it suppresses nothing. | high |
|
|
63
|
+
| <a id="rule-suppression-unknown-marker"></a>`suppression.unknown-marker` | A comment uses a suppression marker Rigor does not recognise — typically the RuboCop reflex `# rigor:disable-next-line <rule>` or `# rigor:enable <rule>`. Rigor's only markers are `# rigor:disable <rules>` (suppresses on its own line) and `# rigor:disable-file <rules>`, so the comment suppresses nothing. | high |
|
|
56
64
|
| <a id="rule-rbs_extended-unsatisfied-conformance"></a>`rbs_extended.unsatisfied-conformance` | A class declares `%a{rigor:v1:conforms-to _Interface}` in its RBS but is missing a method the interface requires. Presence-based: only definitively-absent required methods fire. | — |
|
|
57
65
|
| <a id="rule-assert-type-mismatch"></a>`assert.type-mismatch` | An `assert_type` expectation does not match the inferred type. | high |
|
|
58
66
|
| <a id="rule-dump-type"></a>`dump.type` | A `dump_type` call — informational, prints the inferred type. | — |
|
|
@@ -89,7 +97,21 @@ re-stamps it for the run. Three profiles, set with the
|
|
|
89
97
|
| --- | --- |
|
|
90
98
|
| `lenient` | Only proven diagnostics are errors; uncertain ones drop to `warning` / `info`. For incremental adoption on legacy code. |
|
|
91
99
|
| `balanced` *(default)* | Most rules `error`; `dump.type` `info`; uncertain rules `warning`. |
|
|
92
|
-
| `strict` | Nearly every rule is an `error
|
|
100
|
+
| `strict` | Nearly every rule is an `error`. The exceptions: `call.self-undefined-method` and `static.value-use.void` stay `off` (both opt-in only), `flow.unreachable-clause` stays `warning` pending its false-positive gate, and the three `suppression.*` rules stay `warning` — a stale suppression comment is worth telling you about, but it is not a reason to fail a build. CI-friendly. |
|
|
101
|
+
|
|
102
|
+
Under `balanced`, the rules that do **not** emit as `error`
|
|
103
|
+
are:
|
|
104
|
+
|
|
105
|
+
| Severity | Rules |
|
|
106
|
+
| --- | --- |
|
|
107
|
+
| `warning` | `call.unresolved-toplevel`, `def.ivar-write-mismatch`, `def.return-type-mismatch`, `def.override-visibility-reduced`, `def.override-return-widened`, `def.override-param-narrowed`, `flow.unreachable-branch`, `flow.always-truthy-condition`, `flow.dead-assignment`, `flow.duplicate-hash-key`, `flow.return-in-ensure`, `flow.shadowed-rescue-clause`, `suppression.unknown-rule`, `suppression.empty`, `suppression.unknown-marker` |
|
|
108
|
+
| `info` | `flow.unreachable-clause`, `dump.type` |
|
|
109
|
+
| `off` | `call.self-undefined-method`, `static.value-use.void` |
|
|
110
|
+
|
|
111
|
+
Everything else emits as `error`. For one rule under all three
|
|
112
|
+
profiles, `rigor explain <rule>` prints `Authored severity:` and
|
|
113
|
+
`Severity by profile:` — that output is generated from the rule
|
|
114
|
+
catalogue itself, so it is the per-rule source of truth.
|
|
93
115
|
|
|
94
116
|
For finer control, `severity_overrides:` maps a rule ID or a
|
|
95
117
|
family to one of `error`, `warning`, `info`, or `off`:
|
|
@@ -101,7 +123,15 @@ severity_overrides:
|
|
|
101
123
|
call: warning
|
|
102
124
|
```
|
|
103
125
|
|
|
104
|
-
A rule-specific override beats a family override.
|
|
126
|
+
A rule-specific override beats a family override. `off` drops
|
|
127
|
+
the diagnostic from the result entirely, which makes
|
|
128
|
+
`severity_overrides:` the lighter-touch sibling of `disable:`
|
|
129
|
+
below — both silence a rule; the override reads as "this one
|
|
130
|
+
rule, at this severity" alongside the rest of the profile.
|
|
131
|
+
|
|
132
|
+
YAML reserves the bareword `off` as a boolean. If an override
|
|
133
|
+
that names it seems not to apply, quote it — `"off"` — and the
|
|
134
|
+
same for `on`.
|
|
105
135
|
|
|
106
136
|
## Machine-readable output (`--format json`)
|
|
107
137
|
|
|
@@ -190,11 +220,36 @@ config.merge(extra) # rigor:disable call.undefined-method
|
|
|
190
220
|
```
|
|
191
221
|
|
|
192
222
|
It accepts qualified IDs, family wildcards (`call`), a
|
|
193
|
-
comma- or space-separated list, or `all`.
|
|
223
|
+
comma- or space-separated list, or `all`. The comment must sit
|
|
224
|
+
on the line the diagnostic points at; there is no
|
|
225
|
+
`disable-block` form, so an expression spread over several
|
|
226
|
+
lines needs the comment on each line that fires.
|
|
227
|
+
|
|
228
|
+
A marker that cannot work is flagged rather than silently
|
|
229
|
+
ignored: a token that names no known rule (a typo like
|
|
230
|
+
`call.undefined-metod`) fires
|
|
231
|
+
[`suppression.unknown-rule`](#rule-suppression-unknown-rule),
|
|
232
|
+
a bare marker with no rules at all fires
|
|
233
|
+
[`suppression.empty`](#rule-suppression-empty), and a marker
|
|
234
|
+
word outside Rigor's grammar (the RuboCop reflex
|
|
235
|
+
`# rigor:disable-next-line <rule>`, or `# rigor:enable`)
|
|
236
|
+
fires
|
|
237
|
+
[`suppression.unknown-marker`](#rule-suppression-unknown-marker)
|
|
238
|
+
— all `:warning` in every profile. Tokens under the `plugin.`
|
|
239
|
+
prefix are never flagged (plugin rule vocabularies load
|
|
240
|
+
dynamically), and the surveillance diagnostics are themselves
|
|
241
|
+
suppressible like any other rule.
|
|
194
242
|
|
|
195
243
|
**In-source, whole file.** `# rigor:disable-file <rules>`
|
|
196
244
|
anywhere in a file suppresses those rules for every line;
|
|
197
|
-
`# rigor:disable-file all` silences the file.
|
|
245
|
+
`# rigor:disable-file all` silences the file. Convention is to
|
|
246
|
+
put it near the top — typically on a generated file, a fixture,
|
|
247
|
+
or a vendored snippet — but every comment in the file is
|
|
248
|
+
scanned, so any placement works.
|
|
249
|
+
|
|
250
|
+
The three layers **compose**: a file-scope marker does not
|
|
251
|
+
cancel a line-scope one, and a project-wide `disable:` still
|
|
252
|
+
applies to a file that carries neither.
|
|
198
253
|
|
|
199
254
|
**Project-wide.** The `disable:` config key turns rules off
|
|
200
255
|
across the whole run:
|
data/docs/manual/06-baseline.md
CHANGED
|
@@ -4,7 +4,11 @@ A **baseline** records the diagnostics a project already has,
|
|
|
4
4
|
so `rigor check` can stay silent about them and surface only
|
|
5
5
|
what is *new*. It is the pragmatic on-ramp for adopting Rigor
|
|
6
6
|
on an existing codebase: you do not have to reach zero
|
|
7
|
-
diagnostics before the check becomes useful in CI.
|
|
7
|
+
diagnostics before the check becomes useful in CI. It is also
|
|
8
|
+
what the [`rigor-project-init` skill](08-skills.md) snapshots
|
|
9
|
+
for you at onboarding
|
|
10
|
+
([ADR-22](../adr/22-baseline-and-project-onboarding.md) is the
|
|
11
|
+
design).
|
|
8
12
|
|
|
9
13
|
## The baseline file
|
|
10
14
|
|
|
@@ -89,6 +93,36 @@ unless you specifically need per-message discrimination, and
|
|
|
89
93
|
expect to `regenerate` a `message`-mode baseline after upgrading
|
|
90
94
|
Rigor.
|
|
91
95
|
|
|
96
|
+
## The ad-hoc form — `rigor diff`
|
|
97
|
+
|
|
98
|
+
A managed baseline is not the only way to fail CI on *new*
|
|
99
|
+
diagnostics only. The lightweight alternative keeps a plain JSON
|
|
100
|
+
snapshot in the repository and compares against it explicitly:
|
|
101
|
+
|
|
102
|
+
```sh
|
|
103
|
+
# Once: capture the current diagnostic surface.
|
|
104
|
+
rigor check --format=json > rigor.baseline.json
|
|
105
|
+
git add rigor.baseline.json
|
|
106
|
+
|
|
107
|
+
# Per PR: compare against the committed snapshot.
|
|
108
|
+
rigor diff rigor.baseline.json
|
|
109
|
+
```
|
|
110
|
+
|
|
111
|
+
[`rigor diff`](02-cli-reference.md#rigor-diff) prints a `+ NEW`
|
|
112
|
+
row for every diagnostic absent from the snapshot and a
|
|
113
|
+
`- FIXED` row for every one resolved since, and exits `1` when
|
|
114
|
+
anything is new — so a PR that adds a violation fails while the
|
|
115
|
+
recorded legacy ones stay quiet. `--format=json` is available
|
|
116
|
+
for editor and dashboard integrations. Regenerate the snapshot
|
|
117
|
+
with the same `rigor check --format=json` redirection whenever
|
|
118
|
+
you fix a row, and the project tightens monotonically.
|
|
119
|
+
|
|
120
|
+
The difference from a managed baseline is where the knowledge
|
|
121
|
+
lives: `rigor diff` is a separate step your CI script has to
|
|
122
|
+
run, while a `baseline:` file makes `rigor check` itself exit
|
|
123
|
+
clean on recorded diagnostics. Prefer the managed form unless
|
|
124
|
+
you specifically want the raw JSON snapshot.
|
|
125
|
+
|
|
92
126
|
## Working a baseline down
|
|
93
127
|
|
|
94
128
|
`rigor triage` summarises a diagnostic stream — rule
|
data/docs/manual/07-plugins.md
CHANGED
|
@@ -40,10 +40,10 @@ each plugin's options — but the families today are:
|
|
|
40
40
|
- **Rails** — `rigor-activerecord`, `rigor-actionpack`,
|
|
41
41
|
`rigor-rails-routes`, `rigor-rails-i18n`,
|
|
42
42
|
`rigor-actionmailer`, `rigor-activejob`,
|
|
43
|
-
`rigor-activestorage`, `rigor-actioncable`.
|
|
44
|
-
`
|
|
45
|
-
|
|
46
|
-
|
|
43
|
+
`rigor-activestorage`, `rigor-actioncable`. Enumerate the ones
|
|
44
|
+
you want under `plugins:` — there is no umbrella entry that
|
|
45
|
+
turns on the Rails set as a group
|
|
46
|
+
([ADR-96](../adr/96-plugin-target-gems.md) WD3 proposes one).
|
|
47
47
|
- **Testing** — `rigor-rspec`, `rigor-rspec-rails`,
|
|
48
48
|
`rigor-minitest`, `rigor-shoulda-matchers`,
|
|
49
49
|
`rigor-factorybot`.
|
data/docs/manual/08-skills.md
CHANGED
|
@@ -106,6 +106,7 @@ source checkout. The `rigor skill` command surfaces them:
|
|
|
106
106
|
|
|
107
107
|
```sh
|
|
108
108
|
rigor skill describe # probe the project + recommend the next skill (alias: rigor describe)
|
|
109
|
+
rigor skill describe --deep # same, but run `rigor check` first and route on its result
|
|
109
110
|
rigor skill --list # name + absolute path for each bundled skill
|
|
110
111
|
rigor skill <name> # print the SKILL.md body (with a references/ header)
|
|
111
112
|
rigor skill --full <name> # the body + every references/*.md inline (complete procedure)
|
|
@@ -113,7 +114,11 @@ rigor skill --path <name> # one-line absolute SKILL.md path, for a file-readin
|
|
|
113
114
|
```
|
|
114
115
|
|
|
115
116
|
`rigor skill describe` is the recommendation engine driven by
|
|
116
|
-
`rigor-next-steps
|
|
117
|
+
`rigor-next-steps`. It is a presence-only probe by default — cheap and
|
|
118
|
+
side-effect-free; add `--deep` when you want the recommendation to come
|
|
119
|
+
from a real analysis instead
|
|
120
|
+
([CLI reference](02-cli-reference.md#describe---deep)).
|
|
121
|
+
`rigor skill rigor-project-init` is the
|
|
117
122
|
canonical way to hand an agent the onboarding workflow without pointing
|
|
118
123
|
it at the repository. The `list` / `print` / `path` verb spellings are
|
|
119
124
|
deprecated (removed in v0.3.0). See [CLI reference](02-cli-reference.md#rigor-skill).
|
|
@@ -18,6 +18,7 @@ design + capability matrix lives in
|
|
|
18
18
|
|
|
19
19
|
| LSP method | Behaviour |
|
|
20
20
|
|---|---|
|
|
21
|
+
| `textDocument/didOpen` / `didChange` / `didClose` | Incremental sync (`TextDocumentSyncKind::Incremental`): a `didChange` carries range edits, which are spliced into the buffer the server already holds instead of re-sending and re-parsing the whole document on every keystroke. Positions are read as UTF-16 code-unit offsets (`positionEncoding: "utf-16"`), so an emoji or other non-BMP character counts as two. The full-text change form stays supported. |
|
|
21
22
|
| `textDocument/publishDiagnostics` | Pushed on every `didChange`, 200ms debounced. Severity / rule / source map directly to Rigor's diagnostic taxonomy. |
|
|
22
23
|
| `textDocument/hover` | Type-aware markdown. Per-node-class dispatch surfaces receiver type + RBS signature for method calls, FQN + singleton type + defined-in path for constants, narrowed type + bound-at for locals, canonical refinement names (`non-empty-string`, …) for `Refined` / `Difference` carriers. |
|
|
23
24
|
| `textDocument/completion` | Method completion after `.` (driven by inferred receiver type), constant-path completion after `::`. Composite receivers (Union → intersection of methods, Tuple / HashShape → ancestor nominal, Refined → underlying nominal) handled. Parse-recovery sentinel makes mid-edit `obj.` / `Foo::` buffers work. |
|
|
@@ -201,8 +202,8 @@ For a legacy bundler-based install, swap the connection list to
|
|
|
201
202
|
analyzer assigns the receiver.
|
|
202
203
|
- Mid-edit buffer support is best-effort. If parse fails AND the
|
|
203
204
|
cursor isn't right after `.` / `::`, the v1 LSP returns no
|
|
204
|
-
completions; deeper recovery is queued (see
|
|
205
|
-
|
|
205
|
+
completions; deeper recovery is queued (see the open
|
|
206
|
+
[`area:editor` issues](https://github.com/rigortype/rigor/issues?q=is%3Aissue+is%3Aopen+label%3Aarea%3Aeditor)).
|
|
206
207
|
|
|
207
208
|
**Hover shows `untyped` everywhere.**
|
|
208
209
|
|
|
@@ -234,10 +235,10 @@ Cold start is dominated by RBS environment build; warm starts
|
|
|
234
235
|
|
|
235
236
|
LSP v1 + v2 landed in v0.1.6 and ship in the `0.1.x` line. Queued
|
|
236
237
|
follow-ups (`textDocument/signatureHelp`, hash-key completion,
|
|
237
|
-
`textDocument/definition`,
|
|
238
|
-
|
|
239
|
-
are demand-driven;
|
|
240
|
-
|
|
238
|
+
`textDocument/definition`, Ractor pool dispatch,
|
|
239
|
+
codeAction / rename / semanticTokens / inlayHint)
|
|
240
|
+
are demand-driven; the current queue is the open
|
|
241
|
+
[`area:editor` issues](https://github.com/rigortype/rigor/issues?q=is%3Aissue+is%3Aopen+label%3Aarea%3Aeditor).
|
|
241
242
|
|
|
242
243
|
To request a queued feature or report an LSP issue, open a GitHub
|
|
243
244
|
issue with: the editor + version, the Rigor version
|
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.
|
|
@@ -44,6 +44,30 @@ or `stream_for record`) — the absence of a literal match doesn't prove
|
|
|
44
44
|
the stream is invalid. Non-`Channel` receivers and non-literal stream
|
|
45
45
|
arguments pass through silently.
|
|
46
46
|
|
|
47
|
+
## `#receive(data)` parameter typing
|
|
48
|
+
|
|
49
|
+
The plugin also carries an [ADR-28](../../adr/28-path-scoped-protocol-contracts.md)
|
|
50
|
+
path-scoped protocol contract: inside any `#receive(data)` defined
|
|
51
|
+
under `channel_search_paths`, `data` types as `Hash` instead of
|
|
52
|
+
`Dynamic[Top]`. `#receive` is ActionCable's framework-dispatched
|
|
53
|
+
catch-all action — invoked with the decoded JSON payload when an
|
|
54
|
+
incoming message carries no `"action"` key — so the parameter shape is
|
|
55
|
+
uniform across every channel.
|
|
56
|
+
|
|
57
|
+
```ruby
|
|
58
|
+
# app/channels/chat_channel.rb
|
|
59
|
+
class ChatChannel < ApplicationCable::Channel
|
|
60
|
+
def receive(data)
|
|
61
|
+
data["body"] # data: Hash
|
|
62
|
+
data.no_such_method # error: call.undefined-method
|
|
63
|
+
end
|
|
64
|
+
end
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
Custom action methods (`def speak(data)`) are not covered — their
|
|
68
|
+
names are project-chosen, and a protocol contract names a single fixed
|
|
69
|
+
method. Only `#receive` is a reserved, uniformly-shaped hook.
|
|
70
|
+
|
|
47
71
|
## Configuration
|
|
48
72
|
|
|
49
73
|
```yaml
|
|
@@ -54,6 +78,11 @@ plugins:
|
|
|
54
78
|
channel_base_classes: ["ApplicationCable::Channel", "ActionCable::Channel::Base"] # default
|
|
55
79
|
```
|
|
56
80
|
|
|
81
|
+
`channel_search_paths` also retargets the `#receive` protocol
|
|
82
|
+
contract's glob — including for multiple configured roots — so a
|
|
83
|
+
custom channel directory gets the same `data: Hash` typing as the
|
|
84
|
+
default.
|
|
85
|
+
|
|
57
86
|
## Limitations
|
|
58
87
|
|
|
59
88
|
- **Direct-superclass match only.** An indirect chain (`AdminChannel <
|
|
@@ -68,6 +97,9 @@ plugins:
|
|
|
68
97
|
than directly in the channel body) is out of scope.
|
|
69
98
|
- **Bare `broadcast(...)`** without an explicit `ActionCable.server`
|
|
70
99
|
receiver is skipped to avoid false positives on unrelated methods.
|
|
100
|
+
- **The `#receive` contract is path-scoped, not class-scoped** (ADR-28):
|
|
101
|
+
any `def receive(data)` defined anywhere under `channel_search_paths`
|
|
102
|
+
is typed, even on a class that isn't an ActionCable channel.
|
|
71
103
|
|
|
72
104
|
## Plugin internals
|
|
73
105
|
|
|
@@ -64,7 +64,9 @@ The macro manifest (the trait registry mapping each strategy to its
|
|
|
64
64
|
module), the concern re-targeting walk, the demo, and the
|
|
65
65
|
contract surfaces this plugin exercises are in the
|
|
66
66
|
[plugin's README](../../../plugins/rigor-devise/README.md);
|
|
67
|
-
[
|
|
68
|
-
|
|
67
|
+
[`docs/internal-spec/macro-substrate.md`](../../internal-spec/macro-substrate.md)
|
|
68
|
+
specifies the Tier B trait registry generally, and
|
|
69
|
+
[handbook chapter 9](../../handbook/09-plugins.md) is the
|
|
70
|
+
orientation. To write a plugin, see
|
|
69
71
|
[`examples/`](../../../examples/README.md) and the
|
|
70
72
|
[`rigor-plugin-author`](../08-skills.md) skill.
|
|
@@ -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
|