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
@@ -7,7 +7,7 @@ module Rigor
7
7
  module Cache
8
8
  # Cache producer that materialises the set of every RBS-declared class / module / alias name (top-level
9
9
  # prefixed, e.g. `"::Math"`) currently loaded into the environment. Marshal-clean — the cache value is a
10
- # `Set<String>`.
10
+ # Set of names.
11
11
  #
12
12
  # The set lets `RbsLoader#class_known?` answer point lookups in O(1) without re-parsing the name on each
13
13
  # call, and lets a warm process skip the env walk entirely (the env still has to be built to enumerate
@@ -135,14 +135,6 @@ module Rigor
135
135
  end
136
136
  end
137
137
 
138
- # Walks the on-disk cache rooted at `root` and reports a producer-level inventory. Used by
139
- # `rigor check --cache-stats` to surface cache size and per-producer entry counts without depending on
140
- # in-process counters (which only reflect the current run).
141
- #
142
- # @return `{ root:, schema_version:, total_entries:, total_bytes:, producers: [{ id:, entries:,
143
- # bytes: }, ...] }`. When the root does not exist or has no schema-version marker, `schema_version` is
144
- # nil and the producer list is empty.
145
- #
146
138
  # The `schema_version.txt` marker content. Covers BOTH invalidation axes: the descriptor schema and the
147
139
  # on-disk byte layout ({FORMAT_VERSION}, ADR-54). A format bump leaves the old entries permanently
148
140
  # unreadable (header mismatch → miss) but, alone, would never reclaim their bytes — they can sit below
@@ -152,6 +144,13 @@ module Rigor
152
144
  "#{PAYLOAD_ABI_VERSION}.#{Descriptor::SCHEMA_VERSION}.#{FORMAT_VERSION}"
153
145
  end
154
146
 
147
+ # Walks the on-disk cache rooted at `root` and reports a producer-level inventory. Used by
148
+ # `rigor check --cache-stats` to surface cache size and per-producer entry counts without depending on
149
+ # in-process counters (which only reflect the current run).
150
+ #
151
+ # @return `{ root:, schema_version:, total_entries:, total_bytes:, producers: [{ id:, entries:,
152
+ # bytes: }, ...] }`. When the root does not exist or has no schema-version marker, `schema_version` is
153
+ # nil and the producer list is empty.
155
154
  def self.disk_inventory(root:)
156
155
  root_s = root.to_s
157
156
  marker = File.join(root_s, "schema_version.txt")
@@ -360,7 +359,8 @@ module Rigor
360
359
  # size-based LRU pass below stays gated on `max_bytes:` being configured: it walks all remaining
361
360
  # `.entry` files, sorts by mtime ascending (oldest = least recently used), and unlinks from the oldest
362
361
  # until the total is at or below the cap. Touch-on-disk-read ({read_entry}) is the cross-process LRU
363
- # signal: every disk hit (not in-process-memo hit) updates the mtime so recently-read entries survive
362
+ # signal: a disk hit (not an in-process-memo hit) updates the mtime when `max_bytes:` is set, so
363
+ # recently-read entries survive
364
364
  # the eviction pass. Any FS error is swallowed — eviction must never break a run.
365
365
  def evict!
366
366
  return if @read_only
@@ -591,7 +591,8 @@ module Rigor
591
591
  # — one check per Store is enough; a benign double-check under a thread race would just repeat
592
592
  # idempotent work.
593
593
  #
594
- # A writable store clears the cache root and rewrites the marker on a stale/missing marker, then reports
594
+ # A writable store clears the cache root and rewrites the marker on a stale marker (a missing or empty
595
+ # one is rewritten without clearing), then reports
595
596
  # available. A read-only store never touches the root — no mkdir, no marker write, no destructive clear
596
597
  # — so it reports available ONLY when the on-disk marker already matches current exactly; a stale or
597
598
  # missing marker (e.g. a Rigor upgrade with no writable run yet, as in LSP / editor mode) reports
@@ -20,7 +20,7 @@ module Rigor
20
20
  # non-CLI consumers exist: {Configuration#cache_validation_strict?} resolves the `auto` cache-validation default
21
21
  # through it (a fresh CI checkout regenerates every stat tuple, so ADR-87's stat tier cannot work there).
22
22
  # Detection is fully testable; callers pass `ENV`.
23
- # `RIGOR_CI_DETECT=0` (or `false`/`no`) disables it globally — the seam the spec suite uses for determinism.
23
+ # `RIGOR_CI_DETECT=0` (or `false`/`no`/`off`) disables it globally — the seam the spec suite uses for determinism.
24
24
  module CiDetector
25
25
  Platform = Struct.new(:id, :name, :format, :tier, keyword_init: true) do
26
26
  def native_stdout? = tier == :native_stdout
@@ -16,6 +16,7 @@ require_relative "../inference/statement_evaluator"
16
16
  require_relative "prism_colorizer"
17
17
  require_relative "command"
18
18
  require_relative "../project_environment"
19
+ require_relative "json_document"
19
20
 
20
21
  module Rigor
21
22
  class CLI
@@ -32,10 +33,12 @@ module Rigor
32
33
  class AnnotateCommand < Command
33
34
  USAGE = "Usage: rigor annotate [options] FILE"
34
35
 
35
- # Trailing `#=> …` annotation comment. Matched and stripped before re-annotating so re-running is idempotent —
36
- # this follows xmpfilter's convention of owning the `#=>` marker, and also absorbs the older `#=> dump_type:
37
- # <type>` spelling (idempotency across re-runs). The leading `\s` requirement keeps a `#=>` inside a string
38
- # literal (no preceding whitespace ambiguity aside) from matching mid-expression.
36
+ # Trailing `#=> …` annotation comment, matched and stripped before re-annotating so re-running is idempotent.
37
+ # Follows xmpfilter's convention of owning the `#=>` marker, and also absorbs the older `#=> dump_type: <type>`
38
+ # spelling.
39
+ # Strips the first `#=>` that is preceded by whitespace and followed by whitespace or the end of the line,
40
+ # together with that preceding whitespace, through the end of the line. A string literal is not special-cased,
41
+ # since the pattern does not parse Ruby; `a#=>b` and `x #=>y` are left alone.
39
42
  ANNOTATION_PATTERN = /\s+#=>(?:\s.*)?\z/
40
43
 
41
44
  # Arguments for highlighting through `bat`: the annotated text arrives on stdin, so the language must be explicit;
@@ -59,19 +62,13 @@ module Rigor
59
62
  execute(file, options)
60
63
  end
61
64
 
62
- private
63
-
64
- def parse_options
65
- # Default: colour a tty, unless `NO_COLOR` opts out. An explicit `--color` / `--no-color` overrides both.
66
- options = { config: nil, color: @out.tty? && !no_color_env?, bat: nil, format: :text }
67
-
68
- parser = OptionParser.new do |opts|
65
+ # The parser this command parses with, built without parsing anything: the catalogue reads the
66
+ # flags and formats from it (ADR-126 WD3). `options` is only written by the switches' blocks.
67
+ def option_parser(options = {})
68
+ OptionParser.new do |opts|
69
69
  opts.banner = USAGE
70
70
  Options.add_config(opts, options)
71
- opts.on("--format=FORMAT", %w[text json],
72
- "Output format: text (default) or json (a { line => type } map)") do |value|
73
- options[:format] = value.to_sym
74
- end
71
+ Options.add_format(opts, options, "Output format: text (default) or json (a { line => type } map)")
75
72
  opts.on("--[no-]color",
76
73
  "Force or disable ANSI colour (default: auto-detect a tty; honours NO_COLOR)") do |value|
77
74
  options[:color] = value
@@ -81,7 +78,15 @@ module Rigor
81
78
  options[:bat] = value
82
79
  end
83
80
  end
84
- parser.parse!(@argv)
81
+ end
82
+
83
+ private
84
+
85
+ def parse_options
86
+ # Default: colour a tty, unless `NO_COLOR` opts out. An explicit `--color` / `--no-color` overrides both.
87
+ options = { config: nil, color: @out.tty? && !no_color_env?, bat: nil, format: "text" }
88
+
89
+ option_parser(options).parse!(@argv)
85
90
 
86
91
  options
87
92
  end
@@ -118,7 +123,7 @@ module Rigor
118
123
  line_types = LineTypeCollector.new(scope_index).collect(parse_result.value)
119
124
 
120
125
  case options.fetch(:format)
121
- when :json
126
+ when "json"
122
127
  emit_json(line_types)
123
128
  else
124
129
  @out.puts(render(annotate(source, line_types), color: options.fetch(:color), bat: options.fetch(:bat)))
@@ -132,7 +137,7 @@ module Rigor
132
137
  # object keys).
133
138
  def emit_json(line_types)
134
139
  annotations = line_types.keys.sort.to_h { |line| [line.to_s, line_types[line].describe(:short)] }
135
- @out.puts(JSON.generate({ "annotations" => annotations }))
140
+ @out.puts(JSON.generate(JsonDocument.stamp({ "annotations" => annotations })))
136
141
  end
137
142
 
138
143
  # The plugin-aware environment for the annotated file, so a `#=> <type>` matches what `rigor check`
@@ -225,7 +230,7 @@ module Rigor
225
230
  @scope_index = scope_index
226
231
  end
227
232
 
228
- # @return 1-indexed line => type.
233
+ # @return map of 1-indexed line number to type.
229
234
  def collect(program)
230
235
  by_line = {}
231
236
  each_statement(program) do |statement|
@@ -2,6 +2,8 @@
2
2
 
3
3
  require "optparse"
4
4
 
5
+ require "did_you_mean"
6
+
5
7
  require_relative "../analysis/baseline"
6
8
  require_relative "options"
7
9
  require_relative "../analysis/runner"
@@ -24,7 +26,9 @@ module Rigor
24
26
  # rigor baseline prune
25
27
  class BaselineCommand < Command # rubocop:disable Metrics/ClassLength
26
28
  EXIT_USAGE = 64
29
+ USAGE = "Usage: rigor baseline <subcommand> [options]"
27
30
  DEFAULT_BASELINE_PATH = ".rigor-baseline.yml"
31
+ MAX_UNBASELINEABLE_FILES = 10
28
32
 
29
33
  SUBCOMMANDS = %w[generate regenerate dump drift prune].freeze
30
34
 
@@ -41,6 +45,8 @@ module Rigor
41
45
  when "prune" then run_prune
42
46
  else
43
47
  @err.puts("Unknown baseline subcommand: #{subcommand.inspect}")
48
+ nearest = ::DidYouMean::SpellChecker.new(dictionary: SUBCOMMANDS).correct(subcommand.to_s).first
49
+ @err.puts("Did you mean `rigor baseline #{nearest}`?") if nearest
44
50
  @err.puts(help)
45
51
  EXIT_USAGE
46
52
  end
@@ -49,10 +55,78 @@ module Rigor
49
55
  EXIT_USAGE
50
56
  end
51
57
 
58
+ # The parser of `subcommand`, built without parsing anything (the catalogue reads it, ADR-126 WD3).
59
+ def option_parser(subcommand, options = {})
60
+ case subcommand
61
+ when "generate", "regenerate" then generate_parser(subcommand, options)
62
+ when "dump" then dump_parser(options)
63
+ when "drift" then drift_parser(options)
64
+ when "prune" then prune_parser(options)
65
+ else raise ArgumentError, "unknown baseline subcommand: #{subcommand.inspect}"
66
+ end
67
+ end
68
+
52
69
  private
53
70
 
71
+ def generate_parser(subcommand, options)
72
+ OptionParser.new do |opts|
73
+ opts.banner = "Usage: rigor baseline #{subcommand} [options]"
74
+ Options.add_config(opts, options)
75
+ opts.on("--output=PATH", "Write baseline to PATH (default: #{DEFAULT_BASELINE_PATH})") do |v|
76
+ options[:output] = v
77
+ end
78
+ opts.on("--match-mode=MODE", %i[rule message],
79
+ "Row form: rule (default) or message") do |v|
80
+ options[:match_mode] = v
81
+ end
82
+ # `regenerate` always overwrites — no `--force` to offer.
83
+ if subcommand == "generate"
84
+ opts.on("--force", "Overwrite an existing baseline file") { options[:force] = true }
85
+ end
86
+ end
87
+ end
88
+
89
+ def dump_parser(options)
90
+ OptionParser.new do |opts|
91
+ opts.banner = "Usage: rigor baseline dump [options]"
92
+ opts.on("--baseline=PATH", "Path to the baseline file (default: #{DEFAULT_BASELINE_PATH})") do |v|
93
+ options[:baseline] = v
94
+ end
95
+ Options.add_format(opts, options, "Output format: text (default) or json")
96
+ opts.on("--rule=RULE", "Filter rows by exact rule id") { |v| options[:rule] = v }
97
+ opts.on("--file=GLOB", "Filter rows by File.fnmatch? glob") { |v| options[:file] = v }
98
+ end
99
+ end
100
+
101
+ def drift_parser(options)
102
+ OptionParser.new do |opts|
103
+ opts.banner = "Usage: rigor baseline drift [options]"
104
+ Options.add_config(opts, options)
105
+ opts.on("--baseline=PATH", "Path to the baseline file (default: #{DEFAULT_BASELINE_PATH})") do |v|
106
+ options[:baseline] = v
107
+ end
108
+ opts.on("--only=STATUS", %i[within over cleared reducible],
109
+ "Show only buckets with the given status (within|over|cleared|reducible)") do |v|
110
+ options[:only] = v
111
+ end
112
+ end
113
+ end
114
+
115
+ def prune_parser(options)
116
+ OptionParser.new do |opts|
117
+ opts.banner = "Usage: rigor baseline prune [options]"
118
+ Options.add_config(opts, options)
119
+ opts.on("--baseline=PATH", "Path to the baseline file (default: #{DEFAULT_BASELINE_PATH})") do |v|
120
+ options[:baseline] = v
121
+ end
122
+ opts.on("--dry-run", "Show what would be dropped without writing the file") { options[:dry_run] = true }
123
+ end
124
+ end
125
+
54
126
  def run_generate
55
127
  options = parse_generate_options
128
+ return positional_paths_error("generate") unless @argv.empty?
129
+
56
130
  path = options.fetch(:output)
57
131
 
58
132
  if File.exist?(path) && !options.fetch(:force)
@@ -69,9 +143,32 @@ module Rigor
69
143
  # so `rigor baseline regenerate` reads cleanly as "make the baseline match reality again".
70
144
  def run_regenerate
71
145
  options = parse_generate_options(subcommand: "regenerate")
146
+ return positional_paths_error("regenerate") unless @argv.empty?
147
+
72
148
  write_baseline(options, verb: "regenerated baseline")
73
149
  end
74
150
 
151
+ # Issues #2466 and #2577 — the baseline covers the configured `paths:`, never the positional arguments: a
152
+ # baseline built over a subset would silently drop the entries of every other file. `drift` and `prune`
153
+ # analyse the configured `paths:` and `dump` prints the whole file, so none of the five reads a path argument.
154
+ # Reject them, as `rigor effects update` does (`effects_snapshot_command.rb:54`), rather than honour them.
155
+ def positional_paths_error(subcommand)
156
+ @err.puts("rigor baseline #{subcommand}: unexpected argument(s) #{@argv.map(&:inspect).join(' ')}; " \
157
+ "#{positional_paths_hint(subcommand)}")
158
+ EXIT_USAGE
159
+ end
160
+
161
+ def positional_paths_hint(subcommand)
162
+ case subcommand
163
+ when "dump"
164
+ "dump reads the baseline file, so pass none: use --file=GLOB to filter rows or --baseline=PATH to pick a file"
165
+ when "drift", "prune"
166
+ "#{subcommand} audits the configured `paths:`, so pass none"
167
+ else
168
+ "the baseline is generated from the configured `paths:`, so pass none"
169
+ end
170
+ end
171
+
75
172
  def write_baseline(options, verb:)
76
173
  path = options.fetch(:output)
77
174
  configuration = Configuration.load(options.fetch(:config))
@@ -81,11 +178,13 @@ module Rigor
81
178
  project_root: Dir.pwd)
82
179
  File.write(path, baseline.to_yaml)
83
180
 
181
+ covered = baseline.buckets.sum(&:count)
84
182
  @err.puts(
85
183
  "rigor: #{verb} #{path} " \
86
- "(#{baseline.size} bucket(s) covering #{diagnostics.size} diagnostic(s); " \
184
+ "(#{baseline.size} bucket(s) covering #{covered} diagnostic(s); " \
87
185
  "match-mode: #{options.fetch(:match_mode)})"
88
186
  )
187
+ report_unbaselineable(Analysis::Baseline.unkeyable(diagnostics))
89
188
  if configuration.baseline_path.nil?
90
189
  # Name the config file that actually governs this project (`.rigor.dist.yml` when no local override
91
190
  # exists), so the hint points at the file the user must edit.
@@ -98,6 +197,38 @@ module Rigor
98
197
  0
99
198
  end
100
199
 
200
+ # Diagnostics the baseline cannot key never enter the file, so the next `rigor check` surfaces them. Say so,
201
+ # with how many, why and where; the exit status stays 0 (the file was written as asked).
202
+ def report_unbaselineable(unkeyable)
203
+ return if unkeyable.empty?
204
+
205
+ no_rule, no_path = unkeyable.partition { |d| d.qualified_rule.nil? }
206
+ files = unbaselineable_files(no_rule)
207
+ parts = []
208
+ unless no_rule.empty?
209
+ parts << "#{no_rule.size} diagnostic(s) have no rule (parse, path or internal errors) " \
210
+ "in #{files.size} file(s)"
211
+ end
212
+ parts << "#{no_path.size} diagnostic(s) have no path" unless no_path.empty?
213
+ listing = files.empty? ? "" : ":"
214
+ @err.puts("rigor: #{parts.join('; ')} and cannot be baselined; " \
215
+ "fix them, or remove or exclude the files#{listing}")
216
+ files.first(MAX_UNBASELINEABLE_FILES).each { |f| @err.puts(" #{f}") }
217
+ extra = files.size - MAX_UNBASELINEABLE_FILES
218
+ @err.puts(" ... and #{extra} more file(s)") if extra.positive?
219
+ end
220
+
221
+ def unbaselineable_files(diagnostics)
222
+ root = Pathname.new(Dir.pwd)
223
+ diagnostics.filter_map(&:path).map { |p| relative_to(root, p) }.uniq.sort
224
+ end
225
+
226
+ def relative_to(root, path)
227
+ Pathname.new(path).relative_path_from(root).to_s
228
+ rescue ArgumentError
229
+ path
230
+ end
231
+
101
232
  def parse_generate_options(subcommand: "generate")
102
233
  options = {
103
234
  config: nil,
@@ -105,22 +236,7 @@ module Rigor
105
236
  match_mode: :rule,
106
237
  force: false
107
238
  }
108
- parser = OptionParser.new do |opts|
109
- opts.banner = "Usage: rigor baseline #{subcommand} [options]"
110
- Options.add_config(opts, options)
111
- opts.on("--output=PATH", "Write baseline to PATH (default: #{DEFAULT_BASELINE_PATH})") do |v|
112
- options[:output] = v
113
- end
114
- opts.on("--match-mode=MODE", %i[rule message],
115
- "Row form: rule (default) or message") do |v|
116
- options[:match_mode] = v
117
- end
118
- # `regenerate` always overwrites — no `--force` to offer.
119
- if subcommand == "generate"
120
- opts.on("--force", "Overwrite an existing baseline file") { options[:force] = true }
121
- end
122
- end
123
- parser.parse!(@argv)
239
+ generate_parser(subcommand, options).parse!(@argv)
124
240
  options
125
241
  end
126
242
 
@@ -139,8 +255,6 @@ module Rigor
139
255
  runner.run(configuration_for_generation.paths).diagnostics
140
256
  end
141
257
 
142
- # The loaded configuration with only `baseline` disabled (#1777): any other key can change the
143
- # diagnostic set, and the baseline must record the set `rigor check` will then report.
144
258
  def override_configuration_baseline_off(configuration)
145
259
  configuration.without_baseline
146
260
  end
@@ -149,31 +263,22 @@ module Rigor
149
263
 
150
264
  def run_dump
151
265
  options = parse_dump_options
266
+ return positional_paths_error("dump") unless @argv.empty?
267
+
152
268
  baseline = load_baseline_or_exit(options.fetch(:baseline))
153
269
  return EXIT_USAGE if baseline == :error
154
270
 
155
271
  rows = filter_dump_rows(baseline.buckets, options)
156
272
  case options.fetch(:format)
157
- when :json then @out.puts(JSON.pretty_generate(dump_to_json(rows)))
273
+ when "json" then @out.puts(JSON.pretty_generate(dump_to_json(rows)))
158
274
  else dump_text(rows, options)
159
275
  end
160
276
  0
161
277
  end
162
278
 
163
279
  def parse_dump_options
164
- options = { baseline: DEFAULT_BASELINE_PATH, format: :text, rule: nil, file: nil }
165
- parser = OptionParser.new do |opts|
166
- opts.banner = "Usage: rigor baseline dump [options]"
167
- opts.on("--baseline=PATH", "Path to the baseline file (default: #{DEFAULT_BASELINE_PATH})") do |v|
168
- options[:baseline] = v
169
- end
170
- opts.on("--format=FORMAT", %i[text json], "Output format: text (default) or json") do |v|
171
- options[:format] = v
172
- end
173
- opts.on("--rule=RULE", "Filter rows by exact rule id") { |v| options[:rule] = v }
174
- opts.on("--file=GLOB", "Filter rows by File.fnmatch? glob") { |v| options[:file] = v }
175
- end
176
- parser.parse!(@argv)
280
+ options = { baseline: DEFAULT_BASELINE_PATH, format: "text", rule: nil, file: nil }
281
+ dump_parser(options).parse!(@argv)
177
282
  options
178
283
  end
179
284
 
@@ -226,6 +331,8 @@ module Rigor
226
331
 
227
332
  def run_drift
228
333
  options = parse_drift_options
334
+ return positional_paths_error("drift") unless @argv.empty?
335
+
229
336
  baseline = load_baseline_or_exit(options.fetch(:baseline))
230
337
  return EXIT_USAGE if baseline == :error
231
338
 
@@ -254,18 +361,7 @@ module Rigor
254
361
  baseline: DEFAULT_BASELINE_PATH,
255
362
  only: nil
256
363
  }
257
- parser = OptionParser.new do |opts|
258
- opts.banner = "Usage: rigor baseline drift [options]"
259
- Options.add_config(opts, options)
260
- opts.on("--baseline=PATH", "Path to the baseline file (default: #{DEFAULT_BASELINE_PATH})") do |v|
261
- options[:baseline] = v
262
- end
263
- opts.on("--only=STATUS", %i[within over cleared reducible],
264
- "Show only buckets with the given status (within|over|cleared|reducible)") do |v|
265
- options[:only] = v
266
- end
267
- end
268
- parser.parse!(@argv)
364
+ drift_parser(options).parse!(@argv)
269
365
  options
270
366
  end
271
367
 
@@ -300,6 +396,8 @@ module Rigor
300
396
 
301
397
  def run_prune
302
398
  options = parse_prune_options
399
+ return positional_paths_error("prune") unless @argv.empty?
400
+
303
401
  baseline = load_baseline_or_exit(options.fetch(:baseline))
304
402
  return EXIT_USAGE if baseline == :error
305
403
 
@@ -328,15 +426,7 @@ module Rigor
328
426
  baseline: DEFAULT_BASELINE_PATH,
329
427
  dry_run: false
330
428
  }
331
- parser = OptionParser.new do |opts|
332
- opts.banner = "Usage: rigor baseline prune [options]"
333
- Options.add_config(opts, options)
334
- opts.on("--baseline=PATH", "Path to the baseline file (default: #{DEFAULT_BASELINE_PATH})") do |v|
335
- options[:baseline] = v
336
- end
337
- opts.on("--dry-run", "Show what would be dropped without writing the file") { options[:dry_run] = true }
338
- end
339
- parser.parse!(@argv)
429
+ prune_parser(options).parse!(@argv)
340
430
  options
341
431
  end
342
432
 
@@ -362,7 +452,7 @@ module Rigor
362
452
 
363
453
  def help
364
454
  <<~HELP
365
- Usage: rigor baseline <subcommand> [options]
455
+ #{USAGE}
366
456
 
367
457
  Subcommands:
368
458
  generate Write a fresh baseline file from a `rigor check` run.