rigortype 0.4.6 → 0.4.7

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (417) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +2 -2
  3. data/data/gem_overlay/activesupport/core_ext.rbs +16 -13
  4. data/data/vendored_gem_sigs/bundler/bundler.rbs +1 -1
  5. data/data/vendored_gem_sigs/rubygems/rubygems_extras.rbs +4 -3
  6. data/docs/handbook/01-getting-started.md +5 -2
  7. data/docs/handbook/11-sig-gen.md +50 -3
  8. data/docs/handbook/appendix-phpstan.md +4 -4
  9. data/docs/manual/02-cli-reference.md +152 -26
  10. data/docs/manual/03-configuration.md +36 -3
  11. data/docs/manual/06-baseline.md +6 -4
  12. data/docs/manual/09-editor-integration.md +13 -2
  13. data/docs/manual/10-mcp-server.md +38 -2
  14. data/docs/manual/12-caching.md +4 -3
  15. data/docs/manual/plugins/rigor-actionpack.md +19 -11
  16. data/docs/manual/plugins/rigor-activesupport-core-ext.md +20 -1
  17. data/lib/rigor/analysis/baseline.rb +24 -27
  18. data/lib/rigor/analysis/check_rules/always_truthy_condition_collector.rb +5 -6
  19. data/lib/rigor/analysis/check_rules/dead_assignment_collector.rb +2 -2
  20. data/lib/rigor/analysis/check_rules/declaration_sourced_guard.rb +4 -4
  21. data/lib/rigor/analysis/check_rules/duplicate_hash_key_collector.rb +2 -2
  22. data/lib/rigor/analysis/check_rules/inferred_param_guard.rb +85 -13
  23. data/lib/rigor/analysis/check_rules/ivar_write_collector.rb +3 -2
  24. data/lib/rigor/analysis/check_rules/lexical_method_sites.rb +0 -1
  25. data/lib/rigor/analysis/check_rules/main_pass_collector.rb +5 -8
  26. data/lib/rigor/analysis/check_rules/published_constant_guard.rb +1 -1
  27. data/lib/rigor/analysis/check_rules/rebinding_block_callees.rb +3 -3
  28. data/lib/rigor/analysis/check_rules/return_in_ensure_collector.rb +1 -2
  29. data/lib/rigor/analysis/check_rules/rule_ids.rb +1 -1
  30. data/lib/rigor/analysis/check_rules/rule_walk.rb +9 -7
  31. data/lib/rigor/analysis/check_rules/shadowed_rescue_collector.rb +3 -4
  32. data/lib/rigor/analysis/check_rules/special_global_setters.rb +3 -3
  33. data/lib/rigor/analysis/check_rules/unreachable_clause_collector.rb +2 -2
  34. data/lib/rigor/analysis/check_rules/void_value_use_collector.rb +1 -1
  35. data/lib/rigor/analysis/check_rules.rb +124 -44
  36. data/lib/rigor/analysis/crash_signature.rb +2 -2
  37. data/lib/rigor/analysis/dependency_recorder.rb +4 -4
  38. data/lib/rigor/analysis/dependency_source_inference/boundary_cross_reporter.rb +14 -3
  39. data/lib/rigor/analysis/dependency_source_inference/gem_resolver.rb +1 -1
  40. data/lib/rigor/analysis/dependency_source_inference/index.rb +6 -8
  41. data/lib/rigor/analysis/dependency_source_inference/walker.rb +4 -4
  42. data/lib/rigor/analysis/edit_replay.rb +93 -6
  43. data/lib/rigor/analysis/fact_store.rb +3 -3
  44. data/lib/rigor/analysis/incremental_run_slot/write_guard.rb +2 -2
  45. data/lib/rigor/analysis/incremental_run_slot.rb +2 -2
  46. data/lib/rigor/analysis/incremental_session.rb +27 -16
  47. data/lib/rigor/analysis/rule_catalog.rb +2 -2
  48. data/lib/rigor/analysis/run_stats.rb +2 -2
  49. data/lib/rigor/analysis/runner/buffer_pool_dispatcher.rb +2 -2
  50. data/lib/rigor/analysis/runner/declaration_position.rb +1 -1
  51. data/lib/rigor/analysis/runner/diagnostic_aggregator.rb +193 -93
  52. data/lib/rigor/analysis/runner/effect_envelope_pass.rb +1 -1
  53. data/lib/rigor/analysis/runner/pool_coordinator.rb +147 -323
  54. data/lib/rigor/analysis/runner/project_pre_passes.rb +3 -3
  55. data/lib/rigor/analysis/runner/run_level_facts.rb +304 -0
  56. data/lib/rigor/analysis/runner/run_snapshots.rb +44 -54
  57. data/lib/rigor/analysis/runner/worker_payload.rb +36 -0
  58. data/lib/rigor/analysis/runner.rb +77 -71
  59. data/lib/rigor/analysis/self_call_resolution_recorder.rb +1 -1
  60. data/lib/rigor/analysis/template_unit_positions.rb +2 -3
  61. data/lib/rigor/analysis/template_units.rb +9 -1
  62. data/lib/rigor/analysis/worker_session.rb +24 -31
  63. data/lib/rigor/bleeding_edge.rb +3 -6
  64. data/lib/rigor/builtins/imported_refinements.rb +6 -8
  65. data/lib/rigor/builtins/predefined_constant_refinements.rb +1 -1
  66. data/lib/rigor/builtins/regex_refinement.rb +4 -6
  67. data/lib/rigor/cache/descriptor.rb +6 -5
  68. data/lib/rigor/cache/incremental_snapshot.rb +31 -12
  69. data/lib/rigor/cache/rbs_cache_producer.rb +2 -2
  70. data/lib/rigor/cache/rbs_class_ancestor_table.rb +1 -1
  71. data/lib/rigor/cache/rbs_class_type_param_names.rb +2 -2
  72. data/lib/rigor/cache/rbs_constant_table.rb +1 -1
  73. data/lib/rigor/cache/rbs_descriptor.rb +54 -3
  74. data/lib/rigor/cache/rbs_environment.rb +4 -1
  75. data/lib/rigor/cache/rbs_environment_marshal_patch.rb +2 -2
  76. data/lib/rigor/cache/rbs_known_class_names.rb +1 -1
  77. data/lib/rigor/cache/store.rb +11 -10
  78. data/lib/rigor/ci_detector.rb +1 -1
  79. data/lib/rigor/cli/annotate_command.rb +24 -19
  80. data/lib/rigor/cli/baseline_command.rb +145 -55
  81. data/lib/rigor/cli/catalogue.rb +256 -0
  82. data/lib/rigor/cli/check_command.rb +108 -102
  83. data/lib/rigor/cli/check_runner_factory.rb +2 -2
  84. data/lib/rigor/cli/command.rb +15 -1
  85. data/lib/rigor/cli/coverage_command.rb +21 -8
  86. data/lib/rigor/cli/coverage_mutation.rb +5 -7
  87. data/lib/rigor/cli/coverage_renderer.rb +2 -1
  88. data/lib/rigor/cli/coverage_scan.rb +3 -5
  89. data/lib/rigor/cli/diff_command.rb +25 -20
  90. data/lib/rigor/cli/docs_command.rb +1 -2
  91. data/lib/rigor/cli/doctor_command.rb +12 -12
  92. data/lib/rigor/cli/effects_command.rb +50 -40
  93. data/lib/rigor/cli/effects_diff_renderer.rb +18 -17
  94. data/lib/rigor/cli/effects_explain_renderer.rb +13 -12
  95. data/lib/rigor/cli/effects_renderer.rb +2 -1
  96. data/lib/rigor/cli/effects_snapshot_command.rb +18 -17
  97. data/lib/rigor/cli/explain_command.rb +18 -8
  98. data/lib/rigor/cli/fused_protection_renderer.rb +2 -1
  99. data/lib/rigor/cli/json_document.rb +41 -0
  100. data/lib/rigor/cli/lens_command.rb +12 -8
  101. data/lib/rigor/cli/lsp_command.rb +250 -23
  102. data/lib/rigor/cli/mcp_command.rb +12 -7
  103. data/lib/rigor/cli/mutation_fork_scan.rb +1 -1
  104. data/lib/rigor/cli/mutation_protection_renderer.rb +3 -2
  105. data/lib/rigor/cli/options.rb +40 -1
  106. data/lib/rigor/cli/plugin_command.rb +93 -46
  107. data/lib/rigor/cli/plugins_command.rb +17 -18
  108. data/lib/rigor/cli/plugins_renderer.rb +10 -6
  109. data/lib/rigor/cli/protection_fork_scan.rb +2 -2
  110. data/lib/rigor/cli/protection_renderer.rb +2 -1
  111. data/lib/rigor/cli/show_bleedingedge_command.rb +21 -15
  112. data/lib/rigor/cli/sig_gen_command.rb +84 -27
  113. data/lib/rigor/cli/skill_command.rb +32 -3
  114. data/lib/rigor/cli/skill_deep_probe.rb +1 -1
  115. data/lib/rigor/cli/skill_describe.rb +1 -1
  116. data/lib/rigor/cli/trace_command.rb +14 -9
  117. data/lib/rigor/cli/triage_command.rb +10 -11
  118. data/lib/rigor/cli/triage_renderer.rb +2 -1
  119. data/lib/rigor/cli/type_of_command.rb +16 -11
  120. data/lib/rigor/cli/type_of_project_scope.rb +1 -4
  121. data/lib/rigor/cli/type_of_renderer.rb +4 -1
  122. data/lib/rigor/cli/type_scan_command.rb +14 -9
  123. data/lib/rigor/cli/type_scan_renderer.rb +2 -1
  124. data/lib/rigor/cli/unused_command.rb +27 -17
  125. data/lib/rigor/cli/upgrade_command.rb +5 -0
  126. data/lib/rigor/cli.rb +209 -54
  127. data/lib/rigor/config_audit.rb +1 -1
  128. data/lib/rigor/configuration/severity_profile.rb +5 -5
  129. data/lib/rigor/configuration.rb +72 -30
  130. data/lib/rigor/effects/definition_context.rb +0 -2
  131. data/lib/rigor/effects/envelope.rb +1 -1
  132. data/lib/rigor/effects/framework_units.rb +1 -1
  133. data/lib/rigor/effects/mutation_classifier.rb +4 -7
  134. data/lib/rigor/effects/propagator.rb +1 -1
  135. data/lib/rigor/effects/registry.rb +2 -2
  136. data/lib/rigor/effects/snapshot.rb +1 -1
  137. data/lib/rigor/effects/taint_cause.rb +0 -2
  138. data/lib/rigor/effects/unit_scan.rb +9 -13
  139. data/lib/rigor/environment/bundle_sig_discovery.rb +5 -9
  140. data/lib/rigor/environment/class_registry.rb +3 -3
  141. data/lib/rigor/environment/core_root.rb +77 -0
  142. data/lib/rigor/environment/default_libraries.rb +1 -1
  143. data/lib/rigor/environment/deferred_signature_plan.rb +283 -0
  144. data/lib/rigor/environment/gated_signature_guard.rb +1 -1
  145. data/lib/rigor/environment/gated_signature_plan.rb +5 -2
  146. data/lib/rigor/environment/lockfile_resolver.rb +28 -12
  147. data/lib/rigor/environment/member_consistency/comparator.rb +18 -1
  148. data/lib/rigor/environment/member_consistency.rb +56 -7
  149. data/lib/rigor/environment/missing_gem_constant_index.rb +3 -2
  150. data/lib/rigor/environment/project_path_gems.rb +124 -0
  151. data/lib/rigor/environment/rbs_hierarchy.rb +6 -15
  152. data/lib/rigor/environment/rbs_loader.rb +480 -67
  153. data/lib/rigor/environment/reflection.rb +7 -7
  154. data/lib/rigor/environment/required_features.rb +0 -3
  155. data/lib/rigor/environment/ruby_library_roots.rb +1 -1
  156. data/lib/rigor/environment/vendored_signature_plan.rb +153 -0
  157. data/lib/rigor/environment.rb +41 -10
  158. data/lib/rigor/flow_contribution/merger.rb +4 -4
  159. data/lib/rigor/inference/acceptance.rb +93 -32
  160. data/lib/rigor/inference/block_parameter_binder.rb +2 -3
  161. data/lib/rigor/inference/block_run_count.rb +178 -0
  162. data/lib/rigor/inference/budget_trace.rb +8 -2
  163. data/lib/rigor/inference/builtins/exception_catalog.rb +2 -2
  164. data/lib/rigor/inference/builtins/numeric_catalog.rb +2 -5
  165. data/lib/rigor/inference/builtins/random_catalog.rb +2 -2
  166. data/lib/rigor/inference/callee_mutation_chain.rb +88 -77
  167. data/lib/rigor/inference/callee_write_census.rb +815 -0
  168. data/lib/rigor/inference/construction_escape.rb +217 -0
  169. data/lib/rigor/inference/declaration_walk/traversal.rb +1 -1
  170. data/lib/rigor/inference/deep_erasure.rb +39 -7
  171. data/lib/rigor/inference/def_handle.rb +1 -1
  172. data/lib/rigor/inference/def_node_resolver.rb +1 -1
  173. data/lib/rigor/inference/def_return_typer.rb +46 -4
  174. data/lib/rigor/inference/definer_resolution.rb +4 -2
  175. data/lib/rigor/inference/definers_index.rb +106 -0
  176. data/lib/rigor/inference/direct_ivar_writes.rb +300 -0
  177. data/lib/rigor/inference/element_read_widening.rb +49 -3
  178. data/lib/rigor/inference/expression_typer.rb +116 -49
  179. data/lib/rigor/inference/external_ancestor_resolution.rb +3 -4
  180. data/lib/rigor/inference/global_write_census.rb +76 -2
  181. data/lib/rigor/inference/global_writer_reach.rb +260 -0
  182. data/lib/rigor/inference/hkt_body.rb +1 -2
  183. data/lib/rigor/inference/hkt_body_parser.rb +3 -5
  184. data/lib/rigor/inference/hkt_reducer.rb +2 -2
  185. data/lib/rigor/inference/hkt_registry.rb +3 -5
  186. data/lib/rigor/inference/indexed_narrowing.rb +4 -7
  187. data/lib/rigor/inference/intervening_ivar_reset.rb +26 -4
  188. data/lib/rigor/inference/ivar_writer_census.rb +51 -0
  189. data/lib/rigor/inference/key_presence_guard.rb +2 -1
  190. data/lib/rigor/inference/loop_value_record.rb +8 -4
  191. data/lib/rigor/inference/match_rebinding/frame.rb +46 -0
  192. data/lib/rigor/inference/match_rebinding/yielding_match.rb +45 -0
  193. data/lib/rigor/inference/match_rebinding.rb +11 -1
  194. data/lib/rigor/inference/memo_audit.rb +55 -0
  195. data/lib/rigor/inference/method_dispatcher/alias_strict_nominals.rb +1 -1
  196. data/lib/rigor/inference/method_dispatcher/argument_distribution.rb +40 -0
  197. data/lib/rigor/inference/method_dispatcher/block_folding.rb +1 -1
  198. data/lib/rigor/inference/method_dispatcher/constant_folding.rb +3 -3
  199. data/lib/rigor/inference/method_dispatcher/contested_overloads.rb +89 -0
  200. data/lib/rigor/inference/method_dispatcher/iterator_dispatch.rb +1 -1
  201. data/lib/rigor/inference/method_dispatcher/kernel_dispatch.rb +1 -1
  202. data/lib/rigor/inference/method_dispatcher/no_match_fallback.rb +54 -0
  203. data/lib/rigor/inference/method_dispatcher/overload_selector.rb +39 -27
  204. data/lib/rigor/inference/method_dispatcher/random_folding.rb +1 -1
  205. data/lib/rigor/inference/method_dispatcher/rbs_dispatch.rb +203 -17
  206. data/lib/rigor/inference/method_dispatcher/reduce_folding.rb +1 -1
  207. data/lib/rigor/inference/method_dispatcher/regexp_folding.rb +2 -2
  208. data/lib/rigor/inference/method_dispatcher/self_substitute.rb +2 -2
  209. data/lib/rigor/inference/method_dispatcher/set_folding.rb +9 -7
  210. data/lib/rigor/inference/method_dispatcher/shape_dispatch.rb +83 -20
  211. data/lib/rigor/inference/method_dispatcher/singleton_folding.rb +0 -1
  212. data/lib/rigor/inference/method_dispatcher/singleton_mixin_dispatch.rb +50 -17
  213. data/lib/rigor/inference/method_dispatcher/struct_folding.rb +5 -5
  214. data/lib/rigor/inference/method_dispatcher/struct_materialization.rb +4 -6
  215. data/lib/rigor/inference/method_dispatcher/uri_folding.rb +1 -1
  216. data/lib/rigor/inference/method_dispatcher.rb +64 -14
  217. data/lib/rigor/inference/module_function_state.rb +11 -1
  218. data/lib/rigor/inference/multi_target_binder.rb +1 -1
  219. data/lib/rigor/inference/mutation_rejoin.rb +240 -4
  220. data/lib/rigor/inference/mutation_widening.rb +157 -27
  221. data/lib/rigor/inference/narrowing.rb +47 -33
  222. data/lib/rigor/inference/optimistic_origin.rb +1 -1
  223. data/lib/rigor/inference/out_buffer_mutation.rb +72 -0
  224. data/lib/rigor/inference/parameter_inference_collector.rb +5 -6
  225. data/lib/rigor/inference/pre_eval_constants.rb +1 -1
  226. data/lib/rigor/inference/project_method_ownership.rb +6 -3
  227. data/lib/rigor/inference/project_yielder.rb +11 -17
  228. data/lib/rigor/inference/receiver_alias.rb +20 -0
  229. data/lib/rigor/inference/refined_dispatch/constructed_class.rb +3 -1
  230. data/lib/rigor/inference/refined_dispatch/own_method.rb +2 -1
  231. data/lib/rigor/inference/remover_holder_scan.rb +233 -0
  232. data/lib/rigor/inference/remover_narrowing.rb +224 -0
  233. data/lib/rigor/inference/rewrite_mutation.rb +53 -2
  234. data/lib/rigor/inference/scope_indexer/certainty.rb +0 -1
  235. data/lib/rigor/inference/scope_indexer/constant_ivar_value.rb +93 -0
  236. data/lib/rigor/inference/scope_indexer/declared_param_writes.rb +58 -15
  237. data/lib/rigor/inference/scope_indexer/derived_param_value.rb +112 -0
  238. data/lib/rigor/inference/scope_indexer/empty_seed.rb +99 -0
  239. data/lib/rigor/inference/scope_indexer/ivar_contributions.rb +135 -0
  240. data/lib/rigor/inference/scope_indexer.rb +137 -46
  241. data/lib/rigor/inference/self_call_ivar_writes.rb +84 -12
  242. data/lib/rigor/inference/set_mutation.rb +189 -0
  243. data/lib/rigor/inference/singleton_hook_decline.rb +1 -2
  244. data/lib/rigor/inference/statement_evaluator.rb +2328 -138
  245. data/lib/rigor/inference/struct_fold_safety.rb +4 -6
  246. data/lib/rigor/inference/synthetic_method_scanner.rb +5 -6
  247. data/lib/rigor/inference/toplevel_def_census.rb +59 -0
  248. data/lib/rigor/inference/unknown_store_widening.rb +103 -4
  249. data/lib/rigor/inference/unthreaded_rebinds.rb +2 -3
  250. data/lib/rigor/inference/void_tail_summary.rb +2 -1
  251. data/lib/rigor/language_server/buffer_table.rb +3 -3
  252. data/lib/rigor/language_server/completion_provider.rb +26 -20
  253. data/lib/rigor/language_server/config_warning_announcer.rb +28 -0
  254. data/lib/rigor/language_server/diagnostic_publisher.rb +8 -3
  255. data/lib/rigor/language_server/document_symbol_provider.rb +1 -1
  256. data/lib/rigor/language_server/folding_range_provider.rb +1 -2
  257. data/lib/rigor/language_server/hover_provider.rb +1 -7
  258. data/lib/rigor/language_server/hover_renderer.rb +6 -8
  259. data/lib/rigor/language_server/project_context.rb +39 -1
  260. data/lib/rigor/language_server/publish_batcher.rb +3 -4
  261. data/lib/rigor/language_server/selection_range_provider.rb +2 -4
  262. data/lib/rigor/language_server/server.rb +7 -3
  263. data/lib/rigor/language_server/signature_help_provider.rb +4 -4
  264. data/lib/rigor/lens/plugins.rb +1 -1
  265. data/lib/rigor/mcp/server.rb +73 -2
  266. data/lib/rigor/mcp.rb +1 -1
  267. data/lib/rigor/plugin/access_denied_error.rb +1 -2
  268. data/lib/rigor/plugin/bundled_catalog.rb +1 -12
  269. data/lib/rigor/plugin/io_boundary.rb +1 -1
  270. data/lib/rigor/plugin/loader.rb +25 -8
  271. data/lib/rigor/plugin/macro/heredoc_template.rb +7 -8
  272. data/lib/rigor/plugin/macro/trait_registry.rb +6 -9
  273. data/lib/rigor/plugin/manifest.rb +9 -13
  274. data/lib/rigor/plugin/registry.rb +3 -3
  275. data/lib/rigor/plugin/services.rb +2 -3
  276. data/lib/rigor/plugin/source_rbs_synthesis_reporter.rb +11 -0
  277. data/lib/rigor/plugin/trust_policy.rb +2 -2
  278. data/lib/rigor/plugin/type_node_resolver.rb +2 -4
  279. data/lib/rigor/plugin.rb +6 -8
  280. data/lib/rigor/plugin_gap_advisory.rb +5 -1
  281. data/lib/rigor/project_environment.rb +16 -2
  282. data/lib/rigor/protection/analysis_guard.rb +1 -1
  283. data/lib/rigor/protection/discovery_seed.rb +44 -5
  284. data/lib/rigor/protection/mutator.rb +1 -1
  285. data/lib/rigor/rbs_extended/envelope_scanner.rb +4 -6
  286. data/lib/rigor/rbs_extended/hkt_directives.rb +6 -10
  287. data/lib/rigor/rbs_extended/reporter.rb +27 -3
  288. data/lib/rigor/rbs_extended.rb +2 -3
  289. data/lib/rigor/reflection/constant_ancestors.rb +5 -3
  290. data/lib/rigor/rooted_glob.rb +15 -1
  291. data/lib/rigor/run_scoped_slots.rb +38 -0
  292. data/lib/rigor/runtime/jit.rb +1 -1
  293. data/lib/rigor/scope/discovery_index.rb +19 -6
  294. data/lib/rigor/scope/resolution_chain.rb +3 -1
  295. data/lib/rigor/scope.rb +365 -31
  296. data/lib/rigor/sig_gen/classification.rb +4 -0
  297. data/lib/rigor/sig_gen/effect_annotation.rb +3 -3
  298. data/lib/rigor/sig_gen/generator.rb +371 -58
  299. data/lib/rigor/sig_gen/generic_header_scan.rb +189 -0
  300. data/lib/rigor/sig_gen/inline_declarations.rb +2 -2
  301. data/lib/rigor/sig_gen/layout_index.rb +3 -3
  302. data/lib/rigor/sig_gen/method_candidate.rb +23 -4
  303. data/lib/rigor/sig_gen/observation_collector.rb +3 -5
  304. data/lib/rigor/sig_gen/observed_call.rb +1 -1
  305. data/lib/rigor/sig_gen/path_mapper.rb +6 -0
  306. data/lib/rigor/sig_gen/renderer.rb +15 -9
  307. data/lib/rigor/sig_gen/skip_reason_catalog.rb +15 -2
  308. data/lib/rigor/sig_gen/type_name_qualification.rb +69 -0
  309. data/lib/rigor/sig_gen/write_reporter.rb +63 -14
  310. data/lib/rigor/sig_gen/write_result.rb +23 -12
  311. data/lib/rigor/sig_gen/writer.rb +217 -37
  312. data/lib/rigor/type/acceptance_router.rb +2 -2
  313. data/lib/rigor/type/accepts_result.rb +1 -1
  314. data/lib/rigor/type/app.rb +4 -6
  315. data/lib/rigor/type/combinator.rb +9 -13
  316. data/lib/rigor/type/constant.rb +4 -5
  317. data/lib/rigor/type/hash_shape.rb +21 -40
  318. data/lib/rigor/type/union.rb +1 -1
  319. data/lib/rigor/type_node/identifier.rb +1 -2
  320. data/lib/rigor/type_node/resolver_chain.rb +1 -1
  321. data/lib/rigor/type_node/string_literal.rb +1 -2
  322. data/lib/rigor/type_node/symbol_literal.rb +1 -2
  323. data/lib/rigor/type_node/union.rb +3 -6
  324. data/lib/rigor/value_semantics.rb +1 -10
  325. data/lib/rigor/version.rb +1 -1
  326. data/plugins/rigor-actioncable/lib/rigor/plugin/actioncable/channel_discoverer.rb +2 -2
  327. data/plugins/rigor-actioncable/lib/rigor/plugin/actioncable/channel_index.rb +2 -3
  328. data/plugins/rigor-actioncable/lib/rigor/plugin/actioncable.rb +1 -1
  329. data/plugins/rigor-actionmailer/lib/rigor/plugin/actionmailer/mailer_discoverer.rb +38 -36
  330. data/plugins/rigor-actionmailer/lib/rigor/plugin/actionmailer/mailer_index.rb +4 -2
  331. data/plugins/rigor-actionmailer/lib/rigor/plugin/actionmailer.rb +6 -4
  332. data/plugins/rigor-actionpack/lib/rigor/plugin/actionpack/analyzer.rb +1 -1
  333. data/plugins/rigor-actionpack/lib/rigor/plugin/actionpack/controller_index.rb +1 -5
  334. data/plugins/rigor-actionpack/lib/rigor/plugin/actionpack/controller_scan.rb +0 -1
  335. data/plugins/rigor-actionpack/lib/rigor/plugin/actionpack/erb_compiler.rb +2 -1
  336. data/plugins/rigor-actionpack/lib/rigor/plugin/actionpack/view_assigns.rb +1 -1
  337. data/plugins/rigor-actionpack/lib/rigor/plugin/actionpack.rb +66 -19
  338. data/plugins/rigor-activejob/lib/rigor/plugin/activejob/effects.rb +1 -1
  339. data/plugins/rigor-activejob/lib/rigor/plugin/activejob.rb +4 -3
  340. data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord/association_roots.rb +3 -2
  341. data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord/effects.rb +2 -2
  342. data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord/model_discoverer.rb +21 -31
  343. data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord/model_index.rb +10 -16
  344. data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord/schema_parser.rb +2 -3
  345. data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord/schema_table.rb +0 -1
  346. data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord.rb +5 -4
  347. data/plugins/rigor-activestorage/lib/rigor/plugin/activestorage/attachment_discoverer.rb +3 -4
  348. data/plugins/rigor-activestorage/lib/rigor/plugin/activestorage/attachment_index.rb +1 -1
  349. data/plugins/rigor-activestorage/lib/rigor/plugin/activestorage.rb +7 -12
  350. data/plugins/rigor-activestorage/lib/rigor-activestorage.rb +1 -1
  351. data/plugins/rigor-activesupport-core-ext/lib/rigor/plugin/activesupport_core_ext/effects.rb +1 -1
  352. data/plugins/rigor-activesupport-core-ext/lib/rigor/plugin/activesupport_core_ext/symbolize_keys.rb +102 -0
  353. data/plugins/rigor-activesupport-core-ext/lib/rigor/plugin/activesupport_core_ext.rb +19 -6
  354. data/plugins/rigor-activesupport-core-ext/sig/active_support/core_ext.rbs +26 -13
  355. data/plugins/rigor-alba/lib/rigor/plugin/alba/resource_index.rb +3 -3
  356. data/plugins/rigor-dry-schema/lib/rigor/plugin/dry_schema/schema_scanner.rb +1 -3
  357. data/plugins/rigor-dry-struct/lib/rigor/plugin/dry_struct.rb +6 -6
  358. data/plugins/rigor-dry-types/lib/rigor/plugin/dry_types/alias_scanner.rb +5 -6
  359. data/plugins/rigor-dry-types/lib/rigor/plugin/dry_types.rb +1 -1
  360. data/plugins/rigor-dry-validation/lib/rigor/plugin/dry_validation/params_shape.rb +2 -3
  361. data/plugins/rigor-dry-validation/lib/rigor/plugin/dry_validation.rb +5 -8
  362. data/plugins/rigor-ethon/lib/rigor/plugin/ethon.rb +0 -1
  363. data/plugins/rigor-factorybot/lib/rigor/plugin/factorybot/factory_discoverer.rb +1 -1
  364. data/plugins/rigor-factorybot/lib/rigor/plugin/factorybot.rb +10 -18
  365. data/plugins/rigor-ffi/lib/rigor/plugin/ffi/analyzer.rb +0 -1
  366. data/plugins/rigor-ffi/lib/rigor/plugin/ffi/discoverer.rb +0 -2
  367. data/plugins/rigor-ffi/lib/rigor/plugin/ffi.rb +4 -5
  368. data/plugins/rigor-grape/lib/rigor/plugin/grape.rb +0 -2
  369. data/plugins/rigor-graphql/lib/rigor/plugin/graphql/type_scanner.rb +6 -7
  370. data/plugins/rigor-hanami/lib/rigor/plugin/hanami/action_checker.rb +2 -4
  371. data/plugins/rigor-hanami/lib/rigor/plugin/hanami.rb +2 -5
  372. data/plugins/rigor-mangrove/lib/rigor/plugin/mangrove.rb +2 -2
  373. data/plugins/rigor-minitest/lib/rigor/plugin/minitest/assertion_analyzer.rb +6 -6
  374. data/plugins/rigor-pundit/lib/rigor/plugin/pundit/policy_discoverer.rb +2 -1
  375. data/plugins/rigor-rails-i18n/lib/rigor/plugin/rails_i18n/analyzer.rb +3 -8
  376. data/plugins/rigor-rails-i18n/lib/rigor/plugin/rails_i18n/locale_index.rb +2 -3
  377. data/plugins/rigor-rails-i18n/lib/rigor/plugin/rails_i18n/locale_loader.rb +5 -8
  378. data/plugins/rigor-rails-routes/lib/rigor/plugin/rails_routes/analyzer.rb +1 -3
  379. data/plugins/rigor-rails-routes/lib/rigor/plugin/rails_routes/devise_routes.rb +2 -3
  380. data/plugins/rigor-rails-routes/lib/rigor/plugin/rails_routes/helper_discoverer.rb +1 -1
  381. data/plugins/rigor-rails-routes/lib/rigor/plugin/rails_routes/routes_parser.rb +4 -11
  382. data/plugins/rigor-rails-routes/lib/rigor/plugin/rails_routes.rb +1 -1
  383. data/plugins/rigor-rbs-inline/lib/rigor/plugin/rbs_inline.rb +2 -1
  384. data/plugins/rigor-rspec/lib/rigor/plugin/rspec/matcher_analyzer.rb +1 -1
  385. data/plugins/rigor-rspec/lib/rigor/plugin/rspec.rb +2 -2
  386. data/plugins/rigor-rspec-rails/lib/rigor/plugin/rspec_rails.rb +3 -4
  387. data/plugins/rigor-shoulda-matchers/lib/rigor/plugin/shoulda_matchers/analyzer.rb +6 -8
  388. data/plugins/rigor-sidekiq/lib/rigor/plugin/sidekiq.rb +3 -2
  389. data/plugins/rigor-sorbet/lib/rigor/plugin/sorbet/absurd_recognizer.rb +4 -5
  390. data/plugins/rigor-sorbet/lib/rigor/plugin/sorbet/assertion_recognizer.rb +6 -7
  391. data/plugins/rigor-sorbet/lib/rigor/plugin/sorbet/catalog.rb +1 -1
  392. data/plugins/rigor-sorbet/lib/rigor/plugin/sorbet/sig_parser.rb +2 -2
  393. data/plugins/rigor-sorbet/lib/rigor/plugin/sorbet.rb +17 -21
  394. data/plugins/rigor-statesman/lib/rigor/plugin/statesman.rb +2 -3
  395. data/plugins/rigor-typelizer/lib/rigor/plugin/typelizer/serializer_discoverer.rb +3 -2
  396. data/plugins/rigor-typelizer/lib/rigor/plugin/typelizer/serializer_index.rb +4 -4
  397. data/sig/rigor/analysis/baseline.rbs +0 -3
  398. data/sig/rigor/analysis/fact_store.rbs +1 -0
  399. data/sig/rigor/analysis/reachability/scan_cache.rbs +1 -1
  400. data/sig/rigor/cli/diff_command.rbs +1 -1
  401. data/sig/rigor/cli/explain_command.rbs +1 -1
  402. data/sig/rigor/effects/registry.rbs +2 -2
  403. data/sig/rigor/inference.rbs +4 -1
  404. data/sig/rigor/plugin/additional_initializer.rbs +0 -5
  405. data/sig/rigor/plugin/base.rbs +1 -0
  406. data/sig/rigor/plugin/effect_attribution.rbs +3 -3
  407. data/sig/rigor/plugin/effect_edge.rbs +1 -1
  408. data/sig/rigor/plugin/effect_entry_points.rbs +3 -3
  409. data/sig/rigor/plugin/io_boundary.rbs +2 -1
  410. data/sig/rigor/plugin/manifest.rbs +9 -9
  411. data/sig/rigor/plugin/protocol_contract.rbs +3 -7
  412. data/sig/rigor/scope.rbs +23 -3
  413. data/skills/rigor-plugin-author/SKILL.md +4 -4
  414. data/skills/rigor-plugin-author/references/02-walker-and-types.md +5 -5
  415. data/skills/rigor-plugin-review/SKILL.md +3 -3
  416. data/skills/rigor-project-init/references/03-baseline-and-bugs.md +12 -1
  417. metadata +36 -3
@@ -4,6 +4,7 @@ require "prism"
4
4
 
5
5
  require_relative "../configuration"
6
6
  require_relative "../environment"
7
+ require_relative "../run_scoped_slots"
7
8
  require_relative "../project_environment"
8
9
  require_relative "../scope"
9
10
  require_relative "../reflection"
@@ -26,6 +27,7 @@ require_relative "declaration_equivalence"
26
27
  require_relative "inline_declarations"
27
28
  require_relative "layout_index"
28
29
  require_relative "method_candidate"
30
+ require_relative "type_name_qualification"
29
31
  require_relative "writer"
30
32
  require_relative "../rooted_glob"
31
33
 
@@ -79,16 +81,20 @@ module Rigor
79
81
  attr_reader :unrenderable, :unresolvable_superclasses
80
82
 
81
83
  # @param paths — files / directories to scan.
82
- # @param observations — => Array<Array<Rigor::Type>>}]
84
+ # @param observations —
83
85
  # ADR-14 slice 3 — per-target-method arg-tuple observations
84
86
  # produced by {ObservationCollector}. An empty Hash (the default)
85
87
  # means "no observations available; emit `untyped` for every
86
88
  # parameter position" per ADR-5 clause 2.
89
+ # @param target_writer — the {Writer} a `--write` or `--check` run hands the candidates to. When given, a
90
+ # class counts as written for a subclass only if that writer accepts its target file (#2461); nil (print,
91
+ # diff, JSON) counts every class with a candidate the writer is sure to write.
87
92
  # @param overwrite — `--overwrite`: a member declared inline whose `sig/` copy disagrees is proposed as an
88
93
  # `inline-overwrite` of the whole member instead of refused (ADR-112 WD4).
89
94
  def initialize(configuration:, paths:, observations: {}, include_private: false, effect_annotator: nil,
90
- overwrite: false)
95
+ overwrite: false, target_writer: nil)
91
96
  @configuration = configuration
97
+ @target_writer = target_writer
92
98
  @overwrite = overwrite
93
99
  @paths = paths
94
100
  @observations = normalize_observations(observations)
@@ -114,6 +120,36 @@ module Rigor
114
120
  @unresolvable_superclasses = {}
115
121
  # Issue #722 — whole-run: `{ class name => the lexical nesting its header is written in }`.
116
122
  @superclass_nestings = {}
123
+ # Issue #2437 — whole-run: every class a walked file declares with a `class` keyword or a `Data.define` /
124
+ # `Struct.new` layout, so the report can tell a project superclass this run did not write from one nothing
125
+ # declares.
126
+ @source_classes = Set.new
127
+ end
128
+
129
+ # How the report names an unresolvable superclass token ({#unresolvable_superclasses}). It is a project
130
+ # superclass when it names a class the walked source declares and this run wrote nothing for, whose own chain
131
+ # would close if the project's classes were written:
132
+ # each step a source class, ending at no superclass or at one the RBS environment knows (#2437). What is
133
+ # missing is then the project's own declaration, not a gem's RBS. `class ApplicationRecord <
134
+ # ActiveRecord::Base` with no RBS for the gem is not such a class: writing it would not help.
135
+ #
136
+ # Returns `[:project, token]` for such a class. Otherwise `[:external, root]`, where `root` is the first token
137
+ # on the chain that is not a source class: `User < ApplicationRecord` reports `ActiveRecord::Base`, the class
138
+ # whose RBS is missing, rather than the project class between them.
139
+ def unresolvable_superclass_report(token)
140
+ name = bare_superclass_name(token)
141
+ root = token
142
+ seen = {}
143
+ while @source_classes.include?(name) && !seen[name]
144
+ seen[name] = true
145
+ nesting, superclass = @superclass_nestings[name]
146
+ return [:project, token] if superclass.nil?
147
+
148
+ root = resolve_superclass_token(superclass, nesting, @source_classes, name)
149
+ name = bare_superclass_name(root)
150
+ return [:project, token] if Reflection.rbs_class_known?(name, environment: @environment)
151
+ end
152
+ [:external, root]
117
153
  end
118
154
 
119
155
  # Lifts legacy plain-`Array[Type]` observation entries into {ObservedCall} carriers. Specs from the
@@ -129,12 +165,28 @@ module Rigor
129
165
  resolved = resolve_paths(@paths)
130
166
  @resolved_paths = resolved
131
167
  @environment = build_environment(resolved)
168
+ # ADR-84 WD2, as `Runner#run_analysis` mints it for `check` (#2300): one return-memo bucket for the whole
169
+ # run. Without it the memo keys on each file's merged `discovered_def_nodes`, a new table per file, so a
170
+ # callee reached from two files was re-inferred in each. A fresh token per run keeps a second run in one
171
+ # process from reading the first's entries, and the bucket is released with the other run-scoped slots
172
+ # when the run ends ({RunScopedSlots}), so a finished run holds none of it.
173
+ @run_generation = Object.new.freeze
174
+ # Both read the files as they are now, so a second `run` of one generator re-reads them.
175
+ @project_tables = nil
176
+ @file_base_scope = nil
177
+ # #2461 — which `sig/` targets the writer will write to, read from the files as they are now.
178
+ @writable_targets = {}
132
179
  candidates = resolved.flat_map { |path| analyse_file(path, @environment) }
133
- annotate_effects(
134
- demote_overridden_base_methods(
135
- demote_unresolvable_superclasses(demote_inline_generic_classes(resolve_superclass_spellings(candidates)))
180
+ demoted = demote_overridden_base_methods(
181
+ demote_unresolvable_superclasses(
182
+ demote_generic_reopenings(demote_inline_generic_classes(resolve_superclass_spellings(candidates)))
136
183
  )
137
184
  )
185
+ # Again after the override demotion: a demoted method can leave its file with nothing to write, and a class
186
+ # shell riding on that file then declares nothing a subclass elsewhere could rest on.
187
+ annotate_effects(stamp_generic_reopenings(demote_unresolvable_superclasses(demoted)))
188
+ ensure
189
+ RunScopedSlots.release
138
190
  end
139
191
 
140
192
  private
@@ -209,10 +261,10 @@ module Rigor
209
261
  decls.each do |decl|
210
262
  next unless decl.is_a?(::RBS::AST::Declarations::Class) || decl.is_a?(::RBS::AST::Declarations::Module)
211
263
 
212
- name = (prefix + [decl.name.to_s.delete_prefix("::")]).join("::")
213
- return decl if name == class_name
264
+ path = ::Rigor::Environment::RbsLoader.declared_class_path(decl, prefix)
265
+ return decl if path.join("::") == class_name
214
266
 
215
- found = find_declaration(decl.members, name.split("::"), class_name)
267
+ found = find_declaration(decl.members, path, class_name)
216
268
  return found if found
217
269
  end
218
270
  nil
@@ -223,6 +275,88 @@ module Rigor
223
275
  (1..segments.size).any? { |n| generic.key?(segments.first(n).join("::")) }
224
276
  end
225
277
 
278
+ # Issue #2436 — a class the loaded RBS declares generic (`Array`, `Hash`, a gem's or the project's own) is
279
+ # reopened with that declaration's type parameters copied onto the header ({#stamp_generic_reopenings}),
280
+ # which is exact only while every parameter is plain. A bound or a default is a type spelled relative to the
281
+ # declaring file's namespace, and copying it under another nesting can resolve it to a different class, so
282
+ # rbs would reject the header as it rejects a parameterless one (`GenericParameterMismatchError`): the class,
283
+ # and every class whose signature mentions it, fails its definition build. A method of such a class, or of
284
+ # one nested in it, is skipped instead — unless `sig/` already declares the class, whose own header the
285
+ # writer then writes into ({#demote_inline_generic_classes}' rule).
286
+ def demote_generic_reopenings(candidates)
287
+ loader = @environment&.rbs_loader
288
+ return candidates if loader.nil?
289
+
290
+ layout = nil
291
+ candidates.map do |candidate|
292
+ next candidate unless Classification::EMITTABLE.include?(candidate.classification)
293
+ next candidate unless namespace_prefixes(candidate.class_name).any? { |name| bounded_generic?(loader, name) }
294
+
295
+ layout ||= LayoutIndex.new(signature_paths: @configuration.signature_paths)
296
+ next candidate if layout.file_for(candidate.class_name)
297
+
298
+ demoted_candidate(candidate, :generic_reopening)
299
+ end
300
+ end
301
+
302
+ # The other half of {#demote_generic_reopenings}: every emittable candidate carries the header spelling of
303
+ # each generic class the run may open a header for — its own class, the classes it is nested in, and the
304
+ # file's class shells and theirs — so the writer and `--print` spell the same header. Method bodies keep
305
+ # `untyped` for element types; nothing here names `E` in a member.
306
+ #
307
+ # Issue #2580 — a skipped candidate is stamped too, and its class counts towards the map: the writer
308
+ # repairs a parameterless header an earlier run wrote into the file a candidate routes to, and a file whose
309
+ # every proposal is already declared (or untyped) would otherwise keep a header rbs rejects.
310
+ def stamp_generic_reopenings(candidates)
311
+ loader = @environment&.rbs_loader
312
+ return candidates if loader.nil?
313
+
314
+ spelled = header_type_params(candidates.flat_map { |c| [c.class_name, *c.class_shells] }.compact)
315
+ return candidates if spelled.empty?
316
+
317
+ candidates.map { |candidate| candidate.with_class_type_params(spelled) }
318
+ end
319
+
320
+ # #2603 — the header spelling {#header_type_params} gives `class_name` alone, or nil when the loaded RBS does
321
+ # not declare it generic with plain parameters only. The writer asks it for a bare header in a `sig/` file no
322
+ # candidate of the run routes to. Nil before {#run} has built the environment.
323
+ def generic_header_spelling(class_name)
324
+ loader = @environment&.rbs_loader
325
+ return nil if loader.nil?
326
+
327
+ params = loader.class_declaration_type_params(class_name)
328
+ "[#{params.join(', ')}]" if !params.empty? && params.all? { |p| plain_type_param?(p) }
329
+ end
330
+ public :generic_header_spelling
331
+
332
+ # `{ class name => "[unchecked out E]" }` for every name in `names`, or a namespace it is nested in, whose
333
+ # loaded declaration is generic with plain parameters only.
334
+ def header_type_params(names)
335
+ names.uniq.flat_map { |name| namespace_prefixes(name) }.uniq.each_with_object({}) do |name, acc|
336
+ spelling = generic_header_spelling(name)
337
+ acc[name] = spelling if spelling
338
+ end.freeze
339
+ end
340
+
341
+ def namespace_prefixes(class_name)
342
+ return [] if class_name.nil?
343
+
344
+ segments = class_name.split("::")
345
+ (1..segments.size).map { |n| segments.first(n).join("::") }
346
+ end
347
+
348
+ def bounded_generic?(loader, name)
349
+ params = loader.class_declaration_type_params(name)
350
+ !params.empty? && !params.all? { |param| plain_type_param?(param) }
351
+ end
352
+
353
+ # A name, a variance and `unchecked`, and nothing that spells a type: no upper or lower bound, no default.
354
+ def plain_type_param?(param)
355
+ %i[upper_bound lower_bound default_type].none? do |reader|
356
+ param.respond_to?(reader) && param.public_send(reader)
357
+ end
358
+ end
359
+
226
360
  # Issue #744 — a base class's method is NOT emitted when a project subclass overrides it and the
227
361
  # override is not emitted itself.
228
362
  #
@@ -241,7 +375,7 @@ module Rigor
241
375
  # (#744 half 2) and is not decided here: this pass only stops sig-gen manufacturing the conflict.
242
376
  def demote_overridden_base_methods(candidates)
243
377
  superclasses = candidates.each_with_object({}) do |candidate, acc|
244
- (candidate.class_superclasses || {}).each { |name, sup| acc[name] = sup.sub(/\[.*\]\z/, "") }
378
+ (candidate.class_superclasses || {}).each { |name, sup| acc[name] = bare_superclass_name(sup) }
245
379
  end
246
380
  return candidates if superclasses.empty?
247
381
 
@@ -305,6 +439,10 @@ module Rigor
305
439
  def resolve_superclass_spellings(candidates)
306
440
  return candidates if @superclass_nestings.empty?
307
441
 
442
+ # Every shell counts here, written or not: the question is which class Ruby means by the token, and the
443
+ # source declares `Lib::Error` whether or not its file is written. Whether the resolved superclass reaches
444
+ # disk is {#demote_unresolvable_superclasses}'s question. Counting only written shells would leave `Error`
445
+ # unresolved, and a top-level `Error` some signature declares would then take it (#2454).
308
446
  known = candidates.each_with_object(Set.new) do |candidate, acc|
309
447
  acc << candidate.class_name if candidate.class_name
310
448
  acc.merge(candidate.class_shells || [])
@@ -324,20 +462,32 @@ module Rigor
324
462
  @superclass_nestings.each_with_object({}) do |(full, (nesting, token)), acc|
325
463
  next if token.nil? || nesting.empty?
326
464
 
327
- resolved = resolve_superclass_token(token, nesting, known)
465
+ resolved = resolve_superclass_token(token, nesting, known, full)
328
466
  acc[full] = resolved if resolved && resolved != token
329
467
  end
330
468
  end
331
469
 
332
470
  # The first candidate spelling that names a class this run declares or the RBS environment knows, or
333
- # nil when none does — an unresolvable superclass is #735's business, not this pass's.
334
- def resolve_superclass_token(token, nesting, known)
471
+ # the token itself when none does — an unresolvable superclass is #735's business, not this pass's.
472
+ #
473
+ # `full` is the class whose header carries the token. Ruby evaluates the superclass expression before the
474
+ # new constant exists, so the innermost candidate `full` itself (`module A; module C; class B < B`, where
475
+ # `A::C::B` is the class being opened) is not yet a candidate: the lookup falls through to `A::B` (#2579).
476
+ def resolve_superclass_token(token, nesting, known, full)
477
+ # A rooted token (`::Hash`) already names one class; the nesting must not capture it (#2459).
478
+ return token if token.start_with?("::")
479
+
335
480
  base = token.sub(/\[.*\]\z/, "")
336
- args = token[base.length..] || ""
337
481
  nesting.length.downto(1) do |i|
338
482
  candidate = "#{nesting[0, i].join('::')}::#{base}"
339
- return "#{candidate}#{args}" if known.include?(candidate) ||
340
- Reflection.rbs_class_known?(candidate, environment: @environment)
483
+ next if candidate == full
484
+
485
+ # The arguments {#record_superclass} computed belong to the top-level class of the bare name, so they are
486
+ # computed again for the class the token resolves to: `Hash` inside a `module Foo` declaring a plain
487
+ # `Foo::Hash` takes none (#2459).
488
+ return generic_superclass_spelling(candidate) if known.include?(candidate) ||
489
+ Reflection.rbs_class_known?(candidate,
490
+ environment: @environment)
341
491
  end
342
492
  token
343
493
  end
@@ -370,28 +520,77 @@ module Rigor
370
520
  # ran — source discovery and the plugins still type it. Emitting the class with the superclass merely
371
521
  # OMITTED would be the harmful middle: the class becomes RBS-known with only the emitted methods, so
372
522
  # every inherited call fires `call.undefined-method`.
523
+ #
524
+ # Demoting a class can leave a file with nothing to write, which takes the class shells riding on it off the
525
+ # emitted set, so the pass repeats until no further class becomes unresolvable. The emitted set only shrinks,
526
+ # so the unresolvable set only grows and the loop ends.
373
527
  def demote_unresolvable_superclasses(candidates)
374
528
  superclasses = candidates.each_with_object({}) do |candidate, acc|
375
529
  (candidate.class_superclasses || {}).each { |name, sup| acc[name] = sup }
376
530
  end
377
- # Everything this run declares, superclass or not: a chain that reaches `class Record` — emitted
378
- # here, with no superclass of its own — terminates in a class the next run WILL know.
379
- emitted = candidates.each_with_object(Set.new) do |candidate, acc|
380
- acc << candidate.class_name if candidate.class_name
381
- acc.merge(candidate.class_shells || [])
531
+ current = candidates
532
+ unresolvable = {}
533
+ loop do
534
+ found = unresolvable_superclass_roots(superclasses, declared_by_run(current))
535
+ return current if found.size == unresolvable.size
536
+
537
+ unresolvable = found
538
+ current = candidates.map do |candidate|
539
+ owner = unresolvable_for(candidate.class_name, unresolvable)
540
+ owner ? record_unresolvable_superclass(candidate, owner) : candidate
541
+ end
382
542
  end
383
- unresolvable = superclasses.each_with_object({}) do |(name, _sup), acc|
543
+ end
544
+
545
+ def unresolvable_superclass_roots(superclasses, emitted)
546
+ superclasses.each_with_object({}) do |(name, _sup), acc|
384
547
  root = unresolved_superclass_root(name, superclasses, emitted)
385
548
  acc[name] = root if root
386
549
  end
387
- return candidates if unresolvable.empty?
388
-
389
- candidates.map do |candidate|
390
- owner = unresolvable_for(candidate.class_name, unresolvable)
391
- next candidate if owner.nil?
550
+ end
392
551
 
393
- record_unresolvable_superclass(candidate, owner)
552
+ # Everything this run puts on disk, superclass or not: a chain that reaches `class Record` — emitted here, with
553
+ # no superclass of its own — terminates in a class the next run WILL know. Only what the writer is sure to
554
+ # write counts, because a subclass written against a class nothing declares fails its build
555
+ # (`RBS::NoSuperclassFoundError`) on the next run:
556
+ #
557
+ # - a class with a candidate the writer is sure to write ({#written?}). A class whose every method is skipped
558
+ # is not written, so it does not count (#2437); a class shell on it would close it instead (#2438);
559
+ # - a class shell on a file with such a candidate: a shell on a file whose every method is skipped, already
560
+ # declared by `sig/`, or a tighter return the writer will not apply never reaches disk (#2454);
561
+ # - the wrapper namespaces of either, which the writer renders around them with their recorded superclasses.
562
+ #
563
+ # Counting too few only demotes a subclass, the output before #2454 (ADR-5).
564
+ def declared_by_run(candidates)
565
+ written = candidates.select { |candidate| written?(candidate) }
566
+ written_paths = written.to_set(&:path)
567
+ declared = Set.new
568
+ written.each { |candidate| declared << candidate.class_name if candidate.class_name }
569
+ candidates.each do |candidate|
570
+ declared.merge(candidate.class_shells || []) if written_paths.include?(candidate.path)
394
571
  end
572
+ declared.flat_map { |name| wrapper_names(name) }.to_set | declared
573
+ end
574
+
575
+ # `A::B::C` is written inside `A` and `A::B`.
576
+ def wrapper_names(name)
577
+ segments = name.split("::")
578
+ (1...segments.size).map { |n| segments.first(n).join("::") }
579
+ end
580
+
581
+ # Whether the writer will put `candidate` on disk: a candidate it is sure to write ({Writer.writes?}), and,
582
+ # when the run writes or compares files (`--write`, `--check`), aimed at a target that writer will write to
583
+ # (#2461). Print, diff and JSON runs have no writer: where a file would go depends on the working directory
584
+ # the writer takes its sig root from, and their output must not.
585
+ def written?(candidate)
586
+ return false unless Writer.writes?(candidate)
587
+ return true if @target_writer.nil?
588
+
589
+ target = @target_writer.path_mapper.target_for(candidate.path, class_name: candidate.class_name,
590
+ method_name: candidate.method_name,
591
+ kind: candidate.rbs_kind)
592
+ @writable_targets[target] = @target_writer.accepts_target?(target) unless @writable_targets.key?(target)
593
+ @writable_targets[target]
395
594
  end
396
595
 
397
596
  # A nested class or module goes with its wrapper. `WikiPage::Webhookable`'s declaration is emitted
@@ -417,9 +616,9 @@ module Rigor
417
616
  return superclass if seen[name]
418
617
 
419
618
  seen[name] = true
420
- # The recorded spelling may carry the type arguments {#generic_superclass_spelling} added; the
421
- # resolvability question is about the class, so it is asked of the bare name.
422
- base = superclass.sub(/\[.*\]\z/, "")
619
+ # The recorded spelling may carry the type arguments {#generic_superclass_spelling} added, and a rooted
620
+ # token its `::` (#2459); the resolvability question is about the class, so it is asked of the bare name.
621
+ base = bare_superclass_name(superclass)
423
622
  return nil if Reflection.rbs_class_known?(base, environment: @environment)
424
623
  return superclass unless emitted.include?(base)
425
624
 
@@ -460,15 +659,27 @@ module Rigor
460
659
  end.uniq
461
660
  end
462
661
 
463
- # #1154 — the project's discovery tables, read only by the declared-parameter ivar credit: it must see the ivar
464
- # writers in other files (an ancestor, another object's `instance_variable_set`, a reopening) as `rigor check`
465
- # does. Built once per run over the configured project paths as well as the files sig-gen reads, so a run over
466
- # one file declines what a `check` of it declines; every other part of the generator keeps its per-file scope.
467
- def ivar_writer_scope(base_scope)
468
- @ivar_writer_tables ||= Inference::ProjectSeedTables.for_paths(
662
+ # #1154 / #2300 — the project's discovery tables, built once per run over the configured project paths as well
663
+ # as the files sig-gen reads, so a run over one file reads what a `check` of it reads. Every file is indexed
664
+ # over them ({#file_base_scope}): a helper, an ancestor or a constant another file defines resolves as it does
665
+ # under `check` instead of reading ABSENT. The declared-parameter ivar credit reads them whole
666
+ # ({#ivar_writer_scope}).
667
+ def project_tables
668
+ @project_tables ||= Inference::ProjectSeedTables.for_paths(
469
669
  ((@resolved_paths || []) + resolve_paths(Array(@configuration.paths))).uniq { |path| File.expand_path(path) }
470
670
  )
471
- base_scope.with_discovery(base_scope.discovery.with(**@ivar_writer_tables))
671
+ end
672
+
673
+ # The member layouts stay per-file. sig-gen writes a value class for every `Data.define` / `Struct.new` layout
674
+ # the indexed scope carries ({#collect_meta_layouts}), so a seeded layout would re-emit every value class of the
675
+ # project in every file.
676
+ PER_FILE_TABLES = %i[data_member_layouts struct_member_layouts].freeze
677
+ private_constant :PER_FILE_TABLES
678
+
679
+ # #1154 — the declared-parameter ivar credit must see the ivar writers in other files (an ancestor, another
680
+ # object's `instance_variable_set`, a reopening) as `rigor check` does.
681
+ def ivar_writer_scope(base_scope)
682
+ base_scope.with_discovery(base_scope.discovery.with(**project_tables))
472
683
  end
473
684
 
474
685
  # #1406 — sig-gen keeps the census's own answer: a declared ivar type read back would echo the declaration.
@@ -478,12 +689,24 @@ module Rigor
478
689
  )
479
690
  end
480
691
 
692
+ # The scope each file is indexed under: the project's tables less the per-file ones ({#project_tables}), and the
693
+ # run's return-memo token (see {#run}), which {#ivar_writer_scope} inherits, so every reading of a callee shares
694
+ # one memo bucket. Built once per run.
695
+ def file_base_scope(environment)
696
+ @file_base_scope ||= begin
697
+ base = Scope.empty(environment: environment)
698
+ tables = project_tables.except(*PER_FILE_TABLES)
699
+ tables = tables.merge(run_generation: @run_generation) if @run_generation
700
+ base.with_discovery(base.discovery.with(**tables))
701
+ end
702
+ end
703
+
481
704
  def analyse_file(path, environment)
482
705
  source = File.read(path)
483
706
  parse_result = Prism.parse(source, filepath: path, version: @configuration.target_ruby)
484
707
  return [] if parse_result.errors.any?
485
708
 
486
- scope_index = index_file(parse_result.value, Scope.empty(environment: environment))
709
+ scope_index = index_file(parse_result.value, file_base_scope(environment))
487
710
 
488
711
  reset_file_maps
489
712
  @meta_layouts = collect_meta_layouts(scope_index)
@@ -518,11 +741,12 @@ module Rigor
518
741
  #
519
742
  # Only a definer whose statement certainly runs can be last: a direct statement of a `class`, `module` or
520
743
  # `class << self` body, of a `Data.define` / `Struct.new` block or of the file ({#walk_statements} records
521
- # them). One under a condition, in a block or a lambda, passed to a call (`private def x`), or after an
522
- # uncertain `module_function` reset may not run, as far as this reading tells. A slot such a definer could
744
+ # them), or a `def` passed to `private`, `public` or `protected` in such a statement ({#walk_statements}, #2254).
745
+ # One under a condition, in a block or a lambda, or after an uncertain `module_function` reset may not run, as
746
+ # far as this reading tells. A slot such a definer could
523
747
  # still take over from the last certain one is contested, and nothing is written for it.
524
748
  #
525
- # @return `{ [class_name, kind, method_name] => [[offset, certain], ...] }`.
749
+ # @return the definers of each slot as `[offset, certain]` pairs, keyed by `[class_name, kind, method_name]`.
526
750
  MEMBER_OFFSET = -1
527
751
  private_constant :MEMBER_OFFSET
528
752
 
@@ -677,6 +901,9 @@ module Rigor
677
901
  # the `self` one returns above): it defines a singleton method on `obj`, not a private `Object` method.
678
902
  def walk_def_children(node, prefix, in_singleton_class, module_function_active, out)
679
903
  return if refine_block_call?(node)
904
+ # #2259 — a `def` in `class_methods do … end` (ActiveSupport::Concern) defines a method of the including class's
905
+ # singleton, not of this module. Omitted, not written class-side: no precedent places it on the singleton.
906
+ return if class_methods_block?(node)
680
907
 
681
908
  in_block = node.is_a?(Prism::BlockNode) || node.is_a?(Prism::LambdaNode) ||
682
909
  node.is_a?(Prism::SingletonClassNode)
@@ -706,6 +933,7 @@ module Rigor
706
933
 
707
934
  full = child_prefix.join("::")
708
935
  @namespace_kinds[full] = node.is_a?(Prism::ClassNode) ? :class : :module
936
+ @source_classes << full if node.is_a?(Prism::ClassNode)
709
937
  record_superclass(node, full, prefix)
710
938
  walk_namespace_body(node, child_prefix, out)
711
939
  true
@@ -725,6 +953,17 @@ module Rigor
725
953
  !@in_effect_refinements.class_body_refine?(node)
726
954
  end
727
955
 
956
+ # A block call whose members are not this class body's: a `refine` block (#1763), and a `class_methods` block
957
+ # (#2259), whose members belong to the including class's singleton.
958
+ def skipped_block_call?(node)
959
+ refine_block_call?(node) || class_methods_block?(node)
960
+ end
961
+
962
+ def class_methods_block?(node)
963
+ node.is_a?(Prism::CallNode) && node.receiver.nil? && node.name == :class_methods &&
964
+ node.block.is_a?(Prism::BlockNode)
965
+ end
966
+
728
967
  def declined_self_header?(node)
729
968
  path = node.constant_path
730
969
  path.is_a?(Prism::ConstantPathNode) && path.parent.is_a?(Prism::SelfNode) &&
@@ -750,10 +989,43 @@ module Rigor
750
989
  # the name it resolves to may be a class another file declares. Resolved once every file has been
751
990
  # walked, in {#resolve_superclass_spellings}.
752
991
  spelling = generic_superclass_spelling(superclass)
992
+ # Issue #2579 — a token that names the class being opened (`class A::B < A::B`, or a top-level `class B < B`
993
+ # with nothing else of that name) would write a self-superclass, which RBS rejects as a recursive ancestor.
994
+ # No superclass is safer than that, so the declaration is written without one.
995
+ return if bare_superclass_name(spelling) == full
996
+
753
997
  # The token rides along because `@class_superclasses` is per-FILE scratch, reset before the next
754
998
  # file, while this table is whole-run.
755
999
  @superclass_nestings[full] = [prefix.dup, spelling]
756
1000
  @class_superclasses[full] = spelling
1001
+ register_core_subclass_shell(full, prefix, superclass)
1002
+ end
1003
+
1004
+ # Issue #2224 — a subclass of a core or stdlib class got a declaration only when it had a method of its own to
1005
+ # emit, so after `class MyHash < Hash; end` a method returning one was written `-> MyHash` against a class no
1006
+ # signature declared, and `rbs validate` failed with `NoTypeFoundError`. As a class shell it is declared
1007
+ # `class MyHash < ::Hash[untyped, untyped]` beside the file's other declarations. A class the RBS environment
1008
+ # already declares (the project's `sig/`, an earlier run) is left to that declaration, and a superclass the
1009
+ # header's nesting shadows is not core's: `Hash` inside a `module Foo` that declares `Foo::Hash` is the project's.
1010
+ # A rooted `::Hash` names core's class whatever the nesting declares, so the shadow cannot capture it (#2459).
1011
+ def register_core_subclass_shell(full, prefix, superclass)
1012
+ return if class_known?(full)
1013
+
1014
+ bare = bare_superclass_name(superclass)
1015
+ return unless @environment&.rbs_loader&.core_or_stdlib_class?(bare)
1016
+
1017
+ unless superclass.start_with?("::")
1018
+ namespaces = TypeNameQualification.enclosing_namespaces(prefix.join("::"))
1019
+ return if TypeNameQualification.shadowed?(bare.split("::").first, namespaces, method(:declared_constant?))
1020
+ end
1021
+
1022
+ @class_shells << full
1023
+ end
1024
+
1025
+ # The class a recorded superclass token names, as the fully qualified key every class-name set and the RBS
1026
+ # loader use: the type arguments {#generic_superclass_spelling} added and a rooted token's `::` both dropped.
1027
+ def bare_superclass_name(token)
1028
+ token.sub(/\[.*\]\z/, "").delete_prefix("::")
757
1029
  end
758
1030
 
759
1031
  # Issue #735 — a GENERIC superclass has to be written with its arguments. `class Entries < Array`
@@ -766,7 +1038,7 @@ module Rigor
766
1038
  loader = @environment&.rbs_loader
767
1039
  return superclass if loader.nil?
768
1040
 
769
- arity = loader.class_type_param_names(superclass).size
1041
+ arity = loader.class_type_param_names(bare_superclass_name(superclass)).size
770
1042
  return superclass if arity.zero?
771
1043
 
772
1044
  "#{superclass}[#{(['untyped'] * arity).join(', ')}]"
@@ -814,6 +1086,7 @@ module Rigor
814
1086
  @meta_layouts.each do |class_name, layout|
815
1087
  @namespace_kinds[class_name] = :class
816
1088
  @class_shells << class_name
1089
+ @source_classes << class_name
817
1090
  @class_superclasses[class_name] = MetaClassShape::SUPERCLASSES.fetch(layout.kind)
818
1091
  end
819
1092
  end
@@ -862,6 +1135,8 @@ module Rigor
862
1135
  @certain_definers << stmt
863
1136
  # A `begin … end` statement's body (and `ensure`) runs whenever the statement does.
864
1137
  mark_certain_body(stmt) if stmt.is_a?(Prism::BeginNode)
1138
+ # A `def` passed to a visibility call (`private def x`, #2254) runs with its statement.
1139
+ @certain_definers.merge(Inference::ModuleFunctionState.visibility_call_defs(stmt))
865
1140
  end
866
1141
  walk_defs(stmt, prefix, in_singleton_class, active, out)
867
1142
  end
@@ -959,8 +1234,8 @@ module Rigor
959
1234
  # `Object#initialize: () -> void`. The mismatch surfaces a `Ruby::MethodParameterMismatch` warning even
960
1235
  # when `rigor check` itself is clean.
961
1236
  #
962
- # Returning `nil` here causes `classify_def` to skip emission; returning `:emit_stub` causes
963
- # `initialize_stub_candidate` to emit a permissive `(<param shape>) -> void` stub matching the runtime
1237
+ # A default constructor is skipped (`classify_def` returns nil for it); a non-trivial one is not, and
1238
+ # {#initialize_stub_candidate} emits a permissive `(<param shape>) -> void` stub matching the runtime
964
1239
  # parameter list.
965
1240
  def initialize_excludes?(def_node, kind)
966
1241
  return false unless kind == :instance
@@ -1088,25 +1363,36 @@ module Rigor
1088
1363
  # return position it is not parenthesised.
1089
1364
  def positional_type(observations, index, prefix, owner = nil)
1090
1365
  types = observations.filter_map { |obs| obs.positional[index] }
1091
- "#{prefix}#{union_erase(types, owner: owner)}"
1366
+ "#{prefix}#{union_erase(widen_observed_params(types), owner: owner)}"
1092
1367
  end
1093
1368
 
1094
1369
  def render_keyword_param(keyword, observations, owner = nil)
1095
1370
  optional_marker = keyword.is_a?(Prism::OptionalKeywordParameterNode) ? "?" : ""
1096
1371
  types = observations.filter_map { |obs| obs.keyword[keyword.name] }
1097
- rendered = types.empty? ? "untyped" : paren_wrap_union(union_erase(types, owner: owner))
1372
+ rendered = types.empty? ? "untyped" : paren_wrap_union(union_erase(widen_observed_params(types), owner: owner))
1098
1373
  "#{optional_marker}#{keyword.name}: #{rendered}"
1099
1374
  end
1100
1375
 
1376
+ # #2254 — an observed argument is one call's value, so a literal (`"hello"`) proposes its class (`String`), as
1377
+ # {#widen_value_literals} does for a return. Widening always: a parameter the body only compares against a
1378
+ # literal (a discriminant) is widened too, which loses precision and never adds a false positive.
1379
+ def widen_observed_params(types)
1380
+ types.map { |type| widen_value_literals(type) }
1381
+ end
1382
+
1383
+ # Issue #2459 — a ROOTED path (`::Hash`, `::A::B`) keeps its leading `::`: it names exactly one class, and the
1384
+ # passes after {#record_superclass} must not re-resolve it against the header's nesting, where a project
1385
+ # `Foo::Hash` would capture it. Every pass that compares the token against class names strips the anchor.
1101
1386
  def qualified_constant_path(constant_path)
1102
1387
  case constant_path
1103
1388
  when Prism::ConstantReadNode
1104
1389
  constant_path.name.to_s
1105
1390
  when Prism::ConstantPathNode
1106
- parent = qualified_constant_path(constant_path.parent) if constant_path.parent
1107
1391
  name = constant_path.name&.to_s
1108
1392
  return nil if name.nil?
1393
+ return "::#{name}" if constant_path.parent.nil?
1109
1394
 
1395
+ parent = qualified_constant_path(constant_path.parent)
1110
1396
  parent ? "#{parent}::#{name}" : name
1111
1397
  end
1112
1398
  end
@@ -1520,16 +1806,16 @@ module Rigor
1520
1806
  # about the return. The class's own declaration is kept whole instead: its visibility, the overload's
1521
1807
  # annotations, type parameters, parameters and block, with only the return replaced.
1522
1808
  #
1523
- # Three shapes are declined (`nil`):
1809
+ # Two shapes are declined (`nil`):
1524
1810
  # - several overloads. The body is typed once, so the inferred return covers every overload together and
1525
1811
  # says nothing about which overload returns what: collapsing them into one line would drop the
1526
1812
  # overloads, and giving each the joint return would widen the overloads the author wrote narrower.
1527
1813
  # - a name the class's own `sig/` spells as an `alias`. Its parameters are the aliased method's, and a
1528
1814
  # proposal would read as a rewrite of that method's line rather than of the alias. An alias only an
1529
1815
  # ancestor declares takes the override path below, like any other ancestor declaration.
1530
- # - nothing else: when only an ancestor declares the method, the proposal is a new override on this
1531
- # class, and is rendered from the `def`'s own shape like any new method. The ancestor's parameters
1532
- # describe the ancestor's `def`, not this one, which may take other arguments (ADR-14 clause 2).
1816
+ # A method this class does not declare is not declined: when only an ancestor declares it, the proposal is a
1817
+ # new override on this class, rendered from the `def`'s own shape like any new method. The ancestor's
1818
+ # parameters describe the ancestor's `def`, not this one, which may take other arguments (ADR-14 clause 2).
1533
1819
  def tighter_return_line(def_node, method_def, class_name, kind, inferred)
1534
1820
  member = signature_member(method_def, class_name)
1535
1821
  return render_rbs_line(def_node, inferred, class_name, kind) if member.nil?
@@ -1912,8 +2198,35 @@ module Rigor
1912
2198
  # `owner:` is the class or module the rendered member belongs to. {AliasIndex} needs it to judge whether
1913
2199
  # a matching alias is one this reader would expect to see; a call site that cannot supply it renders the
1914
2200
  # type unfolded, which is the same output as before #1002.
2201
+ #
2202
+ # Issue #2230 — qualified last, after the alias fold has matched the unqualified spelling it keys on, so a core
2203
+ # name a namespace of the owner shadows (`Set` inside a `module Foo` that declares `Foo::Set`) is written `::Set`.
1915
2204
  def elaborated_rbs(type, owner: nil)
1916
- alias_index.fold(TypeElaborator.elaborate(type, environment: @environment).erase_to_rbs, owner)
2205
+ folded = alias_index.fold(TypeElaborator.elaborate(type, environment: @environment).erase_to_rbs, owner)
2206
+ captured = owner_type_param_names(owner)
2207
+ TypeNameQualification.qualify(folded, owner) { |name| captured.include?(name) || declared_constant?(name) }
2208
+ end
2209
+
2210
+ # Issue #2436 — inside `class Array[unchecked out E]` a bare `E` names the type variable, not a class `::E`,
2211
+ # so a class whose spelling equals one of the owner's declared type-parameter names is anchored. The names
2212
+ # come as `"Array::E"`, the form {TypeNameQualification.qualify} asks about for the owner's own namespace;
2213
+ # a namespace the owner is nested in does not bind its parameters inside the owner's declaration.
2214
+ def owner_type_param_names(owner)
2215
+ loader = owner && @environment&.rbs_loader
2216
+ return Set.new if loader.nil?
2217
+
2218
+ @owner_type_param_names ||= {}
2219
+ @owner_type_param_names.fetch(owner) do
2220
+ @owner_type_param_names[owner] = loader.class_declaration_type_params(owner).to_set do |p|
2221
+ "#{owner}::#{p.name}"
2222
+ end
2223
+ end
2224
+ end
2225
+
2226
+ # Whether a type name written as `name` resolves to a constant the run can see: one the RBS environment
2227
+ # declares, or a class or module the project's source declares, whether or not `sig/` has caught up with it.
2228
+ def declared_constant?(name)
2229
+ class_known?(name) || Reflection.discovered_class?(name, scope: file_base_scope(@environment))
1917
2230
  end
1918
2231
 
1919
2232
  # Issue #1002 — the reverse of {#translate_method_type_return}'s `alias_expander:`. Folding happens here,
@@ -2044,7 +2357,7 @@ module Rigor
2044
2357
  return
2045
2358
  end
2046
2359
  when Prism::CallNode
2047
- return if refine_block_call?(node)
2360
+ return if skipped_block_call?(node)
2048
2361
 
2049
2362
  collect_attr_call(node, prefix, in_singleton_class, ctx)
2050
2363
  end
@@ -2151,8 +2464,8 @@ module Rigor
2151
2464
  end
2152
2465
  end
2153
2466
 
2154
- # Build a { class_name => { attr_name_sym => Type } } map that records observation-derived types for
2155
- # ivars assigned directly from `def initialize` parameters. Only populated when `@observations` is
2467
+ # Records the observation-derived types of ivars assigned directly from `def initialize` parameters, keyed
2468
+ # by class name and then attribute name. Only populated when `@observations` is
2156
2469
  # non-empty (i.e. `--params=observed` was supplied). Matches the pattern `@ivar_name = param_name` where
2157
2470
  # `param_name` is a required / optional positional or keyword parameter of `initialize`.
2158
2471
  #
@@ -2285,7 +2598,7 @@ module Rigor
2285
2598
  node.rigor_each_child { |c| collect_init_ivar_obs(c, prefix, result) }
2286
2599
  end
2287
2600
 
2288
- # Derive { attr_name_sym => Type } for a single `def initialize` by matching `@ivar = param_name`
2601
+ # Derives the observation-derived attribute types of a single `def initialize`, matching `@ivar = param_name`
2289
2602
  # assignments against the available `[class_name, :initialize]` observations.
2290
2603
  def ivar_obs_from_initialize(class_name, def_node)
2291
2604
  obs_list = @observations[[class_name, :initialize]]
@@ -2300,8 +2613,8 @@ module Rigor
2300
2613
  build_ivar_obs_type_map(ivar_to_param, param_index, obs_list)
2301
2614
  end
2302
2615
 
2303
- # Map `{ ivar_name => param_name }` → `{ attr_name_sym => Type }` by looking up each param's observation
2304
- # types and unioning them.
2616
+ # Maps `{ ivar_name => param_name }` to attribute types by looking up each param's observation types and
2617
+ # unioning them.
2305
2618
  def build_ivar_obs_type_map(ivar_to_param, param_index, obs_list)
2306
2619
  ivar_to_param.filter_map do |ivar_name, param_name|
2307
2620
  types = collect_param_obs_types(obs_list, param_name, param_index[param_name])