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
@@ -1,6 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require "json"
4
+ require_relative "json_document"
4
5
  require "optionparser"
5
6
 
6
7
  require_relative "../analysis/rule_catalog"
@@ -46,22 +47,28 @@ module Rigor
46
47
  0
47
48
  end
48
49
 
50
+ # The parser this command parses with, built without parsing anything: the catalogue reads the
51
+ # flags and formats from it (ADR-126 WD3). `options` is only written by the switches' blocks.
52
+ def option_parser(options = {})
53
+ OptionParser.new do |opt|
54
+ opt.banner = USAGE
55
+ Options.add_format(opt, options, "Output format (text | json). Default: text.")
56
+ end
57
+ end
58
+
49
59
  private
50
60
 
51
61
  def parse_options
52
62
  options = { format: "text" }
53
- OptionParser.new do |opt|
54
- opt.banner = USAGE
55
- opt.on("--format=FORMAT", %w[text json], "Output format (text | json). Default: text.") do |fmt|
56
- options[:format] = fmt
57
- end
58
- end.parse!(@argv)
63
+ option_parser(options).parse!(@argv)
59
64
  options
60
65
  end
61
66
 
62
67
  def render_index(format)
63
68
  case format
64
- when "json" then @out.puts(JSON.pretty_generate(Analysis::RuleCatalog.all.map(&:to_h)))
69
+ when "json"
70
+ @out.puts(JSON.pretty_generate(Analysis::RuleCatalog.all.map(&:to_h)))
71
+ @err.puts(JsonDocument::SHAPE_NOTICES.fetch(:explain_rules))
65
72
  else render_index_text
66
73
  end
67
74
  end
@@ -87,6 +94,7 @@ module Rigor
87
94
  def render_skip_reason(entry, format)
88
95
  if format == "json"
89
96
  @out.puts(JSON.pretty_generate(entry.to_h))
97
+ @err.puts(JsonDocument::SHAPE_NOTICES.fetch(:explain_skip_reasons))
90
98
  return
91
99
  end
92
100
 
@@ -106,7 +114,9 @@ module Rigor
106
114
 
107
115
  def render_entries(entries, format)
108
116
  case format
109
- when "json" then @out.puts(JSON.pretty_generate(entries.map(&:to_h)))
117
+ when "json"
118
+ @out.puts(JSON.pretty_generate(entries.map(&:to_h)))
119
+ @err.puts(JsonDocument::SHAPE_NOTICES.fetch(:explain_rules))
110
120
  else
111
121
  entries.each_with_index do |entry, index|
112
122
  @out.puts("") if index.positive?
@@ -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
@@ -23,7 +24,7 @@ module Rigor
23
24
  private
24
25
 
25
26
  def render_json(report)
26
- @out.puts(JSON.pretty_generate(report.to_h))
27
+ @out.puts(JSON.pretty_generate(JsonDocument.stamp(report.to_h)))
27
28
  end
28
29
 
29
30
  def render_text(report)
@@ -0,0 +1,41 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Rigor
4
+ class CLI
5
+ # The version marker every `--format=json` document Rigor defines carries (ADR-126 WD2, ADR-50 WD1).
6
+ #
7
+ # `schema_version` is the first top-level key and counts per document (per `mode` where a flag selects a
8
+ # different document). Adding a key keeps the version; removing or renaming one is a major-only change at
9
+ # v1.0.0. `lens` (`schema`/`revision`), `baseline dump` (`version`) and `check`'s external formats keep
10
+ # their own markers and do not pass through here.
11
+ module JsonDocument
12
+ SCHEMA_VERSION = 1
13
+
14
+ # @param payload — the document's top-level object (string or symbol keys).
15
+ # @param mode — names the document when a flag selects among several; nil when the command has one.
16
+ # @return a new Hash whose first key is `schema_version` (then `mode`, when given).
17
+ def self.stamp(payload, mode: nil)
18
+ head = { "schema_version" => SCHEMA_VERSION }
19
+ head["mode"] = mode if mode
20
+ head.merge(payload)
21
+ end
22
+
23
+ # The one-line stderr notice for `type-of`, `explain` and `trace`, whose top-level JSON shape changes in
24
+ # the next minor (ADR-126 WD4). stdout stays byte-identical until then. The MCP server appends the same
25
+ # text as its own content block, recognised by {NOTICE_PATTERN}.
26
+ def self.shape_notice(command, replacement)
27
+ "rigor: note — `rigor #{command} --format=json` will print #{replacement} from the next minor " \
28
+ "release (currently a different top-level shape); update JSON consumers now. See ADR-126."
29
+ end
30
+
31
+ NOTICE_PATTERN = /^rigor: note — `rigor [a-z-]+ --format=json` will print .*$/
32
+
33
+ SHAPE_NOTICES = {
34
+ type_of: shape_notice("type-of", '{ "schema_version": 1, "results": [...] }').freeze,
35
+ trace: shape_notice("trace", '{ "schema_version": 1, "events": [...] }').freeze,
36
+ explain_rules: shape_notice("explain", '{ "schema_version": 1, "rules": [...] }').freeze,
37
+ explain_skip_reasons: shape_notice("explain", '{ "schema_version": 1, "skip_reasons": [...] }').freeze
38
+ }.freeze
39
+ end
40
+ end
41
+ end
@@ -15,18 +15,12 @@ module Rigor
15
15
  # computes them (ADR-113; output contract docs/internal-spec/lens.md). Experimental until `lens/v1`.
16
16
  class LensCommand < Command
17
17
  USAGE = "Usage: rigor lens [--format=text|json] [--config=PATH] [--workers=N] FILE..."
18
- FORMATS = %w[text json].freeze
19
18
 
20
19
  def run
21
20
  options = parse_options
22
21
  return CLI::EXIT_USAGE if options.nil?
23
22
 
24
23
  config = options.fetch(:config)
25
- if config && !File.file?(config)
26
- usage_error("no configuration file at #{config}")
27
- return CLI::EXIT_USAGE
28
- end
29
-
30
24
  configuration = Configuration.load(config)
31
25
  config_path = options.fetch(:config) || Configuration.discover
32
26
  # The working directory: Rigor auto-detects sig/ and the lockfiles there (lens.md § Serialization).
@@ -40,22 +34,32 @@ module Rigor
40
34
  outcome.targets.any? { |target| target.status == "ok" } ? 0 : 1
41
35
  end
42
36
 
43
- private
44
-
45
- def parse_options
46
- options = { format: "text", config: nil, workers: nil }
37
+ # The parser this command parses with, built without parsing anything: the catalogue reads the
38
+ # flags and formats from it (ADR-126 WD3). `options` is only written by the switches' blocks.
39
+ def option_parser(options = {})
47
40
  OptionParser.new do |opts|
48
41
  opts.banner = USAGE
49
- opts.on("--format=FORMAT", "Output format: text (default) or json") { |value| options[:format] = value }
42
+ Options.add_format(opts, options, "Output format: text (default) or json")
50
43
  opts.on("--workers=N", Integer, "Worker processes for a cold run") { |value| options[:workers] = value }
51
44
  Options.add_config(opts, options)
52
- end.parse!(@argv)
53
- return usage_error("--format must be text or json") unless FORMATS.include?(options[:format])
45
+ end
46
+ end
47
+
48
+ private
49
+
50
+ def parse_options
51
+ options = { format: "text", config: nil, workers: nil }
52
+ option_parser(options).parse!(@argv)
54
53
  return usage_error("no FILE given") if @argv.empty?
55
54
 
56
55
  options
57
56
  rescue OptionParser::ParseError => e
58
57
  usage_error(e.message)
58
+ rescue Rigor::ConfigurationError => e
59
+ # The shared --config check (CLI::Options.config_path). Mapped here the way Rigor::CLI#run maps it, so a
60
+ # direct LensCommand run exits with the usage code too.
61
+ @err.puts("rigor: #{e.message}")
62
+ nil
59
63
  end
60
64
 
61
65
  def usage_error(message)
@@ -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)
@@ -1,6 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require_relative "command"
4
+ require_relative "options"
4
5
  require_relative "../runtime/jit"
5
6
 
6
7
  require "optionparser"
@@ -41,12 +42,10 @@ module Rigor
41
42
  0
42
43
  end
43
44
 
44
- private
45
-
46
- def parse_options
47
- options = { transport: "stdio", config: nil }
48
-
49
- 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|
50
49
  opts.banner = USAGE
51
50
  opts.on("--transport=NAME",
52
51
  "Transport (default: stdio; only stdio is supported in v1)") do |value|
@@ -54,10 +53,17 @@ module Rigor
54
53
  end
55
54
  opts.on("--config=PATH",
56
55
  "Session-level default config path (individual tool calls may override)") do |value|
57
- options[:config] = value
56
+ options[:config] = Options.config_path(value)
58
57
  end
59
58
  end
60
- 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)
61
67
  options
62
68
  rescue OptionParser::ParseError => e
63
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?