rigortype 0.4.6 → 0.4.7

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (417) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +2 -2
  3. data/data/gem_overlay/activesupport/core_ext.rbs +16 -13
  4. data/data/vendored_gem_sigs/bundler/bundler.rbs +1 -1
  5. data/data/vendored_gem_sigs/rubygems/rubygems_extras.rbs +4 -3
  6. data/docs/handbook/01-getting-started.md +5 -2
  7. data/docs/handbook/11-sig-gen.md +50 -3
  8. data/docs/handbook/appendix-phpstan.md +4 -4
  9. data/docs/manual/02-cli-reference.md +152 -26
  10. data/docs/manual/03-configuration.md +36 -3
  11. data/docs/manual/06-baseline.md +6 -4
  12. data/docs/manual/09-editor-integration.md +13 -2
  13. data/docs/manual/10-mcp-server.md +38 -2
  14. data/docs/manual/12-caching.md +4 -3
  15. data/docs/manual/plugins/rigor-actionpack.md +19 -11
  16. data/docs/manual/plugins/rigor-activesupport-core-ext.md +20 -1
  17. data/lib/rigor/analysis/baseline.rb +24 -27
  18. data/lib/rigor/analysis/check_rules/always_truthy_condition_collector.rb +5 -6
  19. data/lib/rigor/analysis/check_rules/dead_assignment_collector.rb +2 -2
  20. data/lib/rigor/analysis/check_rules/declaration_sourced_guard.rb +4 -4
  21. data/lib/rigor/analysis/check_rules/duplicate_hash_key_collector.rb +2 -2
  22. data/lib/rigor/analysis/check_rules/inferred_param_guard.rb +85 -13
  23. data/lib/rigor/analysis/check_rules/ivar_write_collector.rb +3 -2
  24. data/lib/rigor/analysis/check_rules/lexical_method_sites.rb +0 -1
  25. data/lib/rigor/analysis/check_rules/main_pass_collector.rb +5 -8
  26. data/lib/rigor/analysis/check_rules/published_constant_guard.rb +1 -1
  27. data/lib/rigor/analysis/check_rules/rebinding_block_callees.rb +3 -3
  28. data/lib/rigor/analysis/check_rules/return_in_ensure_collector.rb +1 -2
  29. data/lib/rigor/analysis/check_rules/rule_ids.rb +1 -1
  30. data/lib/rigor/analysis/check_rules/rule_walk.rb +9 -7
  31. data/lib/rigor/analysis/check_rules/shadowed_rescue_collector.rb +3 -4
  32. data/lib/rigor/analysis/check_rules/special_global_setters.rb +3 -3
  33. data/lib/rigor/analysis/check_rules/unreachable_clause_collector.rb +2 -2
  34. data/lib/rigor/analysis/check_rules/void_value_use_collector.rb +1 -1
  35. data/lib/rigor/analysis/check_rules.rb +124 -44
  36. data/lib/rigor/analysis/crash_signature.rb +2 -2
  37. data/lib/rigor/analysis/dependency_recorder.rb +4 -4
  38. data/lib/rigor/analysis/dependency_source_inference/boundary_cross_reporter.rb +14 -3
  39. data/lib/rigor/analysis/dependency_source_inference/gem_resolver.rb +1 -1
  40. data/lib/rigor/analysis/dependency_source_inference/index.rb +6 -8
  41. data/lib/rigor/analysis/dependency_source_inference/walker.rb +4 -4
  42. data/lib/rigor/analysis/edit_replay.rb +93 -6
  43. data/lib/rigor/analysis/fact_store.rb +3 -3
  44. data/lib/rigor/analysis/incremental_run_slot/write_guard.rb +2 -2
  45. data/lib/rigor/analysis/incremental_run_slot.rb +2 -2
  46. data/lib/rigor/analysis/incremental_session.rb +27 -16
  47. data/lib/rigor/analysis/rule_catalog.rb +2 -2
  48. data/lib/rigor/analysis/run_stats.rb +2 -2
  49. data/lib/rigor/analysis/runner/buffer_pool_dispatcher.rb +2 -2
  50. data/lib/rigor/analysis/runner/declaration_position.rb +1 -1
  51. data/lib/rigor/analysis/runner/diagnostic_aggregator.rb +193 -93
  52. data/lib/rigor/analysis/runner/effect_envelope_pass.rb +1 -1
  53. data/lib/rigor/analysis/runner/pool_coordinator.rb +147 -323
  54. data/lib/rigor/analysis/runner/project_pre_passes.rb +3 -3
  55. data/lib/rigor/analysis/runner/run_level_facts.rb +304 -0
  56. data/lib/rigor/analysis/runner/run_snapshots.rb +44 -54
  57. data/lib/rigor/analysis/runner/worker_payload.rb +36 -0
  58. data/lib/rigor/analysis/runner.rb +77 -71
  59. data/lib/rigor/analysis/self_call_resolution_recorder.rb +1 -1
  60. data/lib/rigor/analysis/template_unit_positions.rb +2 -3
  61. data/lib/rigor/analysis/template_units.rb +9 -1
  62. data/lib/rigor/analysis/worker_session.rb +24 -31
  63. data/lib/rigor/bleeding_edge.rb +3 -6
  64. data/lib/rigor/builtins/imported_refinements.rb +6 -8
  65. data/lib/rigor/builtins/predefined_constant_refinements.rb +1 -1
  66. data/lib/rigor/builtins/regex_refinement.rb +4 -6
  67. data/lib/rigor/cache/descriptor.rb +6 -5
  68. data/lib/rigor/cache/incremental_snapshot.rb +31 -12
  69. data/lib/rigor/cache/rbs_cache_producer.rb +2 -2
  70. data/lib/rigor/cache/rbs_class_ancestor_table.rb +1 -1
  71. data/lib/rigor/cache/rbs_class_type_param_names.rb +2 -2
  72. data/lib/rigor/cache/rbs_constant_table.rb +1 -1
  73. data/lib/rigor/cache/rbs_descriptor.rb +54 -3
  74. data/lib/rigor/cache/rbs_environment.rb +4 -1
  75. data/lib/rigor/cache/rbs_environment_marshal_patch.rb +2 -2
  76. data/lib/rigor/cache/rbs_known_class_names.rb +1 -1
  77. data/lib/rigor/cache/store.rb +11 -10
  78. data/lib/rigor/ci_detector.rb +1 -1
  79. data/lib/rigor/cli/annotate_command.rb +24 -19
  80. data/lib/rigor/cli/baseline_command.rb +145 -55
  81. data/lib/rigor/cli/catalogue.rb +256 -0
  82. data/lib/rigor/cli/check_command.rb +108 -102
  83. data/lib/rigor/cli/check_runner_factory.rb +2 -2
  84. data/lib/rigor/cli/command.rb +15 -1
  85. data/lib/rigor/cli/coverage_command.rb +21 -8
  86. data/lib/rigor/cli/coverage_mutation.rb +5 -7
  87. data/lib/rigor/cli/coverage_renderer.rb +2 -1
  88. data/lib/rigor/cli/coverage_scan.rb +3 -5
  89. data/lib/rigor/cli/diff_command.rb +25 -20
  90. data/lib/rigor/cli/docs_command.rb +1 -2
  91. data/lib/rigor/cli/doctor_command.rb +12 -12
  92. data/lib/rigor/cli/effects_command.rb +50 -40
  93. data/lib/rigor/cli/effects_diff_renderer.rb +18 -17
  94. data/lib/rigor/cli/effects_explain_renderer.rb +13 -12
  95. data/lib/rigor/cli/effects_renderer.rb +2 -1
  96. data/lib/rigor/cli/effects_snapshot_command.rb +18 -17
  97. data/lib/rigor/cli/explain_command.rb +18 -8
  98. data/lib/rigor/cli/fused_protection_renderer.rb +2 -1
  99. data/lib/rigor/cli/json_document.rb +41 -0
  100. data/lib/rigor/cli/lens_command.rb +12 -8
  101. data/lib/rigor/cli/lsp_command.rb +250 -23
  102. data/lib/rigor/cli/mcp_command.rb +12 -7
  103. data/lib/rigor/cli/mutation_fork_scan.rb +1 -1
  104. data/lib/rigor/cli/mutation_protection_renderer.rb +3 -2
  105. data/lib/rigor/cli/options.rb +40 -1
  106. data/lib/rigor/cli/plugin_command.rb +93 -46
  107. data/lib/rigor/cli/plugins_command.rb +17 -18
  108. data/lib/rigor/cli/plugins_renderer.rb +10 -6
  109. data/lib/rigor/cli/protection_fork_scan.rb +2 -2
  110. data/lib/rigor/cli/protection_renderer.rb +2 -1
  111. data/lib/rigor/cli/show_bleedingedge_command.rb +21 -15
  112. data/lib/rigor/cli/sig_gen_command.rb +84 -27
  113. data/lib/rigor/cli/skill_command.rb +32 -3
  114. data/lib/rigor/cli/skill_deep_probe.rb +1 -1
  115. data/lib/rigor/cli/skill_describe.rb +1 -1
  116. data/lib/rigor/cli/trace_command.rb +14 -9
  117. data/lib/rigor/cli/triage_command.rb +10 -11
  118. data/lib/rigor/cli/triage_renderer.rb +2 -1
  119. data/lib/rigor/cli/type_of_command.rb +16 -11
  120. data/lib/rigor/cli/type_of_project_scope.rb +1 -4
  121. data/lib/rigor/cli/type_of_renderer.rb +4 -1
  122. data/lib/rigor/cli/type_scan_command.rb +14 -9
  123. data/lib/rigor/cli/type_scan_renderer.rb +2 -1
  124. data/lib/rigor/cli/unused_command.rb +27 -17
  125. data/lib/rigor/cli/upgrade_command.rb +5 -0
  126. data/lib/rigor/cli.rb +209 -54
  127. data/lib/rigor/config_audit.rb +1 -1
  128. data/lib/rigor/configuration/severity_profile.rb +5 -5
  129. data/lib/rigor/configuration.rb +72 -30
  130. data/lib/rigor/effects/definition_context.rb +0 -2
  131. data/lib/rigor/effects/envelope.rb +1 -1
  132. data/lib/rigor/effects/framework_units.rb +1 -1
  133. data/lib/rigor/effects/mutation_classifier.rb +4 -7
  134. data/lib/rigor/effects/propagator.rb +1 -1
  135. data/lib/rigor/effects/registry.rb +2 -2
  136. data/lib/rigor/effects/snapshot.rb +1 -1
  137. data/lib/rigor/effects/taint_cause.rb +0 -2
  138. data/lib/rigor/effects/unit_scan.rb +9 -13
  139. data/lib/rigor/environment/bundle_sig_discovery.rb +5 -9
  140. data/lib/rigor/environment/class_registry.rb +3 -3
  141. data/lib/rigor/environment/core_root.rb +77 -0
  142. data/lib/rigor/environment/default_libraries.rb +1 -1
  143. data/lib/rigor/environment/deferred_signature_plan.rb +283 -0
  144. data/lib/rigor/environment/gated_signature_guard.rb +1 -1
  145. data/lib/rigor/environment/gated_signature_plan.rb +5 -2
  146. data/lib/rigor/environment/lockfile_resolver.rb +28 -12
  147. data/lib/rigor/environment/member_consistency/comparator.rb +18 -1
  148. data/lib/rigor/environment/member_consistency.rb +56 -7
  149. data/lib/rigor/environment/missing_gem_constant_index.rb +3 -2
  150. data/lib/rigor/environment/project_path_gems.rb +124 -0
  151. data/lib/rigor/environment/rbs_hierarchy.rb +6 -15
  152. data/lib/rigor/environment/rbs_loader.rb +480 -67
  153. data/lib/rigor/environment/reflection.rb +7 -7
  154. data/lib/rigor/environment/required_features.rb +0 -3
  155. data/lib/rigor/environment/ruby_library_roots.rb +1 -1
  156. data/lib/rigor/environment/vendored_signature_plan.rb +153 -0
  157. data/lib/rigor/environment.rb +41 -10
  158. data/lib/rigor/flow_contribution/merger.rb +4 -4
  159. data/lib/rigor/inference/acceptance.rb +93 -32
  160. data/lib/rigor/inference/block_parameter_binder.rb +2 -3
  161. data/lib/rigor/inference/block_run_count.rb +178 -0
  162. data/lib/rigor/inference/budget_trace.rb +8 -2
  163. data/lib/rigor/inference/builtins/exception_catalog.rb +2 -2
  164. data/lib/rigor/inference/builtins/numeric_catalog.rb +2 -5
  165. data/lib/rigor/inference/builtins/random_catalog.rb +2 -2
  166. data/lib/rigor/inference/callee_mutation_chain.rb +88 -77
  167. data/lib/rigor/inference/callee_write_census.rb +815 -0
  168. data/lib/rigor/inference/construction_escape.rb +217 -0
  169. data/lib/rigor/inference/declaration_walk/traversal.rb +1 -1
  170. data/lib/rigor/inference/deep_erasure.rb +39 -7
  171. data/lib/rigor/inference/def_handle.rb +1 -1
  172. data/lib/rigor/inference/def_node_resolver.rb +1 -1
  173. data/lib/rigor/inference/def_return_typer.rb +46 -4
  174. data/lib/rigor/inference/definer_resolution.rb +4 -2
  175. data/lib/rigor/inference/definers_index.rb +106 -0
  176. data/lib/rigor/inference/direct_ivar_writes.rb +300 -0
  177. data/lib/rigor/inference/element_read_widening.rb +49 -3
  178. data/lib/rigor/inference/expression_typer.rb +116 -49
  179. data/lib/rigor/inference/external_ancestor_resolution.rb +3 -4
  180. data/lib/rigor/inference/global_write_census.rb +76 -2
  181. data/lib/rigor/inference/global_writer_reach.rb +260 -0
  182. data/lib/rigor/inference/hkt_body.rb +1 -2
  183. data/lib/rigor/inference/hkt_body_parser.rb +3 -5
  184. data/lib/rigor/inference/hkt_reducer.rb +2 -2
  185. data/lib/rigor/inference/hkt_registry.rb +3 -5
  186. data/lib/rigor/inference/indexed_narrowing.rb +4 -7
  187. data/lib/rigor/inference/intervening_ivar_reset.rb +26 -4
  188. data/lib/rigor/inference/ivar_writer_census.rb +51 -0
  189. data/lib/rigor/inference/key_presence_guard.rb +2 -1
  190. data/lib/rigor/inference/loop_value_record.rb +8 -4
  191. data/lib/rigor/inference/match_rebinding/frame.rb +46 -0
  192. data/lib/rigor/inference/match_rebinding/yielding_match.rb +45 -0
  193. data/lib/rigor/inference/match_rebinding.rb +11 -1
  194. data/lib/rigor/inference/memo_audit.rb +55 -0
  195. data/lib/rigor/inference/method_dispatcher/alias_strict_nominals.rb +1 -1
  196. data/lib/rigor/inference/method_dispatcher/argument_distribution.rb +40 -0
  197. data/lib/rigor/inference/method_dispatcher/block_folding.rb +1 -1
  198. data/lib/rigor/inference/method_dispatcher/constant_folding.rb +3 -3
  199. data/lib/rigor/inference/method_dispatcher/contested_overloads.rb +89 -0
  200. data/lib/rigor/inference/method_dispatcher/iterator_dispatch.rb +1 -1
  201. data/lib/rigor/inference/method_dispatcher/kernel_dispatch.rb +1 -1
  202. data/lib/rigor/inference/method_dispatcher/no_match_fallback.rb +54 -0
  203. data/lib/rigor/inference/method_dispatcher/overload_selector.rb +39 -27
  204. data/lib/rigor/inference/method_dispatcher/random_folding.rb +1 -1
  205. data/lib/rigor/inference/method_dispatcher/rbs_dispatch.rb +203 -17
  206. data/lib/rigor/inference/method_dispatcher/reduce_folding.rb +1 -1
  207. data/lib/rigor/inference/method_dispatcher/regexp_folding.rb +2 -2
  208. data/lib/rigor/inference/method_dispatcher/self_substitute.rb +2 -2
  209. data/lib/rigor/inference/method_dispatcher/set_folding.rb +9 -7
  210. data/lib/rigor/inference/method_dispatcher/shape_dispatch.rb +83 -20
  211. data/lib/rigor/inference/method_dispatcher/singleton_folding.rb +0 -1
  212. data/lib/rigor/inference/method_dispatcher/singleton_mixin_dispatch.rb +50 -17
  213. data/lib/rigor/inference/method_dispatcher/struct_folding.rb +5 -5
  214. data/lib/rigor/inference/method_dispatcher/struct_materialization.rb +4 -6
  215. data/lib/rigor/inference/method_dispatcher/uri_folding.rb +1 -1
  216. data/lib/rigor/inference/method_dispatcher.rb +64 -14
  217. data/lib/rigor/inference/module_function_state.rb +11 -1
  218. data/lib/rigor/inference/multi_target_binder.rb +1 -1
  219. data/lib/rigor/inference/mutation_rejoin.rb +240 -4
  220. data/lib/rigor/inference/mutation_widening.rb +157 -27
  221. data/lib/rigor/inference/narrowing.rb +47 -33
  222. data/lib/rigor/inference/optimistic_origin.rb +1 -1
  223. data/lib/rigor/inference/out_buffer_mutation.rb +72 -0
  224. data/lib/rigor/inference/parameter_inference_collector.rb +5 -6
  225. data/lib/rigor/inference/pre_eval_constants.rb +1 -1
  226. data/lib/rigor/inference/project_method_ownership.rb +6 -3
  227. data/lib/rigor/inference/project_yielder.rb +11 -17
  228. data/lib/rigor/inference/receiver_alias.rb +20 -0
  229. data/lib/rigor/inference/refined_dispatch/constructed_class.rb +3 -1
  230. data/lib/rigor/inference/refined_dispatch/own_method.rb +2 -1
  231. data/lib/rigor/inference/remover_holder_scan.rb +233 -0
  232. data/lib/rigor/inference/remover_narrowing.rb +224 -0
  233. data/lib/rigor/inference/rewrite_mutation.rb +53 -2
  234. data/lib/rigor/inference/scope_indexer/certainty.rb +0 -1
  235. data/lib/rigor/inference/scope_indexer/constant_ivar_value.rb +93 -0
  236. data/lib/rigor/inference/scope_indexer/declared_param_writes.rb +58 -15
  237. data/lib/rigor/inference/scope_indexer/derived_param_value.rb +112 -0
  238. data/lib/rigor/inference/scope_indexer/empty_seed.rb +99 -0
  239. data/lib/rigor/inference/scope_indexer/ivar_contributions.rb +135 -0
  240. data/lib/rigor/inference/scope_indexer.rb +137 -46
  241. data/lib/rigor/inference/self_call_ivar_writes.rb +84 -12
  242. data/lib/rigor/inference/set_mutation.rb +189 -0
  243. data/lib/rigor/inference/singleton_hook_decline.rb +1 -2
  244. data/lib/rigor/inference/statement_evaluator.rb +2328 -138
  245. data/lib/rigor/inference/struct_fold_safety.rb +4 -6
  246. data/lib/rigor/inference/synthetic_method_scanner.rb +5 -6
  247. data/lib/rigor/inference/toplevel_def_census.rb +59 -0
  248. data/lib/rigor/inference/unknown_store_widening.rb +103 -4
  249. data/lib/rigor/inference/unthreaded_rebinds.rb +2 -3
  250. data/lib/rigor/inference/void_tail_summary.rb +2 -1
  251. data/lib/rigor/language_server/buffer_table.rb +3 -3
  252. data/lib/rigor/language_server/completion_provider.rb +26 -20
  253. data/lib/rigor/language_server/config_warning_announcer.rb +28 -0
  254. data/lib/rigor/language_server/diagnostic_publisher.rb +8 -3
  255. data/lib/rigor/language_server/document_symbol_provider.rb +1 -1
  256. data/lib/rigor/language_server/folding_range_provider.rb +1 -2
  257. data/lib/rigor/language_server/hover_provider.rb +1 -7
  258. data/lib/rigor/language_server/hover_renderer.rb +6 -8
  259. data/lib/rigor/language_server/project_context.rb +39 -1
  260. data/lib/rigor/language_server/publish_batcher.rb +3 -4
  261. data/lib/rigor/language_server/selection_range_provider.rb +2 -4
  262. data/lib/rigor/language_server/server.rb +7 -3
  263. data/lib/rigor/language_server/signature_help_provider.rb +4 -4
  264. data/lib/rigor/lens/plugins.rb +1 -1
  265. data/lib/rigor/mcp/server.rb +73 -2
  266. data/lib/rigor/mcp.rb +1 -1
  267. data/lib/rigor/plugin/access_denied_error.rb +1 -2
  268. data/lib/rigor/plugin/bundled_catalog.rb +1 -12
  269. data/lib/rigor/plugin/io_boundary.rb +1 -1
  270. data/lib/rigor/plugin/loader.rb +25 -8
  271. data/lib/rigor/plugin/macro/heredoc_template.rb +7 -8
  272. data/lib/rigor/plugin/macro/trait_registry.rb +6 -9
  273. data/lib/rigor/plugin/manifest.rb +9 -13
  274. data/lib/rigor/plugin/registry.rb +3 -3
  275. data/lib/rigor/plugin/services.rb +2 -3
  276. data/lib/rigor/plugin/source_rbs_synthesis_reporter.rb +11 -0
  277. data/lib/rigor/plugin/trust_policy.rb +2 -2
  278. data/lib/rigor/plugin/type_node_resolver.rb +2 -4
  279. data/lib/rigor/plugin.rb +6 -8
  280. data/lib/rigor/plugin_gap_advisory.rb +5 -1
  281. data/lib/rigor/project_environment.rb +16 -2
  282. data/lib/rigor/protection/analysis_guard.rb +1 -1
  283. data/lib/rigor/protection/discovery_seed.rb +44 -5
  284. data/lib/rigor/protection/mutator.rb +1 -1
  285. data/lib/rigor/rbs_extended/envelope_scanner.rb +4 -6
  286. data/lib/rigor/rbs_extended/hkt_directives.rb +6 -10
  287. data/lib/rigor/rbs_extended/reporter.rb +27 -3
  288. data/lib/rigor/rbs_extended.rb +2 -3
  289. data/lib/rigor/reflection/constant_ancestors.rb +5 -3
  290. data/lib/rigor/rooted_glob.rb +15 -1
  291. data/lib/rigor/run_scoped_slots.rb +38 -0
  292. data/lib/rigor/runtime/jit.rb +1 -1
  293. data/lib/rigor/scope/discovery_index.rb +19 -6
  294. data/lib/rigor/scope/resolution_chain.rb +3 -1
  295. data/lib/rigor/scope.rb +365 -31
  296. data/lib/rigor/sig_gen/classification.rb +4 -0
  297. data/lib/rigor/sig_gen/effect_annotation.rb +3 -3
  298. data/lib/rigor/sig_gen/generator.rb +371 -58
  299. data/lib/rigor/sig_gen/generic_header_scan.rb +189 -0
  300. data/lib/rigor/sig_gen/inline_declarations.rb +2 -2
  301. data/lib/rigor/sig_gen/layout_index.rb +3 -3
  302. data/lib/rigor/sig_gen/method_candidate.rb +23 -4
  303. data/lib/rigor/sig_gen/observation_collector.rb +3 -5
  304. data/lib/rigor/sig_gen/observed_call.rb +1 -1
  305. data/lib/rigor/sig_gen/path_mapper.rb +6 -0
  306. data/lib/rigor/sig_gen/renderer.rb +15 -9
  307. data/lib/rigor/sig_gen/skip_reason_catalog.rb +15 -2
  308. data/lib/rigor/sig_gen/type_name_qualification.rb +69 -0
  309. data/lib/rigor/sig_gen/write_reporter.rb +63 -14
  310. data/lib/rigor/sig_gen/write_result.rb +23 -12
  311. data/lib/rigor/sig_gen/writer.rb +217 -37
  312. data/lib/rigor/type/acceptance_router.rb +2 -2
  313. data/lib/rigor/type/accepts_result.rb +1 -1
  314. data/lib/rigor/type/app.rb +4 -6
  315. data/lib/rigor/type/combinator.rb +9 -13
  316. data/lib/rigor/type/constant.rb +4 -5
  317. data/lib/rigor/type/hash_shape.rb +21 -40
  318. data/lib/rigor/type/union.rb +1 -1
  319. data/lib/rigor/type_node/identifier.rb +1 -2
  320. data/lib/rigor/type_node/resolver_chain.rb +1 -1
  321. data/lib/rigor/type_node/string_literal.rb +1 -2
  322. data/lib/rigor/type_node/symbol_literal.rb +1 -2
  323. data/lib/rigor/type_node/union.rb +3 -6
  324. data/lib/rigor/value_semantics.rb +1 -10
  325. data/lib/rigor/version.rb +1 -1
  326. data/plugins/rigor-actioncable/lib/rigor/plugin/actioncable/channel_discoverer.rb +2 -2
  327. data/plugins/rigor-actioncable/lib/rigor/plugin/actioncable/channel_index.rb +2 -3
  328. data/plugins/rigor-actioncable/lib/rigor/plugin/actioncable.rb +1 -1
  329. data/plugins/rigor-actionmailer/lib/rigor/plugin/actionmailer/mailer_discoverer.rb +38 -36
  330. data/plugins/rigor-actionmailer/lib/rigor/plugin/actionmailer/mailer_index.rb +4 -2
  331. data/plugins/rigor-actionmailer/lib/rigor/plugin/actionmailer.rb +6 -4
  332. data/plugins/rigor-actionpack/lib/rigor/plugin/actionpack/analyzer.rb +1 -1
  333. data/plugins/rigor-actionpack/lib/rigor/plugin/actionpack/controller_index.rb +1 -5
  334. data/plugins/rigor-actionpack/lib/rigor/plugin/actionpack/controller_scan.rb +0 -1
  335. data/plugins/rigor-actionpack/lib/rigor/plugin/actionpack/erb_compiler.rb +2 -1
  336. data/plugins/rigor-actionpack/lib/rigor/plugin/actionpack/view_assigns.rb +1 -1
  337. data/plugins/rigor-actionpack/lib/rigor/plugin/actionpack.rb +66 -19
  338. data/plugins/rigor-activejob/lib/rigor/plugin/activejob/effects.rb +1 -1
  339. data/plugins/rigor-activejob/lib/rigor/plugin/activejob.rb +4 -3
  340. data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord/association_roots.rb +3 -2
  341. data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord/effects.rb +2 -2
  342. data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord/model_discoverer.rb +21 -31
  343. data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord/model_index.rb +10 -16
  344. data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord/schema_parser.rb +2 -3
  345. data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord/schema_table.rb +0 -1
  346. data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord.rb +5 -4
  347. data/plugins/rigor-activestorage/lib/rigor/plugin/activestorage/attachment_discoverer.rb +3 -4
  348. data/plugins/rigor-activestorage/lib/rigor/plugin/activestorage/attachment_index.rb +1 -1
  349. data/plugins/rigor-activestorage/lib/rigor/plugin/activestorage.rb +7 -12
  350. data/plugins/rigor-activestorage/lib/rigor-activestorage.rb +1 -1
  351. data/plugins/rigor-activesupport-core-ext/lib/rigor/plugin/activesupport_core_ext/effects.rb +1 -1
  352. data/plugins/rigor-activesupport-core-ext/lib/rigor/plugin/activesupport_core_ext/symbolize_keys.rb +102 -0
  353. data/plugins/rigor-activesupport-core-ext/lib/rigor/plugin/activesupport_core_ext.rb +19 -6
  354. data/plugins/rigor-activesupport-core-ext/sig/active_support/core_ext.rbs +26 -13
  355. data/plugins/rigor-alba/lib/rigor/plugin/alba/resource_index.rb +3 -3
  356. data/plugins/rigor-dry-schema/lib/rigor/plugin/dry_schema/schema_scanner.rb +1 -3
  357. data/plugins/rigor-dry-struct/lib/rigor/plugin/dry_struct.rb +6 -6
  358. data/plugins/rigor-dry-types/lib/rigor/plugin/dry_types/alias_scanner.rb +5 -6
  359. data/plugins/rigor-dry-types/lib/rigor/plugin/dry_types.rb +1 -1
  360. data/plugins/rigor-dry-validation/lib/rigor/plugin/dry_validation/params_shape.rb +2 -3
  361. data/plugins/rigor-dry-validation/lib/rigor/plugin/dry_validation.rb +5 -8
  362. data/plugins/rigor-ethon/lib/rigor/plugin/ethon.rb +0 -1
  363. data/plugins/rigor-factorybot/lib/rigor/plugin/factorybot/factory_discoverer.rb +1 -1
  364. data/plugins/rigor-factorybot/lib/rigor/plugin/factorybot.rb +10 -18
  365. data/plugins/rigor-ffi/lib/rigor/plugin/ffi/analyzer.rb +0 -1
  366. data/plugins/rigor-ffi/lib/rigor/plugin/ffi/discoverer.rb +0 -2
  367. data/plugins/rigor-ffi/lib/rigor/plugin/ffi.rb +4 -5
  368. data/plugins/rigor-grape/lib/rigor/plugin/grape.rb +0 -2
  369. data/plugins/rigor-graphql/lib/rigor/plugin/graphql/type_scanner.rb +6 -7
  370. data/plugins/rigor-hanami/lib/rigor/plugin/hanami/action_checker.rb +2 -4
  371. data/plugins/rigor-hanami/lib/rigor/plugin/hanami.rb +2 -5
  372. data/plugins/rigor-mangrove/lib/rigor/plugin/mangrove.rb +2 -2
  373. data/plugins/rigor-minitest/lib/rigor/plugin/minitest/assertion_analyzer.rb +6 -6
  374. data/plugins/rigor-pundit/lib/rigor/plugin/pundit/policy_discoverer.rb +2 -1
  375. data/plugins/rigor-rails-i18n/lib/rigor/plugin/rails_i18n/analyzer.rb +3 -8
  376. data/plugins/rigor-rails-i18n/lib/rigor/plugin/rails_i18n/locale_index.rb +2 -3
  377. data/plugins/rigor-rails-i18n/lib/rigor/plugin/rails_i18n/locale_loader.rb +5 -8
  378. data/plugins/rigor-rails-routes/lib/rigor/plugin/rails_routes/analyzer.rb +1 -3
  379. data/plugins/rigor-rails-routes/lib/rigor/plugin/rails_routes/devise_routes.rb +2 -3
  380. data/plugins/rigor-rails-routes/lib/rigor/plugin/rails_routes/helper_discoverer.rb +1 -1
  381. data/plugins/rigor-rails-routes/lib/rigor/plugin/rails_routes/routes_parser.rb +4 -11
  382. data/plugins/rigor-rails-routes/lib/rigor/plugin/rails_routes.rb +1 -1
  383. data/plugins/rigor-rbs-inline/lib/rigor/plugin/rbs_inline.rb +2 -1
  384. data/plugins/rigor-rspec/lib/rigor/plugin/rspec/matcher_analyzer.rb +1 -1
  385. data/plugins/rigor-rspec/lib/rigor/plugin/rspec.rb +2 -2
  386. data/plugins/rigor-rspec-rails/lib/rigor/plugin/rspec_rails.rb +3 -4
  387. data/plugins/rigor-shoulda-matchers/lib/rigor/plugin/shoulda_matchers/analyzer.rb +6 -8
  388. data/plugins/rigor-sidekiq/lib/rigor/plugin/sidekiq.rb +3 -2
  389. data/plugins/rigor-sorbet/lib/rigor/plugin/sorbet/absurd_recognizer.rb +4 -5
  390. data/plugins/rigor-sorbet/lib/rigor/plugin/sorbet/assertion_recognizer.rb +6 -7
  391. data/plugins/rigor-sorbet/lib/rigor/plugin/sorbet/catalog.rb +1 -1
  392. data/plugins/rigor-sorbet/lib/rigor/plugin/sorbet/sig_parser.rb +2 -2
  393. data/plugins/rigor-sorbet/lib/rigor/plugin/sorbet.rb +17 -21
  394. data/plugins/rigor-statesman/lib/rigor/plugin/statesman.rb +2 -3
  395. data/plugins/rigor-typelizer/lib/rigor/plugin/typelizer/serializer_discoverer.rb +3 -2
  396. data/plugins/rigor-typelizer/lib/rigor/plugin/typelizer/serializer_index.rb +4 -4
  397. data/sig/rigor/analysis/baseline.rbs +0 -3
  398. data/sig/rigor/analysis/fact_store.rbs +1 -0
  399. data/sig/rigor/analysis/reachability/scan_cache.rbs +1 -1
  400. data/sig/rigor/cli/diff_command.rbs +1 -1
  401. data/sig/rigor/cli/explain_command.rbs +1 -1
  402. data/sig/rigor/effects/registry.rbs +2 -2
  403. data/sig/rigor/inference.rbs +4 -1
  404. data/sig/rigor/plugin/additional_initializer.rbs +0 -5
  405. data/sig/rigor/plugin/base.rbs +1 -0
  406. data/sig/rigor/plugin/effect_attribution.rbs +3 -3
  407. data/sig/rigor/plugin/effect_edge.rbs +1 -1
  408. data/sig/rigor/plugin/effect_entry_points.rbs +3 -3
  409. data/sig/rigor/plugin/io_boundary.rbs +2 -1
  410. data/sig/rigor/plugin/manifest.rbs +9 -9
  411. data/sig/rigor/plugin/protocol_contract.rbs +3 -7
  412. data/sig/rigor/scope.rbs +23 -3
  413. data/skills/rigor-plugin-author/SKILL.md +4 -4
  414. data/skills/rigor-plugin-author/references/02-walker-and-types.md +5 -5
  415. data/skills/rigor-plugin-review/SKILL.md +3 -3
  416. data/skills/rigor-project-init/references/03-baseline-and-bugs.md +12 -1
  417. metadata +36 -3
@@ -11,7 +11,7 @@ module Rigor
11
11
  # - `target_path` — `.rbs` file the writer was responsible for (`nil` when the source path falls outside
12
12
  # the project signature tree, in which case `action` is `:skipped_outside_sig_root`).
13
13
  # - `action` — one of `:created` / `:updated` / `:noop` / `:skipped_outside_sig_root` /
14
- # `:skipped_invalid_rbs` / `:skipped_invalid_encoding`.
14
+ # `:skipped_invalid_rbs` / `:skipped_invalid_encoding` / `:skipped_unparseable`.
15
15
  # - `applied` — the {MethodCandidate}s that actually landed on disk.
16
16
  # - `replaced` — the subset of `applied` that replaced an existing declaration rather than adding one
17
17
  # (`--overwrite`'s tighter returns, and inline updates).
@@ -23,16 +23,20 @@ module Rigor
23
23
  # target declaration already carries annotations, so its bytes were left alone
24
24
  # (`sig.effect.left-unreadable`; see {Writer#splice_annotations}). The signature line itself was
25
25
  # still updated.
26
+ # - `repaired_headers` — the existing class or module headers the writer gave a generic class's type
27
+ # parameters (`"Array[unchecked out E]"`), because the file opened it with none (#2580).
26
28
  # - `error` — the refusal cause, when `action` is a refusal: for `:skipped_invalid_rbs` the file the writer
27
29
  # assembled does not parse, so it was NOT written (writing it would poison the project's sig tree — the
28
30
  # consumer quarantines an unparseable `.rbs`, taking every other type in that file down with it); for
29
31
  # `:skipped_invalid_encoding` the EXISTING target file is not valid UTF-8, so the writer refuses to merge
30
- # into content it cannot read faithfully.
32
+ # into content it cannot read faithfully; for `:skipped_unparseable` the EXISTING target file does not parse
33
+ # as RBS, so the writer refuses to merge into it (#2480).
31
34
  class WriteResult
32
- attr_reader :source_path, :target_path, :action, :applied, :skipped, :error, :left_unreadable, :replaced
35
+ attr_reader :source_path, :target_path, :action, :applied, :skipped, :error, :left_unreadable, :replaced,
36
+ :repaired_headers
33
37
 
34
38
  def initialize(source_path:, target_path:, action:, applied: [], skipped: [], error: nil, # rubocop:disable Metrics/ParameterLists
35
- left_unreadable: [], replaced: [])
39
+ left_unreadable: [], replaced: [], repaired_headers: [])
36
40
  @source_path = source_path
37
41
  @target_path = target_path
38
42
  @action = action
@@ -41,6 +45,7 @@ module Rigor
41
45
  @error = error
42
46
  @left_unreadable = left_unreadable.freeze
43
47
  @replaced = replaced.freeze
48
+ @repaired_headers = repaired_headers.freeze
44
49
  freeze
45
50
  end
46
51
 
@@ -51,14 +56,20 @@ module Rigor
51
56
  action: action.to_s,
52
57
  applied: applied.map(&:to_h),
53
58
  skipped: skipped.map { |c, reason| c.to_h.merge(write_skip_reason: reason.to_s) }
54
- }.tap do |h|
55
- h[:error] = error if error
56
- # Absent when nothing was replaced, so a run that only adds lines keeps its pre-#1076 payload.
57
- h[:replaced] = replaced.map(&:to_h) unless replaced.empty?
58
- # Absent rather than empty when nothing was left: an effects-off payload stays byte-identical
59
- # to a pre-#391 one.
60
- h[:effect_left_unreadable] = left_unreadable.map(&:to_h) unless left_unreadable.empty?
61
- end
59
+ }.tap { |h| add_optional_entries(h) }
60
+ end
61
+
62
+ private
63
+
64
+ def add_optional_entries(hash)
65
+ hash[:error] = error if error
66
+ # Absent when nothing was replaced, so a run that only adds lines keeps its pre-#1076 payload.
67
+ hash[:replaced] = replaced.map(&:to_h) unless replaced.empty?
68
+ # Absent when no header was repaired (#2580), for the same reason.
69
+ hash[:repaired_headers] = repaired_headers unless repaired_headers.empty?
70
+ # Absent rather than empty when nothing was left: an effects-off payload stays byte-identical
71
+ # to a pre-#391 one.
72
+ hash[:effect_left_unreadable] = left_unreadable.map(&:to_h) unless left_unreadable.empty?
62
73
  end
63
74
  end
64
75
  end
@@ -4,6 +4,7 @@ require "fileutils"
4
4
  require "rbs"
5
5
 
6
6
  require_relative "classification"
7
+ require_relative "generic_header_scan"
7
8
  require_relative "layout_index"
8
9
  require_relative "superclass_spelling"
9
10
  require_relative "write_result"
@@ -15,7 +16,8 @@ module Rigor
15
16
  #
16
17
  # ADR-14 slice 2: the writer parses the target with `RBS::Parser` to find the matching class declaration and
17
18
  # inserts new method declarations just before the class's closing `end` keyword. Existing declarations are
18
- # NEVER touched unless `--overwrite` is set AND the candidate's classification is `tighter-return`.
19
+ # NEVER touched unless `--overwrite` is set and the candidate is eligible for replacement (see
20
+ # {#eligible_for_replacement?}).
19
21
  #
20
22
  # The slice does NOT re-render the whole file through `RBS::Writer`. That would lose comments / blank-line
21
23
  # formatting per upstream design; the byte-range insertion approach taken here preserves untouched
@@ -32,9 +34,18 @@ module Rigor
32
34
 
33
35
  # Per-`update_existing` accumulator. The merge_class helper mutates `source` / `decls` / `applied` /
34
36
  # `skipped` in place as each class is processed so the next class sees the latest byte positions.
35
- MergeState = Struct.new(:source, :decls, :applied, :skipped, :left_unreadable, :replaced, keyword_init: true)
37
+ MergeState = Struct.new(:source, :decls, :applied, :skipped, :left_unreadable, :replaced, :repaired_headers,
38
+ :target, keyword_init: true)
36
39
  private_constant :MergeState
37
40
 
41
+ # The target routing this writer applies; the generator maps candidates through it to ask {#accepts_target?}.
42
+ attr_reader :path_mapper
43
+
44
+ # #2605 — the generic classes whose bare headers the last {#write_all} (or {#write}) left bare, because a
45
+ # body names one of the parameters: `{ class:, targets:, blocking: [{ target:, names: }] }` each
46
+ # ({GenericHeaderScan#left_bare}).
47
+ attr_reader :left_bare
48
+
38
49
  # @param dry_run — assemble and validate every target exactly as a write would, and report it, without
39
50
  # creating or changing a file. `sig-gen --check` (ADR-112 WD4) is this, so it can only ever disagree
40
51
  # with `--write` about the disk, never about the content.
@@ -47,6 +58,15 @@ module Rigor
47
58
  @global_namespace_kinds = {}
48
59
  # Run-level qualified-class-name => superclass-token view, same lifecycle as `@global_namespace_kinds`.
49
60
  @global_superclasses = {}
61
+ # #2436 — the target being rendered's view of {MethodCandidate#class_type_params}, set per target.
62
+ @type_params = {}
63
+ # #2603/#2605 — the run-wide repair decision {#write_all} reads from the sig root before writing: the
64
+ # classes cleared for repair with their spelling, and the classes held back. {#write} leaves both empty and
65
+ # decides per file.
66
+ @scan_repairs = {}
67
+ @held_back = []
68
+ @left_bare = []
69
+ @repair_targets = []
50
70
  end
51
71
 
52
72
  # Process the full candidate list by resolving each candidate's target sig file via the path mapper (which
@@ -56,31 +76,94 @@ module Rigor
56
76
  # ADR-14 follow-up: this is the consolidated-layout entry point. The legacy `write(source_path,
57
77
  # candidates)` below assumes all candidates share a target and remains for spec convenience.
58
78
  #
79
+ # @param header_spelling — called with a class name, returns the header spelling of a generic class the run
80
+ # does not stamp (#2603: a bare header in a file no candidate routes to); the generator's
81
+ # {Generator#generic_header_spelling}. Without it only the run's stamp counts.
59
82
  # @return one per target sig file.
60
- def write_all(candidates)
83
+ def write_all(candidates, header_spelling: nil)
61
84
  emittable = candidates.select { |c| EMITTABLE.include?(c.classification) }
62
- return [] if emittable.empty?
85
+ scan_generic_headers(candidates, header_spelling)
63
86
 
64
87
  @global_namespace_kinds = build_namespace_kinds(candidates)
65
88
  @global_superclasses = build_superclasses(candidates)
66
- grouped = emittable.group_by do |c|
67
- @path_mapper.target_for(c.path, class_name: c.class_name, method_name: c.method_name, kind: c.rbs_kind)
89
+ grouped = emittable.group_by { |c| target_of(c) }
90
+ results = grouped.map { |target, group| write_target(target, group) }
91
+ results + repair_only_results(candidates, grouped.keys.map { |target| target.expand_path.to_s })
92
+ end
93
+
94
+ def target_of(candidate)
95
+ @path_mapper.target_for(candidate.path, class_name: candidate.class_name,
96
+ method_name: candidate.method_name, kind: candidate.rbs_kind)
97
+ end
98
+ private :target_of
99
+
100
+ # Issues #2603 and #2605 — reads every file under the signature paths for bare openings of generic classes
101
+ # before anything is written ({GenericHeaderScan}). A class is repaired in every file or in none: one bare
102
+ # opening rbs still rejects keeps the class failing, so repairing its other openings would be churn. Only files
103
+ # under the sig root are rewritten; a bare opening anywhere else holds its class back.
104
+ def scan_generic_headers(candidates, header_spelling)
105
+ stamp = merged_type_params(candidates)
106
+ scan = GenericHeaderScan.scan(sig_root: @path_mapper.sig_root_dir, signature_dirs: @path_mapper.signature_dirs,
107
+ spelling_for: ->(name) { stamp[name] || header_spelling&.call(name) })
108
+ @scan_repairs = scan.repairs
109
+ @held_back = scan.left_bare.map { |entry| entry.fetch(:class) }
110
+ @left_bare = scan.left_bare
111
+ @repair_targets = scan.repair_targets
112
+ end
113
+ private :scan_generic_headers
114
+
115
+ # The files with a bare header to repair that no emitted member routes to: each gets the repair and nothing
116
+ # else (#2580), whether a skipped candidate routes to it or none does (#2603). The row names a candidate's
117
+ # source when one routes there. A file the writer would not open (not UTF-8, unparseable) is never listed.
118
+ def repair_only_results(candidates, written)
119
+ sources = candidates.each_with_object({}) do |candidate, acc|
120
+ acc[target_of(candidate).expand_path.to_s] ||= candidate.path if candidate.path
121
+ end
122
+ @repair_targets.filter_map do |path|
123
+ target = Pathname(path)
124
+ next if written.include?(target.expand_path.to_s) || !inside_sig_root?(target)
125
+
126
+ result = update_existing(sources[target.expand_path.to_s], target, [], repair_only: true)
127
+ result if %i[updated skipped_invalid_rbs].include?(result.action)
68
128
  end
69
- grouped.map { |target, group| write_target(target, group) }
70
129
  end
130
+ private :repair_only_results
71
131
 
72
- # @param candidates — only
73
- # emittable classifications (new-method /
74
- # tighter-return) are honoured; the caller is
75
- # responsible for filtering.
132
+ # @param candidates — only emittable classifications are honoured; `write` filters on `EMITTABLE` itself.
76
133
  def write(source_path, candidates)
77
134
  emittable = candidates.select { |c| EMITTABLE.include?(c.classification) }
78
135
  return WriteResult.new(source_path: source_path, target_path: nil, action: :noop) if emittable.empty?
79
136
 
137
+ @scan_repairs = {}
138
+ @held_back = []
139
+ @left_bare = []
80
140
  target = @path_mapper.target_for(source_path, class_name: emittable.first.class_name)
81
141
  write_target(target, emittable, source_path: source_path)
82
142
  end
83
143
 
144
+ # Whether the writer is sure to write `candidate`. A file the writer writes nothing to is left as it is, so a
145
+ # class shell riding on that file's candidates never reaches disk; the generator asks this before it counts
146
+ # a shell as declared (#2454). Only a new method is sure: without `--overwrite` the writer skips a tighter
147
+ # return or an inline overwrite whose method the target already declares. Counting too few only demotes a
148
+ # subclass, which is the output before #2454.
149
+ def self.writes?(candidate)
150
+ candidate.classification == Classification::NEW_METHOD
151
+ end
152
+
153
+ # Whether {#write_target} would write to `target` at all, asked before any candidate is rendered (#2461): a
154
+ # target inside the sig root that is missing (the create path), or that exists, is valid UTF-8 and parses.
155
+ # {#update_existing} refuses an unparseable or mis-encoded file and leaves it untouched, so a class shell on it
156
+ # never reaches disk. A spliced result {RbsValidity} rejects is a writer defect and is not predicted here.
157
+ def accepts_target?(target)
158
+ return false unless inside_sig_root?(target)
159
+ return true unless target.exist?
160
+
161
+ source = target.read
162
+ source.valid_encoding? && !parse_signature(source).nil?
163
+ rescue SystemCallError
164
+ false
165
+ end
166
+
84
167
  # Whether `proposed` has STRICTLY FEWER bare `untyped` tokens than `existing`. A count, not a slot-by-slot
85
168
  # comparison: it reads as "at least one more position is concrete", and does not prove that no concrete
86
169
  # position became `untyped` elsewhere. Word-boundary matching counts `untyped` only as a type token, not
@@ -157,10 +240,11 @@ module Rigor
157
240
  tree = build_namespace_tree(candidates, shells)
158
241
  kinds = merged_namespace_kinds(candidates)
159
242
  supers = merged_superclasses(candidates)
243
+ @type_params = merged_type_params(candidates)
160
244
  render_tree_nodes(tree, kinds, supers, 0)
161
245
  end
162
246
 
163
- # Drains `class_shells` from every candidate; the generator's walker populates the same set on every
247
+ # Unions `class_shells` across every candidate; the generator's walker populates the same set on every
164
248
  # candidate produced from a given file (gap-#3 (e)).
165
249
  def collect_class_shells(candidates)
166
250
  shells = Set.new
@@ -202,6 +286,13 @@ module Rigor
202
286
  merged
203
287
  end
204
288
 
289
+ # #2436 — every candidate carries the run's map, so folding the target's candidates is the whole view.
290
+ def merged_type_params(candidates)
291
+ candidates.each_with_object({}) do |c, acc|
292
+ acc.merge!(c.class_type_params) if c.respond_to?(:class_type_params)
293
+ end
294
+ end
295
+
205
296
  def build_superclasses(candidates)
206
297
  candidates.each_with_object({}) do |candidate, acc|
207
298
  next unless candidate.respond_to?(:class_superclasses)
@@ -257,7 +348,7 @@ module Rigor
257
348
  indent = INDENT * depth
258
349
  qualified = (prefix + [node[:name]]).join("::")
259
350
  keyword = node_keyword(node, kinds, qualified)
260
- header = "#{keyword} #{node[:name]}#{superclass_suffix(keyword, supers, qualified)}"
351
+ header = "#{keyword} #{node[:name]}#{@type_params[qualified]}#{superclass_suffix(keyword, supers, qualified)}"
261
352
  body = render_tree_node_body(node, kinds, supers, depth, prefix)
262
353
  "#{indent}#{header}\n#{body}#{indent}end\n"
263
354
  end
@@ -293,13 +384,12 @@ module Rigor
293
384
  :module
294
385
  end
295
386
 
296
- def update_existing(source_path, target, candidates)
387
+ def update_existing(source_path, target, candidates, repair_only: false)
297
388
  source = target.read
298
389
  # Pre-parser encoding guard, mirroring `Environment::RbsLoader.invalid_encoding?`: handed invalid
299
390
  # UTF-8, `RBS::Parser` raises a bare `ArgumentError` on rbs 4.1 (not the `ParsingError` that
300
391
  # `parse_signature` below rescues) and could hang outright on the older releases the gemspec
301
- # supports. The refusal is loud — unlike an unparseable file (`:noop`, surfaced by `rigor check`'s
302
- # own quarantine warning), a mojibake file would otherwise fail with a stack trace naming neither
392
+ # supports. The refusal is loud — a mojibake file would otherwise fail with a stack trace naming neither
303
393
  # the file nor the fix.
304
394
  unless source.valid_encoding?
305
395
  return WriteResult.new(source_path: source_path, target_path: target,
@@ -307,16 +397,19 @@ module Rigor
307
397
  error: "#{target} is not valid UTF-8")
308
398
  end
309
399
 
310
- decls = parse_signature(source)
311
- return WriteResult.new(source_path: source_path, target_path: target, action: :noop) if decls.nil?
312
-
313
- state = MergeState.new(source: source, decls: decls, applied: [], skipped: [], left_unreadable: [],
314
- replaced: [])
315
- merge_candidates(state, candidates)
400
+ # An existing file that does not parse is refused like a mis-encoded one, not skipped as `:noop` (#2480):
401
+ # `--check` must fail on it, because a `--write` that cannot touch the file is not an up-to-date `sig/`.
402
+ # `rigor check` quarantines such a file whole; this refusal names it at the sig-gen boundary too.
403
+ decls, parse_detail = parse_target_signature(target, source)
404
+ if decls.nil?
405
+ return WriteResult.new(source_path: source_path, target_path: target,
406
+ action: :skipped_unparseable,
407
+ error: "#{target} does not parse as RBS: #{parse_detail}")
408
+ end
316
409
 
317
- action = state.applied.empty? ? :noop : :updated
318
- unless action == :updated
319
- return WriteResult.new(source_path: source_path, target_path: target, action: action,
410
+ state = merged_state(source, decls, candidates, repair_only, target)
411
+ unless state_changed?(state)
412
+ return WriteResult.new(source_path: source_path, target_path: target, action: :noop,
320
413
  applied: state.applied, skipped: state.skipped,
321
414
  left_unreadable: state.left_unreadable)
322
415
  end
@@ -333,8 +426,9 @@ module Rigor
333
426
 
334
427
  target.write(state.source) unless @dry_run
335
428
  WriteResult.new(source_path: source_path, target_path: target,
336
- action: action, applied: state.applied, skipped: state.skipped,
337
- left_unreadable: state.left_unreadable, replaced: state.replaced)
429
+ action: :updated, applied: state.applied, skipped: state.skipped,
430
+ left_unreadable: state.left_unreadable, replaced: state.replaced,
431
+ repaired_headers: state.repaired_headers)
338
432
  end
339
433
 
340
434
  # Applies every class group, then every requested shell, then normalises the layout the three steps
@@ -343,12 +437,89 @@ module Rigor
343
437
  supers = merged_superclasses(candidates)
344
438
  kinds = merged_namespace_kinds(candidates)
345
439
  shells = collect_class_shells(candidates)
440
+ @type_params = merged_type_params(candidates)
346
441
  groups = candidates.group_by(&:class_name)
347
442
  # Ancestors first, so `Foo` is created (or found) before `Foo::Bar` looks for a parent to nest under.
348
443
  # A plain sort suffices: a name always sorts before every name it is a strict prefix of.
349
444
  groups.keys.sort.each { |name| merge_class(state, name, groups.fetch(name), kinds, supers) }
350
445
  merge_class_shells(state, shells, kinds, supers)
351
446
  collapse_nested_declarations(state, groups.keys + shells.to_a)
447
+ # Last, so the members this run merged count towards {#shadowed_after_merge?}.
448
+ repair_generic_headers(state, state.target)
449
+ end
450
+
451
+ # Issue #2580 — a declaration this file already holds for a class the loaded RBS declares generic, opened
452
+ # with no type parameters at all (`class Array`, as sig-gen wrote it before #2436), is a guaranteed
453
+ # `RBS::GenericParameterMismatchError`: rbs requires every declaration of a class to repeat the first one's
454
+ # parameters, and the class, plus every class whose ancestry reaches it (`Object` through `Kernel`), then
455
+ # fails its definition build. Writing more members into that header would keep the file broken, so the
456
+ # header gets the same spelling a new header would ({Generator#stamp_generic_reopenings}: plain
457
+ # parameters only). A header that spells parameters of its own is the project's and is left alone, as is
458
+ # every class neither the run's stamp nor {#write_all}'s scan names.
459
+ #
460
+ # A declaration whose body names a type like one of the parameters (`def old: () -> E` for a project class
461
+ # `E`) is left bare: under `class Array[unchecked out E]` that `E` would bind to the parameter, silently
462
+ # changing what the member says. The body is matched as text, so a comment or a nested declaration that
463
+ # spells the name also keeps the header bare, which is the state the file was already in; a method type that
464
+ # declares the name as its own parameter (`def m: [E] (E) -> E`) does not (#2605).
465
+ #
466
+ # Under {#write_all} the class is repaired in every file under the sig root or in none ({GenericHeaderScan},
467
+ # #2605): a class held back there is not repaired here, and a class the run does not stamp is repaired with the
468
+ # scan's spelling (#2603). The check below runs again after the merge, so a member this run merged counts
469
+ # too; the generator anchors a member's type that a parameter would capture (#2436), so it holds back a class
470
+ # the scan cleared only for a member written without that anchor, and is then reported in {#left_bare}.
471
+ # `repaired_headers` gets one entry per declaration repaired.
472
+ def repair_generic_headers(state, target)
473
+ spellings = @scan_repairs.merge(@type_params).except(*@held_back)
474
+ spellings.keys.sort.each do |name|
475
+ spelling = spellings.fetch(name)
476
+ bare = find_class_decls(state.decls, name).select { |d| d.type_params.empty? && d.location&.[](:name) }
477
+ next if bare.empty? || shadowed_after_merge?(state.source, bare, name, spelling, target)
478
+
479
+ insert_type_params(state, bare, spelling)
480
+ bare.each { state.repaired_headers << "#{name}#{spelling}" }
481
+ end
482
+ end
483
+
484
+ # Whether any of `bare` names one of the parameters `spelling` declares ({GenericHeaderScan.shadowing_names});
485
+ # each such opening is recorded in {#left_bare}.
486
+ def shadowed_after_merge?(source, bare, name, spelling, target)
487
+ params = GenericHeaderScan.parameter_names(spelling)
488
+ return true if params.nil?
489
+
490
+ blocking = bare.filter_map do |decl|
491
+ names = GenericHeaderScan.shadowing_names(source, decl, params)
492
+ { target: target.to_s, reason: "names_parameter", names: names } unless names.empty?
493
+ end
494
+ return false if blocking.empty?
495
+
496
+ @left_bare << { class: name, targets: [target.to_s], blocking: blocking }
497
+ true
498
+ end
499
+
500
+ # Splices `spelling` right after each declaration's name, last one first so earlier offsets stay valid.
501
+ def insert_type_params(state, decls, spelling)
502
+ decls.map { |d| d.location[:name].end_pos }.sort.reverse_each do |pos|
503
+ state.source = state.source[0...pos] + spelling + state.source[pos..]
504
+ end
505
+ state.decls = parse_signature(state.source) || state.decls
506
+ end
507
+
508
+ def merged_state(source, decls, candidates, repair_only, target)
509
+ state = MergeState.new(source: source, decls: decls, applied: [], skipped: [], left_unreadable: [],
510
+ replaced: [], repaired_headers: [], target: target)
511
+ repair_only ? repair_only_candidates(state, candidates) : merge_candidates(state, candidates)
512
+ state
513
+ end
514
+
515
+ def state_changed?(state)
516
+ !(state.applied.empty? && state.repaired_headers.empty?)
517
+ end
518
+
519
+ # {#repair_only_results}' merge step: the run's stamp, and the header repair alone.
520
+ def repair_only_candidates(state, candidates)
521
+ @type_params = merged_type_params(candidates)
522
+ repair_generic_headers(state, state.target)
352
523
  end
353
524
 
354
525
  # ADR-14 gap-#3 (e): for every requested class shell that isn't already declared in the target file,
@@ -401,7 +572,9 @@ module Rigor
401
572
  parent_name = segments[0...-1].join("::")
402
573
  parent = find_class_decl(state.decls, parent_name)
403
574
  decl = parent && find_class_decl(state.decls, qualified)
404
- return if decl.nil? || overlapping?(parent, decl)
575
+ # A rooted declaration (`module A; class ::B::C`) keeps the layout the user wrote: cutting it out of its
576
+ # enclosing module would leave that module empty, so the member merges in place instead.
577
+ return if decl.nil? || overlapping?(parent, decl) || decl.name.to_s.start_with?("::")
405
578
 
406
579
  region = decl_region(state.source, decl)
407
580
  block = region && relocated_block(state.source, decl, parent, segments.last, region)
@@ -560,6 +733,15 @@ module Rigor
560
733
  nil
561
734
  end
562
735
 
736
+ # Parses an EXISTING target, naming it as the buffer so the parse error carries `path:line:col` (a bare
737
+ # String would be named `a.rbs`). Returns `[decls, nil]`, or `[nil, first_line_of_message]` on a parse error.
738
+ def parse_target_signature(target, source)
739
+ _, _, decls = RBS::Parser.parse_signature(RBS::Buffer.new(name: target.to_s, content: source))
740
+ [decls, nil]
741
+ rescue RBS::ParsingError => e
742
+ [nil, e.message.to_s.lines.first.to_s.strip]
743
+ end
744
+
563
745
  def merge_class(state, class_name, methods, kinds, supers)
564
746
  decl = find_class_decl(state.decls, class_name)
565
747
  if decl.nil?
@@ -586,11 +768,10 @@ module Rigor
586
768
  decls.each do |decl|
587
769
  next unless decl.is_a?(RBS::AST::Declarations::Class) || decl.is_a?(RBS::AST::Declarations::Module)
588
770
 
589
- local = decl.name.to_s.sub(/\A::/, "")
590
- full = prefix.empty? ? local : "#{prefix.join('::')}::#{local}"
591
- return decl if full == qualified_name
771
+ path = ::Rigor::Environment::RbsLoader.declared_class_path(decl, prefix)
772
+ return decl if path.join("::") == qualified_name
592
773
 
593
- nested = find_class_decl_in(decl.members, prefix + [local], qualified_name)
774
+ nested = find_class_decl_in(decl.members, path, qualified_name)
594
775
  return nested if nested
595
776
  end
596
777
  nil
@@ -602,10 +783,9 @@ module Rigor
602
783
  decls.each do |decl|
603
784
  next unless decl.is_a?(RBS::AST::Declarations::Class) || decl.is_a?(RBS::AST::Declarations::Module)
604
785
 
605
- local = decl.name.to_s.sub(/\A::/, "")
606
- full = prefix.empty? ? local : "#{prefix.join('::')}::#{local}"
607
- found << decl if full == qualified_name
608
- find_class_decls(decl.members, qualified_name, prefix + [local], found)
786
+ path = ::Rigor::Environment::RbsLoader.declared_class_path(decl, prefix)
787
+ found << decl if path.join("::") == qualified_name
788
+ find_class_decls(decl.members, qualified_name, path, found)
609
789
  end
610
790
  found
611
791
  end
@@ -669,7 +849,7 @@ module Rigor
669
849
  # name, slices out the old declaration's source range and substitutes the new RBS one-liner. Members that
670
850
  # are not `MethodDefinition`s are left alone.
671
851
  #
672
- # Two candidate classifications are eligible for replacement under `--overwrite`:
852
+ # Under `--overwrite`, an inline overwrite is always eligible, and so are two other classifications:
673
853
  #
674
854
  # 1. `TIGHTER_RETURN` — the classifier already proved the new return type is a strict subtype of the
675
855
  # declared one (with lenience guards passed).
@@ -6,8 +6,8 @@ module Rigor
6
6
  #
7
7
  # Every carrier's acceptance check is the same fixed forwarding on `self` —
8
8
  # `Inference::Acceptance.accepts(self, other, mode: mode)` — so it lived as an identical copy in
9
- # fourteen carriers. The one exception is `Type::App`, which forwards on its reduced `bound` type rather
10
- # than `self`; it keeps its own `accepts` and does not include this.
9
+ # every carrier that includes it. The one exception is `Type::App`, which forwards on its reduced `bound`
10
+ # type rather than `self`; it keeps its own `accepts` and does not include this.
11
11
  module AcceptanceRouter
12
12
  def accepts(other, mode: :gradual)
13
13
  Inference::Acceptance.accepts(self, other, mode: mode)
@@ -9,7 +9,7 @@ module Rigor
9
9
  # answer alongside the boundary mode the answer was computed under and an ordered list of textual
10
10
  # reasons describing which rules fired.
11
11
  #
12
- # AcceptsResult is the dual of `SubtypeResult` (Slice 5+). Acceptance answers "is `other` passable to
12
+ # AcceptsResult is the dual of the planned `SubtypeResult`. Acceptance answers "is `other` passable to
13
13
  # `self` at a method-parameter or assignment boundary?", consulting the gradual-typing rules in
14
14
  # docs/type-specification/value-lattice.md when `mode` is `:gradual`, and the strict subset relation
15
15
  # when `mode` is `:strict`. Phase 2c ships full `:gradual` semantics; `:strict` is reserved for later
@@ -17,13 +17,11 @@ module Rigor
17
17
  # (use a plain type alias instead).
18
18
  #
19
19
  # `bound` is a `Rigor::Type` representing the value Rigor MUST erase to when this `App` cannot be
20
- # reduced — registered at `%a{rigor:v1:hkt_register}` time, defaulting to
21
- # `Rigor::Type::Dynamic[Rigor::Type::Top]` per ADR-20 D5 / WD2. It also drives the lattice probes
22
- # (`top` / `bot` / `dynamic`) and the acceptance fallback while no reduction is wired (Slice 1).
20
+ # reduced — registered at `%a{rigor:v1:hkt_register}` time, defaulting to the untyped top per ADR-20 D5 /
21
+ # WD2. It also drives the lattice probes (`top` / `bot` / `dynamic`) and `accepts`.
23
22
  #
24
- # Slice 1 ships the carrier as **opaque**: every operation delegates to `bound` since no reduction
25
- # surface exists yet. Slice 2 introduces the conditional / indexed-access evaluator that reduces `App`
26
- # to its registered body before delegating; the carrier shape stays identical.
23
+ # The carrier is opaque: those operations delegate to `bound`, and {#reduce} is the one path that unfolds
24
+ # the application to its registered body first (ADR-20 Slice 2a).
27
25
  #
28
26
  # Display form per ADR-20 OQ5: bare RBS-style `uri[arg1, arg2]`, not the wrapped `App[uri, [arg1,
29
27
  # arg2]]` faithful form. Two `App` values are structurally equal iff their `uri`, `args`, AND `bound`
@@ -245,8 +245,8 @@ module Rigor
245
245
  end
246
246
 
247
247
  # `literal-string` — a `String` that is statically known to come from a source-code literal (or a
248
- # composition of literals). v0.0.9 tracks this flow through interpolation `"#{...}"`, leaving
249
- # propagation through `+` / `<<` to a later slice. Every `Constant<String>` is implicitly
248
+ # composition of literals). Interpolation `"#{...}"` and `+` / `*` composition keep
249
+ # the flow (see {literal_string_compatible?}). Every `Constant<String>` is implicitly
250
250
  # literal-string-compatible; the carrier exists for cases where the concrete value is unknown but
251
251
  # literal-ness has been established (an RBS::Extended `return: literal-string` annotation, or
252
252
  # interpolation over literal-bearing parts).
@@ -433,8 +433,7 @@ module Rigor
433
433
  # - `Type::Tuple[A, B, C]` → `Constant<0> | Constant<1> | Constant<2>`.
434
434
  # - `Type::Nominal["Hash", [K, V]]` → `K` (untyped if absent).
435
435
  # - `Type::Nominal["Array", [E]]` → `non-negative-int`.
436
- # - `Type::Constant` whose value is a Hash / Array / Range — project through the literal's
437
- # per-element keys.
436
+ # - `Type::Constant` wrapping a Range with an Integer begin — `non-negative-int`.
438
437
  #
439
438
  # Other inputs (`Top`, `Dynamic`, untyped Nominals, `Union`, `Refined`, `Difference`,
440
439
  # `Intersection`) project to `top` so the type function always returns a value — callers may narrow
@@ -456,8 +455,7 @@ module Rigor
456
455
  # - `Type::Tuple[A, B, C]` → `A | B | C`.
457
456
  # - `Type::Nominal["Hash", [K, V]]` → `V` (untyped if absent).
458
457
  # - `Type::Nominal["Array", [E]]` → `E` (untyped if absent).
459
- # - `Type::Constant` whose value is a Hash / Array / Range — union of `Constant<…>` for each
460
- # element.
458
+ # - `Type::Constant` wrapping an integer Range — the `IntegerRange` it spans.
461
459
  def value_of(type)
462
460
  case type
463
461
  when HashShape then hash_shape_values(type)
@@ -528,8 +526,7 @@ module Rigor
528
526
  # Phase A handles `Type::HashShape` (literal-key K). Phase B (slice 5) extends to `Type::Tuple`
529
527
  # (integer-index K) — `pick_of[Tuple[A, B, C], 0 | 2]` evaluates to `Tuple[A, C]`. Non-shape inputs
530
528
  # (`Type::Nominal`, etc.) return `type` unchanged ("lossy degradation"; the
531
- # `dynamic.shape.lossy-projection` diagnostic that flags the boundary lands when caller-side
532
- # diagnostic threading arrives).
529
+ # `dynamic.shape.lossy-projection` diagnostic that flags the boundary).
533
530
  def pick_of(type, keys)
534
531
  case type
535
532
  when HashShape then hash_shape_pick(type, keys)
@@ -597,8 +594,7 @@ module Rigor
597
594
  # site where the projection was authored.
598
595
  #
599
596
  # `HashShape` and `Tuple` carry shape-level information the projections honour; every other carrier
600
- # is lossy. Slice 5b wires diagnostic emission through `RbsExtended` / parser callers; this
601
- # predicate stands alone in slice 5 for unit-test coverage and future composition.
597
+ # is lossy.
602
598
  def shape_projection_lossy?(type)
603
599
  !type.is_a?(HashShape) && !type.is_a?(Tuple)
604
600
  end
@@ -792,9 +788,9 @@ module Rigor
792
788
  top
793
789
  end
794
790
 
795
- # `Type::Constant` only carries scalar literals (Integer / Float / String / Symbol / Range /
796
- # Rational / Complex / true / false / nil); Array and Hash literals become Tuple / HashShape
797
- # carriers earlier in the typer. Range is the only scalar with meaningful key/value projections.
791
+ # `Type::Constant` only carries scalar literals (`Constant::SCALAR_CLASSES`); Array and Hash literals
792
+ # become Tuple / HashShape carriers earlier in the typer. Range is the only scalar with meaningful
793
+ # key/value projections.
798
794
  def constant_keys(value)
799
795
  return non_negative_int if value.is_a?(Range) && value.begin.is_a?(Integer)
800
796
 
@@ -8,9 +8,8 @@ require_relative "plain_lattice"
8
8
  module Rigor
9
9
  module Type
10
10
  # A literal carrier under ADR-3 OQ1 Option C (Hybrid). Wraps a Ruby literal value of one of the
11
- # supported immutable-ish classes. Compound literal shapes (Tuple, HashShape, Record) get dedicated
12
- # classes in later slices; Range is carried only when both static endpoints are known enough for
13
- # tuple slicing.
11
+ # supported immutable-ish classes. Compound literal shapes (Tuple, HashShape) have their own classes;
12
+ # Range is carried only when both static endpoints are known enough for tuple slicing.
14
13
  #
15
14
  # See docs/adr/4-type-inference-engine.md for the tentative answer to the open question and
16
15
  # docs/type-specification/rigor-extensions.md for the refinement neighbourhood this carrier lives in.
@@ -74,12 +73,12 @@ module Rigor
74
73
 
75
74
  # RBS supports `Literal` types for booleans, nil, integer literals (positive and negative), symbol
76
75
  # literals, and string literals. Erasing to these preserves the carrier's precision at the RBS
77
- # boundary — `Constant<64>` round-trips as `64`, not as `Integer` — and
76
+ # boundary — the constant 64 round-trips as `64`, not as `Integer` — and
78
77
  # `RbsTypeTranslator#translate_literal` already maps the parsed RBS Literal back to `Constant`.
79
78
  # Scalar carriers without RBS Literal support (Float, Range, Rational, Complex, Regexp, Pathname)
80
79
  # keep their pre-existing widen-to-class-name behaviour because RBS rejects their literal spellings
81
80
  # as syntax errors. `Range` is generic (`Range[out E]`), and a bare `Range` fails `rbs validate`, so
82
- # it carries the class of its endpoints, or `untyped` when both are nil (#1891). `Set[unchecked out A]` is
81
+ # it carries the class of its endpoints, or `untyped` when both are nil (#1891). `Set` is
83
82
  # generic too, and a set is mutable: the constant's elements are only what it held when it was built, and
84
83
  # `x << "a"` adds a member they do not name. It erases to `Set[untyped]`, as an Array or Hash literal's
85
84
  # element type is widened rather than pinned (#2177).