rigortype 0.2.8 → 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.
Files changed (207) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +2 -2
  3. data/data/core_overlay/csv.rbs +28 -0
  4. data/data/core_overlay/psych.rbs +22 -0
  5. data/docs/handbook/01-getting-started.md +9 -1
  6. data/docs/handbook/02-everyday-types.md +4 -1
  7. data/docs/handbook/03-narrowing.md +2 -1
  8. data/docs/handbook/04-tuples-and-shapes.md +7 -3
  9. data/docs/handbook/06-classes.md +18 -9
  10. data/docs/handbook/08-understanding-errors.md +4 -3
  11. data/docs/handbook/11-sig-gen.md +11 -1
  12. data/docs/handbook/appendix-mypy.md +4 -3
  13. data/docs/install.md +14 -6
  14. data/docs/manual/01-installation.md +99 -4
  15. data/docs/manual/02-cli-reference.md +65 -19
  16. data/docs/manual/03-configuration.md +34 -2
  17. data/docs/manual/04-diagnostics.md +23 -0
  18. data/docs/manual/07-plugins.md +5 -5
  19. data/docs/manual/09-editor-integration.md +4 -4
  20. data/docs/manual/11-ci.md +51 -0
  21. data/docs/manual/12-caching.md +65 -0
  22. data/docs/manual/14-rails-quickstart.md +4 -2
  23. data/docs/manual/15-type-protection-coverage.md +21 -0
  24. data/docs/manual/plugins/rigor-actionpack.md +1 -1
  25. data/docs/manual/plugins/rigor-activerecord.md +12 -5
  26. data/docs/manual/plugins/rigor-rails-routes.md +11 -0
  27. data/lib/rigor/analysis/check_rules/always_truthy_condition_collector.rb +9 -1
  28. data/lib/rigor/analysis/check_rules/dead_assignment_collector.rb +5 -3
  29. data/lib/rigor/analysis/check_rules/duplicate_hash_key_collector.rb +128 -0
  30. data/lib/rigor/analysis/check_rules/inferred_param_guard.rb +60 -0
  31. data/lib/rigor/analysis/check_rules/ivar_write_collector.rb +3 -2
  32. data/lib/rigor/analysis/check_rules/return_in_ensure_collector.rb +117 -0
  33. data/lib/rigor/analysis/check_rules/rule_ids.rb +125 -0
  34. data/lib/rigor/analysis/check_rules/rule_walk.rb +2 -1
  35. data/lib/rigor/analysis/check_rules/self_closedness_scanner.rb +2 -1
  36. data/lib/rigor/analysis/check_rules/shadowed_rescue_collector.rb +283 -0
  37. data/lib/rigor/analysis/check_rules/unreachable_clause_collector.rb +10 -1
  38. data/lib/rigor/analysis/check_rules/void_value_use_collector.rb +93 -0
  39. data/lib/rigor/analysis/check_rules.rb +557 -95
  40. data/lib/rigor/analysis/dependency_recorder.rb +93 -9
  41. data/lib/rigor/analysis/dependency_source_inference/walker.rb +2 -1
  42. data/lib/rigor/analysis/incremental_session.rb +456 -51
  43. data/lib/rigor/analysis/path_expansion.rb +42 -0
  44. data/lib/rigor/analysis/plugin_fact_fingerprint.rb +188 -0
  45. data/lib/rigor/analysis/rule_catalog.rb +226 -1
  46. data/lib/rigor/analysis/run_cache_key.rb +68 -0
  47. data/lib/rigor/analysis/run_cache_probe.rb +72 -0
  48. data/lib/rigor/analysis/runner/diagnostic_aggregator.rb +181 -28
  49. data/lib/rigor/analysis/runner/pool_coordinator.rb +77 -15
  50. data/lib/rigor/analysis/runner/project_pre_passes.rb +95 -49
  51. data/lib/rigor/analysis/runner/run_snapshots.rb +9 -2
  52. data/lib/rigor/analysis/runner.rb +343 -68
  53. data/lib/rigor/analysis/severity_stamp.rb +43 -0
  54. data/lib/rigor/analysis/worker_session.rb +30 -2
  55. data/lib/rigor/bleeding_edge.rb +37 -8
  56. data/lib/rigor/builtins/regex_refinement.rb +36 -1
  57. data/lib/rigor/cache/descriptor.rb +88 -28
  58. data/lib/rigor/cache/file_digest.rb +158 -0
  59. data/lib/rigor/cache/incremental_snapshot.rb +41 -5
  60. data/lib/rigor/cache/rbs_descriptor.rb +56 -10
  61. data/lib/rigor/cache/store.rb +207 -60
  62. data/lib/rigor/ci_detector.rb +90 -0
  63. data/lib/rigor/cli/annotate_command.rb +15 -9
  64. data/lib/rigor/cli/baseline_command.rb +4 -1
  65. data/lib/rigor/cli/check_command.rb +212 -27
  66. data/lib/rigor/cli/coverage_command.rb +56 -30
  67. data/lib/rigor/cli/diagnostic_formats.rb +4 -1
  68. data/lib/rigor/cli/docs_command.rb +0 -30
  69. data/lib/rigor/cli/doctor_command.rb +163 -23
  70. data/lib/rigor/cli/lsp_command.rb +5 -0
  71. data/lib/rigor/cli/mcp_command.rb +5 -0
  72. data/lib/rigor/cli/plugins_command.rb +36 -6
  73. data/lib/rigor/cli/plugins_renderer.rb +44 -12
  74. data/lib/rigor/cli/probe_environment.rb +85 -0
  75. data/lib/rigor/cli/protection_fork_scan.rb +55 -0
  76. data/lib/rigor/cli/protection_report.rb +7 -1
  77. data/lib/rigor/cli/sig_gen_command.rb +36 -9
  78. data/lib/rigor/cli/skill_command.rb +1 -32
  79. data/lib/rigor/cli/trace_command.rb +5 -6
  80. data/lib/rigor/cli/type_of_command.rb +9 -8
  81. data/lib/rigor/cli/type_scan_command.rb +8 -8
  82. data/lib/rigor/cli.rb +15 -4
  83. data/lib/rigor/config_audit.rb +48 -2
  84. data/lib/rigor/configuration/severity_profile.rb +31 -0
  85. data/lib/rigor/configuration.rb +156 -8
  86. data/lib/rigor/environment/default_libraries.rb +35 -0
  87. data/lib/rigor/environment/missing_gem_constant_index.rb +128 -0
  88. data/lib/rigor/environment/rbs_loader.rb +164 -5
  89. data/lib/rigor/environment.rb +71 -43
  90. data/lib/rigor/inference/budget_trace.rb +77 -2
  91. data/lib/rigor/inference/closure_escape_analyzer.rb +14 -1
  92. data/lib/rigor/inference/def_handle.rb +23 -0
  93. data/lib/rigor/inference/def_node_resolver.rb +90 -0
  94. data/lib/rigor/inference/def_return_typer.rb +2 -1
  95. data/lib/rigor/inference/expression_typer.rb +379 -113
  96. data/lib/rigor/inference/fork_map.rb +87 -0
  97. data/lib/rigor/inference/method_dispatcher/data_folding.rb +30 -5
  98. data/lib/rigor/inference/method_dispatcher/kernel_dispatch.rb +200 -0
  99. data/lib/rigor/inference/method_dispatcher/literal_string_folding.rb +22 -16
  100. data/lib/rigor/inference/method_dispatcher/member_shape_projection.rb +33 -0
  101. data/lib/rigor/inference/method_dispatcher/overload_selector.rb +11 -0
  102. data/lib/rigor/inference/method_dispatcher/rbs_dispatch.rb +42 -9
  103. data/lib/rigor/inference/method_dispatcher/receiver_affinity.rb +5 -0
  104. data/lib/rigor/inference/method_dispatcher/shape_dispatch.rb +28 -35
  105. data/lib/rigor/inference/method_dispatcher/struct_folding.rb +40 -8
  106. data/lib/rigor/inference/method_dispatcher.rb +57 -1
  107. data/lib/rigor/inference/mutation_widening.rb +42 -8
  108. data/lib/rigor/inference/narrowing.rb +245 -17
  109. data/lib/rigor/inference/parameter_inference_collector.rb +70 -10
  110. data/lib/rigor/inference/project_patched_scanner.rb +2 -1
  111. data/lib/rigor/inference/rbs_type_translator.rb +8 -1
  112. data/lib/rigor/inference/scope_indexer.rb +460 -49
  113. data/lib/rigor/inference/statement_evaluator.rb +111 -17
  114. data/lib/rigor/inference/struct_fold_safety.rb +48 -11
  115. data/lib/rigor/inference/synthetic_method_scanner.rb +15 -14
  116. data/lib/rigor/inference/void_origin.rb +25 -0
  117. data/lib/rigor/inference/void_tail_summary.rb +220 -0
  118. data/lib/rigor/language_server/completion_provider.rb +2 -1
  119. data/lib/rigor/language_server/document_symbol_provider.rb +2 -1
  120. data/lib/rigor/language_server/folding_range_provider.rb +2 -1
  121. data/lib/rigor/language_server/selection_range_provider.rb +2 -1
  122. data/lib/rigor/language_server/signature_help_provider.rb +2 -1
  123. data/lib/rigor/plugin/base.rb +9 -22
  124. data/lib/rigor/plugin/inflector.rb +12 -3
  125. data/lib/rigor/plugin/io_boundary.rb +5 -1
  126. data/lib/rigor/plugin/isolation.rb +81 -11
  127. data/lib/rigor/plugin/load_error.rb +10 -1
  128. data/lib/rigor/plugin/loader.rb +96 -14
  129. data/lib/rigor/plugin/node_rule_walk.rb +5 -3
  130. data/lib/rigor/plugin/registry.rb +32 -23
  131. data/lib/rigor/plugin.rb +26 -0
  132. data/lib/rigor/protection/mutator.rb +3 -2
  133. data/lib/rigor/reflection.rb +64 -0
  134. data/lib/rigor/runtime/jit.rb +128 -0
  135. data/lib/rigor/scope/discovery_index.rb +12 -2
  136. data/lib/rigor/scope.rb +122 -28
  137. data/lib/rigor/sig_gen/classification.rb +5 -1
  138. data/lib/rigor/sig_gen/generator.rb +38 -6
  139. data/lib/rigor/sig_gen/observation_collector.rb +4 -3
  140. data/lib/rigor/sig_gen/rbs_validity.rb +44 -0
  141. data/lib/rigor/sig_gen/renderer.rb +10 -0
  142. data/lib/rigor/sig_gen/write_result.rb +9 -4
  143. data/lib/rigor/sig_gen/writer.rb +24 -2
  144. data/lib/rigor/source/node_children.rb +116 -0
  145. data/lib/rigor/source/node_locator.rb +3 -1
  146. data/lib/rigor/source/node_walker.rb +4 -2
  147. data/lib/rigor/source.rb +1 -0
  148. data/lib/rigor/type/difference.rb +28 -24
  149. data/lib/rigor/type/hash_shape.rb +34 -10
  150. data/lib/rigor/version.rb +1 -1
  151. data/lib/rigortype.rb +24 -0
  152. data/plugins/rigor-actioncable/lib/rigor/plugin/actioncable/channel_discoverer.rb +4 -2
  153. data/plugins/rigor-actionmailer/lib/rigor/plugin/actionmailer/mailer_discoverer.rb +6 -4
  154. data/plugins/rigor-actionpack/lib/rigor/plugin/actionpack/analyzer.rb +23 -8
  155. data/plugins/rigor-actionpack/lib/rigor/plugin/actionpack/controller_discoverer.rb +5 -3
  156. data/plugins/rigor-actionpack/lib/rigor/plugin/actionpack.rb +21 -0
  157. data/plugins/rigor-activejob/lib/rigor/plugin/activejob/job_discoverer.rb +4 -2
  158. data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord/analyzer.rb +13 -2
  159. data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord/model_discoverer.rb +65 -2
  160. data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord/model_index.rb +20 -2
  161. data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord/schema_parser.rb +4 -2
  162. data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord/structure_sql_parser.rb +172 -0
  163. data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord.rb +22 -8
  164. data/plugins/rigor-activestorage/lib/rigor/plugin/activestorage/analyzer.rb +3 -1
  165. data/plugins/rigor-activestorage/lib/rigor/plugin/activestorage/attachment_discoverer.rb +4 -2
  166. data/plugins/rigor-activesupport-core-ext/sig/active_support/core_ext.rbs +32 -0
  167. data/plugins/rigor-dry-types/lib/rigor/plugin/dry_types.rb +38 -6
  168. data/plugins/rigor-factorybot/lib/rigor/plugin/factorybot/factory_discoverer.rb +3 -1
  169. data/plugins/rigor-hanami/lib/rigor/plugin/hanami/action_checker.rb +3 -1
  170. data/plugins/rigor-mangrove/lib/rigor/plugin/mangrove.rb +10 -0
  171. data/plugins/rigor-minitest/lib/rigor/plugin/minitest.rb +11 -0
  172. data/plugins/rigor-pundit/lib/rigor/plugin/pundit/policy_discoverer.rb +3 -1
  173. data/plugins/rigor-rails/lib/rigor-rails.rb +11 -12
  174. data/plugins/rigor-rails-routes/lib/rigor/plugin/rails_routes/grape_api_discoverer.rb +191 -0
  175. data/plugins/rigor-rails-routes/lib/rigor/plugin/rails_routes/helper_discoverer.rb +3 -1
  176. data/plugins/rigor-rails-routes/lib/rigor/plugin/rails_routes/helper_table.rb +19 -1
  177. data/plugins/rigor-rails-routes/lib/rigor/plugin/rails_routes/routes_parser.rb +48 -15
  178. data/plugins/rigor-rails-routes/lib/rigor/plugin/rails_routes.rb +42 -12
  179. data/plugins/rigor-rbs-inline/lib/rigor/plugin/rbs_inline.rb +83 -9
  180. data/plugins/rigor-rbs-inline/lib/rigor-rbs-inline.rb +9 -5
  181. data/plugins/rigor-rspec/lib/rigor/plugin/rspec/let_scope_index.rb +3 -1
  182. data/plugins/rigor-rspec/lib/rigor/plugin/rspec/scope_walker.rb +3 -1
  183. data/plugins/rigor-rspec/lib/rigor/plugin/rspec.rb +11 -0
  184. data/plugins/rigor-rspec-rails/lib/rigor/plugin/rspec_rails/http_status_codes.rb +4 -1
  185. data/plugins/rigor-sidekiq/lib/rigor/plugin/sidekiq/worker_discoverer.rb +4 -2
  186. data/plugins/rigor-sorbet/lib/rigor/plugin/sorbet/catalog_walker.rb +3 -1
  187. data/plugins/rigor-sorbet/lib/rigor/plugin/sorbet.rb +74 -14
  188. data/sig/prism_node_children.rbs +9 -0
  189. data/sig/rigor/environment.rbs +1 -0
  190. data/sig/rigor/inference.rbs +7 -0
  191. data/sig/rigor/plugin/base.rbs +2 -3
  192. data/sig/rigor/reflection.rbs +1 -0
  193. data/sig/rigor/scope.rbs +12 -1
  194. data/skills/rigor-ci-setup/SKILL.md +10 -0
  195. data/skills/rigor-editor-setup/SKILL.md +11 -0
  196. data/skills/rigor-next-steps/SKILL.md +23 -2
  197. data/skills/rigor-plugin-author/SKILL.md +3 -3
  198. data/skills/rigor-plugin-author/references/02-walker-and-types.md +2 -2
  199. data/skills/rigor-plugin-review/SKILL.md +2 -2
  200. data/skills/rigor-plugin-review/references/01-best-practices-checklist.md +5 -4
  201. data/skills/rigor-project-init/SKILL.md +29 -3
  202. data/skills/rigor-project-init/references/01-detect.md +1 -1
  203. data/skills/rigor-project-init/references/02-configure.md +39 -5
  204. data/skills/rigor-project-init/references/03-baseline-and-bugs.md +20 -0
  205. data/skills/rigor-project-init/references/05-jit-performance.md +125 -0
  206. metadata +49 -24
  207. data/lib/rigor/cli/ci_detector.rb +0 -89
@@ -22,6 +22,24 @@ discovery entirely.
22
22
  All relative paths in a config file resolve against that
23
23
  file's own directory.
24
24
 
25
+ ## Editor validation
26
+
27
+ Rigor ships a JSON Schema for this file. Editors that
28
+ understand the [`yaml-language-server`](https://github.com/redhat-developer/yaml-language-server)
29
+ magic comment — VS Code's YAML extension, the IntelliJ
30
+ family, Helix, Neovim with `yaml-ls` — give you
31
+ autocomplete, hover docs, and structural validation as you
32
+ type:
33
+
34
+ ```yaml
35
+ # yaml-language-server: $schema=https://github.com/rigortype/rigor/raw/master/schemas/rigor-config.schema.json
36
+ ```
37
+
38
+ `rigor init` writes that line for you. The schema is not a
39
+ copy of this page — it is a source of truth in its own
40
+ right, kept in step with the loader by a spec, so a key it
41
+ rejects is a key Rigor does not accept.
42
+
25
43
  ## A minimal config
26
44
 
27
45
  ```yaml
@@ -44,6 +62,7 @@ cache:
44
62
  | `exclude` | Array | `[]` | Glob patterns to skip. `vendor/bundle`, `.bundle`, and `node_modules` are always excluded. |
45
63
  | `includes` | Array | `[]` | Other config files to layer underneath this one. |
46
64
  | `fold_platform_specific_paths` | Boolean | `false` | Resolve Ruby-version-conditional load paths when discovering sources. |
65
+ | `parameter_inference` | Boolean | `false` | Opt-in call-site parameter type inference on the `check` walk ([ADR-67](../adr/67-parameter-type-inference.md) WD6). When `true`, an undeclared `def` / `initialize` / setter parameter is typed to the union of its resolved call-site argument types, sharpening downstream ivar reads, folds, and protection coverage. Precision-additive only — the negative rules never fire against an inferred parameter. Cannot be combined with `--incremental`. |
47
66
 
48
67
  ### Type sources
49
68
 
@@ -65,6 +84,7 @@ types it was meant to describe into a high-confidence
65
84
  of real type errors. The audit covers:
66
85
 
67
86
  ```
87
+ rigor: `excludee` is not a recognized configuration key; it has no effect. Did you mean `exclude`?
68
88
  rigor: signature_paths: "/path/to/sig" does not exist (no signatures loaded from it)
69
89
  rigor: signature_paths: "/path/to/sig" matched 0 signature files
70
90
  rigor: libraries: "csb" is not an available RBS library (no signatures loaded from it)
@@ -73,6 +93,11 @@ rigor: severity_overrides: "flow.bogus" is not a recognized rule id; the overrid
73
93
  rigor: bundler.lockfile: "./missing/Gemfile.lock" does not exist
74
94
  ```
75
95
 
96
+ The unrecognised-key check covers **top-level** keys, and skips
97
+ the namespaces reserved for other implementations (see below).
98
+ A typo *inside* a group — `cache: { pth: … }` — is caught by
99
+ the JSON schema as you type rather than at check time.
100
+
76
101
  These are warnings, not errors — partial or optional bundles and
77
102
  forward-looking config are valid setups. The audit only fires on explicit,
78
103
  working-setup-safe signals: an unset default (auto-detected `<root>/sig`,
@@ -91,7 +116,7 @@ statically and may resolve at run time. The same findings appear in the
91
116
  | `severity_profile` | String | `"balanced"` | `lenient`, `balanced`, or `strict` — see [Diagnostics](04-diagnostics.md). |
92
117
  | `severity_overrides` | Hash | `{}` | Per-rule / per-family severity, e.g. `{ call: warning, flow.always-truthy-condition: off }`. |
93
118
  | `baseline` | String / `false` | `nil` | Path to a `.rigor-baseline.yml`, or `false` to disable an inherited one. See [Baselines](06-baseline.md). |
94
- | `bleeding_edge` | Boolean / Array / Hash | `false` | Adopt the next major's queued diagnostic disciplines early ([ADR-50](../adr/50-release-engineering-and-stability-strategy.md) § WD2). `false` adopts none; `true` adopts the whole overlay; a list of feature ids adopts only those; `{ all: true, except: [ids] }` adopts all but the named. Orthogonal to `severity_profile`. Override it for a single run with [`rigor check --bleeding-edge[=ids]`](02-cli-reference.md#rigor-check) / `--no-bleeding-edge`. Inspect with [`rigor show-bleedingedge`](02-cli-reference.md#rigor-show-bleedingedge). The overlay is empty in this release, so every form is currently a no-op. |
119
+ | `bleeding_edge` | Boolean / Array / Hash | `false` | Adopt the next major's queued diagnostic disciplines early ([ADR-50](../adr/50-release-engineering-and-stability-strategy.md) § WD2). `false` adopts none; `true` adopts the whole overlay; a list of feature ids adopts only those; `{ all: true, except: [ids] }` adopts all but the named. Orthogonal to `severity_profile`. Override it for a single run with [`rigor check --bleeding-edge[=ids]`](02-cli-reference.md#rigor-check) / `--no-bleeding-edge`. Inspect with [`rigor show-bleedingedge`](02-cli-reference.md#rigor-show-bleedingedge). |
95
120
 
96
121
  ### Dependency RBS discovery
97
122
 
@@ -128,11 +153,18 @@ explicitly with `bundler.bundle_path:`, or supply signatures another way:
128
153
  | --- | --- | --- | --- |
129
154
  | `cache.path` | String | `.rigor/cache` | Persistent cache directory. See [Caching](12-caching.md). |
130
155
  | `cache.max_bytes` | Integer or `null` | `268435456` (256 MB) | LRU eviction cap for the cache directory; `null` disables eviction. See [Caching § Size and eviction](12-caching.md#size-and-eviction). |
131
- | `parallel.workers` | Integer | `0` | Parallel worker processes for per-file analysis (fork-based pool today; ADR-15); `0` is sequential. CLI `--workers` and `RIGOR_RACTOR_WORKERS` take precedence. |
156
+ | `cache.validation` | String | `"auto"` | How the cache checks whether a file is unchanged: `auto` behaves as `digest` when a CI environment is detected and as `stat` otherwise; `stat` compares size + nanosecond timestamps + inode and only re-hashes a file whose stat moved; `digest` re-hashes every file's content every run. Both keep the content hash as the sole change authority — `stat` just skips the hash when the stat proves a file untouched. See [Caching § How a file is checked for changes](12-caching.md#how-a-file-is-checked-for-changes). The `RIGOR_STRICT_VALIDATION=1` environment variable forces `digest` for one run and wins over this key; `RIGOR_CI_DETECT=0` disables the CI detection. |
157
+ | `parallel.workers` | Integer | `0` | Parallel worker processes for per-file analysis (fork-based pool today; ADR-15); `0` is sequential. CLI `--workers` and `RIGOR_RACTOR_WORKERS` take precedence. Applies to `--incremental` re-checks as well as full runs. |
132
158
  | `plugins_io.network` | String | `"disabled"` | Plugin network policy — `disabled` or `allowlist`. |
133
159
  | `plugins_io.allowed_paths` | Array | `[]` | Filesystem paths plugins may read. |
134
160
  | `plugins_io.allowed_url_hosts` | Array | `[]` | URL hosts plugins may fetch from when `network: allowlist`. |
135
161
 
162
+ ### Reserved for other implementations
163
+
164
+ | Key | Type | Default | Meaning |
165
+ | --- | --- | --- | --- |
166
+ | `rigor_rs` | Hash | — | **Reserved; this implementation skips it.** Another Rigor implementation reads keys under this namespace, so one `.rigor.yml` can serve both. Rigor validates its shape at the schema level only — it never reads the value, and an invalid one is never a runtime error here. Leave it alone unless the tool that reads it tells you otherwise. |
167
+
136
168
  ## A worked example
137
169
 
138
170
  ```yaml
@@ -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. | — |
@@ -192,6 +200,21 @@ config.merge(extra) # rigor:disable call.undefined-method
192
200
  It accepts qualified IDs, family wildcards (`call`), a
193
201
  comma- or space-separated list, or `all`.
194
202
 
203
+ A marker that cannot work is flagged rather than silently
204
+ ignored: a token that names no known rule (a typo like
205
+ `call.undefined-metod`) fires
206
+ [`suppression.unknown-rule`](#rule-suppression-unknown-rule),
207
+ a bare marker with no rules at all fires
208
+ [`suppression.empty`](#rule-suppression-empty), and a marker
209
+ word outside Rigor's grammar (the RuboCop reflex
210
+ `# rigor:disable-next-line <rule>`, or `# rigor:enable`)
211
+ fires
212
+ [`suppression.unknown-marker`](#rule-suppression-unknown-marker)
213
+ — all `:warning` in every profile. Tokens under the `plugin.`
214
+ prefix are never flagged (plugin rule vocabularies load
215
+ dynamically), and the surveillance diagnostics are themselves
216
+ suppressible like any other rule.
217
+
195
218
  **In-source, whole file.** `# rigor:disable-file <rules>`
196
219
  anywhere in a file suppresses those rules for every line;
197
220
  `# rigor:disable-file all` silences the file.
@@ -27,7 +27,7 @@ configuration takes the object form:
27
27
  plugins:
28
28
  - gem: rigor-activerecord
29
29
  config:
30
- schema: db/schema.rb
30
+ schema_file: db/schema.rb
31
31
  ```
32
32
 
33
33
  ## Available plugins
@@ -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`. The
44
- `rigor-rails` meta-gem bundles the Rails set for Gemfile
45
- convenience; you still enumerate the individual plugins you
46
- want under `plugins:`.
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`.
@@ -201,8 +201,8 @@ For a legacy bundler-based install, swap the connection list to
201
201
  analyzer assigns the receiver.
202
202
  - Mid-edit buffer support is best-effort. If parse fails AND the
203
203
  cursor isn't right after `.` / `::`, the v1 LSP returns no
204
- completions; deeper recovery is queued (see ROADMAP §
205
- "Editor / IDE integration").
204
+ completions; deeper recovery is queued (see the open
205
+ [`area:editor` issues](https://github.com/rigortype/rigor/issues?q=is%3Aissue+is%3Aopen+label%3Aarea%3Aeditor)).
206
206
 
207
207
  **Hover shows `untyped` everywhere.**
208
208
 
@@ -236,8 +236,8 @@ LSP v1 + v2 landed in v0.1.6 and ship in the `0.1.x` line. Queued
236
236
  follow-ups (`textDocument/signatureHelp`, hash-key completion,
237
237
  `textDocument/definition`, incremental `didChange` sync, Ractor
238
238
  pool dispatch, codeAction / rename / semanticTokens / inlayHint)
239
- are demand-driven; see ROADMAP § "Editor / IDE integration" for
240
- the current queue.
239
+ are demand-driven; the current queue is the open
240
+ [`area:editor` issues](https://github.com/rigortype/rigor/issues?q=is%3Aissue+is%3Aopen+label%3Aarea%3Aeditor).
241
241
 
242
242
  To request a queued feature or report an LSP issue, open a GitHub
243
243
  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
@@ -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.
@@ -27,7 +27,7 @@ You need:
27
27
  - **`mise` wired into your shell** — add
28
28
  `eval "$(mise activate zsh)"` (or the equivalent for your
29
29
  shell) to your shell rc so that `rigor` reaches your `PATH`.
30
- See [Installing Rigor § Putting rigor on your PATH](01-installation.md)
30
+ See [Installing Rigor § Putting rigor on your PATH](01-installation.md#putting-rigor-on-your-path)
31
31
  for detail.
32
32
  - **An existing Rails project** at a known path.
33
33
 
@@ -156,7 +156,9 @@ source-of-truth copy is
156
156
 
157
157
  If your first `rigor check` reports more than ~100 diagnostics,
158
158
  acknowledge mode is the natural starting point. You can tighten
159
- it later.
159
+ it later. In `.rigor.dist.yml` (Step 3) the mode maps to a
160
+ `severity_profile:` — acknowledge → `lenient`, strict → `strict`
161
+ (omit the key for the default `balanced`).
160
162
 
161
163
  ### Step 3 — Write .rigor.dist.yml
162
164
 
@@ -48,6 +48,14 @@ power. `--threshold=RATIO` turns it into a CI gate (exit `1`
48
48
  below the ratio) and `--format=json` carries the structured
49
49
  fields.
50
50
 
51
+ On a large project, `--workers=N` fork-parallelizes the scan
52
+ (both the parameter-inference pre-pass and the per-file scan),
53
+ with output byte-identical to a sequential run. The worker count
54
+ resolves the same way `rigor check` does — `--workers` ›
55
+ `RIGOR_RACTOR_WORKERS` › [`parallel.workers:`](03-configuration.md)
56
+ › `0` (sequential default) — so a project already configured for
57
+ parallel `check` gets parallel coverage for free.
58
+
51
59
  This is the everyday number. When you want the truth behind it,
52
60
  move to Tier 2.
53
61
 
@@ -200,6 +208,19 @@ the "Add a type here" header, and the JSON carries the same totals as
200
208
  `tractability_summary`. Start with the `add_rbs` holes — they are the
201
209
  ones a type actually catches.
202
210
 
211
+ > **`external_gem_without_rbs` needs your gems installed where Rigor
212
+ > can read them.** To tell whether an unresolved constant belongs to
213
+ > a gem, Rigor reads that gem's source — so it must find the gem on
214
+ > disk. It looks in the project's Bundler install tree (a
215
+ > `vendor/bundle`, or a path set with `bundler.bundle_path:`; see
216
+ > [Configuration](03-configuration.md)). A project whose gems live in
217
+ > the active Ruby's default gem home — the common `rbenv` / `mise`
218
+ > case with no `--path` set — is invisible to the isolated analyzer by
219
+ > design ([ADR-27](https://github.com/rigortype/rigor/blob/master/docs/adr/27-tool-distribution-model.md)):
220
+ > point Rigor at it with `bundler.bundle_path:`. Until you do, these
221
+ > holes keep the generic `engine_gap` cause instead of `add_rbs` —
222
+ > the label is missing, never wrong.
223
+
203
224
  Provenance is precision-additive only: it never changes a type, fires
204
225
  no diagnostic, and never affects severity or the protection ratio.
205
226
 
@@ -39,7 +39,7 @@ no-op rather than erroring.
39
39
  | `plugin.actionpack.render-target` | info | an explicit `render :symbol` / `"string"` / `partial:` resolved to a view template |
40
40
  | `plugin.actionpack.missing-template` | error | an explicit `render` resolved to a view path that doesn't exist under any `view_search_paths` |
41
41
  | `plugin.actionpack.permit-call` | info | a `params.require(:m).permit(:key, …)` chain resolved to a known model; keys matched against its columns |
42
- | `plugin.actionpack.unknown-permit-key` | error | a literal `permit(:key)` isn't a column on the model (with a did-you-mean) |
42
+ | `plugin.actionpack.unknown-permit-key` | error | a literal `permit(:key)` is a near-miss (edit distance ≤ 2) of a real column but not one — a likely typo (with a did-you-mean). A key nothing like any column (a legitimate virtual attribute) does not fire |
43
43
 
44
44
  Filter and render resolution honours nested-module controller
45
45
  qualification (`module Admin; class WidgetsController` resolves
@@ -30,9 +30,9 @@ errors_demo.rb:25:1: error: `User.find` expects at least 1 argument, got 0 [plug
30
30
  | Recognised `Model.find` / `Model.find_by` / `Model.where` call | `:info` | `plugin.activerecord.model-call` |
31
31
  | `Model.find_by(unknown: ...)` / `Model.where(unknown: ...)` | `:error` | `plugin.activerecord.unknown-column` |
32
32
  | `Model.find` with 0 args | `:error` | `plugin.activerecord.wrong-arity` |
33
- | `db/schema.rb` not readable | `:warning` | `plugin.activerecord.load-error` |
33
+ | No schema source (`db/schema.rb` or `db/structure.sql`) readable | `:warning` | `plugin.activerecord.load-error` |
34
34
 
35
- Did-you-mean suggestions use Levenshtein distance 3 against
35
+ Did-you-mean suggestions use `DidYouMean` fuzzy matching against
36
36
  the resolved table's column names.
37
37
 
38
38
  ## Configuration
@@ -42,13 +42,16 @@ plugins:
42
42
  - gem: rigor-activerecord
43
43
  config:
44
44
  schema_file: "db/schema.rb" # default
45
+ structure_sql_file: "db/structure.sql" # default (fallback when schema_file is absent)
45
46
  model_search_paths: ["app/models"] # default
46
47
  model_base_classes: ["ApplicationRecord", "ActiveRecord::Base"] # default
47
48
  ```
48
49
 
49
- All three keys are optional. Tweak them when:
50
+ All keys are optional. Tweak them when:
50
51
 
51
52
  - the schema lives elsewhere (`schema_file: "shared/db/schema.rb"`);
53
+ - the project uses `schema_format = :sql` and its dump is not at the
54
+ default path (`structure_sql_file: "db/structure.sql"`);
52
55
  - models are in a non-standard directory
53
56
  (`model_search_paths: ["domain/models", "engines/billing/app/models"]`);
54
57
  - the base class is custom
@@ -78,8 +81,12 @@ never surfaces a false `call.undefined-method`.
78
81
  `User < ApplicationRecord` is not discovered. Either add `User`
79
82
  to `model_base_classes`, or list every concrete model
80
83
  explicitly.
81
- - **`db/schema.rb` only.** `db/structure.sql` (raw SQL dumps) is
82
- not supported in this iteration.
84
+ - **PostgreSQL `db/structure.sql` fallback.** When `db/schema.rb` is
85
+ absent, the plugin parses `db/structure.sql` (the `schema_format =
86
+ :sql` dump) for the same column/type table. It reads PostgreSQL DDL
87
+ only; a column whose SQL type has no Ruby mapping (a custom enum,
88
+ `tsvector`, `ltree`) degrades to `Object` (never dropped), and
89
+ non-`public`-schema partition tables are skipped.
83
90
  - **Column reads, not setters.** The plugin types instance-side
84
91
  column *reads* (`user.name`, `user.admin?`) and singular
85
92
  associations, but not the `name=` setter or the dirty-tracking
@@ -59,6 +59,8 @@ plugins:
59
59
  routes_file: "config/routes.rb" # default
60
60
  helper_paths: ["app"] # default; dirs scanned for
61
61
  # project-defined *_path / *_url methods
62
+ grape_api_paths: ["lib/api", "app/api"]
63
+ # default; dirs scanned for Grape API classes
62
64
  ```
63
65
 
64
66
  `helper_paths` lets the plugin also register URL builders you
@@ -66,6 +68,15 @@ define yourself (e.g. a private `def callback_url` under
66
68
  `app/controllers` or `app/lib`), so calls to them are not flagged
67
69
  as unknown helpers.
68
70
 
71
+ `grape_api_paths` is where the plugin looks for Grape API classes.
72
+ If you mount one, the `grape-path-helpers` gem generates helpers
73
+ named after each route's path (`api_v4_groups_badges_path`) from
74
+ grape's *runtime* route table, which no static parser can
75
+ enumerate. The plugin reads your API's `prefix` and `version`
76
+ declarations instead and treats the namespace they open as
77
+ unknowable-but-valid, so those calls are never flagged. The `_url`
78
+ form still is: `grape-path-helpers` defines only `_path` helpers.
79
+
69
80
  ## What it provides
70
81
 
71
82
  The parsed helper table is published as the `:helper_table`
@@ -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.compact_child_nodes.each { |child| walk(child, in_loop_or_block: child_in_loop_or_block) }
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.compact_child_nodes.each { |child| walk_for_def_nodes(child) }
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.compact_child_nodes.each { |child| gather_read_names(child, accumulator) }
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.compact_child_nodes.each { |child| gather_write_nodes(child, accumulator) }
115
+ node.rigor_each_child { |child| gather_write_nodes(child, accumulator) }
114
116
  accumulator
115
117
  end
116
118