rigortype 0.4.6 → 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 (417) 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 +50 -3
  8. data/docs/handbook/appendix-phpstan.md +4 -4
  9. data/docs/manual/02-cli-reference.md +152 -26
  10. data/docs/manual/03-configuration.md +36 -3
  11. data/docs/manual/06-baseline.md +6 -4
  12. data/docs/manual/09-editor-integration.md +13 -2
  13. data/docs/manual/10-mcp-server.md +38 -2
  14. data/docs/manual/12-caching.md +4 -3
  15. data/docs/manual/plugins/rigor-actionpack.md +19 -11
  16. data/docs/manual/plugins/rigor-activesupport-core-ext.md +20 -1
  17. data/lib/rigor/analysis/baseline.rb +24 -27
  18. data/lib/rigor/analysis/check_rules/always_truthy_condition_collector.rb +5 -6
  19. data/lib/rigor/analysis/check_rules/dead_assignment_collector.rb +2 -2
  20. data/lib/rigor/analysis/check_rules/declaration_sourced_guard.rb +4 -4
  21. data/lib/rigor/analysis/check_rules/duplicate_hash_key_collector.rb +2 -2
  22. data/lib/rigor/analysis/check_rules/inferred_param_guard.rb +85 -13
  23. data/lib/rigor/analysis/check_rules/ivar_write_collector.rb +3 -2
  24. data/lib/rigor/analysis/check_rules/lexical_method_sites.rb +0 -1
  25. data/lib/rigor/analysis/check_rules/main_pass_collector.rb +5 -8
  26. data/lib/rigor/analysis/check_rules/published_constant_guard.rb +1 -1
  27. data/lib/rigor/analysis/check_rules/rebinding_block_callees.rb +3 -3
  28. data/lib/rigor/analysis/check_rules/return_in_ensure_collector.rb +1 -2
  29. data/lib/rigor/analysis/check_rules/rule_ids.rb +1 -1
  30. data/lib/rigor/analysis/check_rules/rule_walk.rb +9 -7
  31. data/lib/rigor/analysis/check_rules/shadowed_rescue_collector.rb +3 -4
  32. data/lib/rigor/analysis/check_rules/special_global_setters.rb +3 -3
  33. data/lib/rigor/analysis/check_rules/unreachable_clause_collector.rb +2 -2
  34. data/lib/rigor/analysis/check_rules/void_value_use_collector.rb +1 -1
  35. data/lib/rigor/analysis/check_rules.rb +124 -44
  36. data/lib/rigor/analysis/crash_signature.rb +2 -2
  37. data/lib/rigor/analysis/dependency_recorder.rb +4 -4
  38. data/lib/rigor/analysis/dependency_source_inference/boundary_cross_reporter.rb +14 -3
  39. data/lib/rigor/analysis/dependency_source_inference/gem_resolver.rb +1 -1
  40. data/lib/rigor/analysis/dependency_source_inference/index.rb +6 -8
  41. data/lib/rigor/analysis/dependency_source_inference/walker.rb +4 -4
  42. data/lib/rigor/analysis/edit_replay.rb +93 -6
  43. data/lib/rigor/analysis/fact_store.rb +3 -3
  44. data/lib/rigor/analysis/incremental_run_slot/write_guard.rb +2 -2
  45. data/lib/rigor/analysis/incremental_run_slot.rb +2 -2
  46. data/lib/rigor/analysis/incremental_session.rb +27 -16
  47. data/lib/rigor/analysis/rule_catalog.rb +2 -2
  48. data/lib/rigor/analysis/run_stats.rb +2 -2
  49. data/lib/rigor/analysis/runner/buffer_pool_dispatcher.rb +2 -2
  50. data/lib/rigor/analysis/runner/declaration_position.rb +1 -1
  51. data/lib/rigor/analysis/runner/diagnostic_aggregator.rb +193 -93
  52. data/lib/rigor/analysis/runner/effect_envelope_pass.rb +1 -1
  53. data/lib/rigor/analysis/runner/pool_coordinator.rb +147 -323
  54. data/lib/rigor/analysis/runner/project_pre_passes.rb +3 -3
  55. data/lib/rigor/analysis/runner/run_level_facts.rb +304 -0
  56. data/lib/rigor/analysis/runner/run_snapshots.rb +44 -54
  57. data/lib/rigor/analysis/runner/worker_payload.rb +36 -0
  58. data/lib/rigor/analysis/runner.rb +77 -71
  59. data/lib/rigor/analysis/self_call_resolution_recorder.rb +1 -1
  60. data/lib/rigor/analysis/template_unit_positions.rb +2 -3
  61. data/lib/rigor/analysis/template_units.rb +9 -1
  62. data/lib/rigor/analysis/worker_session.rb +24 -31
  63. data/lib/rigor/bleeding_edge.rb +3 -6
  64. data/lib/rigor/builtins/imported_refinements.rb +6 -8
  65. data/lib/rigor/builtins/predefined_constant_refinements.rb +1 -1
  66. data/lib/rigor/builtins/regex_refinement.rb +4 -6
  67. data/lib/rigor/cache/descriptor.rb +6 -5
  68. data/lib/rigor/cache/incremental_snapshot.rb +31 -12
  69. data/lib/rigor/cache/rbs_cache_producer.rb +2 -2
  70. data/lib/rigor/cache/rbs_class_ancestor_table.rb +1 -1
  71. data/lib/rigor/cache/rbs_class_type_param_names.rb +2 -2
  72. data/lib/rigor/cache/rbs_constant_table.rb +1 -1
  73. data/lib/rigor/cache/rbs_descriptor.rb +54 -3
  74. data/lib/rigor/cache/rbs_environment.rb +4 -1
  75. data/lib/rigor/cache/rbs_environment_marshal_patch.rb +2 -2
  76. data/lib/rigor/cache/rbs_known_class_names.rb +1 -1
  77. data/lib/rigor/cache/store.rb +11 -10
  78. data/lib/rigor/ci_detector.rb +1 -1
  79. data/lib/rigor/cli/annotate_command.rb +24 -19
  80. data/lib/rigor/cli/baseline_command.rb +145 -55
  81. data/lib/rigor/cli/catalogue.rb +256 -0
  82. data/lib/rigor/cli/check_command.rb +108 -102
  83. data/lib/rigor/cli/check_runner_factory.rb +2 -2
  84. data/lib/rigor/cli/command.rb +15 -1
  85. data/lib/rigor/cli/coverage_command.rb +21 -8
  86. data/lib/rigor/cli/coverage_mutation.rb +5 -7
  87. data/lib/rigor/cli/coverage_renderer.rb +2 -1
  88. data/lib/rigor/cli/coverage_scan.rb +3 -5
  89. data/lib/rigor/cli/diff_command.rb +25 -20
  90. data/lib/rigor/cli/docs_command.rb +1 -2
  91. data/lib/rigor/cli/doctor_command.rb +12 -12
  92. data/lib/rigor/cli/effects_command.rb +50 -40
  93. data/lib/rigor/cli/effects_diff_renderer.rb +18 -17
  94. data/lib/rigor/cli/effects_explain_renderer.rb +13 -12
  95. data/lib/rigor/cli/effects_renderer.rb +2 -1
  96. data/lib/rigor/cli/effects_snapshot_command.rb +18 -17
  97. data/lib/rigor/cli/explain_command.rb +18 -8
  98. data/lib/rigor/cli/fused_protection_renderer.rb +2 -1
  99. data/lib/rigor/cli/json_document.rb +41 -0
  100. data/lib/rigor/cli/lens_command.rb +12 -8
  101. data/lib/rigor/cli/lsp_command.rb +250 -23
  102. data/lib/rigor/cli/mcp_command.rb +12 -7
  103. data/lib/rigor/cli/mutation_fork_scan.rb +1 -1
  104. data/lib/rigor/cli/mutation_protection_renderer.rb +3 -2
  105. data/lib/rigor/cli/options.rb +40 -1
  106. data/lib/rigor/cli/plugin_command.rb +93 -46
  107. data/lib/rigor/cli/plugins_command.rb +17 -18
  108. data/lib/rigor/cli/plugins_renderer.rb +10 -6
  109. data/lib/rigor/cli/protection_fork_scan.rb +2 -2
  110. data/lib/rigor/cli/protection_renderer.rb +2 -1
  111. data/lib/rigor/cli/show_bleedingedge_command.rb +21 -15
  112. data/lib/rigor/cli/sig_gen_command.rb +84 -27
  113. data/lib/rigor/cli/skill_command.rb +32 -3
  114. data/lib/rigor/cli/skill_deep_probe.rb +1 -1
  115. data/lib/rigor/cli/skill_describe.rb +1 -1
  116. data/lib/rigor/cli/trace_command.rb +14 -9
  117. data/lib/rigor/cli/triage_command.rb +10 -11
  118. data/lib/rigor/cli/triage_renderer.rb +2 -1
  119. data/lib/rigor/cli/type_of_command.rb +16 -11
  120. data/lib/rigor/cli/type_of_project_scope.rb +1 -4
  121. data/lib/rigor/cli/type_of_renderer.rb +4 -1
  122. data/lib/rigor/cli/type_scan_command.rb +14 -9
  123. data/lib/rigor/cli/type_scan_renderer.rb +2 -1
  124. data/lib/rigor/cli/unused_command.rb +27 -17
  125. data/lib/rigor/cli/upgrade_command.rb +5 -0
  126. data/lib/rigor/cli.rb +209 -54
  127. data/lib/rigor/config_audit.rb +1 -1
  128. data/lib/rigor/configuration/severity_profile.rb +5 -5
  129. data/lib/rigor/configuration.rb +72 -30
  130. data/lib/rigor/effects/definition_context.rb +0 -2
  131. data/lib/rigor/effects/envelope.rb +1 -1
  132. data/lib/rigor/effects/framework_units.rb +1 -1
  133. data/lib/rigor/effects/mutation_classifier.rb +4 -7
  134. data/lib/rigor/effects/propagator.rb +1 -1
  135. data/lib/rigor/effects/registry.rb +2 -2
  136. data/lib/rigor/effects/snapshot.rb +1 -1
  137. data/lib/rigor/effects/taint_cause.rb +0 -2
  138. data/lib/rigor/effects/unit_scan.rb +9 -13
  139. data/lib/rigor/environment/bundle_sig_discovery.rb +5 -9
  140. data/lib/rigor/environment/class_registry.rb +3 -3
  141. data/lib/rigor/environment/core_root.rb +77 -0
  142. data/lib/rigor/environment/default_libraries.rb +1 -1
  143. data/lib/rigor/environment/deferred_signature_plan.rb +283 -0
  144. data/lib/rigor/environment/gated_signature_guard.rb +1 -1
  145. data/lib/rigor/environment/gated_signature_plan.rb +5 -2
  146. data/lib/rigor/environment/lockfile_resolver.rb +28 -12
  147. data/lib/rigor/environment/member_consistency/comparator.rb +18 -1
  148. data/lib/rigor/environment/member_consistency.rb +56 -7
  149. data/lib/rigor/environment/missing_gem_constant_index.rb +3 -2
  150. data/lib/rigor/environment/project_path_gems.rb +124 -0
  151. data/lib/rigor/environment/rbs_hierarchy.rb +6 -15
  152. data/lib/rigor/environment/rbs_loader.rb +480 -67
  153. data/lib/rigor/environment/reflection.rb +7 -7
  154. data/lib/rigor/environment/required_features.rb +0 -3
  155. data/lib/rigor/environment/ruby_library_roots.rb +1 -1
  156. data/lib/rigor/environment/vendored_signature_plan.rb +153 -0
  157. data/lib/rigor/environment.rb +41 -10
  158. data/lib/rigor/flow_contribution/merger.rb +4 -4
  159. data/lib/rigor/inference/acceptance.rb +93 -32
  160. data/lib/rigor/inference/block_parameter_binder.rb +2 -3
  161. data/lib/rigor/inference/block_run_count.rb +178 -0
  162. data/lib/rigor/inference/budget_trace.rb +8 -2
  163. data/lib/rigor/inference/builtins/exception_catalog.rb +2 -2
  164. data/lib/rigor/inference/builtins/numeric_catalog.rb +2 -5
  165. data/lib/rigor/inference/builtins/random_catalog.rb +2 -2
  166. data/lib/rigor/inference/callee_mutation_chain.rb +88 -77
  167. data/lib/rigor/inference/callee_write_census.rb +815 -0
  168. data/lib/rigor/inference/construction_escape.rb +217 -0
  169. data/lib/rigor/inference/declaration_walk/traversal.rb +1 -1
  170. data/lib/rigor/inference/deep_erasure.rb +39 -7
  171. data/lib/rigor/inference/def_handle.rb +1 -1
  172. data/lib/rigor/inference/def_node_resolver.rb +1 -1
  173. data/lib/rigor/inference/def_return_typer.rb +46 -4
  174. data/lib/rigor/inference/definer_resolution.rb +4 -2
  175. data/lib/rigor/inference/definers_index.rb +106 -0
  176. data/lib/rigor/inference/direct_ivar_writes.rb +300 -0
  177. data/lib/rigor/inference/element_read_widening.rb +49 -3
  178. data/lib/rigor/inference/expression_typer.rb +116 -49
  179. data/lib/rigor/inference/external_ancestor_resolution.rb +3 -4
  180. data/lib/rigor/inference/global_write_census.rb +76 -2
  181. data/lib/rigor/inference/global_writer_reach.rb +260 -0
  182. data/lib/rigor/inference/hkt_body.rb +1 -2
  183. data/lib/rigor/inference/hkt_body_parser.rb +3 -5
  184. data/lib/rigor/inference/hkt_reducer.rb +2 -2
  185. data/lib/rigor/inference/hkt_registry.rb +3 -5
  186. data/lib/rigor/inference/indexed_narrowing.rb +4 -7
  187. data/lib/rigor/inference/intervening_ivar_reset.rb +26 -4
  188. data/lib/rigor/inference/ivar_writer_census.rb +51 -0
  189. data/lib/rigor/inference/key_presence_guard.rb +2 -1
  190. data/lib/rigor/inference/loop_value_record.rb +8 -4
  191. data/lib/rigor/inference/match_rebinding/frame.rb +46 -0
  192. data/lib/rigor/inference/match_rebinding/yielding_match.rb +45 -0
  193. data/lib/rigor/inference/match_rebinding.rb +11 -1
  194. data/lib/rigor/inference/memo_audit.rb +55 -0
  195. data/lib/rigor/inference/method_dispatcher/alias_strict_nominals.rb +1 -1
  196. data/lib/rigor/inference/method_dispatcher/argument_distribution.rb +40 -0
  197. data/lib/rigor/inference/method_dispatcher/block_folding.rb +1 -1
  198. data/lib/rigor/inference/method_dispatcher/constant_folding.rb +3 -3
  199. data/lib/rigor/inference/method_dispatcher/contested_overloads.rb +89 -0
  200. data/lib/rigor/inference/method_dispatcher/iterator_dispatch.rb +1 -1
  201. data/lib/rigor/inference/method_dispatcher/kernel_dispatch.rb +1 -1
  202. data/lib/rigor/inference/method_dispatcher/no_match_fallback.rb +54 -0
  203. data/lib/rigor/inference/method_dispatcher/overload_selector.rb +39 -27
  204. data/lib/rigor/inference/method_dispatcher/random_folding.rb +1 -1
  205. data/lib/rigor/inference/method_dispatcher/rbs_dispatch.rb +203 -17
  206. data/lib/rigor/inference/method_dispatcher/reduce_folding.rb +1 -1
  207. data/lib/rigor/inference/method_dispatcher/regexp_folding.rb +2 -2
  208. data/lib/rigor/inference/method_dispatcher/self_substitute.rb +2 -2
  209. data/lib/rigor/inference/method_dispatcher/set_folding.rb +9 -7
  210. data/lib/rigor/inference/method_dispatcher/shape_dispatch.rb +83 -20
  211. data/lib/rigor/inference/method_dispatcher/singleton_folding.rb +0 -1
  212. data/lib/rigor/inference/method_dispatcher/singleton_mixin_dispatch.rb +50 -17
  213. data/lib/rigor/inference/method_dispatcher/struct_folding.rb +5 -5
  214. data/lib/rigor/inference/method_dispatcher/struct_materialization.rb +4 -6
  215. data/lib/rigor/inference/method_dispatcher/uri_folding.rb +1 -1
  216. data/lib/rigor/inference/method_dispatcher.rb +64 -14
  217. data/lib/rigor/inference/module_function_state.rb +11 -1
  218. data/lib/rigor/inference/multi_target_binder.rb +1 -1
  219. data/lib/rigor/inference/mutation_rejoin.rb +240 -4
  220. data/lib/rigor/inference/mutation_widening.rb +157 -27
  221. data/lib/rigor/inference/narrowing.rb +47 -33
  222. data/lib/rigor/inference/optimistic_origin.rb +1 -1
  223. data/lib/rigor/inference/out_buffer_mutation.rb +72 -0
  224. data/lib/rigor/inference/parameter_inference_collector.rb +5 -6
  225. data/lib/rigor/inference/pre_eval_constants.rb +1 -1
  226. data/lib/rigor/inference/project_method_ownership.rb +6 -3
  227. data/lib/rigor/inference/project_yielder.rb +11 -17
  228. data/lib/rigor/inference/receiver_alias.rb +20 -0
  229. data/lib/rigor/inference/refined_dispatch/constructed_class.rb +3 -1
  230. data/lib/rigor/inference/refined_dispatch/own_method.rb +2 -1
  231. data/lib/rigor/inference/remover_holder_scan.rb +233 -0
  232. data/lib/rigor/inference/remover_narrowing.rb +224 -0
  233. data/lib/rigor/inference/rewrite_mutation.rb +53 -2
  234. data/lib/rigor/inference/scope_indexer/certainty.rb +0 -1
  235. data/lib/rigor/inference/scope_indexer/constant_ivar_value.rb +93 -0
  236. data/lib/rigor/inference/scope_indexer/declared_param_writes.rb +58 -15
  237. data/lib/rigor/inference/scope_indexer/derived_param_value.rb +112 -0
  238. data/lib/rigor/inference/scope_indexer/empty_seed.rb +99 -0
  239. data/lib/rigor/inference/scope_indexer/ivar_contributions.rb +135 -0
  240. data/lib/rigor/inference/scope_indexer.rb +137 -46
  241. data/lib/rigor/inference/self_call_ivar_writes.rb +84 -12
  242. data/lib/rigor/inference/set_mutation.rb +189 -0
  243. data/lib/rigor/inference/singleton_hook_decline.rb +1 -2
  244. data/lib/rigor/inference/statement_evaluator.rb +2328 -138
  245. data/lib/rigor/inference/struct_fold_safety.rb +4 -6
  246. data/lib/rigor/inference/synthetic_method_scanner.rb +5 -6
  247. data/lib/rigor/inference/toplevel_def_census.rb +59 -0
  248. data/lib/rigor/inference/unknown_store_widening.rb +103 -4
  249. data/lib/rigor/inference/unthreaded_rebinds.rb +2 -3
  250. data/lib/rigor/inference/void_tail_summary.rb +2 -1
  251. data/lib/rigor/language_server/buffer_table.rb +3 -3
  252. data/lib/rigor/language_server/completion_provider.rb +26 -20
  253. data/lib/rigor/language_server/config_warning_announcer.rb +28 -0
  254. data/lib/rigor/language_server/diagnostic_publisher.rb +8 -3
  255. data/lib/rigor/language_server/document_symbol_provider.rb +1 -1
  256. data/lib/rigor/language_server/folding_range_provider.rb +1 -2
  257. data/lib/rigor/language_server/hover_provider.rb +1 -7
  258. data/lib/rigor/language_server/hover_renderer.rb +6 -8
  259. data/lib/rigor/language_server/project_context.rb +39 -1
  260. data/lib/rigor/language_server/publish_batcher.rb +3 -4
  261. data/lib/rigor/language_server/selection_range_provider.rb +2 -4
  262. data/lib/rigor/language_server/server.rb +7 -3
  263. data/lib/rigor/language_server/signature_help_provider.rb +4 -4
  264. data/lib/rigor/lens/plugins.rb +1 -1
  265. data/lib/rigor/mcp/server.rb +73 -2
  266. data/lib/rigor/mcp.rb +1 -1
  267. data/lib/rigor/plugin/access_denied_error.rb +1 -2
  268. data/lib/rigor/plugin/bundled_catalog.rb +1 -12
  269. data/lib/rigor/plugin/io_boundary.rb +1 -1
  270. data/lib/rigor/plugin/loader.rb +25 -8
  271. data/lib/rigor/plugin/macro/heredoc_template.rb +7 -8
  272. data/lib/rigor/plugin/macro/trait_registry.rb +6 -9
  273. data/lib/rigor/plugin/manifest.rb +9 -13
  274. data/lib/rigor/plugin/registry.rb +3 -3
  275. data/lib/rigor/plugin/services.rb +2 -3
  276. data/lib/rigor/plugin/source_rbs_synthesis_reporter.rb +11 -0
  277. data/lib/rigor/plugin/trust_policy.rb +2 -2
  278. data/lib/rigor/plugin/type_node_resolver.rb +2 -4
  279. data/lib/rigor/plugin.rb +6 -8
  280. data/lib/rigor/plugin_gap_advisory.rb +5 -1
  281. data/lib/rigor/project_environment.rb +16 -2
  282. data/lib/rigor/protection/analysis_guard.rb +1 -1
  283. data/lib/rigor/protection/discovery_seed.rb +44 -5
  284. data/lib/rigor/protection/mutator.rb +1 -1
  285. data/lib/rigor/rbs_extended/envelope_scanner.rb +4 -6
  286. data/lib/rigor/rbs_extended/hkt_directives.rb +6 -10
  287. data/lib/rigor/rbs_extended/reporter.rb +27 -3
  288. data/lib/rigor/rbs_extended.rb +2 -3
  289. data/lib/rigor/reflection/constant_ancestors.rb +5 -3
  290. data/lib/rigor/rooted_glob.rb +15 -1
  291. data/lib/rigor/run_scoped_slots.rb +38 -0
  292. data/lib/rigor/runtime/jit.rb +1 -1
  293. data/lib/rigor/scope/discovery_index.rb +19 -6
  294. data/lib/rigor/scope/resolution_chain.rb +3 -1
  295. data/lib/rigor/scope.rb +365 -31
  296. data/lib/rigor/sig_gen/classification.rb +4 -0
  297. data/lib/rigor/sig_gen/effect_annotation.rb +3 -3
  298. data/lib/rigor/sig_gen/generator.rb +371 -58
  299. data/lib/rigor/sig_gen/generic_header_scan.rb +189 -0
  300. data/lib/rigor/sig_gen/inline_declarations.rb +2 -2
  301. data/lib/rigor/sig_gen/layout_index.rb +3 -3
  302. data/lib/rigor/sig_gen/method_candidate.rb +23 -4
  303. data/lib/rigor/sig_gen/observation_collector.rb +3 -5
  304. data/lib/rigor/sig_gen/observed_call.rb +1 -1
  305. data/lib/rigor/sig_gen/path_mapper.rb +6 -0
  306. data/lib/rigor/sig_gen/renderer.rb +15 -9
  307. data/lib/rigor/sig_gen/skip_reason_catalog.rb +15 -2
  308. data/lib/rigor/sig_gen/type_name_qualification.rb +69 -0
  309. data/lib/rigor/sig_gen/write_reporter.rb +63 -14
  310. data/lib/rigor/sig_gen/write_result.rb +23 -12
  311. data/lib/rigor/sig_gen/writer.rb +217 -37
  312. data/lib/rigor/type/acceptance_router.rb +2 -2
  313. data/lib/rigor/type/accepts_result.rb +1 -1
  314. data/lib/rigor/type/app.rb +4 -6
  315. data/lib/rigor/type/combinator.rb +9 -13
  316. data/lib/rigor/type/constant.rb +4 -5
  317. data/lib/rigor/type/hash_shape.rb +21 -40
  318. data/lib/rigor/type/union.rb +1 -1
  319. data/lib/rigor/type_node/identifier.rb +1 -2
  320. data/lib/rigor/type_node/resolver_chain.rb +1 -1
  321. data/lib/rigor/type_node/string_literal.rb +1 -2
  322. data/lib/rigor/type_node/symbol_literal.rb +1 -2
  323. data/lib/rigor/type_node/union.rb +3 -6
  324. data/lib/rigor/value_semantics.rb +1 -10
  325. data/lib/rigor/version.rb +1 -1
  326. data/plugins/rigor-actioncable/lib/rigor/plugin/actioncable/channel_discoverer.rb +2 -2
  327. data/plugins/rigor-actioncable/lib/rigor/plugin/actioncable/channel_index.rb +2 -3
  328. data/plugins/rigor-actioncable/lib/rigor/plugin/actioncable.rb +1 -1
  329. data/plugins/rigor-actionmailer/lib/rigor/plugin/actionmailer/mailer_discoverer.rb +38 -36
  330. data/plugins/rigor-actionmailer/lib/rigor/plugin/actionmailer/mailer_index.rb +4 -2
  331. data/plugins/rigor-actionmailer/lib/rigor/plugin/actionmailer.rb +6 -4
  332. data/plugins/rigor-actionpack/lib/rigor/plugin/actionpack/analyzer.rb +1 -1
  333. data/plugins/rigor-actionpack/lib/rigor/plugin/actionpack/controller_index.rb +1 -5
  334. data/plugins/rigor-actionpack/lib/rigor/plugin/actionpack/controller_scan.rb +0 -1
  335. data/plugins/rigor-actionpack/lib/rigor/plugin/actionpack/erb_compiler.rb +2 -1
  336. data/plugins/rigor-actionpack/lib/rigor/plugin/actionpack/view_assigns.rb +1 -1
  337. data/plugins/rigor-actionpack/lib/rigor/plugin/actionpack.rb +66 -19
  338. data/plugins/rigor-activejob/lib/rigor/plugin/activejob/effects.rb +1 -1
  339. data/plugins/rigor-activejob/lib/rigor/plugin/activejob.rb +4 -3
  340. data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord/association_roots.rb +3 -2
  341. data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord/effects.rb +2 -2
  342. data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord/model_discoverer.rb +21 -31
  343. data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord/model_index.rb +10 -16
  344. data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord/schema_parser.rb +2 -3
  345. data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord/schema_table.rb +0 -1
  346. data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord.rb +5 -4
  347. data/plugins/rigor-activestorage/lib/rigor/plugin/activestorage/attachment_discoverer.rb +3 -4
  348. data/plugins/rigor-activestorage/lib/rigor/plugin/activestorage/attachment_index.rb +1 -1
  349. data/plugins/rigor-activestorage/lib/rigor/plugin/activestorage.rb +7 -12
  350. data/plugins/rigor-activestorage/lib/rigor-activestorage.rb +1 -1
  351. data/plugins/rigor-activesupport-core-ext/lib/rigor/plugin/activesupport_core_ext/effects.rb +1 -1
  352. data/plugins/rigor-activesupport-core-ext/lib/rigor/plugin/activesupport_core_ext/symbolize_keys.rb +102 -0
  353. data/plugins/rigor-activesupport-core-ext/lib/rigor/plugin/activesupport_core_ext.rb +19 -6
  354. data/plugins/rigor-activesupport-core-ext/sig/active_support/core_ext.rbs +26 -13
  355. data/plugins/rigor-alba/lib/rigor/plugin/alba/resource_index.rb +3 -3
  356. data/plugins/rigor-dry-schema/lib/rigor/plugin/dry_schema/schema_scanner.rb +1 -3
  357. data/plugins/rigor-dry-struct/lib/rigor/plugin/dry_struct.rb +6 -6
  358. data/plugins/rigor-dry-types/lib/rigor/plugin/dry_types/alias_scanner.rb +5 -6
  359. data/plugins/rigor-dry-types/lib/rigor/plugin/dry_types.rb +1 -1
  360. data/plugins/rigor-dry-validation/lib/rigor/plugin/dry_validation/params_shape.rb +2 -3
  361. data/plugins/rigor-dry-validation/lib/rigor/plugin/dry_validation.rb +5 -8
  362. data/plugins/rigor-ethon/lib/rigor/plugin/ethon.rb +0 -1
  363. data/plugins/rigor-factorybot/lib/rigor/plugin/factorybot/factory_discoverer.rb +1 -1
  364. data/plugins/rigor-factorybot/lib/rigor/plugin/factorybot.rb +10 -18
  365. data/plugins/rigor-ffi/lib/rigor/plugin/ffi/analyzer.rb +0 -1
  366. data/plugins/rigor-ffi/lib/rigor/plugin/ffi/discoverer.rb +0 -2
  367. data/plugins/rigor-ffi/lib/rigor/plugin/ffi.rb +4 -5
  368. data/plugins/rigor-grape/lib/rigor/plugin/grape.rb +0 -2
  369. data/plugins/rigor-graphql/lib/rigor/plugin/graphql/type_scanner.rb +6 -7
  370. data/plugins/rigor-hanami/lib/rigor/plugin/hanami/action_checker.rb +2 -4
  371. data/plugins/rigor-hanami/lib/rigor/plugin/hanami.rb +2 -5
  372. data/plugins/rigor-mangrove/lib/rigor/plugin/mangrove.rb +2 -2
  373. data/plugins/rigor-minitest/lib/rigor/plugin/minitest/assertion_analyzer.rb +6 -6
  374. data/plugins/rigor-pundit/lib/rigor/plugin/pundit/policy_discoverer.rb +2 -1
  375. data/plugins/rigor-rails-i18n/lib/rigor/plugin/rails_i18n/analyzer.rb +3 -8
  376. data/plugins/rigor-rails-i18n/lib/rigor/plugin/rails_i18n/locale_index.rb +2 -3
  377. data/plugins/rigor-rails-i18n/lib/rigor/plugin/rails_i18n/locale_loader.rb +5 -8
  378. data/plugins/rigor-rails-routes/lib/rigor/plugin/rails_routes/analyzer.rb +1 -3
  379. data/plugins/rigor-rails-routes/lib/rigor/plugin/rails_routes/devise_routes.rb +2 -3
  380. data/plugins/rigor-rails-routes/lib/rigor/plugin/rails_routes/helper_discoverer.rb +1 -1
  381. data/plugins/rigor-rails-routes/lib/rigor/plugin/rails_routes/routes_parser.rb +4 -11
  382. data/plugins/rigor-rails-routes/lib/rigor/plugin/rails_routes.rb +1 -1
  383. data/plugins/rigor-rbs-inline/lib/rigor/plugin/rbs_inline.rb +2 -1
  384. data/plugins/rigor-rspec/lib/rigor/plugin/rspec/matcher_analyzer.rb +1 -1
  385. data/plugins/rigor-rspec/lib/rigor/plugin/rspec.rb +2 -2
  386. data/plugins/rigor-rspec-rails/lib/rigor/plugin/rspec_rails.rb +3 -4
  387. data/plugins/rigor-shoulda-matchers/lib/rigor/plugin/shoulda_matchers/analyzer.rb +6 -8
  388. data/plugins/rigor-sidekiq/lib/rigor/plugin/sidekiq.rb +3 -2
  389. data/plugins/rigor-sorbet/lib/rigor/plugin/sorbet/absurd_recognizer.rb +4 -5
  390. data/plugins/rigor-sorbet/lib/rigor/plugin/sorbet/assertion_recognizer.rb +6 -7
  391. data/plugins/rigor-sorbet/lib/rigor/plugin/sorbet/catalog.rb +1 -1
  392. data/plugins/rigor-sorbet/lib/rigor/plugin/sorbet/sig_parser.rb +2 -2
  393. data/plugins/rigor-sorbet/lib/rigor/plugin/sorbet.rb +17 -21
  394. data/plugins/rigor-statesman/lib/rigor/plugin/statesman.rb +2 -3
  395. data/plugins/rigor-typelizer/lib/rigor/plugin/typelizer/serializer_discoverer.rb +3 -2
  396. data/plugins/rigor-typelizer/lib/rigor/plugin/typelizer/serializer_index.rb +4 -4
  397. data/sig/rigor/analysis/baseline.rbs +0 -3
  398. data/sig/rigor/analysis/fact_store.rbs +1 -0
  399. data/sig/rigor/analysis/reachability/scan_cache.rbs +1 -1
  400. data/sig/rigor/cli/diff_command.rbs +1 -1
  401. data/sig/rigor/cli/explain_command.rbs +1 -1
  402. data/sig/rigor/effects/registry.rbs +2 -2
  403. data/sig/rigor/inference.rbs +4 -1
  404. data/sig/rigor/plugin/additional_initializer.rbs +0 -5
  405. data/sig/rigor/plugin/base.rbs +1 -0
  406. data/sig/rigor/plugin/effect_attribution.rbs +3 -3
  407. data/sig/rigor/plugin/effect_edge.rbs +1 -1
  408. data/sig/rigor/plugin/effect_entry_points.rbs +3 -3
  409. data/sig/rigor/plugin/io_boundary.rbs +2 -1
  410. data/sig/rigor/plugin/manifest.rbs +9 -9
  411. data/sig/rigor/plugin/protocol_contract.rbs +3 -7
  412. data/sig/rigor/scope.rbs +23 -3
  413. data/skills/rigor-plugin-author/SKILL.md +4 -4
  414. data/skills/rigor-plugin-author/references/02-walker-and-types.md +5 -5
  415. data/skills/rigor-plugin-review/SKILL.md +3 -3
  416. data/skills/rigor-project-init/references/03-baseline-and-bugs.md +12 -1
  417. metadata +36 -3
@@ -4,6 +4,7 @@ require_relative "command"
4
4
  require_relative "options"
5
5
  require_relative "../runtime/jit"
6
6
 
7
+ require "json"
7
8
  require "optionparser"
8
9
 
9
10
  module Rigor
@@ -15,6 +16,179 @@ module Rigor
15
16
  class LspCommand < Command
16
17
  USAGE = "Usage: rigor lsp [options]"
17
18
 
19
+ # Records each JSON-RPC message one per line, prefixed with the direction (`<-` read from the client, `->`
20
+ # written to it) and a timestamp. Only the message framing is logged; the server's own debug output joins the
21
+ # same file through `$stderr` (see {#with_debug_output}).
22
+ #
23
+ # It never raises into the protocol path. A message the JSON encoder rejects (raw invalid UTF-8 in a string) is
24
+ # logged as a scrubbed rendering, so its line is still written. Any IO failure disables the log for the rest of
25
+ # the session: records are dropped, and debug output falls back to `fallback` (the real stderr). Nothing is
26
+ # `warn`ed into the failing log.
27
+ class WireLog
28
+ def initialize(io, fallback)
29
+ @io = io
30
+ @fallback = fallback
31
+ @broken = false
32
+ end
33
+
34
+ def record(direction, message)
35
+ deliver("#{stamp} #{direction} #{render(message)}\n")
36
+ nil
37
+ end
38
+
39
+ # Debug output written to `$stderr` while the session runs (`Kernel#warn` calls `write`). The parts are joined
40
+ # as binary, so an odd encoding in one part cannot raise here.
41
+ def write(*parts)
42
+ text = parts.map { |part| part.to_s.b }.join
43
+ @fallback.write(text) unless deliver(text)
44
+ text.bytesize
45
+ end
46
+
47
+ def print(*parts)
48
+ write(*parts)
49
+ nil
50
+ end
51
+
52
+ def puts(*args)
53
+ lines = if args.empty?
54
+ ["\n"]
55
+ else
56
+ args.flatten.map { |arg| arg.to_s.end_with?("\n") ? arg.to_s : "#{arg}\n" }
57
+ end
58
+ write(*lines)
59
+ nil
60
+ end
61
+
62
+ def printf(format_string, *)
63
+ write(format(format_string, *))
64
+ nil
65
+ end
66
+
67
+ def putc(char)
68
+ write(char.is_a?(Integer) ? char.chr : char.to_s[0])
69
+ char
70
+ end
71
+
72
+ def syswrite(text)
73
+ write(text)
74
+ end
75
+
76
+ def <<(part)
77
+ write(part)
78
+ self
79
+ end
80
+
81
+ def flush
82
+ self
83
+ end
84
+
85
+ # The exception that escaped the server loop, with its backtrace, so a crash is in the log, not only on stderr.
86
+ def crash(error)
87
+ lines = ["#{stamp} ! #{error.class}: #{error.message}\n"]
88
+ lines.concat((error.backtrace || []).map { |frame| " #{frame}\n" })
89
+ text = lines.join
90
+ @fallback.write(text) unless deliver(text)
91
+ nil
92
+ end
93
+
94
+ def close
95
+ @io.close unless @io.closed?
96
+ end
97
+
98
+ # Every other IO method (`tty?`, `closed?`, `fileno`, `sync=`, `to_io` for `system(err: $stderr)`, ...) goes to
99
+ # the current sink: the file while the log works, the real stderr once it has failed.
100
+ def method_missing(name, ...)
101
+ return super unless sink.respond_to?(name)
102
+
103
+ sink.public_send(name, ...)
104
+ end
105
+
106
+ def respond_to_missing?(name, include_private = false)
107
+ sink.respond_to?(name) || super
108
+ end
109
+
110
+ private
111
+
112
+ def sink
113
+ @broken ? @fallback : @io
114
+ end
115
+
116
+ # Writes `text` to the file. Returns false when the log is (or just became) unusable. Only a failure of the
117
+ # write itself disables the log.
118
+ def deliver(text)
119
+ return false if @broken
120
+
121
+ @io.write(text)
122
+ true
123
+ rescue StandardError
124
+ @broken = true
125
+ false
126
+ end
127
+
128
+ def stamp
129
+ Time.now.strftime("%FT%T.%6N%:z")
130
+ end
131
+
132
+ # The JSON line for `message`. One the encoder rejects (invalid UTF-8, nesting past its limit, a `to_json` that
133
+ # raises) is rendered from its scrubbed copy, or from its `inspect` when even that does not encode.
134
+ def render(message)
135
+ JSON.generate(message)
136
+ rescue StandardError
137
+ begin
138
+ JSON.generate(scrub(message))
139
+ rescue StandardError
140
+ JSON.generate(message.inspect)
141
+ end
142
+ end
143
+
144
+ def scrub(value)
145
+ case value
146
+ when Hash then value.to_h { |key, val| [scrub(key), scrub(val)] }
147
+ when Array then value.map { |item| scrub(item) }
148
+ when Symbol then scrub(value.to_s).to_sym
149
+ when String then value.dup.force_encoding(Encoding::UTF_8).scrub
150
+ else value
151
+ end
152
+ end
153
+ end
154
+
155
+ # Wraps the framed reader so every request or notification it yields is logged before the Loop dispatches it.
156
+ class WireLogReader
157
+ def initialize(inner, log)
158
+ @inner = inner
159
+ @log = log
160
+ end
161
+
162
+ def read(&block)
163
+ @inner.read do |message|
164
+ @log.record("<-", message)
165
+ block.call(message)
166
+ end
167
+ end
168
+
169
+ def close
170
+ @inner.close
171
+ end
172
+ end
173
+
174
+ # Wraps the framed writer so every outgoing message is logged as it goes on the wire. It sits inside
175
+ # `SynchronizedWriter`, so the log line and the frame it describes are written under the same lock.
176
+ class WireLogWriter
177
+ def initialize(inner, log)
178
+ @inner = inner
179
+ @log = log
180
+ end
181
+
182
+ def write(message)
183
+ @log.record("->", message.merge(jsonrpc: "2.0"))
184
+ @inner.write(message)
185
+ end
186
+
187
+ def close
188
+ @inner.close
189
+ end
190
+ end
191
+
18
192
  # @return CLI exit status.
19
193
  def run
20
194
  options = parse_options
@@ -26,6 +200,9 @@ module Rigor
26
200
  return CLI::EXIT_USAGE
27
201
  end
28
202
 
203
+ log = open_wire_log(options.fetch(:log))
204
+ return CLI::EXIT_USAGE if log == :usage_error
205
+
29
206
  # A long-lived server always outlasts the JIT compile cost, so enable
30
207
  # YJIT at boot rather than on the analysis deadline (Runtime::Jit).
31
208
  Runtime::Jit.enable_now
@@ -34,23 +211,83 @@ module Rigor
34
211
  require_relative "../configuration"
35
212
  require "language_server-protocol"
36
213
 
37
- # STDIN is read frame-by-frame via the gem's `Io::Reader`; STDOUT is wrapped in `SynchronizedWriter` so
38
- # concurrent writes from the main dispatch thread + the Debouncer's async threads don't interleave frames. The
39
- # Loop runs until either STDIN hits EOF or `server.exited?`; the process then exits with the server's recorded
40
- # code (0 after a clean shutdown+exit, 1 otherwise).
41
- writer = LanguageServer::SynchronizedWriter.new(
42
- ::LanguageServer::Protocol::Transport::Io::Writer.new($stdout)
43
- )
44
- server, loop_runner = build_server(writer: writer, config_path: options.fetch(:config))
45
- loop_runner.run
46
- server.exit_code || 0
214
+ begin
215
+ serve(config_path: options.fetch(:config), log: log)
216
+ ensure
217
+ log&.close
218
+ end
219
+ end
220
+
221
+ # The parser this command parses with, built without parsing anything: the catalogue reads the
222
+ # flags and formats from it (ADR-126 WD3). `options` is only written by the switches' blocks.
223
+ def option_parser(options = {})
224
+ OptionParser.new do |opts|
225
+ opts.banner = USAGE
226
+ opts.on("--transport=NAME", "Transport (default: stdio; only stdio supported in v1)") do |value|
227
+ options[:transport] = value
228
+ end
229
+ opts.on("--log=PATH",
230
+ "Append the LSP wire log and server debug output to PATH " \
231
+ "(default: no wire log; debug output goes to stderr)") do |value|
232
+ options[:log] = value
233
+ end
234
+ Options.add_config(opts, options)
235
+ end
47
236
  end
48
237
 
49
238
  private
50
239
 
240
+ # STDIN is read frame-by-frame via the gem's `Io::Reader`; STDOUT is wrapped in `SynchronizedWriter` so
241
+ # concurrent writes from the main dispatch thread + the Debouncer's async threads don't interleave frames. The
242
+ # Loop runs until either STDIN hits EOF or `server.exited?`; the process then exits with the server's recorded
243
+ # code (0 after a clean shutdown+exit, 1 otherwise). With a wire log, both directions are logged inside the lock.
244
+ def serve(config_path:, log:)
245
+ reader = ::LanguageServer::Protocol::Transport::Io::Reader.new($stdin)
246
+ frame_writer = ::LanguageServer::Protocol::Transport::Io::Writer.new($stdout)
247
+ if log
248
+ reader = WireLogReader.new(reader, log)
249
+ frame_writer = WireLogWriter.new(frame_writer, log)
250
+ end
251
+ writer = LanguageServer::SynchronizedWriter.new(frame_writer)
252
+ server, loop_runner = build_server(writer: writer, reader: reader, config_path: config_path)
253
+ with_debug_output(log) { loop_runner.run }
254
+ server.exit_code || 0
255
+ end
256
+
257
+ # Opens PATH for appending with synchronous writes, so a crash loses no logged line. Returns nil without a
258
+ # PATH, the open file, or `:usage_error` after reporting why it could not be opened.
259
+ def open_wire_log(path)
260
+ return nil if path.nil?
261
+
262
+ io = File.new(path, "a")
263
+ io.sync = true
264
+ WireLog.new(io, $stderr)
265
+ rescue SystemCallError => e
266
+ @err.puts("rigor lsp: cannot open --log file #{path.inspect}: #{e.message}")
267
+ :usage_error
268
+ end
269
+
270
+ # Routes the server's debug output (`warn` and the Ruby runtime's warnings, both written to `$stderr`) into the
271
+ # wire log for the length of the session. An exception that escapes the loop is written to the log before
272
+ # `$stderr` is restored and the exception is re-raised. Without a log, `$stderr` is left alone.
273
+ def with_debug_output(log)
274
+ return yield if log.nil?
275
+
276
+ saved = $stderr
277
+ begin
278
+ $stderr = log
279
+ yield
280
+ rescue StandardError, ScriptError, SystemStackError => e
281
+ log.crash(e)
282
+ raise
283
+ ensure
284
+ $stderr = saved
285
+ end
286
+ end
287
+
51
288
  # Builds the full collaborator graph from a fresh `Configuration` + `ProjectContext`. Returns `[server, loop]` so
52
289
  # the caller drives the loop and reads `server.exit_code` for the process exit status.
53
- def build_server(writer:, config_path:) # rubocop:disable Metrics/MethodLength
290
+ def build_server(writer:, reader:, config_path:) # rubocop:disable Metrics/MethodLength
54
291
  configuration = Configuration.load(config_path)
55
292
  # ProjectContext caches Environment + Cache::Store across requests so hover / publish hit the warm path.
56
293
  # Invalidated by `workspace/didChangeWatchedFiles` and `workspace/didChangeConfiguration`; every save round
@@ -89,7 +326,7 @@ module Rigor
89
326
  project_context: project_context
90
327
  )
91
328
  loop_runner = LanguageServer::Loop.new(
92
- reader: ::LanguageServer::Protocol::Transport::Io::Reader.new($stdin),
329
+ reader: reader,
93
330
  writer: writer,
94
331
  server: server
95
332
  )
@@ -99,17 +336,7 @@ module Rigor
99
336
  def parse_options
100
337
  options = { transport: "stdio", log: nil, config: nil }
101
338
 
102
- parser = OptionParser.new do |opts|
103
- opts.banner = USAGE
104
- opts.on("--transport=NAME", "Transport (default: stdio; only stdio supported in v1)") do |value|
105
- options[:transport] = value
106
- end
107
- opts.on("--log=PATH", "Write LSP wire log + server debug to PATH (default: stderr)") do |value|
108
- options[:log] = value
109
- end
110
- Options.add_config(opts, options)
111
- end
112
- parser.parse!(@argv)
339
+ option_parser(options).parse!(@argv)
113
340
  options
114
341
  rescue OptionParser::ParseError => e
115
342
  @err.puts(e.message)
@@ -42,12 +42,10 @@ module Rigor
42
42
  0
43
43
  end
44
44
 
45
- private
46
-
47
- def parse_options
48
- options = { transport: "stdio", config: nil }
49
-
50
- parser = OptionParser.new do |opts|
45
+ # The parser this command parses with, built without parsing anything: the catalogue reads the
46
+ # flags and formats from it (ADR-126 WD3). `options` is only written by the switches' blocks.
47
+ def option_parser(options = {})
48
+ OptionParser.new do |opts|
51
49
  opts.banner = USAGE
52
50
  opts.on("--transport=NAME",
53
51
  "Transport (default: stdio; only stdio is supported in v1)") do |value|
@@ -58,7 +56,14 @@ module Rigor
58
56
  options[:config] = Options.config_path(value)
59
57
  end
60
58
  end
61
- parser.parse!(@argv)
59
+ end
60
+
61
+ private
62
+
63
+ def parse_options
64
+ options = { transport: "stdio", config: nil }
65
+
66
+ option_parser(options).parse!(@argv)
62
67
  options
63
68
  rescue OptionParser::ParseError => e
64
69
  @err.puts(e.message)
@@ -40,7 +40,7 @@ module Rigor
40
40
  # @return one entry per path.
41
41
  def run(paths:, scanner:, environment:, configuration:, workers:)
42
42
  # Force the full RBS load on the parent so children copy-on-write inherit a warm environment rather
43
- # than each rebuilding it after the fork. A no-op on the sequential path but cheap.
43
+ # than each rebuilding it after the fork. Skipped when the run is sequential.
44
44
  environment.rbs_loader&.prewarm if Inference::ForkMap.parallel?([workers, paths.size].min)
45
45
 
46
46
  payloads = Inference::ForkMap.call(items: paths, workers: workers) do |slice|
@@ -1,6 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require "json"
4
+ require_relative "json_document"
4
5
 
5
6
  module Rigor
6
7
  class CLI
@@ -22,7 +23,7 @@ module Rigor
22
23
  private
23
24
 
24
25
  def render_json(report)
25
- @out.puts(JSON.pretty_generate(report.to_h))
26
+ @out.puts(JSON.pretty_generate(JsonDocument.stamp(report.to_h)))
26
27
  end
27
28
 
28
29
  def render_text(report)
@@ -46,7 +47,7 @@ module Rigor
46
47
  end
47
48
 
48
49
  # Issue #686 — the count that makes the command exit non-zero, on stdout beside the ratio it qualifies
49
- # (the stderr twin in `CoverageMutation#warn_unmeasured_files` carries the explanation).
50
+ # (the stderr twin in {MeasurementIntegrityWarning} carries the explanation).
50
51
  def render_unmeasured_files(report)
51
52
  count = report.unmeasured_files
52
53
  return if count.zero?
@@ -16,6 +16,13 @@ module Rigor
16
16
  module Options
17
17
  module_function
18
18
 
19
+ # The formats `parser` accepts for `--format`, or nil when {.add_format} did not define the switch. A
20
+ # block-validated switch carries no list of its own, so {.add_format} leaves it on the parser for the CLI
21
+ # catalogue (ADR-126 WD3) to read.
22
+ def accepted_formats(parser)
23
+ parser.instance_variable_get(:@rigor_accepted_formats)
24
+ end
25
+
19
26
  # Defines the standard `--config=PATH` flag on `parser`, writing the path into `options[:config]`. Used by every
20
27
  # subcommand that loads a `.rigor.yml`; the few whose `--config` help text is intentionally bespoke (`diff`,
21
28
  # `mcp`, `show-bleedingedge`) keep their own `parser.on` and call {.config_path} for the same check.
@@ -25,6 +32,20 @@ module Rigor
25
32
  end
26
33
  end
27
34
 
35
+ # Defines `--format=FORMAT` restricted to `formats`, so an unsupported value is an `OptionParser::InvalidArgument`
36
+ # raised while the options are parsed (#2502). A check left to the renderer ran the whole analysis first, and a
37
+ # renderer that read only `format == "json"` printed text for any other value. The match is exact: an
38
+ # OptionParser enum would also complete a prefix (`--format=j`), which then turns ambiguous the day a second
39
+ # format shares it. The renderers' own "unsupported format" branches remain as a guard against internal drift.
40
+ def add_format(parser, options, description, formats: %w[text json])
41
+ parser.instance_variable_set(:@rigor_accepted_formats, formats.dup.freeze)
42
+ parser.on("--format=FORMAT", description) do |value|
43
+ raise OptionParser::InvalidArgument, value unless formats.include?(value)
44
+
45
+ options[:format] = value
46
+ end
47
+ end
48
+
28
49
  # The `--config` value, checked where the flag is read (#1992). A named file that is not there is an error, not
29
50
  # a request for the defaults: the run would otherwise analyse under a configuration the caller did not ask for,
30
51
  # and an MCP caller would get a result for it with no sign anything was missing. Auto-discovery, with no
@@ -42,22 +63,40 @@ module Rigor
42
63
  def add_editor_mode(parser, options)
43
64
  parser.on("--tmp-file=PATH",
44
65
  "Editor mode: read source bytes from PATH instead of --instead-of (paired)") do |value|
66
+ note_repeated_editor_flag(options, :tmp_file, "--tmp-file")
45
67
  options[:tmp_file] = value
46
68
  end
47
69
  parser.on("--instead-of=PATH",
48
70
  "Editor mode: the logical project path the buffer represents (paired with --tmp-file)") do |value|
71
+ note_repeated_editor_flag(options, :instead_of, "--instead-of")
49
72
  options[:instead_of] = value
50
73
  end
51
74
  end
52
75
 
76
+ # Issue #2233 — editor mode binds one buffer per run, so a second `--tmp-file` or `--instead-of` cannot be
77
+ # honoured beside the first. Overwriting it dropped the first pair with no message; the repeat is recorded here
78
+ # and rejected by {resolve_buffer_binding} as a usage error instead.
79
+ def note_repeated_editor_flag(options, key, flag)
80
+ return if options[key].nil?
81
+
82
+ (options[:repeated_editor_flags] ||= []) << flag
83
+ end
84
+
53
85
  # Resolves the editor-mode buffer binding from parsed `options`: returns nil when neither editor-mode flag is set,
54
86
  # a `Analysis::BufferBinding` when the pair is valid, or `:usage_error` (after writing the reason to `err`) when
55
- # the flags are unpaired or the temp file is unreadable.
87
+ # a flag is repeated, the flags are unpaired, or the temp file is unreadable.
56
88
  def resolve_buffer_binding(options, err:)
57
89
  tmp = options[:tmp_file]
58
90
  instead = options[:instead_of]
59
91
  return nil if tmp.nil? && instead.nil?
60
92
 
93
+ repeated = Array(options[:repeated_editor_flags])
94
+ unless repeated.empty?
95
+ err.puts("#{repeated.join(' and ')} given more than once; editor mode takes one buffer per run " \
96
+ "(a single --tmp-file / --instead-of pair)")
97
+ return :usage_error
98
+ end
99
+
61
100
  if tmp.nil? || instead.nil?
62
101
  err.puts("--tmp-file and --instead-of must appear together")
63
102
  return :usage_error