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
@@ -3,6 +3,7 @@
3
3
  require "optionparser"
4
4
 
5
5
  require_relative "../configuration"
6
+ require_relative "../analysis/path_expansion"
6
7
  require_relative "../effects/config_envelopes"
7
8
  require_relative "../effects/registry"
8
9
  require_relative "options"
@@ -37,7 +38,11 @@ module Rigor
37
38
 
38
39
  VALID_MODES = %w[print diff write check].freeze
39
40
  VALID_PARAM_POLICIES = %w[untyped observed observed-strict].freeze
40
- VALID_FORMATS = %w[text json].freeze
41
+
42
+ # The per-target outcomes `--write` refuses: the file is left untouched and the run exits 1. `--check` counts
43
+ # them apart from the targets it would update, since `--write` does not fix a refused file.
44
+ TARGET_REFUSALS = %i[skipped_invalid_rbs skipped_invalid_encoding skipped_unparseable].freeze
45
+ private_constant :TARGET_REFUSALS
41
46
 
42
47
  # The skip reasons {#report_skipped} counts. The four left out each have a report of their own
43
48
  # ({#report_unrenderable}, {#report_unresolvable_superclasses}, {#report_inline_declared}, and the refusal
@@ -55,29 +60,43 @@ module Rigor
55
60
  configuration = Configuration.load(options.fetch(:config))
56
61
  paths = @argv.empty? ? configuration.paths : @argv
57
62
 
63
+ path_errors = path_diagnostics(paths, configuration)
64
+ report_path_errors(path_errors, options)
65
+
58
66
  observation_roots = observation_roots(configuration, options)
59
- observations = collect_observations(configuration, options, observation_roots)
67
+ observations = collect_observations(configuration, options, observation_roots, paths)
68
+ mode = options.fetch(:mode).to_sym
60
69
  generator = SigGen::Generator.new(configuration: configuration, paths: paths,
61
70
  observations: observations,
62
71
  include_private: options.fetch(:include_private),
63
72
  effect_annotator: effect_annotator(configuration, paths, options),
64
- overwrite: options.fetch(:overwrite))
73
+ overwrite: options.fetch(:overwrite),
74
+ target_writer: target_writer(configuration, options, mode))
65
75
  candidates = generator.run
66
- mode = options.fetch(:mode).to_sym
67
76
 
68
77
  status = case mode
69
- when :write then dispatch_write(candidates, configuration, options)
70
- when :check then dispatch_check(candidates, configuration, options)
78
+ when :write then dispatch_write(candidates, configuration, options, generator)
79
+ when :check then dispatch_check(candidates, configuration, options, generator)
71
80
  else
72
- dispatch_print_or_diff(candidates, mode, options, observation_roots)
81
+ dispatch_print_or_diff(candidates, mode, options, observation_roots, path_errors)
73
82
  0
74
83
  end
84
+ status = 1 if path_errors.any?(&:error?)
85
+ report_run_notes(candidates, generator, options)
86
+ status
87
+ end
88
+
89
+ def report_run_notes(candidates, generator, options)
75
90
  report_skipped(candidates, options)
76
91
  report_inline_declared(candidates, options)
77
92
  report_withheld_annotations(candidates, options)
78
93
  report_unrenderable(generator.unrenderable)
79
- report_unresolvable_superclasses(generator.unresolvable_superclasses)
80
- status
94
+ report_unresolvable_superclasses(generator.unresolvable_superclasses, generator)
95
+ end
96
+
97
+ # The parser `run` parses with, built without parsing anything (the catalogue reads it, ADR-126 WD3).
98
+ def option_parser(options = {})
99
+ build_option_parser(options)
81
100
  end
82
101
 
83
102
  private
@@ -208,41 +227,87 @@ module Rigor
208
227
  # declaring it would fail to build and take the class's type coverage with it. Naming the unresolved
209
228
  # superclass is the actionable part — it is usually one gem's missing RBS standing between the user
210
229
  # and signatures for a whole layer of their app.
211
- def report_unresolvable_superclasses(unresolvable)
212
- return if unresolvable.empty?
230
+ #
231
+ # Issue #2437 — a superclass the project declares in source but this run wrote nothing for gets different
232
+ # advice: no gem RBS is missing; the superclass's own methods could not be typed, or were not written.
233
+ def report_unresolvable_superclasses(unresolvable, generator)
234
+ reports = unresolvable.map { |name, superclass| [name, *generator.unresolvable_superclass_report(superclass)] }
235
+ project, external = reports.partition { |_name, kind, _root| kind == :project }
236
+ .map { |rows| rows.to_h { |name, _kind, root| [name, root] } }
237
+ report_unwritten_project_superclasses(project)
238
+ return if external.empty?
213
239
 
214
240
  @err.puts(
215
- "rigor sig-gen: skipped #{unresolvable.size} class(es) whose superclass no loaded RBS declares. " \
241
+ "rigor sig-gen: skipped #{external.size} class(es) whose superclass no loaded RBS declares. " \
216
242
  "A signature for them would fail to build (`RBS::NoSuperclassFoundError`) and would leave the " \
217
243
  "class LESS typed than it is now. Provide RBS for the superclass — `rbs collection install`, the " \
218
244
  "gem's own `sig/`, or a `signature_paths:` entry — and re-run."
219
245
  )
220
- unresolvable.sort.each { |class_name, superclass| @err.puts(" #{class_name} < #{superclass}") }
246
+ external.sort.each { |class_name, superclass| @err.puts(" #{class_name} < #{superclass}") }
247
+ end
248
+
249
+ def report_unwritten_project_superclasses(project)
250
+ return if project.empty?
251
+
252
+ @err.puts(
253
+ "rigor sig-gen: skipped #{project.size} class(es) whose superclass this run wrote no signature for. " \
254
+ "The superclass is declared in the project's source, but its own methods could not be typed, or its " \
255
+ "`sig/` file could not be written, so a signature for these subclasses would fail to build " \
256
+ "(`RBS::NoSuperclassFoundError`). Annotate or type the superclass's methods, fix its `sig/` file, or " \
257
+ "hand-write its declaration, and re-run."
258
+ )
259
+ project.sort.each { |class_name, superclass| @err.puts(" #{class_name} < #{superclass}") }
221
260
  end
222
261
 
223
- def dispatch_print_or_diff(candidates, mode, options, observation_roots)
262
+ # #1991 — the path errors `rigor check` reports for the arguments: a path that is neither a directory nor a
263
+ # `.rb` file. Warned about beside files that will be read, an error when nothing will be, as `check` grades it.
264
+ def path_diagnostics(paths, configuration)
265
+ bad = Analysis::PathExpansion.unusable(paths)
266
+ return [] if bad.empty?
267
+
268
+ any_files = !Analysis::PathExpansion.ruby_files(paths, configuration.exclude_patterns,
269
+ base: configuration.exclude_base).empty?
270
+ Analysis::PathExpansion.diagnostics(bad, any_files: any_files)
271
+ end
272
+
273
+ # Without this a missing path was read as empty, so the run said "No candidates" and exited 0 (#1991). JSON
274
+ # print/diff carries the errors in its payload (see {#path_errors_in_payload?}); every other form says them on
275
+ # stderr, as `rigor check` does.
276
+ def report_path_errors(path_errors, options)
277
+ return if path_errors_in_payload?(options)
278
+
279
+ path_errors.each { |diagnostic| @err.puts(diagnostic.to_s) }
280
+ end
281
+
282
+ def path_errors_in_payload?(options)
283
+ options.fetch(:format) == "json" && %w[print diff].include?(options.fetch(:mode))
284
+ end
285
+
286
+ def dispatch_print_or_diff(candidates, mode, options, observation_roots, path_errors)
224
287
  SigGen::Renderer.new(out: @out).render(
225
288
  candidates: candidates,
226
289
  mode: mode,
227
290
  format: options.fetch(:format),
228
291
  selection: options.fetch(:selection),
229
- observation: observation_report(observation_roots, options)
292
+ observation: observation_report(observation_roots, options),
293
+ diagnostics: path_errors_in_payload?(options) ? path_errors : []
230
294
  )
231
295
  end
232
296
 
233
297
  # @return exit status — non-zero when a file the user asked to write could not be written.
234
- def dispatch_write(candidates, configuration, options)
235
- results = build_writer(configuration, options).write_all(candidates)
298
+ def dispatch_write(candidates, configuration, options, generator)
299
+ writer = build_writer(configuration, options)
300
+ results = writer.write_all(candidates, header_spelling: generator.method(:generic_header_spelling))
236
301
 
237
302
  refused = refused_candidates(candidates)
238
- SigGen::Renderer.new(out: @out).render_write(results: results, format: options.fetch(:format), refused: refused)
303
+ SigGen::Renderer.new(out: @out).render_write(results: results, format: options.fetch(:format), refused: refused,
304
+ left_bare: writer.left_bare)
239
305
  SigGen::Renderer.refusal_lines(refused).each { |line| @err.puts(line) }
240
- # A refused write (an assembled file that does not parse, an existing target that is not valid UTF-8,
241
- # or a method whose inline and `sig/` declarations disagree without `--overwrite`) means the user asked for a
242
- # write and did not get one, so the command must not report success — a green `sig-gen --write` in CI
243
- # would otherwise mean nothing.
244
- refusals = %i[skipped_invalid_rbs skipped_invalid_encoding]
245
- results.any? { |result| refusals.include?(result.action) } || !refused.empty? ? 1 : 0
306
+ # A refused write (an assembled file that does not parse, an existing target that is not valid UTF-8 or
307
+ # does not parse, or a method whose inline and `sig/` declarations disagree without `--overwrite`) means the
308
+ # user asked for a write and did not get one, so the command must not report success — a green
309
+ # `sig-gen --write` in CI would otherwise mean nothing.
310
+ results.any? { |result| TARGET_REFUSALS.include?(result.action) } || !refused.empty? ? 1 : 0
246
311
  end
247
312
 
248
313
  # ADR-112 WD4 — the methods whose inline and `sig/` declarations disagree, refused because the run did not
@@ -258,11 +323,15 @@ module Rigor
258
323
  # --overwrite` would apply it.
259
324
  #
260
325
  # @return 1 when `sig/` is out of date (or a write would be refused), 0 when it is current.
261
- def dispatch_check(candidates, configuration, options)
262
- results = build_writer(configuration, options, dry_run: true).write_all(candidates)
326
+ #
327
+ # A generic header held back from the repair (#2605) is reported, but does not fail the check: `--write`
328
+ # cannot change it, and it was failing the project's env build before the run as well.
329
+ def dispatch_check(candidates, configuration, options, generator)
330
+ writer = build_writer(configuration, options, dry_run: true)
331
+ results = writer.write_all(candidates, header_spelling: generator.method(:generic_header_spelling))
263
332
  refused = refused_candidates(candidates)
264
333
  SigGen::Renderer.new(out: @out).render_check(results: results, format: options.fetch(:format),
265
- refused: refused)
334
+ refused: refused, left_bare: writer.left_bare)
266
335
  stale = SigGen::Renderer.out_of_date(results)
267
336
  return 0 if stale.empty? && refused.empty?
268
337
 
@@ -271,16 +340,40 @@ module Rigor
271
340
  end
272
341
 
273
342
  def report_check_failure(stale, refused)
274
- unless stale.empty?
275
- @err.puts("rigor sig-gen --check: #{stale.size} signature file(s) out of date; " \
343
+ pending, refusals = stale.partition { |result| !TARGET_REFUSALS.include?(result.action) }
344
+ unless pending.empty?
345
+ @err.puts("rigor sig-gen --check: #{pending.size} signature file(s) out of date; " \
276
346
  "run `rigor sig-gen --write` with the same options to update them.")
277
347
  end
348
+ @err.puts(refused_targets_line(refusals)) unless refusals.empty?
278
349
  return if refused.empty?
279
350
 
280
351
  @err.puts("rigor sig-gen --check: #{refused.size} method(s) whose inline declaration and sig/ copy " \
281
352
  "disagree; make them agree, or pass --overwrite to replace the sig/ member.")
282
353
  end
283
354
 
355
+ # A target `--write` refuses is not one it would update: it is counted apart from the out-of-date targets. Where
356
+ # the existing file is the user's to repair (encoding, parse), it is named for a hand fix. An invalid generated
357
+ # file is a Rigor rendering bug (its REFUSED line says so), so it is counted but never listed as a hand fix.
358
+ def refused_targets_line(results)
359
+ by_hand = results.reject { |result| result.action == :skipped_invalid_rbs }
360
+ .map { |result| result.target_path.to_s }
361
+ head = "rigor sig-gen --check: #{results.size} signature file(s) refused; `--write` does not update them"
362
+ if by_hand.empty?
363
+ return "#{head}. The generated RBS did not parse: a Rigor bug, see its REFUSED line on stdout."
364
+ end
365
+
366
+ "#{head}. Repair by hand: #{by_hand.join(', ')}."
367
+ end
368
+
369
+ # #2461 — only a run that writes or compares files asks whether the writer will accept each target: where a
370
+ # target lands depends on the working directory, and print, diff and JSON output must not.
371
+ def target_writer(configuration, options, mode)
372
+ return nil unless %i[write check].include?(mode)
373
+
374
+ build_writer(configuration, options, dry_run: true)
375
+ end
376
+
284
377
  def build_writer(configuration, options, dry_run: false)
285
378
  layout_index = SigGen::LayoutIndex.new(signature_paths: configuration.signature_paths)
286
379
  path_mapper = SigGen::PathMapper.new(configuration: configuration, layout_index: layout_index)
@@ -291,7 +384,11 @@ module Rigor
291
384
  # specified, observe the project's test roots: `test_paths:`, or whichever of `spec/` and `test/` exist. With no
292
385
  # root at all the run still succeeds, but says so — otherwise every parameter stays `untyped` with nothing
293
386
  # naming the reason.
294
- def collect_observations(configuration, options, observe_paths)
387
+ #
388
+ # The collector pre-indexes the classes of the paths the run generates for as well as the configured ones, so
389
+ # a call site on a class named only on the command line (`sig-gen ../lib` from a subdirectory, where no
390
+ # config file is discovered and the default `paths:` resolve under the subdirectory) attaches (#2078).
391
+ def collect_observations(configuration, options, observe_paths, paths)
295
392
  return {} if options.fetch(:params) != "observed"
296
393
 
297
394
  if observe_paths.empty?
@@ -299,7 +396,8 @@ module Rigor
299
396
  else
300
397
  warn_missing_test_roots(observe_paths)
301
398
  end
302
- SigGen::ObservationCollector.new(configuration: configuration, paths: observe_paths).collect
399
+ SigGen::ObservationCollector.new(configuration: configuration, paths: observe_paths,
400
+ source_paths: (configuration.paths + paths).uniq).collect
303
401
  end
304
402
 
305
403
  # The roots `--params=observed` reads: `--observe=PATH...` when given, else the project's test roots.
@@ -387,7 +485,7 @@ module Rigor
387
485
  opts.on("--no-cache", "Do not read or write the analysis cache (effect collection only)") do
388
486
  options[:no_cache] = true
389
487
  end
390
- opts.on("--format=FORMAT", "Output format: text or json") { |value| options[:format] = value }
488
+ Options.add_format(opts, options, "Output format: text or json")
391
489
  opts.on("--params=POLICY", "Parameter policy: untyped (default), observed, observed-strict") do |value|
392
490
  options[:params] = value
393
491
  end
@@ -417,11 +515,9 @@ module Rigor
417
515
 
418
516
  def validation_error(options)
419
517
  mode = options.fetch(:mode)
420
- format = options.fetch(:format)
421
518
  params = options.fetch(:params)
422
519
 
423
520
  return "--print, --diff, --write, and --check are mutually exclusive flags; pick one" if mode == "conflict"
424
- return "unsupported --format=#{format}" unless VALID_FORMATS.include?(format)
425
521
  return "unsupported --params=#{params}" unless VALID_PARAM_POLICIES.include?(params)
426
522
  if params == "observed-strict"
427
523
  return "--params=observed-strict is reserved until the capability-role catalog ships"
@@ -1,5 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require "optparse"
4
+
3
5
  require_relative "command"
4
6
  require_relative "../rooted_glob"
5
7
 
@@ -77,6 +79,28 @@ module Rigor
77
79
  # up.
78
80
  SKILLS_ROOT = File.expand_path("../../../skills", __dir__).freeze
79
81
 
82
+ # The parser of `rigor skill`, built without parsing anything: the catalogue reads its flags from it (ADR-126
83
+ # WD3). The positional grammar is read by hand in {#run}, so this describes the switches and parses nothing.
84
+ def option_parser(options = {})
85
+ OptionParser.new do |opts|
86
+ opts.banner = USAGE.lines.first.chomp
87
+ opts.on("--full NAME", "Print the SKILL.md body and its references/ inline") { |v| options[:full] = v }
88
+ opts.on("--path NAME", "Print the absolute path of the SKILL.md file") { |v| options[:path] = v }
89
+ opts.on("--list", "List bundled skills (name + absolute path)") { options[:list] = true }
90
+ opts.on("--describe", "Report project state and recommend the next skill") { options[:describe] = true }
91
+ end
92
+ end
93
+
94
+ # The parser of `rigor skill describe` and `rigor describe`; `banner` is the usage line of the spelling used.
95
+ def self.describe_option_parser(banner = "Usage: rigor skill describe [--deep]", options = {})
96
+ OptionParser.new do |opts|
97
+ opts.banner = banner
98
+ opts.on("--deep", "Run `rigor check` first and route the recommendation on its result") do
99
+ options[:deep] = true
100
+ end
101
+ end
102
+ end
103
+
80
104
  # @return CLI exit status.
81
105
  def run
82
106
  case @argv.first
@@ -87,7 +111,7 @@ module Rigor
87
111
  0
88
112
  when "describe", "--describe"
89
113
  @argv.shift
90
- run_describe
114
+ run_describe("Usage: rigor skill describe [--deep]")
91
115
  when "--list"
92
116
  @argv.shift
93
117
  run_list
@@ -176,7 +200,12 @@ module Rigor
176
200
  # `--deep` is the one opt-in that breaks the "never runs `rigor check`" guarantee — and only for the invocation
177
201
  # that asks for it (issue #148 / ADR-73 § "Field-trial follow-ups"). It runs a real check and lets the result
178
202
  # pick the headline; the un-flagged command is unchanged, down to loading no analysis code at all.
179
- def run_describe
203
+ def run_describe(usage)
204
+ if @argv.intersect?(%w[-h --help])
205
+ @out.puts(usage)
206
+ return 0
207
+ end
208
+
180
209
  deep = !@argv.delete("--deep").nil?
181
210
  unknown = @argv.first
182
211
  return usage_error("unknown option for `describe`: #{unknown}") unless unknown.nil?
@@ -222,7 +251,7 @@ module Rigor
222
251
  discover_skills.find { |s| s.fetch(:name) == name }
223
252
  end
224
253
 
225
- # The `references/*.md` files bundled alongside a skill, sorted so the `NN-` prefixes drive read order.
254
+ # The `references/*.md` files bundled alongside a skill.
226
255
  def reference_files(skill)
227
256
  dir = File.join(File.dirname(skill.fetch(:path)), "references")
228
257
  return [] unless File.directory?(dir)
@@ -96,7 +96,7 @@ module Rigor
96
96
  end
97
97
  end
98
98
 
99
- # The `rigor-doctor` signal, read exactly as `rigor doctor` itself reads it: an RBS environment that built to
99
+ # The `rigor-doctor` signal: an RBS environment that built to
100
100
  # zero classes (analysis is hollow, so every other count is meaningless), or a `configuration-error` the run
101
101
  # already emitted. Checked first — with a broken setup the remaining diagnostics are not evidence of anything.
102
102
  def broken_environment?(result)
@@ -361,7 +361,7 @@ module Rigor
361
361
  end
362
362
 
363
363
  # The one-line essence of a skill's frontmatter `description`, read live from the SKILL.md so the catalogue can
364
- # never go stale relative to the shipped skill. Drops the `Triggers:` / `NOT for` tail and caps the length.
364
+ # never go stale relative to the shipped skill. Drops the `Triggers:` tail and caps the length.
365
365
  def catalog_blurb(path)
366
366
  text = frontmatter(path).fetch("description", "").to_s.strip.tr("\n", " ").squeeze(" ")
367
367
  head = text.split(/\s*Triggers?:/, 2).first.to_s.strip
@@ -12,6 +12,7 @@ require_relative "../inference/flow_tracer"
12
12
  require_relative "../inference/scope_indexer"
13
13
  require_relative "command"
14
14
  require_relative "trace_renderer"
15
+ require_relative "json_document"
15
16
  require_relative "../project_environment"
16
17
 
17
18
  module Rigor
@@ -43,15 +44,12 @@ module Rigor
43
44
  execute(file: file, options: options)
44
45
  end
45
46
 
46
- private
47
-
48
- def parse_options
49
- options = { format: "text", delay: nil, verbose: false, line: nil, config: nil }
50
- parser = OptionParser.new do |opts|
47
+ # The parser this command parses with, built without parsing anything: the catalogue reads the
48
+ # flags and formats from it (ADR-126 WD3). `options` is only written by the switches' blocks.
49
+ def option_parser(options = {})
50
+ OptionParser.new do |opts|
51
51
  opts.banner = USAGE
52
- opts.on("--format=FORMAT", "Output format: text (animation) or json (raw event stream)") do |value|
53
- options[:format] = value
54
- end
52
+ Options.add_format(opts, options, "Output format: text (animation) or json (raw event stream)")
55
53
  opts.on("--delay=SECONDS", Float,
56
54
  "Autoplay with SECONDS between frames (default: step on key press)") do |value|
57
55
  options[:delay] = value
@@ -62,7 +60,13 @@ module Rigor
62
60
  opts.on("--verbose", "Include every expression enter/result frame") { options[:verbose] = true }
63
61
  Options.add_config(opts, options)
64
62
  end
65
- parser.parse!(@argv)
63
+ end
64
+
65
+ private
66
+
67
+ def parse_options
68
+ options = { format: "text", delay: nil, verbose: false, line: nil, config: nil }
69
+ option_parser(options).parse!(@argv)
66
70
  options
67
71
  end
68
72
 
@@ -82,6 +86,7 @@ module Rigor
82
86
 
83
87
  if options.fetch(:format) == "json"
84
88
  @out.puts(JSON.pretty_generate(frames.map { |event| event_to_h(event) }))
89
+ @err.puts(JsonDocument::SHAPE_NOTICES.fetch(:trace))
85
90
  return 0
86
91
  end
87
92
 
@@ -36,14 +36,13 @@ module Rigor
36
36
  0
37
37
  end
38
38
 
39
- private
40
-
41
- def parse_options
42
- options = { config: nil, format: "text", top: 10, sections: DEFAULT_SECTIONS, include_info: false }
39
+ # The parser this command parses with, built without parsing anything: the catalogue reads the
40
+ # flags and formats from it (ADR-126 WD3). `options` is only written by the switches' blocks.
41
+ def option_parser(options = {})
43
42
  OptionParser.new do |opts|
44
43
  opts.banner = USAGE
45
44
  Options.add_config(opts, options)
46
- opts.on("--format=FORMAT", "Output format: text (default) or json") { |v| options[:format] = v }
45
+ Options.add_format(opts, options, "Output format: text (default) or json")
47
46
  opts.on("--top=N", Integer, "Hotspot-file count (default 10)") { |v| options[:top] = v }
48
47
  opts.on("--include-info",
49
48
  "Route info diagnostics into distribution / selectors / hotspots " \
@@ -57,15 +56,15 @@ module Rigor
57
56
  opts.on("--selectors-only", "Print only the class/method selectors section") do
58
57
  options[:sections] = %i[selectors]
59
58
  end
60
- end.parse!(@argv)
61
- validate!(options)
62
- options
59
+ end
63
60
  end
64
61
 
65
- def validate!(options)
66
- return if %w[text json].include?(options.fetch(:format))
62
+ private
67
63
 
68
- raise OptionParser::InvalidArgument, "unsupported format: #{options.fetch(:format)}"
64
+ def parse_options
65
+ options = { config: nil, format: "text", top: 10, sections: DEFAULT_SECTIONS, include_info: false }
66
+ option_parser(options).parse!(@argv)
67
+ options
69
68
  end
70
69
 
71
70
  # Sequential, cache-backed, no run-stats: triage only needs the diagnostic stream, and sequential keeps the rule
@@ -3,6 +3,7 @@
3
3
  require "json"
4
4
 
5
5
  require_relative "../triage"
6
+ require_relative "json_document"
6
7
 
7
8
  module Rigor
8
9
  class CLI
@@ -17,7 +18,7 @@ module Rigor
17
18
  end
18
19
 
19
20
  def json
20
- JSON.pretty_generate(Triage.report_to_h(@report))
21
+ JSON.pretty_generate(JsonDocument.stamp(Triage.report_to_h(@report)))
21
22
  end
22
23
 
23
24
  def text
@@ -13,10 +13,12 @@ require_relative "../inference/scope_indexer"
13
13
  require_relative "../inference/precision_scanner"
14
14
  require_relative "../source/node_children"
15
15
  require_relative "type_of_renderer"
16
+ require_relative "json_document"
16
17
  require_relative "type_of_template_probe"
17
18
  require_relative "command"
18
19
  require_relative "options"
19
20
  require_relative "../project_environment"
21
+ require_relative "type_of_project_scope"
20
22
 
21
23
  module Rigor
22
24
  class CLI
@@ -28,7 +30,7 @@ module Rigor
28
30
  # Encapsulating the command in its own class keeps `Rigor::CLI` focused on dispatching and lets us evolve the
29
31
  # type-of UX (extra flags, watch mode, streaming output) without bloating the CLI shell. Output formatting is
30
32
  # delegated to {TypeOfRenderer}.
31
- class TypeOfCommand < Command
33
+ class TypeOfCommand < Command # rubocop:disable Metrics/ClassLength
32
34
  include TypeOfTemplateProbe
33
35
 
34
36
  USAGE = "Usage: rigor type-of [options] FILE:LINE[:COL] [FILE:LINE[:COL] ...]"
@@ -64,19 +66,24 @@ module Rigor
64
66
  execute(targets: targets, options: options, buffer: buffer)
65
67
  end
66
68
 
67
- private
68
-
69
- def parse_options
70
- options = { format: "text", trace: false, config: nil, tmp_file: nil, instead_of: nil }
71
-
72
- parser = OptionParser.new do |opts|
69
+ # The parser this command parses with, built without parsing anything: the catalogue reads the
70
+ # flags and formats from it (ADR-126 WD3). `options` is only written by the switches' blocks.
71
+ def option_parser(options = {})
72
+ OptionParser.new do |opts|
73
73
  opts.banner = USAGE
74
- opts.on("--format=FORMAT", "Output format: text or json") { |value| options[:format] = value }
74
+ Options.add_format(opts, options, "Output format: text or json")
75
75
  opts.on("--trace", "Record fail-soft fallbacks via FallbackTracer") { options[:trace] = true }
76
76
  Options.add_config(opts, options)
77
77
  Options.add_editor_mode(opts, options)
78
78
  end
79
- parser.parse!(@argv)
79
+ end
80
+
81
+ private
82
+
83
+ def parse_options
84
+ options = { format: "text", trace: false, config: nil, tmp_file: nil, instead_of: nil }
85
+
86
+ option_parser(options).parse!(@argv)
80
87
 
81
88
  options
82
89
  end
@@ -88,7 +95,7 @@ module Rigor
88
95
  def execute(targets:, options:, buffer: nil)
89
96
  configuration = Configuration.load(options.fetch(:config))
90
97
  environment = project_environment(targets.map(&:file).uniq, configuration, buffer)
91
- base_scope = Scope.empty(environment: environment)
98
+ base_scope = TypeOfProjectScope.build(environment: environment, configuration: configuration, buffer: buffer)
92
99
 
93
100
  results_by_target = Array.new(targets.length)
94
101
  targets.each_with_index.group_by { |target, _index| target.file }.each do |file, indexed_targets|
@@ -100,7 +107,7 @@ module Rigor
100
107
  results = results_by_target.compact.flatten(1)
101
108
  return 1 if results.empty?
102
109
 
103
- TypeOfRenderer.new(out: @out).render(results, format: options.fetch(:format))
110
+ TypeOfRenderer.new(out: @out, err: @err).render(results, format: options.fetch(:format))
104
111
  0
105
112
  end
106
113
 
@@ -205,7 +212,6 @@ module Rigor
205
212
  end
206
213
  end
207
214
 
208
- # `presentation` is the Result's optional `enumeration:` / `location_mapper:`.
209
215
  def type_result(file, line, column, node, scope_index, options, **presentation)
210
216
  tracer = options[:trace] ? Inference::FallbackTracer.new : nil
211
217
  type = scope_index[node].type_of(node, tracer: tracer)
@@ -0,0 +1,24 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "../scope"
4
+ require_relative "../protection/discovery_seed"
5
+
6
+ module Rigor
7
+ class CLI
8
+ # Issue #512 — the scope `rigor type-of` probes with: single-file, plus the cross-file tables a prior
9
+ # `rigor check --incremental` left in the snapshot ({Protection::DiscoverySeed.from_snapshot}) when there are
10
+ # any. A probe never re-parses the project for its seed, so without a reusable snapshot this is the bare
11
+ # `Scope.empty` it always was.
12
+ #
13
+ # The seed is the full {Inference::ProjectSeedTables} fold, so a `contested_*` sibling travels with the member
14
+ # it qualifies (ADR-119): this is a recorded whole-index consumer in `admission_census.yml`.
15
+ module TypeOfProjectScope
16
+ module_function
17
+
18
+ def build(environment:, configuration:, buffer: nil)
19
+ seed = Protection::DiscoverySeed.from_snapshot(configuration: configuration, buffer: buffer)
20
+ Protection::DiscoverySeed.seeded_scope(environment: environment, seed: seed)
21
+ end
22
+ end
23
+ end
24
+ end
@@ -4,6 +4,7 @@ require "json"
4
4
  require "optionparser"
5
5
 
6
6
  require_relative "renderable"
7
+ require_relative "json_document"
7
8
 
8
9
  module Rigor
9
10
  class CLI
@@ -14,8 +15,9 @@ module Rigor
14
15
  class TypeOfRenderer
15
16
  include Renderable
16
17
 
17
- def initialize(out:)
18
+ def initialize(out:, err: nil)
18
19
  @out = out
20
+ @err = err
19
21
  end
20
22
 
21
23
  private
@@ -96,6 +98,7 @@ module Rigor
96
98
  enumerations = line_enumerations(list)
97
99
  payload[:line_enumerations] = enumerations unless enumerations.empty?
98
100
  @out.puts(JSON.pretty_generate(payload))
101
+ @err&.puts(JsonDocument::SHAPE_NOTICES.fetch(:type_of))
99
102
  end
100
103
 
101
104
  def line_enumerations(results)
@@ -38,15 +38,12 @@ module Rigor
38
38
  determine_exit(report, options)
39
39
  end
40
40
 
41
- private
42
-
43
- def parse_options
44
- options = { format: "text", limit: 10, show_recognized: false, threshold: nil,
45
- config: nil }
46
-
47
- parser = OptionParser.new do |opts|
41
+ # The parser this command parses with, built without parsing anything: the catalogue reads the
42
+ # flags and formats from it (ADR-126 WD3). `options` is only written by the switches' blocks.
43
+ def option_parser(options = {})
44
+ OptionParser.new do |opts|
48
45
  opts.banner = USAGE
49
- opts.on("--format=FORMAT", "Output format: text or json") { |value| options[:format] = value }
46
+ Options.add_format(opts, options, "Output format: text or json")
50
47
  Options.add_config(opts, options)
51
48
  opts.on("--limit=N", Integer, "Max example events to print (text only)") do |value|
52
49
  options[:limit] = value
@@ -58,7 +55,15 @@ module Rigor
58
55
  options[:threshold] = value
59
56
  end
60
57
  end
61
- parser.parse!(@argv)
58
+ end
59
+
60
+ private
61
+
62
+ def parse_options
63
+ options = { format: "text", limit: 10, show_recognized: false, threshold: nil,
64
+ config: nil }
65
+
66
+ option_parser(options).parse!(@argv)
62
67
 
63
68
  options
64
69
  end