rigortype 0.4.0 → 0.4.2

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 (237) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +4 -3
  3. data/data/builtins/ruby_core/io.yml +1 -0
  4. data/data/builtins/ruby_core/proc.yml +4 -0
  5. data/data/builtins/ruby_core/random.yml +4 -0
  6. data/data/builtins/ruby_core/struct.yml +3 -0
  7. data/data/core_overlay/env.rbs +34 -0
  8. data/data/core_overlay/gc.rbs +50 -0
  9. data/data/core_overlay/hash.rbs +45 -0
  10. data/data/core_overlay/integer.rbs +31 -0
  11. data/data/core_overlay/io_buffer.rbs +27 -0
  12. data/data/core_overlay/kernel.rbs +40 -0
  13. data/data/core_overlay/match_data.rbs +28 -0
  14. data/data/core_overlay/module.rbs +51 -0
  15. data/data/core_overlay/object_space.rbs +29 -0
  16. data/data/core_overlay/proc.rbs +45 -0
  17. data/data/core_overlay/range.rbs +40 -0
  18. data/data/core_overlay/source_range.rbs +78 -0
  19. data/data/core_overlay/string.rbs +86 -0
  20. data/data/vendored_gem_sigs/prime/integer-extension.rbs +41 -0
  21. data/data/vendored_gem_sigs/prime/prime.rbs +372 -0
  22. data/docs/handbook/01-getting-started.md +3 -3
  23. data/docs/handbook/06-classes.md +3 -1
  24. data/docs/handbook/08-understanding-errors.md +3 -2
  25. data/docs/handbook/11-sig-gen.md +6 -0
  26. data/docs/handbook/appendix-liskov.md +5 -0
  27. data/docs/install.md +87 -15
  28. data/docs/manual/02-cli-reference.md +2 -2
  29. data/docs/manual/03-configuration.md +6 -1
  30. data/docs/manual/04-diagnostics.md +2 -1
  31. data/docs/manual/06-baseline.md +2 -0
  32. data/docs/manual/07-plugins.md +1 -1
  33. data/docs/manual/08-skills.md +4 -3
  34. data/docs/manual/12-caching.md +43 -4
  35. data/docs/manual/14-rails-quickstart.md +19 -11
  36. data/docs/manual/18-removing-dead-code.md +32 -2
  37. data/docs/manual/plugins/README.md +6 -1
  38. data/docs/manual/plugins/rigor-activerecord.md +21 -0
  39. data/docs/manual/plugins/rigor-alba.md +60 -0
  40. data/docs/manual/plugins/rigor-minitest.md +22 -4
  41. data/docs/manual/plugins/rigor-typelizer.md +71 -0
  42. data/lib/rigor/analysis/check_rules/ivar_write_collector.rb +27 -3
  43. data/lib/rigor/analysis/check_rules/lexical_method_sites.rb +21 -92
  44. data/lib/rigor/analysis/check_rules/ruby_deprecations.rb +275 -0
  45. data/lib/rigor/analysis/check_rules/rule_ids.rb +4 -0
  46. data/lib/rigor/analysis/check_rules/source_arity.rb +184 -61
  47. data/lib/rigor/analysis/check_rules.rb +407 -144
  48. data/lib/rigor/analysis/dependency_recorder.rb +215 -18
  49. data/lib/rigor/analysis/dependency_source_inference/builder.rb +8 -1
  50. data/lib/rigor/analysis/dependency_source_inference/gem_resolver.rb +21 -0
  51. data/lib/rigor/analysis/dependency_source_inference/index.rb +36 -11
  52. data/lib/rigor/analysis/dependency_source_inference/walker.rb +196 -27
  53. data/lib/rigor/analysis/erb_template_detector.rb +2 -1
  54. data/lib/rigor/analysis/incremental.rb +29 -9
  55. data/lib/rigor/analysis/incremental_run_slot/write_guard.rb +291 -0
  56. data/lib/rigor/analysis/incremental_run_slot.rb +173 -0
  57. data/lib/rigor/analysis/incremental_session.rb +287 -50
  58. data/lib/rigor/analysis/plugin_fact_fingerprint.rb +24 -13
  59. data/lib/rigor/analysis/reachability/graph.rb +123 -23
  60. data/lib/rigor/analysis/reachability/scan.rb +290 -36
  61. data/lib/rigor/analysis/reachability/scan_cache.rb +2 -1
  62. data/lib/rigor/analysis/rule_catalog.rb +43 -0
  63. data/lib/rigor/analysis/run_cache_key.rb +43 -9
  64. data/lib/rigor/analysis/run_stats.rb +2 -2
  65. data/lib/rigor/analysis/runner/diagnostic_aggregator.rb +88 -6
  66. data/lib/rigor/analysis/runner/pool_coordinator.rb +52 -13
  67. data/lib/rigor/analysis/runner/project_pre_passes.rb +21 -13
  68. data/lib/rigor/analysis/runner/run_snapshots.rb +6 -1
  69. data/lib/rigor/analysis/runner.rb +371 -49
  70. data/lib/rigor/analysis/shadow_harness.rb +27 -0
  71. data/lib/rigor/analysis/source_rbs_gate.rb +157 -0
  72. data/lib/rigor/analysis/template_unit_paths.rb +6 -4
  73. data/lib/rigor/analysis/template_units.rb +15 -5
  74. data/lib/rigor/analysis/worker_session.rb +11 -7
  75. data/lib/rigor/cache/descriptor.rb +40 -1
  76. data/lib/rigor/cache/file_digest.rb +28 -0
  77. data/lib/rigor/cache/incremental_snapshot.rb +92 -8
  78. data/lib/rigor/cache/store.rb +63 -3
  79. data/lib/rigor/cache/value_digest.rb +325 -0
  80. data/lib/rigor/cli/annotate_command.rb +5 -0
  81. data/lib/rigor/cli/baseline_command.rb +7 -18
  82. data/lib/rigor/cli/check_command.rb +85 -14
  83. data/lib/rigor/cli/doctor_command.rb +1 -1
  84. data/lib/rigor/cli/effects_report.rb +2 -2
  85. data/lib/rigor/cli/trace_command.rb +5 -0
  86. data/lib/rigor/cli/type_of_command.rb +5 -3
  87. data/lib/rigor/cli/unused_command.rb +16 -10
  88. data/lib/rigor/cli.rb +11 -3
  89. data/lib/rigor/config_audit.rb +3 -2
  90. data/lib/rigor/config_path.rb +29 -0
  91. data/lib/rigor/configuration/severity_profile.rb +11 -0
  92. data/lib/rigor/configuration.rb +52 -7
  93. data/lib/rigor/effects/plugin_facts.rb +6 -6
  94. data/lib/rigor/environment/bundle_sig_discovery.rb +5 -3
  95. data/lib/rigor/environment/gated_signature_guard.rb +43 -0
  96. data/lib/rigor/environment/gated_signature_plan.rb +339 -0
  97. data/lib/rigor/environment/lockfile_resolver.rb +10 -1
  98. data/lib/rigor/environment/rbs_collection_discovery.rb +10 -1
  99. data/lib/rigor/environment/rbs_coverage_report.rb +1 -1
  100. data/lib/rigor/environment/rbs_loader.rb +284 -15
  101. data/lib/rigor/environment/required_features.rb +206 -0
  102. data/lib/rigor/environment/source_rbs_synthesis.rb +147 -0
  103. data/lib/rigor/environment.rb +34 -73
  104. data/lib/rigor/inference/acceptance.rb +28 -3
  105. data/lib/rigor/inference/block_parameter_binder.rb +29 -13
  106. data/lib/rigor/inference/builtins/cruby_definers.rb +61 -0
  107. data/lib/rigor/inference/builtins/method_catalog.rb +3 -0
  108. data/lib/rigor/inference/captured_locals.rb +7 -1
  109. data/lib/rigor/inference/declaration_walk/context.rb +195 -0
  110. data/lib/rigor/inference/declaration_walk/errors.rb +18 -0
  111. data/lib/rigor/inference/declaration_walk/shadow.rb +147 -0
  112. data/lib/rigor/inference/declaration_walk/traversal.rb +441 -0
  113. data/lib/rigor/inference/declaration_walk.rb +7 -0
  114. data/lib/rigor/inference/def_node_resolver.rb +26 -3
  115. data/lib/rigor/inference/definer_resolution.rb +266 -0
  116. data/lib/rigor/inference/expression_typer.rb +351 -61
  117. data/lib/rigor/inference/external_ancestor_resolution.rb +13 -16
  118. data/lib/rigor/inference/global_write_census.rb +7 -4
  119. data/lib/rigor/inference/guard_rebinding.rb +54 -13
  120. data/lib/rigor/inference/in_effect_refinements/proc_literals.rb +83 -0
  121. data/lib/rigor/inference/in_effect_refinements/refine_defs.rb +54 -0
  122. data/lib/rigor/inference/in_effect_refinements/scope_reads.rb +282 -0
  123. data/lib/rigor/inference/in_effect_refinements/walk.rb +276 -0
  124. data/lib/rigor/inference/in_effect_refinements.rb +256 -0
  125. data/lib/rigor/inference/key_presence_guard/scan.rb +281 -0
  126. data/lib/rigor/inference/key_presence_guard.rb +303 -0
  127. data/lib/rigor/inference/macro_block_self_type.rb +73 -14
  128. data/lib/rigor/inference/method_dispatcher/alias_strict_nominals.rb +36 -0
  129. data/lib/rigor/inference/method_dispatcher/arithmetic_sequence_elements.rb +73 -0
  130. data/lib/rigor/inference/method_dispatcher/imprecise_argument.rb +68 -0
  131. data/lib/rigor/inference/method_dispatcher/integer_step_block_params.rb +81 -0
  132. data/lib/rigor/inference/method_dispatcher/iterator_dispatch.rb +6 -0
  133. data/lib/rigor/inference/method_dispatcher/keyword_arguments.rb +267 -0
  134. data/lib/rigor/inference/method_dispatcher/method_folding.rb +2 -1
  135. data/lib/rigor/inference/method_dispatcher/overload_selector.rb +122 -105
  136. data/lib/rigor/inference/method_dispatcher/rbs_dispatch.rb +305 -54
  137. data/lib/rigor/inference/method_dispatcher/splat_arity.rb +71 -0
  138. data/lib/rigor/inference/method_dispatcher.rb +44 -11
  139. data/lib/rigor/inference/module_function_state.rb +214 -0
  140. data/lib/rigor/inference/multi_target_binder.rb +21 -16
  141. data/lib/rigor/inference/narrowing.rb +166 -22
  142. data/lib/rigor/inference/object_mixins.rb +180 -0
  143. data/lib/rigor/inference/parameter_inference_collector.rb +10 -9
  144. data/lib/rigor/inference/project_patched_methods.rb +10 -5
  145. data/lib/rigor/inference/project_patched_scanner.rb +71 -3
  146. data/lib/rigor/inference/refine_census.rb +239 -0
  147. data/lib/rigor/inference/refine_self.rb +317 -0
  148. data/lib/rigor/inference/refined_dispatch/own_method.rb +403 -0
  149. data/lib/rigor/inference/refined_dispatch.rb +272 -0
  150. data/lib/rigor/inference/scope_indexer/certainty.rb +176 -0
  151. data/lib/rigor/inference/scope_indexer/class_cvars_collector.rb +35 -0
  152. data/lib/rigor/inference/scope_indexer/def_nestings_collector.rb +47 -0
  153. data/lib/rigor/inference/scope_indexer/layered_def_nestings.rb +36 -0
  154. data/lib/rigor/inference/scope_indexer/member_layouts_collector.rb +61 -0
  155. data/lib/rigor/inference/scope_indexer/mixin_accumulator.rb +143 -0
  156. data/lib/rigor/inference/scope_indexer/superclasses_collector.rb +64 -0
  157. data/lib/rigor/inference/scope_indexer.rb +2129 -354
  158. data/lib/rigor/inference/singleton_hook_decline.rb +253 -0
  159. data/lib/rigor/inference/statement_evaluator.rb +149 -32
  160. data/lib/rigor/inference/string_mutation.rb +9 -3
  161. data/lib/rigor/inference/synthetic_method_scanner.rb +2 -1
  162. data/lib/rigor/inference/version_guard.rb +19 -10
  163. data/lib/rigor/language_server/project_context.rb +31 -0
  164. data/lib/rigor/language_server/server.rb +18 -0
  165. data/lib/rigor/plugin/base.rb +28 -8
  166. data/lib/rigor/plugin/inflector.rb +1 -1
  167. data/lib/rigor/plugin/io_boundary.rb +124 -7
  168. data/lib/rigor/plugin/isolation.rb +1 -1
  169. data/lib/rigor/plugin/macro/block_as_method.rb +51 -8
  170. data/lib/rigor/plugin/trust_policy.rb +3 -3
  171. data/lib/rigor/project_environment.rb +29 -6
  172. data/lib/rigor/protection/discovery_seed.rb +18 -9
  173. data/lib/rigor/protection/mutation_cache.rb +4 -4
  174. data/lib/rigor/protection/mutator.rb +1 -1
  175. data/lib/rigor/reflection/constant_ancestors.rb +211 -46
  176. data/lib/rigor/reflection/constant_path.rb +33 -7
  177. data/lib/rigor/reflection.rb +41 -10
  178. data/lib/rigor/scope/discovery_index.rb +258 -3
  179. data/lib/rigor/scope/resolution_chain/relevance.rb +165 -0
  180. data/lib/rigor/scope/resolution_chain.rb +1032 -0
  181. data/lib/rigor/scope.rb +480 -328
  182. data/lib/rigor/sig_gen/alias_index.rb +1 -1
  183. data/lib/rigor/sig_gen/classification.rb +4 -1
  184. data/lib/rigor/sig_gen/effect_annotation.rb +2 -2
  185. data/lib/rigor/sig_gen/generator.rb +77 -21
  186. data/lib/rigor/sig_gen/method_candidate.rb +2 -2
  187. data/lib/rigor/sig_gen/skip_reason_catalog.rb +12 -0
  188. data/lib/rigor/source/alias_names.rb +52 -0
  189. data/lib/rigor/source/literals.rb +3 -2
  190. data/lib/rigor/source/symbol_name.rb +20 -0
  191. data/lib/rigor/type/combinator.rb +59 -3
  192. data/lib/rigor/type/float_range.rb +4 -1
  193. data/lib/rigor/type/nominal.rb +6 -1
  194. data/lib/rigor/version.rb +1 -1
  195. data/plugins/rigor-ac-library-rb/lib/rigor/plugin/ac_library_rb.rb +28 -0
  196. data/plugins/rigor-ac-library-rb/lib/rigor-ac-library-rb.rb +3 -0
  197. data/plugins/rigor-ac-library-rb/sig/ac_library_rb/convolution.rbs +7 -0
  198. data/plugins/rigor-ac-library-rb/sig/ac_library_rb/data_structures.rbs +160 -0
  199. data/plugins/rigor-ac-library-rb/sig/ac_library_rb/graph.rbs +94 -0
  200. data/plugins/rigor-ac-library-rb/sig/ac_library_rb/math.rbs +42 -0
  201. data/plugins/rigor-ac-library-rb/sig/ac_library_rb/modint.rbs +71 -0
  202. data/plugins/rigor-ac-library-rb/sig/ac_library_rb/object.rbs +37 -0
  203. data/plugins/rigor-ac-library-rb/sig/ac_library_rb/segtree.rbs +83 -0
  204. data/plugins/rigor-actionpack/lib/rigor/plugin/actionpack/analyzer.rb +2 -1
  205. data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord/association_roots.rb +100 -0
  206. data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord/model_discoverer.rb +94 -10
  207. data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord.rb +43 -2
  208. data/plugins/rigor-activerecord/sig/active_record/relation.rbs +3 -1
  209. data/plugins/rigor-alba/lib/rigor/plugin/alba/resource_collector.rb +149 -0
  210. data/plugins/rigor-alba/lib/rigor/plugin/alba/resource_discoverer.rb +74 -0
  211. data/plugins/rigor-alba/lib/rigor/plugin/alba/resource_index.rb +104 -0
  212. data/plugins/rigor-alba/lib/rigor/plugin/alba.rb +85 -0
  213. data/plugins/rigor-alba/lib/rigor-alba.rb +3 -0
  214. data/plugins/rigor-alba/sig/alba.rbs +4 -0
  215. data/plugins/rigor-ffi/lib/rigor/plugin/ffi/analyzer.rb +2 -2
  216. data/plugins/rigor-ffi/lib/rigor/plugin/ffi/target_detector.rb +29 -12
  217. data/plugins/rigor-ffi/lib/rigor/plugin/ffi.rb +13 -2
  218. data/plugins/rigor-minitest/lib/rigor/plugin/minitest/assertion_analyzer.rb +130 -21
  219. data/plugins/rigor-minitest/lib/rigor/plugin/minitest.rb +6 -4
  220. data/plugins/rigor-pundit/lib/rigor/plugin/pundit/analyzer.rb +4 -4
  221. data/plugins/rigor-rails-i18n/lib/rigor/plugin/rails_i18n/analyzer.rb +1 -1
  222. data/plugins/rigor-rails-routes/lib/rigor/plugin/rails_routes/routes_parser.rb +1 -1
  223. data/plugins/rigor-rbs-inline/lib/rigor/plugin/rbs_inline.rb +31 -0
  224. data/plugins/rigor-typelizer/lib/rigor/plugin/typelizer/serializer_collector.rb +102 -0
  225. data/plugins/rigor-typelizer/lib/rigor/plugin/typelizer/serializer_discoverer.rb +87 -0
  226. data/plugins/rigor-typelizer/lib/rigor/plugin/typelizer/serializer_index.rb +76 -0
  227. data/plugins/rigor-typelizer/lib/rigor/plugin/typelizer.rb +73 -0
  228. data/plugins/rigor-typelizer/lib/rigor-typelizer.rb +3 -0
  229. data/sig/rigor/analysis/dependency_source_inference/index.rbs +1 -1
  230. data/sig/rigor/environment.rbs +2 -0
  231. data/sig/rigor/plugin/io_boundary.rbs +4 -0
  232. data/sig/rigor/scope.rbs +69 -4
  233. data/sig/rigor/source.rbs +4 -0
  234. data/sig/rigor.rbs +3 -1
  235. data/skills/rigor-project-init/SKILL.md +37 -11
  236. data/skills/rigor-project-init/references/02-configure.md +7 -2
  237. metadata +90 -1
data/docs/install.md CHANGED
@@ -10,19 +10,42 @@ skill (`rigor-project-init` for a project that has never run Rigor).
10
10
  **Do not add Rigor to the project's `Gemfile`** — Rigor is a
11
11
  standalone tool, not a library.
12
12
 
13
+ **Install the latest release, not whatever is already on the
14
+ machine.** A `rigor` already on PATH, or a version manager that
15
+ resolves to an older release, is not a reason to stop early: Rigor
16
+ changes quickly, and the skills this guide hands off to are written
17
+ for the current release. Step 1 looks up the latest version; Step 2
18
+ installs it and Step 3 checks it.
19
+
13
20
  ---
14
21
 
15
22
  ## Step 1 — Detect the environment
16
23
 
17
- Run these four checks and note which tools are available:
24
+ Run these checks and note which tools are available:
18
25
 
19
26
  ```sh
20
27
  which mise # preferred — see Step 2A
21
28
  which asdf # fallback — see Step 2B
22
29
  ruby --version 2>/dev/null | head -1 # is Ruby 4.0 already on PATH?
23
- which docker # last resort — see Step 2D
30
+ which docker # last resort — see "Last resort — Docker"
31
+ rigor --version 2>/dev/null # an existing install, if any
32
+ ```
33
+
34
+ Then look up the latest Rigor release on RubyGems:
35
+
36
+ ```sh
37
+ curl -fsS https://rubygems.org/api/v1/versions/rigortype/latest.json
24
38
  ```
25
39
 
40
+ It prints `{"version":"X.Y.Z"}`. Below, `<LATEST>` stands for that
41
+ `X.Y.Z`: replace it with the version you got, never type `<LATEST>`
42
+ literally. Without `curl`, `gem search --remote --exact rigortype`
43
+ gives the same answer once Ruby is available. If neither works, tell
44
+ the user you could not determine the latest version instead of
45
+ guessing one.
46
+
47
+ An existing `rigor` older than `<LATEST>` still goes through Step 2;
48
+ tell the user which version they have and which you are installing.
26
49
  Then proceed to the **first** matching case below.
27
50
 
28
51
  ---
@@ -40,23 +63,51 @@ that can be committed alongside the code. Other contributors — and CI
40
63
  — run `mise install` to restore those versions with no `Gemfile`
41
64
  involvement.
42
65
 
43
- Run in the project root:
66
+ First ask mise which Rigor version it would pick:
67
+
68
+ ```sh
69
+ mise latest gem:rigortype
70
+ ```
71
+
72
+ If that is older than `<LATEST>`, mise is holding the newer release
73
+ back — usually its `minimum_release_age` quarantine, which hides
74
+ releases for a while after they are published (`mise ls-remote
75
+ gem:rigortype` then warns `1 newer gem:rigortype release hidden by
76
+ minimum_release_age` on stderr). A bare `mise use gem:rigortype`
77
+ would install the older version with no error, so this is the step where agents
78
+ silently end up on a stale Rigor. The quarantine is a supply-chain
79
+ safeguard, so do not bypass it on your own: tell the user both
80
+ versions and ask which to install. `mise settings get
81
+ minimum_release_age` tells you whose choice it is: an error ("not
82
+ set") means mise's built-in default is in effect and you can recommend
83
+ `<LATEST>`; a value means the user configured it, so recommend
84
+ respecting it.
85
+
86
+ Then run in the project root, with the version the user agreed to:
44
87
 
45
88
  ```sh
46
89
  mise use ruby@4.0
47
- mise use --pin gem:rigortype
90
+ mise use --pin gem:rigortype@<LATEST>
48
91
  ```
49
92
 
50
93
  `mise use` installs the tools and writes their versions to `mise.toml`
51
- in one step. Commit `mise.toml` so the version is shared.
94
+ in one step. Commit `mise.toml` so the version is shared. Name the
95
+ version explicitly, as above: an explicit version is installed even
96
+ while the quarantine hides it from `mise latest`.
52
97
 
53
- `--pin` records the exact Rigor version (`"gem:rigortype" = "0.2.9"`).
98
+ `--pin` records the exact Rigor version (`"gem:rigortype" = "X.Y.Z"`).
54
99
  Without it mise writes `"gem:rigortype" = "latest"`, which every
55
100
  machine re-resolves to whatever is newest when it first installs — so
56
101
  a committed `latest` does not give the team one shared version. The
57
102
  trade-off: a pin will not move on its own, and `mise outdated` cannot
58
- report a pinned tool as behind. Upgrade with `mise upgrade --bump
59
- gem:rigortype`.
103
+ report a pinned tool as behind. Upgrade by running `mise use --pin
104
+ gem:rigortype@<version>` again with the newer version; `mise upgrade
105
+ --bump gem:rigortype` goes through the same quarantine as a bare
106
+ `mise use`.
107
+
108
+ A user-wide `~/.config/mise/config.toml` entry for `gem:rigortype`
109
+ keeps applying outside this project. Mention it if it names an older
110
+ version, but do not edit global config without the user's consent.
60
111
 
61
112
  Then verify:
62
113
 
@@ -88,7 +139,8 @@ gem is installed with `gem install` after setting the Ruby version.
88
139
  ```sh
89
140
  asdf install ruby latest:4.0
90
141
  asdf local ruby latest:4.0
91
- gem install rigortype
142
+ gem install rigortype -v <LATEST>
143
+ asdf reshim ruby
92
144
  ```
93
145
 
94
146
  Verify:
@@ -108,7 +160,7 @@ pinning; see <https://mise.jdx.dev/getting-started.html>.
108
160
  If `ruby --version` reports `ruby 4.0.*`, install the gem directly:
109
161
 
110
162
  ```sh
111
- gem install rigortype
163
+ gem install rigortype -v <LATEST>
112
164
  ```
113
165
 
114
166
  Verify:
@@ -152,6 +204,11 @@ and does not integrate well with editor LSP. Use it only when a
152
204
  host-side Ruby 4.0 is genuinely unavailable (for example, Windows
153
205
  without WSL). For all other environments, prefer Case A–D above.
154
206
 
207
+ With Docker there is no `rigor` on the host PATH, so Step 3 checks the
208
+ image instead: `docker run --rm ghcr.io/rigortype/rigor:latest rigor
209
+ --version`. The image can trail RubyGems by a release; report its
210
+ version to the user rather than treating a mismatch as a failure.
211
+
155
212
  ---
156
213
 
157
214
  ## Step 3 — Verify the installation
@@ -160,8 +217,23 @@ without WSL). For all other environments, prefer Case A–D above.
160
217
  rigor --version
161
218
  ```
162
219
 
163
- A version string like `rigor 0.2.x` confirms a successful install.
164
- If the command is not found, revisit Step 2 for your case.
220
+ The reported version should be the one you installed in Step 2
221
+ (normally `<LATEST>`). If it is older, you are running a different
222
+ `rigor` than the one just installed. With mise, first check what the
223
+ project resolves to:
224
+
225
+ ```sh
226
+ mise exec -- rigor --version
227
+ mise which rigor
228
+ ```
229
+
230
+ If `mise exec` reports the new version, the shell's PATH is stale (a
231
+ non-interactive shell does not re-run mise's activation after `mise
232
+ use`); run `eval "$(mise hook-env)"`, or use `mise exec -- rigor` for
233
+ the remaining steps. Otherwise run `which -a rigor` to find the older
234
+ install. Do not uninstall anything to fix this without asking, and do
235
+ not continue on the older version without telling the user. If the
236
+ command is not found, revisit Step 2 for your case.
165
237
 
166
238
  ---
167
239
 
@@ -192,6 +264,6 @@ project is set up, re-run `rigor skill describe` for the step after
192
264
  that.
193
265
 
194
266
  If `rigor skill describe` is not recognised, your Rigor version predates
195
- it. Run `rigor --version` and upgrade with `mise upgrade --bump
196
- gem:rigortype` (or `gem update rigortype` for Case B/C); on an older
197
- version, run `rigor skill rigor-project-init` directly.
267
+ it: you are not on the version Step 1 found. Go back to Step 3. If
268
+ you are staying on an older version with the user's agreement, run
269
+ `rigor skill rigor-project-init` directly.
@@ -34,8 +34,8 @@ the `paths:` list from the configuration file.
34
34
  | `--format=FORMAT` | Output format. Default `text`. Also `json` (structured stream), plus the CI-native renderings `sarif`, `github`, `gitlab`, `checkstyle`, `junit`, and `teamcity` — see [Running Rigor in CI](11-ci.md). |
35
35
  | `--no-ci-detect` | Disable CI auto-detection — by default `text` output also emits the running CI's native annotations / hint (`RIGOR_CI_DETECT=0` does the same). See [Running Rigor in CI § auto-detection](11-ci.md). |
36
36
  | `--explain` | Surface fail-soft fallback events as `info` diagnostics. |
37
- | `--no-cache` | Skip the persistent cache for this run. |
38
- | `--incremental` | Re-analyse only the files changed since the last run plus the files that depend on them, serving the rest from a cross-process disk snapshot (ADR-46). Diagnostics are identical to a full run; a config / gem / version change (or a file added or removed) transparently forces a full re-analysis. See [Caching](12-caching.md). |
37
+ | `--no-cache` | Skip the persistent cache for this run. With `--incremental` it is a cold full analysis: the incremental snapshot and the run-result cache are neither read nor written, so the next plain `--incremental` run still uses the snapshot it had before. |
38
+ | `--incremental` | Re-analyse only the files changed since the last run plus the files that depend on them, serving the rest from a cross-process disk snapshot (ADR-46). Diagnostics are identical to a full run; a config / gem / version change (or a file added or removed) transparently forces a full re-analysis. A run with nothing changed is answered from the cache without starting the analyzer. See [Caching](12-caching.md). |
39
39
  | `--verify-incremental` | Acceptance gate: run the incremental analyzer against a full `--no-cache` run and assert the diagnostics are byte-identical, then exit (0 on match, 1 with the differing diagnostics on mismatch). Used in CI to guarantee `--incremental` never serves a stale result. |
40
40
  | `--clear-cache` | Delete the cache directory before running. |
41
41
  | `--cache-stats` | Print the on-disk cache inventory when finished — on stdout under `--format text`, on stderr under every other format, so machine-readable output stays a parseable document. `--clear-cache`'s and `--verify-incremental`'s notes follow the same rule. |
@@ -21,6 +21,11 @@ discovery entirely.
21
21
 
22
22
  All relative paths in a config file resolve against that
23
23
  file's own directory.
24
+ A leading `~/` (or a bare `~`) in a path setting expands to your home directory; `~name` is an ordinary
25
+ directory called `~name`, not another user's home. This covers `paths:`, `signature_paths:`, `test_paths:`,
26
+ `pre_eval:`, `plugins_io.allowed_paths:`, `includes:`, `bundler.bundle_path:`, `bundler.lockfile:` and
27
+ `rbs_collection.lockfile:`. It does not apply to `cache.path:`, `baseline:` or `effects.snapshot.path:`, which stay
28
+ literal and relative to the working directory.
24
29
 
25
30
  ## Editor validation
26
31
 
@@ -57,7 +62,7 @@ cache:
57
62
 
58
63
  | Key | Type | Default | Meaning |
59
64
  | --- | --- | --- | --- |
60
- | `target_ruby` | String | `"4.0"` | The Ruby version *your* project runs — `"X.Y"`, `"X.Y.Z"`, or `"latest"`. Independent of the Ruby Rigor itself runs on. |
65
+ | `target_ruby` | String | `"4.0"` | The lowest Ruby version *your* project runs on — `"X.Y"`, `"X.Y.Z"`, or `"latest"`. Independent of the Ruby Rigor itself runs on. Every value selects the syntax Rigor parses. A version you set explicitly also turns on the Ruby-deprecation rules for that version ([`call.deprecated-ruby2-keywords`](04-diagnostics.md#rule-call-deprecated-ruby2-keywords) from `"4.1"`); the default and `"latest"` never do, which is why `rigor init` writes the key commented out. |
61
66
  | `paths` | Array | `["lib"]` | Directories or files to analyse. |
62
67
  | `exclude` | Array | `[]` | Glob patterns to skip. `vendor/bundle`, `.bundle`, and `node_modules` are always excluded. |
63
68
  | `test_paths` | Array | `nil` | The project's test roots: the directories (or files) holding its tests. Relative entries resolve against the config file's directory. Unset auto-detects whichever of `spec/` and `test/` exist; `[]` declares none. `rigor sig-gen --params=observed` reads call sites there to type parameters, and names on stderr a declared root that does not exist. Test roots are not analysed unless `paths:` also lists them, and changing them invalidates no cache. |
@@ -56,6 +56,7 @@ carries no `documentation_url`.
56
56
  | <a id="rule-call-argument-type-mismatch"></a>`call.argument-type-mismatch` | An argument's type provably violates the parameter contract. | high |
57
57
  | <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 |
58
58
  | <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 |
59
+ | <a id="rule-call-deprecated-ruby2-keywords"></a>`call.deprecated-ruby2-keywords` | A call to the `ruby2_keywords` family Ruby 4.1 deprecates: `Module#ruby2_keywords`, top-level `ruby2_keywords` and `Proc#ruby2_keywords` (removed in Ruby 4.4), or `Hash.ruby2_keywords_hash?` and `Hash.ruby2_keywords_hash` (removed in Ruby 4.5), directly or through `send` with a literal Symbol. Runs only when `.rigor.yml` sets `target_ruby` explicitly to `"4.1"` or later: the default, an older version and `"latest"` report nothing, because on Ruby 4.0 the call is correct. Not reported when a project or `pre_eval:` file defines a method by the same name, inside a block at the top level, an `instance_eval` / `instance_exec` block or an instance method of a class or module, or where a `RUBY_VERSION` guard rules the call out on the stated Ruby or cannot be decided. `info` under `lenient`, `warning` under `balanced`, `error` under `strict`. | high |
59
60
  | <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 |
60
61
  | <a id="rule-flow-always-raises"></a>`flow.always-raises` | The expression provably raises on every reachable path. | high |
61
62
  | <a id="rule-flow-unreachable-branch"></a>`flow.unreachable-branch` | An `if` / `unless` / ternary branch is statically dead. | high |
@@ -66,7 +67,7 @@ carries no `documentation_url`.
66
67
  | <a id="rule-flow-return-in-ensure"></a>`flow.return-in-ensure` | An explicit `return` inside an `ensure` clause — it overrides the method's in-flight return value and silently swallows any in-flight exception. A `return` in a nested `def`, lambda, or `define_method` block inside the `ensure` does not fire (it exits that inner frame). | high |
67
68
  | <a id="rule-flow-shadowed-rescue-clause"></a>`flow.shadowed-rescue-clause` | A `rescue` clause can never run because an earlier clause of the same chain already catches a superclass (or the same class) of every exception class it names. | high |
68
69
  | <a id="rule-def-return-type-mismatch"></a>`def.return-type-mismatch` | The method body's result violates its declared RBS return type. | medium |
69
- | <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 |
70
+ | <a id="rule-def-ivar-write-mismatch"></a>`def.ivar-write-mismatch` | An instance variable is written with a type disagreeing with its first write, unless its RBS declaration (`@x: T` or an `attr_*` member) admits both writes. | high |
70
71
  | <a id="rule-def-method-visibility-mismatch"></a>`def.method-visibility-mismatch` | An explicit-receiver call reaches a private method. | high |
71
72
  | <a id="rule-def-override-visibility-reduced"></a>`def.override-visibility-reduced` | An override reduces the visibility it inherits from a project-defined ancestor. | high |
72
73
  | <a id="rule-def-override-return-widened"></a>`def.override-return-widened` | An override's declared return type widens the inherited return (covariance). | high |
@@ -77,6 +77,8 @@ a diagnostic another layer has already suppressed.
77
77
  | `rigor baseline drift` | Show how buckets have moved — `--only=over` for buckets that grew, `reducible` for ones you have already improved past, `cleared` for empty ones. |
78
78
  | `rigor baseline prune` | Drop buckets that match nothing any more. `--dry-run` previews. |
79
79
 
80
+ `generate` and `regenerate` analyse with your loaded configuration unchanged except that `baseline:` is off, so options such as `bleeding_edge`, `target_ruby` or `severity_overrides` shape the recorded set exactly as they shape `rigor check`.
81
+
80
82
  `generate` and `regenerate` take `--match-mode=rule` (the
81
83
  default — one bucket per file × rule) or `--match-mode=message`
82
84
  (a bucket per distinct message: more precise, more churn).
@@ -54,7 +54,7 @@ each plugin's options — but the families today are:
54
54
  `rigor-ffi` provides the `ffi_binding_recognizer` class DSL on
55
55
  `Rigor::Plugin::Base` to recognize custom binding definitions.
56
56
  - **Other ecosystems** — `rigor-sinatra`, `rigor-hanami`,
57
- `rigor-devise`, `rigor-pundit`, `rigor-sidekiq`,
57
+ `rigor-devise`, `rigor-pundit`, `rigor-alba`, `rigor-typelizer`, `rigor-sidekiq`,
58
58
  `rigor-graphql`, `rigor-statesman`, `rigor-sorbet`,
59
59
  `rigor-typescript-utility-types`,
60
60
  `rigor-activesupport-core-ext`.
@@ -60,9 +60,10 @@ is not repeated here.)
60
60
  - **`rigor-project-init`** — onboards a project from a cold start. It
61
61
  detects the stack (Rails, RSpec, dry-rb, …), proposes the matching
62
62
  [plugins](07-plugins.md), picks an adoption mode — a
63
- [baseline](06-baseline.md) snapshot for an existing codebase or a
64
- zero-diagnostic gate for a clean one — writes a `.rigor.dist.yml`, and
65
- generates the first baseline. Reach for it when setting Rigor up for
63
+ [baseline](06-baseline.md) snapshot by default, new projects
64
+ included, or an opt-in zero-diagnostic gate for users fluent in type
65
+ theory and RBS — writes a `.rigor.dist.yml`, and generates the first
66
+ baseline. Reach for it when setting Rigor up for
66
67
  the first time.
67
68
  - **`rigor-rbs-setup`** — installs community RBS for your gems
68
69
  (`rbs collection install`) so RBS-less dependencies stop typing as
@@ -135,7 +135,7 @@ CI detection that `auto` relies on.
135
135
 
136
136
  | Flag | Effect |
137
137
  | --- | --- |
138
- | `rigor check --no-cache` | Run without reading or writing the persistent cache. |
138
+ | `rigor check --no-cache` | Run without reading or writing the persistent cache. With `--incremental` this includes the snapshot: every file is analysed and the snapshot on disk is left untouched. |
139
139
  | `rigor check --clear-cache` | Delete the cache directory, then run. |
140
140
  | `rigor check --cache-stats` | Print the on-disk cache inventory when the run finishes (on stderr under every `--format` but `text`, so the document on stdout stays parseable). |
141
141
  | `rigor check --incremental` | Re-analyse only what changed; serve the rest from the incremental snapshot (see below). |
@@ -144,9 +144,12 @@ There is no config key to disable caching permanently — the
144
144
  flags are per-run toggles. To run without a persistent cache
145
145
  habitually, point `cache.path` at a disposable directory.
146
146
 
147
+ With `--incremental --no-cache` and an editor buffer (`--tmp-file` with `--instead-of`), the buffer is analysed on its own, as it is when no snapshot exists, and the note says that `--no-cache` skipped the snapshot rather than suggesting `rigor check --incremental`.
148
+
147
149
  ## Size and eviction
148
150
 
149
- A project's active cache set is small (around 2 MB). Entries
151
+ A project's active cache set is a few megabytes (about 10 MB
152
+ for a Rails application the size of Mastodon). Entries
150
153
  are content-keyed, so events like a gem upgrade or an `.rbs`
151
154
  edit write fresh entries and leave the old ones *orphaned* —
152
155
  nothing references them, and no run would otherwise delete
@@ -219,9 +222,45 @@ dependents of removed ones.) As with the rest of the cache, a
219
222
  missing or corrupt snapshot is simply a full run; it can never
220
223
  wedge analysis.
221
224
 
225
+ When nothing has changed since the last `--incremental` run, the
226
+ answer comes straight from the cache, without starting the
227
+ analyzer at all. Rigor first checks the inputs that answer was
228
+ computed from — your source files and view templates, your RBS,
229
+ your configuration and the files your plugins read — and runs the
230
+ ordinary incremental re-check the moment any of them differs. On
231
+ a Rails application the size of Mastodon, an unchanged
232
+ `--incremental` run takes about a quarter of a second. An
233
+ unchanged plain `rigor check` skips the analyzer the same way,
234
+ except when a plugin compiles your view templates, as
235
+ rigor-actionpack does.
236
+
237
+ The shortcut stands aside under `--workers=N` (or
238
+ `parallel.workers:`), `--explain`, `--cache-stats`, `--coverage`,
239
+ an editor buffer, and an `effects:` block, and when the cache
240
+ directory is on a different filesystem from the project; those
241
+ runs re-check as before. It also switches itself off, silently:
242
+
243
+ - when an input changes that the ordinary re-check does not look
244
+ at itself: an auto-detected `sig/` directory (one you did not
245
+ list under `signature_paths:`), a file a narrower
246
+ `rigor check --incremental DIR` only discovers from the rest of
247
+ your configured `paths:`, or a `pre_eval:` file outside the
248
+ analysed paths. The re-check may keep printing the answer from
249
+ before that change; the shortcut does not repeat it;
250
+ - when an `--incremental` run cannot record its answer safely:
251
+ after a run with `--workers=N` that re-analysed files, when a file or directory changes while a run is reading
252
+ it, after a CI cache is restored onto a fresh checkout, or when
253
+ two runs race.
254
+
255
+ Either way it stays off, and every run is an ordinary re-check,
256
+ until the next full analysis. The ordinary re-check never starts
257
+ one by itself: only a configuration, `Gemfile.lock` or Rigor
258
+ version change does, or `--clear-cache`. So the shortcut can stay
259
+ off indefinitely; run `rigor check --incremental --clear-cache`
260
+ once to turn it back on.
261
+
222
262
  `--incremental` is most useful for fast local re-checks and CI
223
- on a changed branch. For a one-shot run on an unchanged project,
224
- the ordinary cache already serves the whole result in one step.
263
+ on a changed branch.
225
264
 
226
265
  ## Concurrency
227
266
 
@@ -90,8 +90,9 @@ It runs eight phases in order:
90
90
  2. **Choose an adoption mode** — proposes either *acknowledge*
91
91
  (snapshot today's diagnostics into a baseline; catch
92
92
  regressions going forward) or *strict* (drive the project to
93
- zero and keep it there). It recommends acknowledge for
94
- codebases with more than ~100 initial diagnostics.
93
+ zero and keep it there). It recommends acknowledge for every
94
+ project, new ones included; strict is an opt-in for users
95
+ fluent in type theory and RBS.
95
96
  3. **Select plugins** — proposes the plugin set matching your
96
97
  detected stack; you confirm or trim the list.
97
98
  4. **Write `.rigor.dist.yml`** — the committed shared config,
@@ -151,14 +152,19 @@ source-of-truth copy is
151
152
 
152
153
  | Mode | When | What happens |
153
154
  | --- | --- | --- |
154
- | **Acknowledge** | Existing codebase with many diagnostics | Record today's diagnostics in a baseline; surface only new ones on each PR. |
155
- | **Strict** | New or small project | Zero outstanding diagnostics; no baseline. |
155
+ | **Acknowledge** | Recommended for every project, new ones included | Record today's diagnostics in a baseline; surface only new ones on each PR. |
156
+ | **Strict** | Opt-in, for users fluent in type theory and RBS | Zero outstanding diagnostics; no baseline. |
156
157
 
157
- If your first `rigor check` reports more than ~100 diagnostics,
158
- acknowledge mode is the natural starting point. You can tighten
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`).
158
+ Start with acknowledge mode even on a brand-new project. Rigor's
159
+ inference still has gaps, so a zero-diagnostic gate meets
160
+ diagnostics on correct code, and clearing those means fixing
161
+ signatures or RBS rather than your code. Choose strict only if
162
+ you are comfortable doing that yourself. You can tighten later.
163
+
164
+ In `.rigor.dist.yml` (Step 3) the mode maps to a
165
+ `severity_profile:`. Acknowledge mode uses the default `balanced`
166
+ (omit the key), or `lenient` when the first `rigor check` reports
167
+ more than ~100 diagnostics; strict mode uses `strict`.
162
168
 
163
169
  ### Step 3 — Write .rigor.dist.yml
164
170
 
@@ -198,7 +204,7 @@ plugins:
198
204
  - rigor-rspec
199
205
  - rigor-factorybot
200
206
 
201
- severity_profile: lenient # "strict" for strict mode; omit for "balanced"
207
+ # severity_profile: lenient # only for >100 initial diagnostics; "strict" for strict mode
202
208
 
203
209
  # baseline: .rigor-baseline.yml # uncomment after Step 6 (acknowledge mode only)
204
210
  ```
@@ -288,7 +294,9 @@ first, then large clusters to record in a baseline.
288
294
 
289
295
  ### Step 6 — Generate a baseline (acknowledge mode)
290
296
 
291
- *Skip this step if you chose strict mode.*
297
+ *Skip this step if you chose strict mode.* Settle `severity_profile:`
298
+ first: changing it afterwards changes which rules fire and leaves the
299
+ baseline out of step.
292
300
 
293
301
  ```sh
294
302
  rigor baseline generate
@@ -145,7 +145,7 @@ Four sections, needing four different decisions:
145
145
  | **Reachable only from test code** | Live test, no production caller Rigor can see or suspect | Work these first |
146
146
  | **Candidates** | Nothing reachable names it | Adjudicate — most are still live |
147
147
  | **Cannot decide** | Something can name it at runtime | Read the reason; do not delete from here |
148
- | **Namespace-only** | A module wrapping live code | Excluded from candidates; count only |
148
+ | **Namespace-only** | A module wrapping code production reaches | Excluded from candidates; count only |
149
149
 
150
150
  Work them in that order — which is not the order you *read* the
151
151
  summary in, where `roots` comes first because it tells you whether to
@@ -162,6 +162,12 @@ section held 22 policy classes whose only remaining caller was
162
162
  `spec/policies/`. Those 22 were not adjudicated, so treat the section
163
163
  as high-signal rather than high-yield; the yield is unmeasured.
164
164
 
165
+ "Test code" is decided by path, relative to the directory you run `rigor unused` in (run it from the
166
+ project root): `spec/` and `test/` anywhere below it, `*_spec.rb` and `*_test.rb`, and the end-to-end
167
+ trees `qa/`, `e2e/` and `features/` at that root. Tooling directories such as
168
+ `rubocop/`, `keeps/`, `tooling/` and `scripts/` count as production, so a class used only by them is
169
+ rooted rather than listed: deleting it would break the tooling.
170
+
165
171
  ### Candidates: sort by how conventional the class is
166
172
 
167
173
  A candidate row is its name and declaration site:
@@ -171,6 +177,23 @@ Candidates — nothing reachable references these (45)
171
177
  1 Api::V1::Timelines::TopicController app/controllers/api/v1/timelines/topic_controller.rb:3
172
178
  ```
173
179
 
180
+ A base class is reachable whenever one of its subclasses is:
181
+ `class Sub < Base` names `Base` on `Sub`'s behalf. So a base class
182
+ that nothing else names appears together with its subclasses when none
183
+ of them is reachable, and the decision belongs to the whole family
184
+ rather than to one row. When a subclass sits under **Cannot decide**,
185
+ its base moves there too.
186
+
187
+ A class declared outside `paths:` — `config/application.rb`'s
188
+ `Application`, a class in an initializer, a spec helper — is not
189
+ itself in the report, so what it names counts as named from its file:
190
+ `config.middleware.use MyMw` keeps `MyMw` live, and a fake subclass in
191
+ `spec/support` keeps its base reachable from tests. The same holds for
192
+ a gem class you reopen inside `paths:`. A name the file declares
193
+ itself still means that declaration: a migration's local
194
+ `class LegacyThing < ActiveRecord::Base` stub does not keep the app's
195
+ `LegacyThing` model live.
196
+
174
197
  The 53 false positives in the adjudicated run fell into a few recurring
175
198
  shapes, and 28 of them were the first one. Recognising these lets you
176
199
  skip most of a list quickly:
@@ -230,7 +253,14 @@ belongs here instead, with the file named.
230
253
 
231
254
  `"Foo".constantize` names `Foo` exactly, so it counts as an ordinary
232
255
  reference and never reaches this section. `"Foo::#{key}".constantize`
233
- can only bound the namespace, so everything under `Foo` is demoted. A
256
+ can only bound the namespace, so everything under `Foo` is demoted;
257
+ `"V#{version}"` is the start of a name, so `V1`, `V2_0` and everything
258
+ under them are. `const_get` looks the name up from its receiver, so
259
+ `const_get("V#{version}")` inside `class Migration` demotes
260
+ `Migration::V1` — and the top-level `V*` names as well, unless the call
261
+ passes `false` as its second argument. A name built into a local
262
+ variable first (`name = "V#{version}"; const_get(name)`) is read the
263
+ same way. A
234
264
  class name appearing as a string in a `.yml`, `.json` or template file
235
265
  demotes the same way — weaker evidence than a constant reference, and
236
266
  neither proof of use nor grounds to call it dead.
@@ -14,7 +14,8 @@ The full catalogue, with a one-line scope for every plugin, is
14
14
  ## Available pages
15
15
 
16
16
  - [rigor-activerecord](rigor-activerecord.md) — ActiveRecord
17
- finder / relation typing and schema-checked columns.
17
+ finder / relation typing and schema-checked columns; roots models reached only through an
18
+ association for `rigor unused`.
18
19
  - [rigor-rails-routes](rigor-rails-routes.md) — `*_path` / `*_url`
19
20
  helper validation against a parsed `config/routes.rb`.
20
21
  - [rigor-rails-i18n](rigor-rails-i18n.md) — `t(...)` / `I18n.t(...)`
@@ -69,6 +70,10 @@ The full catalogue, with a one-line scope for every plugin, is
69
70
  (`params`, `namespace`, verb macros, `desc`, `route_setting`) and
70
71
  `Grape::Entity` `expose` declarations, including the `instance_eval`'d
71
72
  block `self` bindings.
73
+ - [rigor-alba](rigor-alba.md) — types the inline `Alba.serialize { }` block `self`, and
74
+ roots the resources alba infers from `many :articles` for `rigor unused`.
75
+ - [rigor-typelizer](rigor-typelizer.md) — roots the serializer classes typelizer generates TypeScript
76
+ interfaces from, for `rigor unused`.
72
77
  - [rigor-rspec-rails](rigor-rspec-rails.md) — `have_http_status`
73
78
  argument validation (out-of-range codes, unknown status symbols).
74
79
  - [rigor-shoulda-matchers](rigor-shoulda-matchers.md) — shoulda matcher
@@ -228,6 +228,27 @@ member left out stays lenient rather than reported.
228
228
  `ActiveRecord::Base` is deliberately **not** declared: closing it would
229
229
  close every model in the project.
230
230
 
231
+ ## Roots for `rigor unused`
232
+
233
+ ```ruby
234
+ class Recipe < ApplicationRecord
235
+ belongs_to :user # ActiveRecord loads User
236
+ has_many :comments, as: :commentable # ... and Comment
237
+ has_many :tags, through: :taggings # no root of its own, see below
238
+ belongs_to :owner, polymorphic: true # no single target: no root
239
+ end
240
+ ```
241
+
242
+ `User` and `Comment` are named nowhere in the source, so [`rigor unused`](../02-cli-reference.md#rigor-unused) would list a model reached only this way. The plugin publishes the class each association resolves to as a `:reachability_roots` fact, mirroring ActiveRecord's own lookup:
243
+
244
+ - a literal `class_name:` (String or Symbol, or a rooted `"::Foo"`) wins;
245
+ - otherwise `has_many` / `has_and_belongs_to_many` use the singularized, camelized name and `belongs_to` / `has_one` the camelized name;
246
+ - the name is tried as nested in the owner itself (`Post` looks for `Post::Comment`), then in the owner's enclosing namespaces innermost first (`Admin::Recipe` looks for `Admin::User`), then at top level. If a candidate is not a model but is a namespace of one, nothing is rooted, since ActiveRecord would pick that module.
247
+
248
+ A root is published only when the project declares a model of that name. Nothing is published for a `polymorphic: true` association, a non-literal `class_name:` or `**options`, or a `through:` association without a literal `class_name:` / `source_type:` (a literal `source_type:` names the class; otherwise its target is the source association's, and that association roots it). Associations written inside `class << self`, a `def` or a block other than `with_options` are not seen, and neither is the block-parameter form `with_options(...) do |o| o.belongs_to :x end`. Literal options of a `with_options` group are merged into the associations inside it, the innermost group winning as in Rails; a group with non-literal options (`with_options opts do`) is skipped, as is `anonymous_class:`, an association whose own options hide the class (a `**splat`, an options variable, a non-literal `class_name:`), and a computed name equal to the owner's own name (Rails 7.2+ tries the top-level class first). The merged options also decide the association reader's type: `with_options optional: true` makes `belongs_to` readers nilable, and `class_name:` and `polymorphic:` set the target or make it `Dynamic`. The name is inflected with the real `ActiveSupport::Inflector`; if it cannot be loaded the plugin publishes nothing.
249
+
250
+ The roots are flat: an association on a model `rigor unused` itself reports as dead still roots its target, so a chain of dead models can leave the last one unlisted. Project-custom inflections are not read, so a model whose name relies on one is under-rooted rather than wrongly rooted. The roots are published in reduced mode too, since they do not depend on the schema.
251
+
231
252
  ## Limitations
232
253
 
233
254
  - **Direct-superclass match only.** `class Admin < User` where
@@ -0,0 +1,60 @@
1
+ # rigor-alba
2
+
3
+ Teaches Rigor about the [alba](https://github.com/okuramasafumi/alba) JSON serializer. alba does not ship its
4
+ RBS in the gem, so without this plugin every alba call in your project reads `Dynamic[top]`. The plugin only
5
+ removes false diagnostics and publishes `rigor unused` roots; it adds no diagnostic and no return type.
6
+
7
+ It ships bundled in `rigortype`. Activate it under `plugins:`:
8
+
9
+ ```yaml
10
+ plugins:
11
+ - rigor-alba
12
+ ```
13
+
14
+ ## What it does
15
+
16
+ ```ruby
17
+ # Inline resources: the block runs on an anonymous Alba::Resource class, so `attributes` and friends resolve
18
+ # instead of firing call.unresolved-toplevel.
19
+ json = Alba.serialize(user) { attributes :id, :name }
20
+ hash = Alba.hashify(user) { attributes :id }
21
+ ```
22
+
23
+ The plugin contributes no return types: `Alba.serialize(obj)` runs your own `<Class>Resource#serialize`, which
24
+ you may override to return anything.
25
+
26
+ The block may sit anywhere a value can: `render json: Alba.serialize(x) { ... }` resolves the same as an
27
+ assignment.
28
+
29
+ ## Roots for `rigor unused`
30
+
31
+ ```ruby
32
+ class UserResource
33
+ include Alba::Resource
34
+
35
+ many :articles # alba loads ArticleResource, then ArticleSerializer
36
+ one :editor, resource: EditorResource # explicit: nothing inferred
37
+ end
38
+ ```
39
+
40
+ `ArticleResource` appears nowhere in the source, so `rigor unused` would list it. The plugin publishes it as a
41
+ root. It does so only for an association that gives no `resource:` / `serializer:`, no second argument and no
42
+ block, and only when a class of the inferred name exists in the project (nesting-aware: inside `module Admin`
43
+ alba tries `Admin::ArticleResource` first). The name is classified with the real `ActiveSupport::Inflector`;
44
+ if it cannot be loaded the plugin publishes nothing.
45
+
46
+ ## Configuration
47
+
48
+ ```yaml
49
+ plugins:
50
+ - gem: rigor-alba
51
+ config:
52
+ resource_search_paths: ["app"] # default
53
+ ```
54
+
55
+ `resource_search_paths` is where the plugin looks for resource classes. Widen it if your resources live
56
+ outside `app/`.
57
+
58
+ ## More
59
+
60
+ Internals and the known limits: [plugins/rigor-alba/README.md](../../../plugins/rigor-alba/README.md).
@@ -44,18 +44,36 @@ their `refute_*` equivalents.
44
44
  | --- | --- |
45
45
  | `assert_kind_of(T, x)` / `assert_instance_of(T, x)` | narrow to `T` |
46
46
  | `assert_nil(x)` | narrow to `Constant<nil>` |
47
- | `assert_equal(literal, x)` | narrow to `Constant<literal>` |
47
+ | `assert_equal(literal, x)` | meet with `Constant<literal>` (see below) |
48
48
  | `assert_match(regex, x)` | narrow to `String` |
49
49
  | `refute_kind_of` / `refute_instance_of` (+ `assert_not_*`) | narrow away from `T` |
50
50
  | `refute_nil(x)` / `assert_not_nil(x)` | narrow away from nil |
51
- | `refute_equal(literal, x)` / `assert_not_equal(...)` | narrow away from `Constant<literal>` |
51
+ | `refute_equal(literal, x)` / `assert_not_equal(...)` | unchanged |
52
52
  | `_(x).must_be_kind_of(T)` / `must_be_a(T)` / `must_be_instance_of(T)` / `must_be_an_instance_of(T)` | narrow to `T` |
53
53
  | `_(x).must_be_nil` | narrow to `Constant<nil>` |
54
- | `_(x).must_equal(literal)` | narrow to `Constant<literal>` |
54
+ | `_(x).must_equal(literal)` | meet with `Constant<literal>` (see below) |
55
55
  | `_(x).must_match(regex)` | narrow to `String` |
56
56
  | `_(x).wont_be_kind_of(T)` / `wont_be_instance_of(T)` | narrow away from `T` |
57
57
  | `_(x).wont_be_nil` | narrow away from nil |
58
- | `_(x).wont_equal(literal)` | narrow away from `Constant<literal>` |
58
+ | `_(x).wont_equal(literal)` | unchanged |
59
+
60
+ ### Equality is not identity
61
+
62
+ `assert_equal(literal, x)` and `_(x).must_equal(literal)` pass when
63
+ `literal == x`, and `==` can hold across classes: `1 == 1.0`,
64
+ `1 == 1r`, and a class with its own `==` (a `ModInt` that compares
65
+ `to_i`) passes `assert_equal 9, m`. The assertion therefore never
66
+ replaces `x`'s type:
67
+
68
+ - a `nil`, `true`, `false`, or Symbol literal has identity `==`, so `x`
69
+ narrows to that literal whatever its type was;
70
+ - an Integer, Float, or String literal narrows `x` only when every
71
+ member of `x`'s type is already of the literal's class (or a `nil`,
72
+ boolean, or Symbol the literal cannot equal). After
73
+ `x = rand(10); assert_equal 9, x`, `x` is `9`;
74
+ - otherwise — `x` is `Dynamic`, of a user class, or of another numeric
75
+ class (`assert_equal 9.0, x` with an Integer `x`), or the literal is
76
+ `0.0`, which `-0.0` also equals — `x` keeps its type.
59
77
 
60
78
  ## No diagnostics, no config
61
79