rigortype 0.4.5 → 0.4.7

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 (452) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +2 -2
  3. data/data/gem_overlay/activesupport/core_ext.rbs +16 -13
  4. data/data/vendored_gem_sigs/bundler/bundler.rbs +1 -1
  5. data/data/vendored_gem_sigs/rubygems/rubygems_extras.rbs +4 -3
  6. data/docs/handbook/01-getting-started.md +5 -2
  7. data/docs/handbook/11-sig-gen.md +61 -5
  8. data/docs/handbook/appendix-phpstan.md +4 -4
  9. data/docs/manual/02-cli-reference.md +192 -27
  10. data/docs/manual/03-configuration.md +38 -4
  11. data/docs/manual/04-diagnostics.md +1 -1
  12. data/docs/manual/06-baseline.md +6 -4
  13. data/docs/manual/09-editor-integration.md +13 -2
  14. data/docs/manual/10-mcp-server.md +47 -3
  15. data/docs/manual/12-caching.md +9 -1
  16. data/docs/manual/plugins/rigor-actionmailer.md +30 -0
  17. data/docs/manual/plugins/rigor-actionpack.md +49 -11
  18. data/docs/manual/plugins/rigor-activesupport-core-ext.md +20 -1
  19. data/docs/manual/plugins/rigor-devise.md +15 -0
  20. data/docs/manual/plugins/rigor-rspec.md +2 -1
  21. data/lib/rigor/analysis/baseline.rb +24 -27
  22. data/lib/rigor/analysis/buffer_binding.rb +14 -0
  23. data/lib/rigor/analysis/check_rules/always_truthy_condition_collector.rb +5 -6
  24. data/lib/rigor/analysis/check_rules/dead_assignment_collector.rb +2 -2
  25. data/lib/rigor/analysis/check_rules/declaration_sourced_guard.rb +4 -4
  26. data/lib/rigor/analysis/check_rules/duplicate_hash_key_collector.rb +2 -2
  27. data/lib/rigor/analysis/check_rules/inferred_param_guard.rb +105 -12
  28. data/lib/rigor/analysis/check_rules/ivar_write_collector.rb +143 -15
  29. data/lib/rigor/analysis/check_rules/lexical_method_sites.rb +4 -1
  30. data/lib/rigor/analysis/check_rules/main_pass_collector.rb +5 -8
  31. data/lib/rigor/analysis/check_rules/published_constant_guard.rb +1 -1
  32. data/lib/rigor/analysis/check_rules/rbs_receiver_surface.rb +66 -3
  33. data/lib/rigor/analysis/check_rules/rebinding_block_callees/dispatch.rb +112 -0
  34. data/lib/rigor/analysis/check_rules/rebinding_block_callees.rb +394 -0
  35. data/lib/rigor/analysis/check_rules/return_in_ensure_collector.rb +1 -2
  36. data/lib/rigor/analysis/check_rules/ruby_deprecations.rb +7 -0
  37. data/lib/rigor/analysis/check_rules/rule_ids.rb +1 -1
  38. data/lib/rigor/analysis/check_rules/rule_walk.rb +9 -7
  39. data/lib/rigor/analysis/check_rules/shadowed_rescue_collector.rb +3 -4
  40. data/lib/rigor/analysis/check_rules/special_global_setters.rb +3 -3
  41. data/lib/rigor/analysis/check_rules/unreachable_clause_collector.rb +53 -2
  42. data/lib/rigor/analysis/check_rules/void_value_use_collector.rb +1 -1
  43. data/lib/rigor/analysis/check_rules.rb +217 -52
  44. data/lib/rigor/analysis/crash_signature.rb +2 -2
  45. data/lib/rigor/analysis/dependency_recorder.rb +15 -4
  46. data/lib/rigor/analysis/dependency_source_inference/boundary_cross_reporter.rb +14 -3
  47. data/lib/rigor/analysis/dependency_source_inference/gem_resolver.rb +1 -1
  48. data/lib/rigor/analysis/dependency_source_inference/index.rb +6 -8
  49. data/lib/rigor/analysis/dependency_source_inference/walker.rb +4 -4
  50. data/lib/rigor/analysis/edit_replay.rb +154 -0
  51. data/lib/rigor/analysis/fact_store.rb +3 -3
  52. data/lib/rigor/analysis/incremental.rb +37 -0
  53. data/lib/rigor/analysis/incremental_run_slot/write_guard.rb +2 -2
  54. data/lib/rigor/analysis/incremental_run_slot.rb +2 -2
  55. data/lib/rigor/analysis/incremental_session.rb +202 -43
  56. data/lib/rigor/analysis/path_expansion.rb +41 -0
  57. data/lib/rigor/analysis/require_relative_closure.rb +100 -0
  58. data/lib/rigor/analysis/rule_catalog.rb +7 -3
  59. data/lib/rigor/analysis/run_disclosure_credit.rb +80 -0
  60. data/lib/rigor/analysis/run_disclosure_ledger.rb +155 -0
  61. data/lib/rigor/analysis/run_stats.rb +2 -2
  62. data/lib/rigor/analysis/runner/buffer_pool_dispatcher.rb +8 -4
  63. data/lib/rigor/analysis/runner/declaration_position.rb +1 -1
  64. data/lib/rigor/analysis/runner/diagnostic_aggregator.rb +204 -96
  65. data/lib/rigor/analysis/runner/effect_envelope_pass.rb +1 -1
  66. data/lib/rigor/analysis/runner/pool_coordinator.rb +157 -320
  67. data/lib/rigor/analysis/runner/project_pre_passes.rb +48 -6
  68. data/lib/rigor/analysis/runner/run_level_facts.rb +304 -0
  69. data/lib/rigor/analysis/runner/run_snapshots.rb +44 -54
  70. data/lib/rigor/analysis/runner/scan_environment.rb +51 -0
  71. data/lib/rigor/analysis/runner/worker_payload.rb +36 -0
  72. data/lib/rigor/analysis/runner.rb +233 -120
  73. data/lib/rigor/analysis/self_call_resolution_recorder.rb +1 -1
  74. data/lib/rigor/analysis/template_unit_positions.rb +2 -3
  75. data/lib/rigor/analysis/template_units.rb +9 -1
  76. data/lib/rigor/analysis/worker_session.rb +38 -33
  77. data/lib/rigor/bleeding_edge.rb +3 -6
  78. data/lib/rigor/builtins/imported_refinements.rb +6 -8
  79. data/lib/rigor/builtins/predefined_constant_refinements.rb +1 -1
  80. data/lib/rigor/builtins/regex_refinement.rb +4 -6
  81. data/lib/rigor/cache/descriptor.rb +6 -5
  82. data/lib/rigor/cache/incremental_snapshot.rb +60 -18
  83. data/lib/rigor/cache/rbs_cache_producer.rb +2 -2
  84. data/lib/rigor/cache/rbs_class_ancestor_table.rb +1 -1
  85. data/lib/rigor/cache/rbs_class_type_param_names.rb +2 -2
  86. data/lib/rigor/cache/rbs_constant_table.rb +1 -1
  87. data/lib/rigor/cache/rbs_descriptor.rb +54 -3
  88. data/lib/rigor/cache/rbs_environment.rb +4 -1
  89. data/lib/rigor/cache/rbs_environment_marshal_patch.rb +2 -2
  90. data/lib/rigor/cache/rbs_known_class_names.rb +1 -1
  91. data/lib/rigor/cache/store.rb +11 -10
  92. data/lib/rigor/ci_detector.rb +1 -1
  93. data/lib/rigor/cli/annotate_command.rb +24 -19
  94. data/lib/rigor/cli/baseline_command.rb +145 -55
  95. data/lib/rigor/cli/catalogue.rb +256 -0
  96. data/lib/rigor/cli/check_command.rb +188 -93
  97. data/lib/rigor/cli/check_runner_factory.rb +2 -2
  98. data/lib/rigor/cli/command.rb +15 -1
  99. data/lib/rigor/cli/coverage_command.rb +21 -8
  100. data/lib/rigor/cli/coverage_mutation.rb +5 -7
  101. data/lib/rigor/cli/coverage_renderer.rb +2 -1
  102. data/lib/rigor/cli/coverage_scan.rb +3 -5
  103. data/lib/rigor/cli/diff_command.rb +27 -21
  104. data/lib/rigor/cli/docs_command.rb +1 -2
  105. data/lib/rigor/cli/doctor_command.rb +12 -12
  106. data/lib/rigor/cli/effects_command.rb +50 -40
  107. data/lib/rigor/cli/effects_diff_renderer.rb +18 -17
  108. data/lib/rigor/cli/effects_explain_renderer.rb +13 -12
  109. data/lib/rigor/cli/effects_renderer.rb +2 -1
  110. data/lib/rigor/cli/effects_snapshot_command.rb +18 -17
  111. data/lib/rigor/cli/explain_command.rb +18 -8
  112. data/lib/rigor/cli/fused_protection_renderer.rb +2 -1
  113. data/lib/rigor/cli/json_document.rb +41 -0
  114. data/lib/rigor/cli/lens_command.rb +17 -13
  115. data/lib/rigor/cli/lsp_command.rb +250 -23
  116. data/lib/rigor/cli/mcp_command.rb +14 -8
  117. data/lib/rigor/cli/mutation_fork_scan.rb +1 -1
  118. data/lib/rigor/cli/mutation_protection_renderer.rb +3 -2
  119. data/lib/rigor/cli/options.rb +60 -4
  120. data/lib/rigor/cli/plugin_command.rb +93 -46
  121. data/lib/rigor/cli/plugins_command.rb +22 -18
  122. data/lib/rigor/cli/plugins_renderer.rb +34 -8
  123. data/lib/rigor/cli/protection_fork_scan.rb +2 -2
  124. data/lib/rigor/cli/protection_renderer.rb +2 -1
  125. data/lib/rigor/cli/show_bleedingedge_command.rb +23 -16
  126. data/lib/rigor/cli/sig_gen_command.rb +130 -34
  127. data/lib/rigor/cli/skill_command.rb +32 -3
  128. data/lib/rigor/cli/skill_deep_probe.rb +1 -1
  129. data/lib/rigor/cli/skill_describe.rb +1 -1
  130. data/lib/rigor/cli/trace_command.rb +14 -9
  131. data/lib/rigor/cli/triage_command.rb +10 -11
  132. data/lib/rigor/cli/triage_renderer.rb +2 -1
  133. data/lib/rigor/cli/type_of_command.rb +18 -12
  134. data/lib/rigor/cli/type_of_project_scope.rb +24 -0
  135. data/lib/rigor/cli/type_of_renderer.rb +4 -1
  136. data/lib/rigor/cli/type_scan_command.rb +14 -9
  137. data/lib/rigor/cli/type_scan_renderer.rb +2 -1
  138. data/lib/rigor/cli/unused_command.rb +27 -17
  139. data/lib/rigor/cli/upgrade_command.rb +5 -0
  140. data/lib/rigor/cli.rb +209 -54
  141. data/lib/rigor/config_audit.rb +17 -6
  142. data/lib/rigor/configuration/severity_profile.rb +5 -5
  143. data/lib/rigor/configuration.rb +86 -34
  144. data/lib/rigor/effects/definition_context.rb +0 -2
  145. data/lib/rigor/effects/envelope.rb +1 -1
  146. data/lib/rigor/effects/framework_units.rb +1 -1
  147. data/lib/rigor/effects/mutation_classifier.rb +4 -7
  148. data/lib/rigor/effects/propagator.rb +1 -1
  149. data/lib/rigor/effects/registry.rb +2 -2
  150. data/lib/rigor/effects/snapshot.rb +1 -1
  151. data/lib/rigor/effects/taint_cause.rb +0 -2
  152. data/lib/rigor/effects/unit_scan.rb +9 -13
  153. data/lib/rigor/environment/bundle_sig_discovery.rb +5 -9
  154. data/lib/rigor/environment/class_registry.rb +3 -3
  155. data/lib/rigor/environment/core_root.rb +77 -0
  156. data/lib/rigor/environment/default_libraries.rb +1 -1
  157. data/lib/rigor/environment/deferred_signature_plan.rb +283 -0
  158. data/lib/rigor/environment/gated_signature_guard.rb +1 -1
  159. data/lib/rigor/environment/gated_signature_plan.rb +5 -2
  160. data/lib/rigor/environment/lockfile_resolver.rb +28 -12
  161. data/lib/rigor/environment/member_consistency/comparator.rb +18 -1
  162. data/lib/rigor/environment/member_consistency.rb +56 -7
  163. data/lib/rigor/environment/missing_gem_constant_index.rb +3 -2
  164. data/lib/rigor/environment/project_path_gems.rb +124 -0
  165. data/lib/rigor/environment/rbs_hierarchy.rb +6 -15
  166. data/lib/rigor/environment/rbs_loader.rb +592 -70
  167. data/lib/rigor/environment/reflection.rb +7 -7
  168. data/lib/rigor/environment/required_features.rb +136 -32
  169. data/lib/rigor/environment/ruby_library_roots.rb +122 -0
  170. data/lib/rigor/environment/vendored_signature_plan.rb +153 -0
  171. data/lib/rigor/environment.rb +44 -11
  172. data/lib/rigor/flow_contribution/merger.rb +4 -4
  173. data/lib/rigor/inference/acceptance.rb +93 -32
  174. data/lib/rigor/inference/block_parameter_binder.rb +2 -3
  175. data/lib/rigor/inference/block_run_count.rb +178 -0
  176. data/lib/rigor/inference/budget_trace.rb +8 -2
  177. data/lib/rigor/inference/builtins/exception_catalog.rb +2 -2
  178. data/lib/rigor/inference/builtins/numeric_catalog.rb +2 -5
  179. data/lib/rigor/inference/builtins/random_catalog.rb +2 -2
  180. data/lib/rigor/inference/callee_mutation_chain.rb +126 -0
  181. data/lib/rigor/inference/callee_write_census.rb +815 -0
  182. data/lib/rigor/inference/construction_escape.rb +217 -0
  183. data/lib/rigor/inference/declaration_walk/traversal.rb +1 -1
  184. data/lib/rigor/inference/deep_erasure.rb +170 -0
  185. data/lib/rigor/inference/def_handle.rb +1 -1
  186. data/lib/rigor/inference/def_node_resolver.rb +1 -1
  187. data/lib/rigor/inference/def_return_typer.rb +204 -12
  188. data/lib/rigor/inference/definer_resolution.rb +21 -5
  189. data/lib/rigor/inference/definers_index.rb +106 -0
  190. data/lib/rigor/inference/direct_ivar_writes.rb +300 -0
  191. data/lib/rigor/inference/element_read_widening.rb +49 -3
  192. data/lib/rigor/inference/error_info.rb +17 -3
  193. data/lib/rigor/inference/expression_typer.rb +173 -57
  194. data/lib/rigor/inference/external_ancestor_resolution.rb +3 -4
  195. data/lib/rigor/inference/global_write_census.rb +110 -1
  196. data/lib/rigor/inference/global_writer_reach.rb +260 -0
  197. data/lib/rigor/inference/hkt_body.rb +1 -2
  198. data/lib/rigor/inference/hkt_body_parser.rb +3 -5
  199. data/lib/rigor/inference/hkt_reducer.rb +2 -2
  200. data/lib/rigor/inference/hkt_registry.rb +3 -5
  201. data/lib/rigor/inference/indexed_narrowing.rb +4 -7
  202. data/lib/rigor/inference/intervening_ivar_reset.rb +26 -4
  203. data/lib/rigor/inference/ivar_writer_census.rb +246 -0
  204. data/lib/rigor/inference/key_presence_guard.rb +2 -1
  205. data/lib/rigor/inference/loop_value_record.rb +57 -0
  206. data/lib/rigor/inference/match_rebinding/frame.rb +65 -0
  207. data/lib/rigor/inference/match_rebinding/yielding_match.rb +45 -0
  208. data/lib/rigor/inference/match_rebinding.rb +11 -1
  209. data/lib/rigor/inference/memo_audit.rb +55 -0
  210. data/lib/rigor/inference/method_dispatcher/alias_strict_nominals.rb +1 -1
  211. data/lib/rigor/inference/method_dispatcher/argument_distribution.rb +40 -0
  212. data/lib/rigor/inference/method_dispatcher/block_folding.rb +1 -1
  213. data/lib/rigor/inference/method_dispatcher/constant_folding.rb +113 -12
  214. data/lib/rigor/inference/method_dispatcher/contested_overloads.rb +89 -0
  215. data/lib/rigor/inference/method_dispatcher/iterator_dispatch.rb +1 -1
  216. data/lib/rigor/inference/method_dispatcher/kernel_dispatch.rb +7 -3
  217. data/lib/rigor/inference/method_dispatcher/no_match_fallback.rb +54 -0
  218. data/lib/rigor/inference/method_dispatcher/overload_selector.rb +39 -27
  219. data/lib/rigor/inference/method_dispatcher/random_folding.rb +1 -1
  220. data/lib/rigor/inference/method_dispatcher/rbs_dispatch.rb +203 -17
  221. data/lib/rigor/inference/method_dispatcher/reduce_folding.rb +1 -1
  222. data/lib/rigor/inference/method_dispatcher/regexp_folding.rb +2 -2
  223. data/lib/rigor/inference/method_dispatcher/self_substitute.rb +2 -2
  224. data/lib/rigor/inference/method_dispatcher/set_folding.rb +59 -24
  225. data/lib/rigor/inference/method_dispatcher/shape_dispatch.rb +83 -20
  226. data/lib/rigor/inference/method_dispatcher/singleton_folding.rb +0 -1
  227. data/lib/rigor/inference/method_dispatcher/singleton_mixin_dispatch.rb +50 -17
  228. data/lib/rigor/inference/method_dispatcher/struct_folding.rb +5 -5
  229. data/lib/rigor/inference/method_dispatcher/struct_materialization.rb +4 -6
  230. data/lib/rigor/inference/method_dispatcher/uri_folding.rb +1 -1
  231. data/lib/rigor/inference/method_dispatcher.rb +109 -29
  232. data/lib/rigor/inference/module_function_state/spellings.rb +64 -0
  233. data/lib/rigor/inference/module_function_state.rb +23 -1
  234. data/lib/rigor/inference/multi_target_binder.rb +1 -1
  235. data/lib/rigor/inference/mutation_rejoin.rb +240 -4
  236. data/lib/rigor/inference/mutation_widening.rb +162 -29
  237. data/lib/rigor/inference/narrowing.rb +113 -38
  238. data/lib/rigor/inference/object_mixins.rb +13 -1
  239. data/lib/rigor/inference/optimistic_origin.rb +1 -1
  240. data/lib/rigor/inference/out_buffer_mutation.rb +72 -0
  241. data/lib/rigor/inference/parameter_inference_collector.rb +5 -6
  242. data/lib/rigor/inference/pre_eval_constants.rb +1 -1
  243. data/lib/rigor/inference/project_method_ownership.rb +6 -3
  244. data/lib/rigor/inference/project_seed_tables.rb +2 -1
  245. data/lib/rigor/inference/project_yielder.rb +122 -0
  246. data/lib/rigor/inference/receiver_alias.rb +20 -0
  247. data/lib/rigor/inference/refined_dispatch/constructed_class.rb +3 -1
  248. data/lib/rigor/inference/refined_dispatch/own_method.rb +2 -1
  249. data/lib/rigor/inference/remover_holder_scan.rb +233 -0
  250. data/lib/rigor/inference/remover_narrowing.rb +224 -0
  251. data/lib/rigor/inference/rewrite_mutation.rb +53 -2
  252. data/lib/rigor/inference/scope_indexer/certainty.rb +9 -1
  253. data/lib/rigor/inference/scope_indexer/constant_ivar_value.rb +93 -0
  254. data/lib/rigor/inference/scope_indexer/declared_ivar_types.rb +64 -0
  255. data/lib/rigor/inference/scope_indexer/declared_param_writes.rb +205 -0
  256. data/lib/rigor/inference/scope_indexer/derived_param_value.rb +112 -0
  257. data/lib/rigor/inference/scope_indexer/empty_seed.rb +99 -0
  258. data/lib/rigor/inference/scope_indexer/ivar_contributions.rb +135 -0
  259. data/lib/rigor/inference/scope_indexer/unseen_ivar_writers.rb +133 -0
  260. data/lib/rigor/inference/scope_indexer.rb +614 -146
  261. data/lib/rigor/inference/self_call_ivar_writes.rb +101 -17
  262. data/lib/rigor/inference/set_mutation.rb +189 -0
  263. data/lib/rigor/inference/singleton_hook_decline.rb +1 -2
  264. data/lib/rigor/inference/statement_evaluator.rb +3036 -278
  265. data/lib/rigor/inference/struct_fold_safety.rb +4 -6
  266. data/lib/rigor/inference/synthetic_method_index.rb +23 -0
  267. data/lib/rigor/inference/synthetic_method_scanner/project_members.rb +130 -0
  268. data/lib/rigor/inference/synthetic_method_scanner.rb +73 -14
  269. data/lib/rigor/inference/toplevel_def_census.rb +59 -0
  270. data/lib/rigor/inference/unknown_store_widening.rb +103 -4
  271. data/lib/rigor/inference/unthreaded_rebinds.rb +2 -3
  272. data/lib/rigor/inference/void_tail_summary.rb +2 -1
  273. data/lib/rigor/language_server/buffer_table.rb +9 -3
  274. data/lib/rigor/language_server/completion_provider.rb +26 -20
  275. data/lib/rigor/language_server/config_warning_announcer.rb +28 -0
  276. data/lib/rigor/language_server/diagnostic_publisher.rb +28 -9
  277. data/lib/rigor/language_server/document_symbol_provider.rb +1 -1
  278. data/lib/rigor/language_server/folding_range_provider.rb +1 -2
  279. data/lib/rigor/language_server/hover_provider.rb +1 -7
  280. data/lib/rigor/language_server/hover_renderer.rb +6 -8
  281. data/lib/rigor/language_server/project_context.rb +98 -4
  282. data/lib/rigor/language_server/publish_batcher.rb +3 -4
  283. data/lib/rigor/language_server/selection_range_provider.rb +2 -4
  284. data/lib/rigor/language_server/server.rb +12 -6
  285. data/lib/rigor/language_server/signature_help_provider.rb +4 -4
  286. data/lib/rigor/lens/plugins.rb +1 -1
  287. data/lib/rigor/mcp/server.rb +82 -3
  288. data/lib/rigor/mcp.rb +1 -1
  289. data/lib/rigor/plugin/access_denied_error.rb +1 -2
  290. data/lib/rigor/plugin/base.rb +26 -11
  291. data/lib/rigor/plugin/bundled_catalog.rb +1 -12
  292. data/lib/rigor/plugin/io_boundary.rb +1 -1
  293. data/lib/rigor/plugin/loader.rb +25 -8
  294. data/lib/rigor/plugin/macro/heredoc_template.rb +7 -8
  295. data/lib/rigor/plugin/macro/trait_registry.rb +6 -9
  296. data/lib/rigor/plugin/manifest.rb +38 -16
  297. data/lib/rigor/plugin/registry.rb +41 -3
  298. data/lib/rigor/plugin/services.rb +2 -3
  299. data/lib/rigor/plugin/source_rbs_synthesis_reporter.rb +11 -0
  300. data/lib/rigor/plugin/trust_policy.rb +2 -2
  301. data/lib/rigor/plugin/type_node_resolver.rb +2 -4
  302. data/lib/rigor/plugin.rb +6 -8
  303. data/lib/rigor/plugin_gap_advisory.rb +5 -1
  304. data/lib/rigor/project_environment.rb +16 -2
  305. data/lib/rigor/protection/analysis_guard.rb +1 -1
  306. data/lib/rigor/protection/discovery_seed.rb +96 -2
  307. data/lib/rigor/protection/mutator.rb +1 -1
  308. data/lib/rigor/rbs_extended/envelope_scanner.rb +4 -6
  309. data/lib/rigor/rbs_extended/hkt_directives.rb +6 -10
  310. data/lib/rigor/rbs_extended/reporter.rb +27 -3
  311. data/lib/rigor/rbs_extended.rb +2 -3
  312. data/lib/rigor/reflection/constant_ancestors.rb +5 -3
  313. data/lib/rigor/rooted_glob.rb +25 -2
  314. data/lib/rigor/run_scoped_slots.rb +38 -0
  315. data/lib/rigor/runtime/jit.rb +1 -1
  316. data/lib/rigor/scope/discovery_index.rb +19 -6
  317. data/lib/rigor/scope/resolution_chain.rb +3 -1
  318. data/lib/rigor/scope.rb +379 -36
  319. data/lib/rigor/sig_gen/classification.rb +4 -0
  320. data/lib/rigor/sig_gen/effect_annotation.rb +3 -3
  321. data/lib/rigor/sig_gen/generator.rb +668 -84
  322. data/lib/rigor/sig_gen/generic_header_scan.rb +189 -0
  323. data/lib/rigor/sig_gen/inline_declarations.rb +2 -2
  324. data/lib/rigor/sig_gen/layout_index.rb +3 -3
  325. data/lib/rigor/sig_gen/method_candidate.rb +23 -4
  326. data/lib/rigor/sig_gen/observation_collector.rb +7 -6
  327. data/lib/rigor/sig_gen/observed_call.rb +1 -1
  328. data/lib/rigor/sig_gen/path_mapper.rb +6 -0
  329. data/lib/rigor/sig_gen/rbs_validity.rb +1 -1
  330. data/lib/rigor/sig_gen/renderer.rb +21 -12
  331. data/lib/rigor/sig_gen/skip_reason_catalog.rb +15 -2
  332. data/lib/rigor/sig_gen/type_elaborator.rb +21 -5
  333. data/lib/rigor/sig_gen/type_name_qualification.rb +69 -0
  334. data/lib/rigor/sig_gen/write_reporter.rb +63 -14
  335. data/lib/rigor/sig_gen/write_result.rb +23 -12
  336. data/lib/rigor/sig_gen/writer.rb +217 -37
  337. data/lib/rigor/type/acceptance_router.rb +2 -2
  338. data/lib/rigor/type/accepts_result.rb +1 -1
  339. data/lib/rigor/type/app.rb +4 -6
  340. data/lib/rigor/type/combinator.rb +13 -16
  341. data/lib/rigor/type/constant.rb +48 -7
  342. data/lib/rigor/type/hash_shape.rb +26 -40
  343. data/lib/rigor/type/struct_instance.rb +3 -2
  344. data/lib/rigor/type/union.rb +1 -1
  345. data/lib/rigor/type_node/identifier.rb +1 -2
  346. data/lib/rigor/type_node/resolver_chain.rb +1 -1
  347. data/lib/rigor/type_node/string_literal.rb +1 -2
  348. data/lib/rigor/type_node/symbol_literal.rb +1 -2
  349. data/lib/rigor/type_node/union.rb +3 -6
  350. data/lib/rigor/value_semantics.rb +1 -10
  351. data/lib/rigor/version.rb +1 -1
  352. data/plugins/rigor-actioncable/lib/rigor/plugin/actioncable/channel_discoverer.rb +2 -2
  353. data/plugins/rigor-actioncable/lib/rigor/plugin/actioncable/channel_index.rb +2 -3
  354. data/plugins/rigor-actioncable/lib/rigor/plugin/actioncable.rb +1 -1
  355. data/plugins/rigor-actionmailer/lib/rigor/plugin/actionmailer/mailer_discoverer.rb +89 -70
  356. data/plugins/rigor-actionmailer/lib/rigor/plugin/actionmailer/mailer_index.rb +4 -2
  357. data/plugins/rigor-actionmailer/lib/rigor/plugin/actionmailer.rb +25 -4
  358. data/plugins/rigor-actionpack/lib/rigor/plugin/actionpack/analyzer.rb +1 -1
  359. data/plugins/rigor-actionpack/lib/rigor/plugin/actionpack/controller_index.rb +1 -5
  360. data/plugins/rigor-actionpack/lib/rigor/plugin/actionpack/controller_scan.rb +0 -1
  361. data/plugins/rigor-actionpack/lib/rigor/plugin/actionpack/erb_compiler.rb +2 -1
  362. data/plugins/rigor-actionpack/lib/rigor/plugin/actionpack/view_assigns.rb +1 -1
  363. data/plugins/rigor-actionpack/lib/rigor/plugin/actionpack.rb +79 -19
  364. data/plugins/rigor-activejob/lib/rigor/plugin/activejob/effects.rb +1 -1
  365. data/plugins/rigor-activejob/lib/rigor/plugin/activejob.rb +6 -3
  366. data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord/association_roots.rb +3 -2
  367. data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord/effects.rb +2 -2
  368. data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord/model_discoverer.rb +21 -31
  369. data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord/model_index.rb +10 -16
  370. data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord/schema_parser.rb +2 -3
  371. data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord/schema_table.rb +0 -1
  372. data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord.rb +5 -4
  373. data/plugins/rigor-activestorage/lib/rigor/plugin/activestorage/attachment_discoverer.rb +3 -4
  374. data/plugins/rigor-activestorage/lib/rigor/plugin/activestorage/attachment_index.rb +1 -1
  375. data/plugins/rigor-activestorage/lib/rigor/plugin/activestorage.rb +7 -12
  376. data/plugins/rigor-activestorage/lib/rigor-activestorage.rb +1 -1
  377. data/plugins/rigor-activesupport-core-ext/lib/rigor/plugin/activesupport_core_ext/effects.rb +1 -1
  378. data/plugins/rigor-activesupport-core-ext/lib/rigor/plugin/activesupport_core_ext/symbolize_keys.rb +102 -0
  379. data/plugins/rigor-activesupport-core-ext/lib/rigor/plugin/activesupport_core_ext.rb +19 -6
  380. data/plugins/rigor-activesupport-core-ext/sig/active_support/core_ext.rbs +26 -13
  381. data/plugins/rigor-alba/lib/rigor/plugin/alba/resource_index.rb +3 -3
  382. data/plugins/rigor-devise/lib/rigor/plugin/devise.rb +5 -3
  383. data/plugins/rigor-dry-schema/lib/rigor/plugin/dry_schema/schema_scanner.rb +1 -3
  384. data/plugins/rigor-dry-struct/lib/rigor/plugin/dry_struct.rb +6 -6
  385. data/plugins/rigor-dry-types/lib/rigor/plugin/dry_types/alias_scanner.rb +5 -6
  386. data/plugins/rigor-dry-types/lib/rigor/plugin/dry_types.rb +7 -6
  387. data/plugins/rigor-dry-validation/lib/rigor/plugin/dry_validation/params_shape.rb +2 -3
  388. data/plugins/rigor-dry-validation/lib/rigor/plugin/dry_validation.rb +5 -8
  389. data/plugins/rigor-ethon/lib/rigor/plugin/ethon.rb +0 -1
  390. data/plugins/rigor-factorybot/lib/rigor/plugin/factorybot/factory_discoverer.rb +1 -1
  391. data/plugins/rigor-factorybot/lib/rigor/plugin/factorybot.rb +10 -18
  392. data/plugins/rigor-ffi/lib/rigor/plugin/ffi/analyzer.rb +0 -1
  393. data/plugins/rigor-ffi/lib/rigor/plugin/ffi/discoverer.rb +0 -2
  394. data/plugins/rigor-ffi/lib/rigor/plugin/ffi.rb +4 -5
  395. data/plugins/rigor-grape/lib/rigor/plugin/grape.rb +0 -2
  396. data/plugins/rigor-graphql/lib/rigor/plugin/graphql/type_scanner.rb +6 -7
  397. data/plugins/rigor-hanami/lib/rigor/plugin/hanami/action_checker.rb +2 -4
  398. data/plugins/rigor-hanami/lib/rigor/plugin/hanami.rb +2 -5
  399. data/plugins/rigor-mangrove/lib/rigor/plugin/mangrove.rb +2 -2
  400. data/plugins/rigor-minitest/lib/rigor/plugin/minitest/assertion_analyzer.rb +6 -6
  401. data/plugins/rigor-pundit/lib/rigor/plugin/pundit/policy_discoverer.rb +2 -1
  402. data/plugins/rigor-rails-i18n/lib/rigor/plugin/rails_i18n/analyzer.rb +3 -8
  403. data/plugins/rigor-rails-i18n/lib/rigor/plugin/rails_i18n/locale_index.rb +2 -3
  404. data/plugins/rigor-rails-i18n/lib/rigor/plugin/rails_i18n/locale_loader.rb +5 -8
  405. data/plugins/rigor-rails-routes/lib/rigor/plugin/rails_routes/analyzer.rb +1 -3
  406. data/plugins/rigor-rails-routes/lib/rigor/plugin/rails_routes/devise_routes.rb +2 -3
  407. data/plugins/rigor-rails-routes/lib/rigor/plugin/rails_routes/helper_discoverer.rb +1 -1
  408. data/plugins/rigor-rails-routes/lib/rigor/plugin/rails_routes/routes_parser.rb +4 -11
  409. data/plugins/rigor-rails-routes/lib/rigor/plugin/rails_routes.rb +1 -1
  410. data/plugins/rigor-rbs-inline/lib/rigor/plugin/rbs_inline.rb +2 -1
  411. data/plugins/rigor-rspec/lib/rigor/plugin/rspec/matcher_analyzer.rb +102 -11
  412. data/plugins/rigor-rspec/lib/rigor/plugin/rspec.rb +2 -2
  413. data/plugins/rigor-rspec-rails/lib/rigor/plugin/rspec_rails.rb +3 -4
  414. data/plugins/rigor-shoulda-matchers/lib/rigor/plugin/shoulda_matchers/analyzer.rb +6 -8
  415. data/plugins/rigor-sidekiq/lib/rigor/plugin/sidekiq.rb +5 -2
  416. data/plugins/rigor-sorbet/lib/rigor/plugin/sorbet/absurd_recognizer.rb +4 -5
  417. data/plugins/rigor-sorbet/lib/rigor/plugin/sorbet/assertion_recognizer.rb +6 -7
  418. data/plugins/rigor-sorbet/lib/rigor/plugin/sorbet/catalog.rb +1 -1
  419. data/plugins/rigor-sorbet/lib/rigor/plugin/sorbet/sig_parser.rb +2 -2
  420. data/plugins/rigor-sorbet/lib/rigor/plugin/sorbet.rb +17 -21
  421. data/plugins/rigor-statesman/lib/rigor/plugin/statesman.rb +2 -3
  422. data/plugins/rigor-typelizer/lib/rigor/plugin/typelizer/serializer_discoverer.rb +3 -2
  423. data/plugins/rigor-typelizer/lib/rigor/plugin/typelizer/serializer_index.rb +4 -4
  424. data/plugins/rigor-typelizer/lib/rigor/plugin/typelizer.rb +1 -1
  425. data/sig/rigor/analysis/baseline.rbs +0 -2
  426. data/sig/rigor/analysis/fact_store.rbs +1 -0
  427. data/sig/rigor/analysis/reachability/scan_cache.rbs +1 -1
  428. data/sig/rigor/cli/diff_command.rbs +1 -1
  429. data/sig/rigor/cli/explain_command.rbs +1 -1
  430. data/sig/rigor/effects/file_collection.rbs +5 -6
  431. data/sig/rigor/effects/registry.rbs +8 -6
  432. data/sig/rigor/effects/summary.rbs +6 -5
  433. data/sig/rigor/environment.rbs +5 -3
  434. data/sig/rigor/inference/optimistic_origin.rbs +11 -0
  435. data/sig/rigor/inference.rbs +10 -1
  436. data/sig/rigor/plugin/additional_initializer.rbs +0 -5
  437. data/sig/rigor/plugin/base.rbs +1 -0
  438. data/sig/rigor/plugin/effect_ancestry.rbs +6 -4
  439. data/sig/rigor/plugin/effect_attribution.rbs +25 -10
  440. data/sig/rigor/plugin/effect_edge.rbs +12 -5
  441. data/sig/rigor/plugin/effect_entry_points.rbs +8 -3
  442. data/sig/rigor/plugin/io_boundary.rbs +6 -5
  443. data/sig/rigor/plugin/manifest.rbs +16 -18
  444. data/sig/rigor/plugin/protocol_contract.rbs +6 -9
  445. data/sig/rigor/plugin/registry.rbs +1 -1
  446. data/sig/rigor/scope.rbs +51 -50
  447. data/sig/rigor/type.rbs +0 -6
  448. data/skills/rigor-plugin-author/SKILL.md +4 -4
  449. data/skills/rigor-plugin-author/references/02-walker-and-types.md +5 -5
  450. data/skills/rigor-plugin-review/SKILL.md +3 -3
  451. data/skills/rigor-project-init/references/03-baseline-and-bugs.md +12 -1
  452. metadata +55 -3
@@ -24,7 +24,7 @@ Relative paths in a config file resolve against that file's own directory, with
24
24
  `rigor` runs from. They match only when you run `rigor` from the directory that holds the config file, which is
25
25
  the usual case; with `--config path/to/.rigor.yml` from elsewhere, give these three as absolute paths.
26
26
  A leading `~/` (or a bare `~`) in a path setting expands to your home directory; `~name` is an ordinary
27
- directory called `~name`, not another user's home. This covers `paths:`, `signature_paths:`, `test_paths:`,
27
+ directory called `~name`, not another user's home. This covers `paths:`, `signature_paths:`, `core_root:`, `test_paths:`,
28
28
  `pre_eval:`, `plugins_io.allowed_paths:`, `includes:`, `bundler.bundle_path:`, `bundler.lockfile:` and
29
29
  `rbs_collection.lockfile:`. It does not apply to `cache.path:`, `baseline:` or `effects.snapshot.path:`, which stay
30
30
  literal and relative to the working directory.
@@ -67,7 +67,7 @@ cache:
67
67
  | `target_ruby` | String | `"4.0"` | The lowest Ruby version *your* project runs on — `"X.Y"`, `"X.Y.Z"`, or `"latest"`. Independent of the Ruby Rigor itself runs on. Every value selects the syntax Rigor parses. A version you set explicitly also turns on the Ruby-deprecation rules for that version ([`call.deprecated-ruby2-keywords`](04-diagnostics.md#rule-call-deprecated-ruby2-keywords) from `"4.1"`); the default and `"latest"` never do, which is why `rigor init` writes the key commented out. |
68
68
  | `paths` | Array | `["lib"]` | Directories or files to analyse. |
69
69
  | `exclude` | Array | `[]` | Glob patterns to skip. A relative pattern is matched against the file's path relative to the directory of the config file in use (the working directory when no config file is used), so it excludes the same files whether the path came from `paths:` or from a command-line argument, and it never matches on a directory above that base (a file outside it is seen as `../…`, so `../shared/lib/gen/**` reaches a sibling directory that `paths:` names). A leading `./` is ignored, and a pattern starting with `.` (such as `.*`) also matches files outside the base, which appear as `../…`. A symlink to a file is matched only under its own name: the realpath pass resolves directories, not files. `*` and `**` also cross `/`, so `lib/*.rb` matches nested files too. An absolute pattern is matched against the absolute path. Patterns from an `includes:` file resolve against the top-level config's directory, not the included file's. `vendor/bundle`, `.bundle`, and `node_modules` are always excluded. |
70
- | `test_paths` | Array | `nil` | The project's test roots: the directories (or files) holding its tests. Relative entries resolve against the config file's directory. Unset auto-detects whichever of `spec/` and `test/` exist; `[]` declares none. `rigor sig-gen --params=observed` reads call sites there to type parameters, and names on stderr a declared root that does not exist. Test roots are not analysed unless `paths:` also lists them, and changing them invalidates no cache. |
70
+ | `test_paths` | Array | `nil` | The project's test roots: the directories (or files) holding its tests. Relative entries resolve against the config file's directory. Unset auto-detects whichever of `spec/` and `test/` exist in the config file's directory, or in the working directory when the config file's directory has neither or no config file is used; `[]` declares none. `rigor sig-gen --params=observed` reads call sites there to type parameters, and names on stderr a declared root that does not exist. Test roots are not analysed unless `paths:` also lists them, and changing them invalidates no cache. |
71
71
  | `includes` | Array | `[]` | Other config files to layer underneath this one. |
72
72
  | `fold_platform_specific_paths` | Boolean | `false` | Resolve Ruby-version-conditional load paths when discovering sources. |
73
73
  | `parameter_inference` | Boolean | `false` | Opt-in call-site parameter type inference on the `check` walk ([ADR-67](../adr/67-parameter-type-inference.md) WD6). When `true`, an undeclared `def` / `initialize` / setter parameter is typed to the union of its resolved call-site argument types, sharpening downstream ivar reads, folds, and protection coverage. Precision-additive only — the negative rules never fire against an inferred parameter. Cannot be combined with `--incremental`. |
@@ -78,9 +78,39 @@ cache:
78
78
  | --- | --- | --- | --- |
79
79
  | `libraries` | Array | `[]` | Standard-library / gem names whose bundled RBS to load. |
80
80
  | `signature_paths` | Array | `nil` | Extra directories of `.rbs` files. Relative entries resolve against the config file's directory. |
81
+ | `core_root` | String | `nil` | A directory of core `.rbs` files loaded **instead of** the `rbs` gem's own core, such as one [rbs-shifter](https://github.com/rigortype/rbs-shifter) generates for an older CRuby. A relative path resolves against the config file's directory. `nil` loads the gem's core. See [Analysing for an older Ruby](#analysing-for-an-older-ruby). |
81
82
  | `pre_eval` | Array | `[]` | Files (or globs) walked before per-file analysis, to register project monkey-patches and publish their top-level constants project-wide. The walker does not follow `require` / `require_relative`, so list the files in load order: a method declared in two files takes the last-listed one, and a file that requires a later-listed one is still read in list order. `type-of`, `type-scan`, `trace`, `annotate` and `sig-gen` read the patches too, so they type a patched method as `rigor check` does. |
82
83
  | `plugins` | Array | `[]` | Plugins to activate — see [Using plugins](07-plugins.md). |
83
84
 
85
+ #### Analysing for an older Ruby
86
+
87
+ Rigor runs on Ruby 4.0, and its bundled core signatures describe Ruby 4.0. A project that runs on an older
88
+ Ruby can call a method that version does not have, and Rigor accepts the call. To check against the older
89
+ API, point `core_root:` at a core signature directory written for it:
90
+
91
+ ```yaml
92
+ target_ruby: "3.3"
93
+ core_root: vendor/rbs-shifter/3.3/core
94
+ ```
95
+
96
+ `core_root:` replaces the core, while `signature_paths:` only adds to it. A method that a signature directory
97
+ omits stays declared by the default core, so `signature_paths:` cannot remove one. With the core above,
98
+ `[1, 2].rfind { ... }` reports `call.undefined-method` and `" text ".strip(" ")` reports `call.wrong-arity`.
99
+ Project signatures, plugins and `libraries:` still load on top of it. `target_ruby:` sets the syntax Rigor
100
+ parses; it does not choose a core.
101
+
102
+ A `core_root:` that does not exist, is not a directory, holds no `.rbs` file, lies inside (or contains) one of
103
+ your `signature_paths:`, or does not build an RBS environment stops the run with exit code 64. A core class
104
+ whose definition fails to build because of the core's own files is reported as an
105
+ `rbs.coverage.definition-build-failed` error:
106
+
107
+ ```
108
+ rigor: core_root: "/path/to/vendor/rbs-shifter/3.3/core" does not exist
109
+ ```
110
+
111
+ The cache keys on the directory and on the contents of its files, so switching cores or regenerating one
112
+ invalidates cached results, including in a running language server.
113
+
84
114
  ### Signature generation
85
115
 
86
116
  | Key | Type | Default | Meaning |
@@ -106,6 +136,7 @@ rigor: disable: "call.undefined-methdo" is not a recognized rule id; the suppres
106
136
  rigor: disable: "plugin_loader.load-error" names a run-level row, which `disable:` does not reach; use `severity_overrides:` or a baseline
107
137
  rigor: disable: "plugin.shoulda-matchers.unknown-column" names no plugin `plugins:` configures; the suppression has no effect
108
138
  rigor: severity_overrides: "flow.bogus" is not a recognized rule id; the override has no effect
139
+ rigor: severity_overrides: "undefined-method" is a legacy alias of `call.undefined-method`; the override has no effect, so write `call.undefined-method`
109
140
  rigor: bundler.lockfile: "./missing/Gemfile.lock" does not exist
110
141
  ```
111
142
 
@@ -143,7 +174,10 @@ is left alone, other than the reserved run-level rows (`rbs.coverage.…`,
143
174
  those rows are the intended keys and are never flagged, while a bare plugin
144
175
  rule, `all`, or a `plugin.`-qualified key for an unconfigured plugin is. The same findings appear in the
145
176
  `--format=json` payload under `config_warnings` (each tagged with a
146
- `kind`), so CI can assert on them.
177
+ `kind`), so CI can assert on them. The [language server](09-editor-integration.md)
178
+ shows each one as a `window/showMessage` warning, and every
179
+ [MCP tool](10-mcp-server.md#tool-reference) that takes a `config`
180
+ argument includes them in its result.
147
181
 
148
182
  ### Diagnostics
149
183
 
@@ -193,7 +227,7 @@ project Ruby's gem home without running your toolchain. If `rigor check`'s
193
227
  `--stats` shows gems whose RBS it could not find, point it at the bundle
194
228
  explicitly with `bundler.bundle_path:`, or supply signatures another way:
195
229
  `rbs collection install` (auto-discovered) or `dependencies.source_inference:`.
196
- | `rbs_collection.auto_detect` | Boolean | `true` | Auto-discover `rbs_collection.lock.yaml`. |
230
+ | `rbs_collection.auto_detect` | Boolean | `true` | Auto-discover `rbs_collection.lock.yaml`. An entry is not loaded for a gem your `Gemfile.lock` sources from a `path:` directory that is at or under one of your `paths:` entries, or contains one, and is not left out by `exclude:` (the gem is code Rigor analyses, which your `sig/` declares). |
197
231
  | `rbs_collection.lockfile` | String | `nil` | Explicit `rbs_collection.lock.yaml` path. |
198
232
  | `dependencies.source_inference` | Array | `[]` | Per-gem source-inference modes (ADR-10). |
199
233
  | `dependencies.budget_per_gem` | Integer | `5000` | Per-gem source-walk cap, counted in **method definitions** (not time): the walker stops harvesting a gem's catalog once it reaches this many `def`s, then emits `dynamic.dependency-source.budget-exceeded` and degrades the rest to `Dynamic[top]`. Range 1250–20000. |
@@ -67,7 +67,7 @@ carries no `documentation_url`.
67
67
  | <a id="rule-flow-return-in-ensure"></a>`flow.return-in-ensure` | An explicit `return` inside an `ensure` clause — it overrides the method's in-flight return value and silently swallows any in-flight exception. A `return` in a nested `def`, lambda, or `define_method` block inside the `ensure` does not fire (it exits that inner frame). | high |
68
68
  | <a id="rule-flow-shadowed-rescue-clause"></a>`flow.shadowed-rescue-clause` | A `rescue` clause can never run because an earlier clause of the same chain already catches a superclass (or the same class) of every exception class it names. | high |
69
69
  | <a id="rule-def-return-type-mismatch"></a>`def.return-type-mismatch` | The method body's result violates its declared RBS return type. | medium |
70
- | <a id="rule-def-ivar-write-mismatch"></a>`def.ivar-write-mismatch` | An instance variable is written with a type disagreeing with its first write, unless its RBS declaration (`@x: T` or an `attr_*` member) admits both writes. | high |
70
+ | <a id="rule-def-ivar-write-mismatch"></a>`def.ivar-write-mismatch` | An instance variable is written with a type disagreeing with its first write, unless its RBS declaration (`@x: T` or an `attr_*` member) admits both writes. With `rigor-actionpack` or `rigor-actionmailer` loaded, writes in two different actions of a controller or mailer are not compared, since Rails runs one action per instance. A write in a private or protected helper, `initialize` or the same action still is, and so is one in a callback target or `helper_method` that a macro in the same file names. A public method named only by a macro in another file or a concern, or reached through a lambda callback, counts as an action, and so does an action another action calls. | high |
71
71
  | <a id="rule-def-method-visibility-mismatch"></a>`def.method-visibility-mismatch` | An explicit-receiver call reaches a private method. | high |
72
72
  | <a id="rule-def-override-visibility-reduced"></a>`def.override-visibility-reduced` | An override reduces the visibility it inherits from a project-defined ancestor. | high |
73
73
  | <a id="rule-def-override-return-widened"></a>`def.override-return-widened` | An override's declared return type widens the inherited return (covariance). | high |
@@ -77,7 +77,9 @@ 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`.
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`. They analyse the configured `paths:` only: a positional path argument is rejected with a usage error (exit `64`), since a baseline over a subset of files would silently drop the entries of every other file. The other three subcommands reject a positional path the same way.
81
+
82
+ The summary counts only the diagnostics the written buckets cover. A diagnostic with no rule — a **parse error** in a file Rigor could not parse, a missing `paths:` entry, or an internal error — or no path cannot be keyed, so it never enters the baseline. The command prints a second line saying how many, why, and which files. The exit status stays 0, but the next `rigor check` with `baseline:` wired still reports them and exits non-zero until you fix those files, exclude them, or remove a bad `paths:` entry.
81
83
 
82
84
  `generate` and `regenerate` take `--match-mode=rule` (the
83
85
  default — one bucket per file × rule) or `--match-mode=message`
@@ -103,11 +105,11 @@ snapshot in the repository and compares against it explicitly:
103
105
 
104
106
  ```sh
105
107
  # Once: capture the current diagnostic surface.
106
- rigor check --format=json > rigor.baseline.json
107
- git add rigor.baseline.json
108
+ rigor check --format=json > rigor-check.json
109
+ git add rigor-check.json
108
110
 
109
111
  # Per PR: compare against the committed snapshot.
110
- rigor diff rigor.baseline.json
112
+ rigor diff rigor-check.json
111
113
  ```
112
114
 
113
115
  [`rigor diff`](02-cli-reference.md#rigor-diff) prints a `+ NEW`
@@ -25,6 +25,7 @@ design + capability matrix lives in
25
25
  | `textDocument/documentSymbol` | Outline tree from Prism AST: `class` / `module` / `def` with nesting. |
26
26
  | `workspace/didChangeWatchedFiles` | Invalidates the per-session `Environment` + `Cache::Store` cache so saved files repropagate. |
27
27
  | `workspace/didChangeConfiguration` | Same — re-reads `.rigor.yml` / `Gemfile.lock` etc. |
28
+ | `window/showMessage` | One warning per [configuration warning](03-configuration.md) — the lines `rigor check` prints on STDERR, such as a key that has no effect or a `disable:` token that names no rule. Sent on `initialized`, and again after a save only when re-reading `.rigor.yml` changed the set of warnings. |
28
29
 
29
30
  ## Prerequisites
30
31
 
@@ -51,8 +52,10 @@ rigor lsp [--transport=stdio] [--log=PATH] [--config=PATH]
51
52
 
52
53
  - `--transport=stdio` — default; only value accepted in v1. TCP /
53
54
  Unix-socket transports are queued.
54
- - `--log=PATH` — wire log + server debug to a file. Without it,
55
- server-side logs go to stderr.
55
+ - `--log=PATH` — append the wire log (each message in and out, one
56
+ line each) and the server's debug output from the start of the loop to
57
+ a file. Output before the loop starts still goes to stderr. Without
58
+ it, no wire log is written and server-side logs go to stderr.
56
59
  - `--config=PATH` — mirrors `rigor check --config=PATH`. Without
57
60
  it, `Configuration.discover` walks `.rigor.yml` / `.rigor.dist.yml`
58
61
  from the project root.
@@ -194,6 +197,14 @@ For a legacy bundler-based install, swap the connection list to
194
197
  there, the LSP should too. If `rigor check` fails, fix that
195
198
  first.
196
199
 
200
+ **A rule still fires although `.rigor.yml` turns it off.**
201
+
202
+ - Look for a `rigor:` warning the server showed when it started (or
203
+ after the save that changed `.rigor.yml`): a misspelt key such as
204
+ `undefined-method: "off"` at the top level, or a `disable:` token
205
+ that names no rule, has no effect, and the server says so once.
206
+ The same warnings appear on STDERR from `rigor check`.
207
+
197
208
  **Completion popup is empty.**
198
209
 
199
210
  - Completion only fires on a node with a known type. Receivers
@@ -66,7 +66,9 @@ rigor mcp [--transport=stdio] [--config=PATH]
66
66
  transport is queued for v2.
67
67
  - `--config=PATH` — session-level default config path. Individual tool
68
68
  calls may supply their own `config` argument, which takes precedence.
69
- Without either, `Configuration.discover` walks `.rigor.yml` /
69
+ A path that names no file is an error, not a silent switch to the
70
+ defaults, for the session default and for a per-call `config` alike:
71
+ the call returns `isError: true`. Without either, `Configuration.discover` walks `.rigor.yml` /
70
72
  `.rigor.dist.yml` from the working directory.
71
73
 
72
74
  Exit codes: 0 (clean shutdown — stdin EOF), 64 (unknown
@@ -191,6 +193,28 @@ All messages except `notifications/*` require a response.
191
193
 
192
194
  ## Tool reference
193
195
 
196
+ Every tool that takes a `config` argument reports the configuration's
197
+ warnings — the ones `rigor check` prints on STDERR, such as a key that
198
+ has no effect or a `disable:` token that names no rule. `rigor_check`
199
+ carries them in its own document; every other such tool appends a
200
+ second `text` content block holding one JSON object, after the tool's
201
+ own output, which is left unchanged:
202
+
203
+ ```json
204
+ {"config_warnings": [{"kind": "unknown_key", "message": "`undefined-method` is not a recognized configuration key; it has no effect.", "key": "undefined-method", "suggestion": null}]}
205
+ ```
206
+
207
+ Each entry has the shape of `rigor check --format json`'s
208
+ `config_warnings` entries. There is no such block when the
209
+ configuration has no warnings, or when it fails to load (the tool
210
+ reports that error itself).
211
+
212
+ `rigor_type_of` and `rigor_explain` print JSON whose top-level shape changes
213
+ in the next minor release (`{ "schema_version": 1, "results" | "rules" |
214
+ "skip_reasons": [...] }`). Until then, a successful call carries a final
215
+ `text` content block, after any `config_warnings` block, with the one-line
216
+ notice the CLI prints on STDERR. The tool's own document is left unchanged.
217
+
194
218
  ### rigor_check
195
219
 
196
220
  Analyze one or more Ruby files or directories for type errors,
@@ -220,9 +244,16 @@ undefined methods, arity mismatches, and nil-receiver risks.
220
244
  }
221
245
  ```
222
246
 
247
+ When `.rigor.yml` has [configuration warnings](03-configuration.md)
248
+ (a key or a `disable:` token that has no effect), the document carries
249
+ them under `config_warnings`, as `rigor check --format json` does.
250
+
223
251
  `isError` is `false` when the run completes normally, even if
224
- diagnostics were found. `isError: true` signals a configuration or
225
- runtime failure (bad path, unreadable config, etc.).
252
+ diagnostics were found. `isError: true` signals a usage or runtime
253
+ failure: an unreadable `config`, or a `rigor_coverage` call whose
254
+ `paths` leave nothing to measure. A missing path given to `rigor_check`
255
+ is not one of these: it is reported in the diagnostics, and the call
256
+ returns `isError: false`.
226
257
 
227
258
  **Typical agent use:** call `rigor_check` on the files an agent is
228
259
  about to edit, record the baseline, apply the edit, call again, diff
@@ -417,6 +448,12 @@ The key is absent under the default `untyped` policy.
417
448
  Classifications: `new-file`, `new-method`, `tighter-return`,
418
449
  `equivalent`, `skipped`.
419
450
 
451
+ A `paths` entry that is neither a directory nor a `.rb` file is reported
452
+ under a top-level `diagnostics` array, in the shape `rigor_check` uses
453
+ (`"message": "no such file or directory"`). It is a warning beside
454
+ paths that were read, and an error when none was. An empty `candidates`
455
+ list with no `diagnostics` means nothing was found to generate.
456
+
420
457
  **Typical agent use:** call `rigor_sig_gen` to see which methods have
421
458
  precise enough returns to be worth writing into `sig/`, then generate
422
459
  and insert the RBS. The agent should NOT write signatures
@@ -476,6 +513,13 @@ as `Constant` / `Nominal` / shaped / refined (precise) versus
476
513
 
477
514
  **Returns:** JSON — the same as `rigor coverage --format json`.
478
515
 
516
+ A `paths` entry that is missing, or is not a Ruby file, is reported as
517
+ `rigor coverage` reports it. Beside files that are measured it is skipped. The result
518
+ then carries a second text block with the CLI's stderr, unfiltered: path
519
+ warnings such as `missing.rb:1:1: warning: no such file or directory (skipped)`,
520
+ and any other warnings. When no entry holds a Ruby file, the call is
521
+ `isError: true` and the block says so.
522
+
479
523
  ```json
480
524
  {
481
525
  "summary": {
@@ -185,7 +185,15 @@ exactly which files an edit can affect. A continuous-integration
185
185
  gate, `rigor check --verify-incremental`, asserts this on every
186
186
  build: it runs the incremental analyzer and a full analysis and
187
187
  fails if they disagree on a single diagnostic, or list the same
188
- diagnostics in a different order.
188
+ diagnostics in a different order. That gate re-analyses an
189
+ unchanged tree, so it cannot see an edit that fails to re-check a
190
+ file it affects. `--verify-incremental-edits=N` also edits `N`
191
+ files one at a time, in memory and not on disk, and compares each
192
+ warm recheck with a full analysis of the edited tree. Each file is
193
+ edited up to four times: once emptied, once reduced to its
194
+ `class` / `module` declarations with every body dropped, and up
195
+ to twice with one statement of a `class` / `module` body removed
196
+ and the rest kept. It replays only removals, never an addition.
189
197
 
190
198
  Rigor is also precise about *what* an edit changed. Re-checking a
191
199
  file that many others depend on — a base class, a widely-used
@@ -77,6 +77,36 @@ plugins:
77
77
  `--fail-on=warning` run — regenerate with `rigor baseline
78
78
  regenerate`.
79
79
 
80
+ ## Instance variable writes across actions
81
+
82
+ The plugin tells the engine that a mailer runs one action per instance
83
+ ([ADR-125](../../adr/125-per-invocation-classes.md)). Its manifest lists
84
+ `ActionMailer::Base` in `per_invocation_classes:`, and the callback macros
85
+ (`before_action` and the rest) and `helper_method` in
86
+ `per_invocation_non_action_macros:`, so a method those name is not an action.
87
+
88
+ So [`def.ivar-write-mismatch`](../04-diagnostics.md#rule-def-ivar-write-mismatch)
89
+ stays silent when `welcome` assigns `@items = [1, 2]` and `digest` assigns
90
+ `@items = { a: 1 }`. It still reports two divergent writes in the same action,
91
+ and a divergence that involves a private or protected helper, `initialize`, or a
92
+ callback target or `helper_method` that a macro in the same file names.
93
+
94
+ The callback macros include `before_deliver`, `after_deliver` and
95
+ `around_deliver`, which run on the action's own instance.
96
+
97
+ Two shapes stay silent because their macros are not listed: a public
98
+ method named by `rescue_from X, with: :handler` or by `layout :method_name`
99
+ counts as an action, as does one a lambda callback
100
+ (`before_action -> { load_roles }`) reaches ([#2320](https://github.com/rigortype/rigor/issues/2320) tracks the fix).
101
+
102
+ Visibility is read in source order, as Rails builds `action_methods` from the
103
+ public instance methods. A method that ends up protected or private is not an
104
+ action: it follows a bare `protected` or `private`, is named by `private :x` or
105
+ `protected :x`, or is defined inline (`private def x`). `public :x` or
106
+ `public def x` makes it one again. So it is no longer checked for a view
107
+ template, and calling it (`UserMailer.helper`) reports `unknown-action` at
108
+ error severity, as Rails raises `NoMethodError` for it.
109
+
80
110
  ## Plugin internals
81
111
 
82
112
  The mailer/concern discoverer, the cached `:mailer_index` producer,
@@ -41,6 +41,36 @@ no-op rather than erroring.
41
41
  | `plugin.actionpack.permit-call` | info | a `params.require(:m).permit(:key, …)` chain resolved to a known model; keys matched against its columns |
42
42
  | `plugin.actionpack.unknown-permit-key` | error | a literal `permit(:key)` is a near-miss (edit distance ≤ 2) of a real column but not one — a likely typo (with a did-you-mean). A key nothing like any column (a legitimate virtual attribute) does not fire |
43
43
 
44
+ The plugin also tells the engine that a controller runs one action
45
+ per instance ([ADR-125](../../adr/125-per-invocation-classes.md)).
46
+ Its manifest lists `ActionController::Base` and
47
+ `ActionController::API` in `per_invocation_classes:`. It also lists
48
+ the callback macros (`before_action` and the rest) and
49
+ `helper_method` in `per_invocation_non_action_macros:`, so a method
50
+ those name is not an action.
51
+
52
+ So `def.ivar-write-mismatch` stays silent when `index` assigns
53
+ `@roles = Role.sorted.to_a` and `permissions` assigns
54
+ `@roles = Role.sorted`. It still reports a divergence that involves
55
+ a private or protected helper, `initialize`, or two writes in the
56
+ same action. It also still reports one that involves a callback
57
+ target or `helper_method`, when the macro naming it sits in the same
58
+ file as the method. Actions defined in a concern module and
59
+ `ActionController::Metal` controllers keep reporting too.
60
+
61
+ Two shapes stay silent because their macros are not listed: a public
62
+ method named by `rescue_from X, with: :handler` or by `layout :method_name`
63
+ counts as an action, as does one a lambda callback
64
+ (`before_action -> { load_roles }`) reaches ([#2320](https://github.com/rigortype/rigor/issues/2320) tracks the fix).
65
+
66
+ These shapes stay silent:
67
+
68
+ - a public method named only by a macro in another file or a
69
+ concern, such as `ApplicationController`;
70
+ - a public method a lambda callback reaches
71
+ (`before_action -> { load_roles }`);
72
+ - an action that calls another action.
73
+
44
74
  Filter and render resolution honours nested-module controller
45
75
  qualification (`module Admin; class WidgetsController` resolves
46
76
  views under `admin/widgets/…`) and silences gem-shipped parent
@@ -367,21 +397,29 @@ diff — not a policy scoped to `app/views/**/*`. See
367
397
  knows what `rigor-rails-routes` published, and `permit`
368
398
  validation only what `rigor-activerecord` published — enabling
369
399
  those producers widens what this plugin can check.
370
- - **`params[:key]` stays untyped.** Inside a controller, `params`
371
- types as `ActionController::Parameters`, and so does the result
372
- of every builder method that always returns one — `require`,
400
+ - **`params[:key]` is a gradual carrier.** Inside a controller,
401
+ `params` types as `ActionController::Parameters`, and so does the
402
+ result of every builder method that always returns one — `require`,
373
403
  `permit`, `permit!`, `expect`, `slice`, `slice!`, `except`,
374
404
  `without`, `extract!`, `merge`, `merge!`, `reverse_merge`,
375
405
  `reverse_merge!`, `with_defaults`, `with_defaults!`, `compact`,
376
406
  `compact_blank`, `deep_dup` — so a chain built from them keeps a
377
- concrete receiver throughout. A subscript read is deliberately
378
- left untyped: `params[:missing]` is `nil` at runtime, and a type
379
- that says otherwise would let the flow rules fold live
380
- conditions (`if params[:q]`, `url.nil?`) to a constant and report
381
- working code. Methods whose result depends on the call — `dig`,
382
- `fetch`, `compact!`, and the block-less `select` / `reject` /
383
- `transform_keys` / `transform_values` — are untyped for the same
384
- reason.
407
+ concrete receiver throughout. A subscript read answers
408
+ `Dynamic[ActionController::Parameters | nil]` (#2430): the static
409
+ facet is the nilable `Parameters`, so `params[:user][:name]` and
410
+ `params[:user].permit(:name)` keep dispatching on that facet, and
411
+ the `Dynamic` wrapper lets the value cross a declared boundary such
412
+ as a `String` parameter, as a scalar param does at runtime. The
413
+ facet is nilable rather than the non-nil nominal on purpose:
414
+ `params[:missing]` is `nil` at runtime, and a non-nil type would
415
+ let the flow rules fold live conditions (`if params[:q]`,
416
+ `url.nil?`) to a constant and report working code. Methods on the
417
+ carrier outside the chain (`params[:q].to_s`) answer `Dynamic[top]`,
418
+ because the engine dispatches a `Dynamic` receiver without projecting
419
+ its facet: a loss of precision, not a false positive. Methods whose
420
+ result depends on the call — `dig`, `fetch`, `compact!`, and the
421
+ block-less `select` / `reject` / `transform_keys` /
422
+ `transform_values` — are untyped for the same reason.
385
423
  - **`flash[:key]` and `session[:key]` stay untyped too**, for that
386
424
  same reason and measured the same way. Both are leaf reads that
387
425
  return whatever was stored — or `nil` for a key that is not set.
@@ -32,6 +32,17 @@ path, no vendoring, no `signature_paths:` wiring.
32
32
  > surface. When it is loaded, the auto overlay stands down so the two
33
33
  > never double-declare.
34
34
 
35
+ If the project also runs `rbs collection install` for `activesupport`,
36
+ some methods are declared twice — `Object#acts_like?`,
37
+ `Object#html_safe?`, `Time#to_fs` among them. Rigor merges the two:
38
+ the plugin's signatures are tried first and the collection's still
39
+ apply, so a call either one accepts type-checks. When your own `sig/`
40
+ or inline RBS declares one of the plugin's methods, yours is used
41
+ instead and the plugin's stands down; the run reports one
42
+ `rbs.coverage.plugin-signature-stood-down` info row for the plugin's
43
+ file, naming those methods and the file whose declaration is used for
44
+ each.
45
+
35
46
  ## What it covers
36
47
 
37
48
  Roughly the top ~40 selectors plus their close neighbours, across:
@@ -55,7 +66,15 @@ Roughly the top ~40 selectors plus their close neighbours, across:
55
66
  - **Hash** — `#symbolize_keys` / `#stringify_keys` (+ deep / bang) and
56
67
  their `#to_options` alias, `#deep_merge`, `#reverse_merge` /
57
68
  `#with_defaults` (+ bang), `#with_indifferent_access`, `#except!`,
58
- `#extract!`.
69
+ `#extract!`. `#symbolize_keys` / `#to_options` map the key type per
70
+ member, as ActiveSupport does (`key.to_sym rescue key`): a `String` or
71
+ `Symbol` key reads as `Symbol`, and every other key (an `Integer`, a
72
+ `Float`, a project class) reads as `untyped`, because a mixin or a
73
+ project file can give any class a `to_sym`. So
74
+ `Hash[String, Integer]` gives `Hash[Symbol, Integer]` and
75
+ `Hash[Integer, Integer]` gives `Hash[untyped, Integer]`. Without the
76
+ plugin, on the activesupport overlay alone, the key reads `untyped`.
77
+ The bang forms answer the receiver's own type.
59
78
  - **Range** — `#overlaps?`, `#to_fs` / `#to_formatted_s`.
60
79
  - **Enumerable** — `#index_by`, `#index_with`, `#pluck`, `#exclude?`.
61
80
 
@@ -48,6 +48,21 @@ whatever class includes the concern.
48
48
 
49
49
  ## Limitations
50
50
 
51
+ - **Needs RBS for the Devise modules.** The methods come from the
52
+ strategy modules' signatures (`Devise::Models::DatabaseAuthenticatable`
53
+ and the rest), which Rigor does not bundle and the `devise` gem does
54
+ not ship. Supply them through `signature_paths:` (or a
55
+ `rbs collection`); a strategy module with no signature contributes
56
+ nothing, and the model's `valid_password?` and friends stay as they
57
+ were. `plugins/rigor-devise/demo/sig/devise.rbs` is a minimal
58
+ example.
59
+ - **Your own definitions win.** A method the model defines itself
60
+ (`def`, `alias_method`, `attr_*`, `define_method`, `delegate`), or
61
+ that a module of your project it includes or prepends defines, is
62
+ left to that definition rather than typed from the Devise module.
63
+ This holds even for a module included before the `devise` call,
64
+ which Devise's module would override at runtime. Only the module's
65
+ own public methods are contributed.
51
66
  - **Instance methods only.** Per-module `ClassMethods` (e.g.
52
67
  `User.reset_password_by_token`) are not yet synthesised.
53
68
  - **Controller helpers deferred.** `current_user` /
@@ -73,10 +73,11 @@ end
73
73
  | `eq(literal)` / `eql(literal)` | meet with `Constant<literal>` (see below) |
74
74
  | `be_a(T)` / `be_kind_of(T)` / `be_instance_of(T)` / `be_an_instance_of(T)` | meet with `T` |
75
75
  | `be_nil` | narrow to `Constant<nil>` |
76
- | `match(/regex/)` | narrow to `String` |
76
+ | `match(/regex/)` | keep the `String` and `Symbol` members, read an `Object`, `BasicObject` or module member as `String \| Symbol`, and remove `nil`, `true`, `false` and numeric members (unless the project defines `match`, `to_str` or `==` on them); unchanged when a member may be anything else |
77
77
  | `not_to be_nil` / `to_not be_nil` | narrow away from `nil` |
78
78
  | `not_to be_a(T)` / `not_to be_kind_of(T)` / `not_to be_instance_of(T)` / `not_to be_an_instance_of(T)` | narrow away from `T` |
79
79
  | `not_to eq(literal)` / `not_to eql(literal)` (and `to_not`) | unchanged (see below) |
80
+ | `not_to match(/regex/)` (and `to_not`) | unchanged: a `String` that does not match passes |
80
81
 
81
82
  Other matchers emit nothing. `be_truthy` is not narrowed, nor is
82
83
  `expect { ... }.to raise_error(...)` (the block form), nor an
@@ -97,6 +97,13 @@ module Rigor
97
97
  new(buckets, project_root: project_root)
98
98
  end
99
99
 
100
+ # The diagnostics a baseline cannot address: no rule (a Prism parse error, a path or internal error) or no path.
101
+ # `#filter` passes these through and `from_diagnostics` leaves them out of every bucket; this is the one
102
+ # definition of both.
103
+ def unkeyable(diagnostics)
104
+ diagnostics.reject { |d| d.qualified_rule && d.path }
105
+ end
106
+
100
107
  private
101
108
 
102
109
  def parse_loaded(raw, path:, project_root:)
@@ -138,17 +145,12 @@ module Rigor
138
145
  raise LoadError, "#{path}: ignored[#{index}] `message:` is not a valid Regexp: #{e.message}"
139
146
  end
140
147
 
141
- # Returns Hash{[file, rule, regex_or_nil] => Array<Diagnostic>}.
142
- # In message mode, each unique message gets its own bucket;
143
- # in rule mode, every diagnostic for a (file, rule) pair
144
- # contributes to a single bucket regardless of message.
145
- # File paths are normalised to relative before keying.
148
+ # Groups diagnostics by [file, rule, regex_or_nil]. In message mode, each unique message gets its own
149
+ # bucket; in rule mode, every diagnostic for a (file, rule) pair contributes to a single bucket regardless
150
+ # of message. File paths are normalised to relative before keying.
146
151
  def group_for_baseline(diagnostics, match_mode, project_root)
147
152
  root = Pathname.new(project_root)
148
- diagnostics.each_with_object({}) do |diag, into|
149
- next if diag.qualified_rule.nil?
150
- next if diag.path.nil?
151
-
153
+ (diagnostics - unkeyable(diagnostics)).each_with_object({}) do |diag, into|
152
154
  rel = relative_path(diag.path, root)
153
155
  key = case match_mode
154
156
  when :rule
@@ -160,18 +162,18 @@ module Rigor
160
162
  end
161
163
  end
162
164
 
165
+ def relative_path(path, root_pathname)
166
+ Pathname.new(path).relative_path_from(root_pathname).to_s
167
+ rescue ArgumentError
168
+ path # different drive letter on Windows or non-child path
169
+ end
170
+
163
171
  # Generates a Regexp source string for the baseline row. The string is `Regexp.escape`d so the YAML
164
172
  # round-trip produces a regex that matches the literal message. Users hand-editing the row can replace
165
173
  # the escaped form with a pattern.
166
174
  def message_pattern_for(message)
167
175
  Regexp.new(Regexp.escape(message.to_s))
168
176
  end
169
-
170
- def relative_path(path, root_pathname)
171
- Pathname.new(path).relative_path_from(root_pathname).to_s
172
- rescue ArgumentError
173
- path # different drive letter on Windows or non-child path
174
- end
175
177
  end
176
178
 
177
179
  class LoadError < StandardError; end
@@ -181,11 +183,8 @@ module Rigor
181
183
  def initialize(buckets, project_root: Dir.pwd)
182
184
  @project_root = Pathname.new(project_root)
183
185
  @buckets = buckets.freeze
184
- # For each (file, qualified_rule) pair, two arrays:
185
- # - rule-ID rows (message_regex == nil)
186
- # - message-pattern rows (message_regex != nil)
187
- # The matcher walks message-pattern rows first (tighter match takes precedence); diagnostics that
188
- # don't match any message row fall through to the rule-ID row if one exists.
186
+ # Buckets grouped by (file, rule). `claim_bucket_for` splits each group into message-pattern rows, tried
187
+ # first as the tighter match, and the rule-ID row (message_regex == nil) as the fallback.
189
188
  @by_pair = buckets.group_by { |b| [b.file, b.rule] }.freeze
190
189
  freeze
191
190
  end
@@ -216,7 +215,7 @@ module Rigor
216
215
 
217
216
  # Diagnostics that lacked a rule or a path bypass the baseline entirely (the baseline can't address
218
217
  # them).
219
- unkeyable = diagnostics.reject { |d| d.qualified_rule && d.path }
218
+ unkeyable = self.class.unkeyable(diagnostics)
220
219
  [surfaced + unkeyable, silenced_count]
221
220
  end
222
221
 
@@ -225,7 +224,7 @@ module Rigor
225
224
  # - `:within` — `actual <= count` (silenced by the filter).
226
225
  # - `:over` — `actual > count` (over threshold; surfaced in the regular `rigor check` output).
227
226
  # - `:cleared` — `actual == 0` (the bucket can be pruned).
228
- # - `:reducible` — `0 < actual < count` (the bucket's count can be tightened; future `regenerate` slice 5
227
+ # - `:reducible` — `0 < actual < count` (the bucket's count can be tightened; `rigor baseline regenerate`
229
228
  # handles this).
230
229
  DriftRow = Struct.new(:bucket, :actual_count, :status, keyword_init: true) do
231
230
  def delta
@@ -254,8 +253,8 @@ module Rigor
254
253
  end
255
254
  end
256
255
 
257
- # Returns a new Baseline with the given buckets dropped. Used by `rigor baseline prune` (slice 2) to
258
- # remove cleared buckets (`actual == 0`) from the on-disk file.
256
+ # Returns a new Baseline with the given buckets dropped. Used by `rigor baseline prune` to remove cleared
257
+ # buckets (`actual == 0`) from the on-disk file.
259
258
  def without(buckets_to_drop)
260
259
  dropset = buckets_to_drop.to_set
261
260
  self.class.new(buckets.reject { |b| dropset.include?(b) }, project_root: @project_root)
@@ -311,9 +310,7 @@ module Rigor
311
310
  # over rule-ID rows because they're more specific. A diagnostic that matches no row goes into a
312
311
  # synthetic "no-bucket" bin keyed by (file, rule).
313
312
  bins = {}
314
- diagnostics.each do |diag|
315
- next if diag.qualified_rule.nil? || diag.path.nil?
316
-
313
+ (diagnostics - self.class.unkeyable(diagnostics)).each do |diag|
317
314
  bucket = claim_bucket_for(diag)
318
315
  key = if bucket
319
316
  [bucket.file, bucket.rule,
@@ -24,6 +24,20 @@ module Rigor
24
24
  def display_path(path)
25
25
  path == physical_path ? logical_path : path
26
26
  end
27
+
28
+ # The file's identity for comparing two spellings of one path: its real path, resolved through its directory
29
+ # when the file is absent from disk (a new buffer's file), so a symlinked root or parent spells it as it spells
30
+ # an existing file. Two accepted limits: a missing name is compared as written, so a name that differs only in
31
+ # case is not matched; and a `..` in a missing path is resolved textually, not through the filesystem.
32
+ def self.real_path(path)
33
+ File.realpath(path)
34
+ rescue SystemCallError
35
+ absolute = File.absolute_path(path)
36
+ parent = File.dirname(absolute)
37
+ return absolute if parent == absolute
38
+
39
+ File.join(real_path(parent), File.basename(absolute))
40
+ end
27
41
  end
28
42
  end
29
43
  end
@@ -23,10 +23,11 @@ module Rigor
23
23
  # `BlockNode` ancestors. Mutation tracking through loop bodies is incomplete (`shift = 0; loop {
24
24
  # shift += 7 }` keeps `shift` at `Constant<7>` per the analyzer), so a `Constant<bool>` predicate
25
25
  # inside the loop body is suspect.
26
- # - **Defensive predicate calls**: `.nil?` / `.empty?` / `.zero?` / `.any?` / `.none?` / `.all?`. These
27
- # read like the user explicitly checking for a runtime condition that the type system already proves
28
- # can't happen — Rigor's strict-on-returns RBS often disagrees with the user's defensive check (e.g.
29
- # `Module#name` returns `String` per RBS but anonymous classes really do return `nil`). Skipping
26
+ # - **Defensive predicate calls**: `.nil?` / `.empty?` / `.zero?` / `.any?` / `.none?` / `.all?` /
27
+ # `.respond_to?`. These read like the user explicitly checking for a runtime condition that the type
28
+ # system already proves can't happen — Rigor's strict-on-returns RBS often disagrees with the user's
29
+ # defensive check (e.g. `Module#name` returns `String` per RBS but anonymous classes really do return
30
+ # `nil`). Skipping
30
31
  # these forms keeps the rule useful for genuine logic errors without faulting defensive code.
31
32
  #
32
33
  # Also skipped (so the literal-only `unreachable-branch` rule doesn't double-fire alongside this one):
@@ -53,8 +54,6 @@ module Rigor
53
54
  NODE_CLASSES = [Prism::IfNode, Prism::UnlessNode].freeze
54
55
  RULE_WALK_GATES = [:loop_or_block].freeze
55
56
 
56
- # @return one entry per qualifying predicate. Empty when the tree carries no firing
57
- # predicates.
58
57
  def initialize(scope_index)
59
58
  @scope_index = scope_index
60
59
  @results = []
@@ -35,8 +35,8 @@ module Rigor
35
35
  # exactly like the legacy walk.
36
36
  NODE_CLASSES = [Prism::DefNode].freeze
37
37
 
38
- # Returns `Array<{def_class:, def_name:, write_node:}>` — one entry per dead-assignment write. Empty
39
- # when the tree has no qualifying writes.
38
+ # One entry per dead-assignment write, as `{def_node:, write_node:}`. Empty when the tree has no
39
+ # qualifying writes.
40
40
  def initialize(scope_index)
41
41
  @scope_index = scope_index
42
42
  @results = []