rigortype 0.2.7 → 0.2.8

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 (414) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +1 -1
  3. data/docs/manual/04-diagnostics.md +7 -4
  4. data/docs/manual/06-baseline.md +5 -2
  5. data/docs/manual/15-type-protection-coverage.md +6 -4
  6. data/lib/rigor/analysis/baseline.rb +56 -96
  7. data/lib/rigor/analysis/buffer_binding.rb +13 -20
  8. data/lib/rigor/analysis/check_rules/always_truthy_condition_collector.rb +29 -48
  9. data/lib/rigor/analysis/check_rules/dead_assignment_collector.rb +35 -54
  10. data/lib/rigor/analysis/check_rules/ivar_write_collector.rb +23 -37
  11. data/lib/rigor/analysis/check_rules/main_pass_collector.rb +16 -23
  12. data/lib/rigor/analysis/check_rules/rule_walk.rb +48 -71
  13. data/lib/rigor/analysis/check_rules/self_closedness_scanner.rb +19 -28
  14. data/lib/rigor/analysis/check_rules/unreachable_clause_collector.rb +48 -67
  15. data/lib/rigor/analysis/check_rules.rb +174 -274
  16. data/lib/rigor/analysis/dependency_recorder.rb +26 -38
  17. data/lib/rigor/analysis/dependency_source_inference/boundary_cross_reporter.rb +13 -25
  18. data/lib/rigor/analysis/dependency_source_inference/builder.rb +17 -30
  19. data/lib/rigor/analysis/dependency_source_inference/gem_resolver.rb +15 -26
  20. data/lib/rigor/analysis/dependency_source_inference/index.rb +45 -76
  21. data/lib/rigor/analysis/dependency_source_inference/return_type_heuristic.rb +23 -40
  22. data/lib/rigor/analysis/dependency_source_inference/walker.rb +50 -83
  23. data/lib/rigor/analysis/dependency_source_inference.rb +6 -10
  24. data/lib/rigor/analysis/diagnostic.rb +48 -75
  25. data/lib/rigor/analysis/erb_template_detector.rb +13 -18
  26. data/lib/rigor/analysis/fact_store.rb +12 -22
  27. data/lib/rigor/analysis/incremental.rb +47 -63
  28. data/lib/rigor/analysis/incremental_session.rb +74 -100
  29. data/lib/rigor/analysis/project_scan.rb +12 -23
  30. data/lib/rigor/analysis/result.rb +3 -5
  31. data/lib/rigor/analysis/rule_catalog.rb +67 -95
  32. data/lib/rigor/analysis/run_stats.rb +38 -61
  33. data/lib/rigor/analysis/runner/diagnostic_aggregator.rb +101 -160
  34. data/lib/rigor/analysis/runner/pool_coordinator.rb +121 -188
  35. data/lib/rigor/analysis/runner/project_pre_passes.rb +62 -96
  36. data/lib/rigor/analysis/runner/run_snapshots.rb +12 -18
  37. data/lib/rigor/analysis/runner.rb +220 -333
  38. data/lib/rigor/analysis/self_call_resolution_recorder.rb +30 -43
  39. data/lib/rigor/analysis/worker_session.rb +64 -102
  40. data/lib/rigor/ast/type_node.rb +6 -8
  41. data/lib/rigor/ast.rb +6 -10
  42. data/lib/rigor/bleeding_edge.rb +27 -35
  43. data/lib/rigor/builtins/hkt_builtins.rb +7 -14
  44. data/lib/rigor/builtins/imported_refinements.rb +106 -149
  45. data/lib/rigor/builtins/predefined_constant_refinements.rb +51 -65
  46. data/lib/rigor/builtins/regex_refinement.rb +32 -43
  47. data/lib/rigor/builtins/static_return_refinements.rb +57 -81
  48. data/lib/rigor/cache/descriptor.rb +58 -94
  49. data/lib/rigor/cache/incremental_snapshot.rb +32 -48
  50. data/lib/rigor/cache/rbs_cache_producer.rb +10 -15
  51. data/lib/rigor/cache/rbs_class_ancestor_table.rb +10 -16
  52. data/lib/rigor/cache/rbs_class_type_param_names.rb +10 -16
  53. data/lib/rigor/cache/rbs_constant_table.rb +8 -13
  54. data/lib/rigor/cache/rbs_descriptor.rb +8 -13
  55. data/lib/rigor/cache/rbs_environment.rb +11 -19
  56. data/lib/rigor/cache/rbs_environment_marshal_patch.rb +11 -19
  57. data/lib/rigor/cache/rbs_known_class_names.rb +8 -12
  58. data/lib/rigor/cache/store.rb +96 -151
  59. data/lib/rigor/cli/annotate_command.rb +51 -88
  60. data/lib/rigor/cli/baseline_command.rb +10 -17
  61. data/lib/rigor/cli/check_command.rb +121 -199
  62. data/lib/rigor/cli/check_runner_factory.rb +8 -14
  63. data/lib/rigor/cli/ci_detector.rb +9 -14
  64. data/lib/rigor/cli/command.rb +8 -14
  65. data/lib/rigor/cli/coverage_command.rb +19 -26
  66. data/lib/rigor/cli/coverage_mutation.rb +14 -19
  67. data/lib/rigor/cli/coverage_report.rb +0 -2
  68. data/lib/rigor/cli/coverage_scan.rb +7 -11
  69. data/lib/rigor/cli/diagnostic_formats.rb +43 -62
  70. data/lib/rigor/cli/diff_command.rb +10 -20
  71. data/lib/rigor/cli/docs_command.rb +26 -40
  72. data/lib/rigor/cli/doctor_command.rb +8 -9
  73. data/lib/rigor/cli/explain_command.rb +4 -7
  74. data/lib/rigor/cli/fused_protection_renderer.rb +4 -5
  75. data/lib/rigor/cli/fused_protection_report.rb +6 -8
  76. data/lib/rigor/cli/lsp_command.rb +11 -19
  77. data/lib/rigor/cli/mcp_command.rb +4 -6
  78. data/lib/rigor/cli/mutation_protection_renderer.rb +3 -4
  79. data/lib/rigor/cli/mutation_protection_report.rb +6 -9
  80. data/lib/rigor/cli/options.rb +11 -19
  81. data/lib/rigor/cli/plugin_command.rb +18 -30
  82. data/lib/rigor/cli/plugins_command.rb +29 -51
  83. data/lib/rigor/cli/plugins_renderer.rb +12 -20
  84. data/lib/rigor/cli/prism_colorizer.rb +13 -19
  85. data/lib/rigor/cli/protection_renderer.rb +5 -7
  86. data/lib/rigor/cli/protection_report.rb +27 -17
  87. data/lib/rigor/cli/renderable.rb +4 -6
  88. data/lib/rigor/cli/show_bleedingedge_command.rb +5 -8
  89. data/lib/rigor/cli/sig_gen_command.rb +14 -26
  90. data/lib/rigor/cli/skill_command.rb +30 -47
  91. data/lib/rigor/cli/skill_describe.rb +40 -64
  92. data/lib/rigor/cli/trace_command.rb +9 -16
  93. data/lib/rigor/cli/trace_renderer.rb +35 -51
  94. data/lib/rigor/cli/triage_command.rb +6 -10
  95. data/lib/rigor/cli/triage_renderer.rb +1 -2
  96. data/lib/rigor/cli/type_of_command.rb +14 -23
  97. data/lib/rigor/cli/type_of_renderer.rb +3 -5
  98. data/lib/rigor/cli/type_scan_command.rb +6 -10
  99. data/lib/rigor/cli/type_scan_renderer.rb +3 -5
  100. data/lib/rigor/cli/type_scan_report.rb +2 -3
  101. data/lib/rigor/cli/upgrade_command.rb +2 -3
  102. data/lib/rigor/cli.rb +16 -28
  103. data/lib/rigor/config_audit.rb +35 -44
  104. data/lib/rigor/configuration/dependencies.rb +41 -70
  105. data/lib/rigor/configuration/severity_profile.rb +28 -42
  106. data/lib/rigor/configuration.rb +150 -241
  107. data/lib/rigor/environment/bundle_sig_discovery.rb +74 -118
  108. data/lib/rigor/environment/class_registry.rb +19 -26
  109. data/lib/rigor/environment/constant_type_cache_holder.rb +14 -21
  110. data/lib/rigor/environment/hkt_registry_holder.rb +6 -12
  111. data/lib/rigor/environment/lockfile_resolver.rb +31 -47
  112. data/lib/rigor/environment/rbs_collection_discovery.rb +39 -63
  113. data/lib/rigor/environment/rbs_coverage_report.rb +24 -42
  114. data/lib/rigor/environment/rbs_loader.rb +420 -518
  115. data/lib/rigor/environment/reflection.rb +28 -53
  116. data/lib/rigor/environment/reporters.rb +13 -24
  117. data/lib/rigor/environment.rb +176 -291
  118. data/lib/rigor/flow_contribution/conflict.rb +16 -27
  119. data/lib/rigor/flow_contribution/element.rb +7 -12
  120. data/lib/rigor/flow_contribution/fact.rb +33 -57
  121. data/lib/rigor/flow_contribution/merge_result.rb +6 -9
  122. data/lib/rigor/flow_contribution/merger.rb +32 -47
  123. data/lib/rigor/flow_contribution.rb +37 -55
  124. data/lib/rigor/inference/acceptance.rb +133 -219
  125. data/lib/rigor/inference/block_parameter_binder.rb +52 -82
  126. data/lib/rigor/inference/body_fixpoint.rb +22 -30
  127. data/lib/rigor/inference/budget_trace.rb +43 -63
  128. data/lib/rigor/inference/builtins/array_catalog.rb +4 -6
  129. data/lib/rigor/inference/builtins/comparable_catalog.rb +4 -6
  130. data/lib/rigor/inference/builtins/complex_catalog.rb +13 -19
  131. data/lib/rigor/inference/builtins/date_catalog.rb +38 -67
  132. data/lib/rigor/inference/builtins/encoding_catalog.rb +22 -36
  133. data/lib/rigor/inference/builtins/enumerable_catalog.rb +4 -6
  134. data/lib/rigor/inference/builtins/exception_catalog.rb +38 -57
  135. data/lib/rigor/inference/builtins/hash_catalog.rb +9 -14
  136. data/lib/rigor/inference/builtins/method_catalog.rb +29 -48
  137. data/lib/rigor/inference/builtins/numeric_catalog.rb +13 -18
  138. data/lib/rigor/inference/builtins/pathname_catalog.rb +8 -13
  139. data/lib/rigor/inference/builtins/proc_catalog.rb +38 -62
  140. data/lib/rigor/inference/builtins/random_catalog.rb +21 -32
  141. data/lib/rigor/inference/builtins/range_catalog.rb +12 -21
  142. data/lib/rigor/inference/builtins/rational_catalog.rb +9 -15
  143. data/lib/rigor/inference/builtins/re_catalog.rb +29 -48
  144. data/lib/rigor/inference/builtins/set_catalog.rb +18 -27
  145. data/lib/rigor/inference/builtins/string_catalog.rb +12 -19
  146. data/lib/rigor/inference/builtins/struct_catalog.rb +18 -30
  147. data/lib/rigor/inference/builtins/time_catalog.rb +28 -44
  148. data/lib/rigor/inference/closure_escape_analyzer.rb +44 -67
  149. data/lib/rigor/inference/coverage_scanner.rb +13 -18
  150. data/lib/rigor/inference/def_return_typer.rb +8 -14
  151. data/lib/rigor/inference/dynamic_origin.rb +23 -20
  152. data/lib/rigor/inference/expression_typer.rb +774 -1152
  153. data/lib/rigor/inference/fallback.rb +8 -12
  154. data/lib/rigor/inference/fallback_tracer.rb +4 -10
  155. data/lib/rigor/inference/flow_tracer.rb +25 -36
  156. data/lib/rigor/inference/hkt_body.rb +45 -68
  157. data/lib/rigor/inference/hkt_body_parser.rb +33 -52
  158. data/lib/rigor/inference/hkt_reducer.rb +38 -59
  159. data/lib/rigor/inference/hkt_registry.rb +50 -76
  160. data/lib/rigor/inference/indexed_narrowing.rb +55 -81
  161. data/lib/rigor/inference/macro_block_self_type.rb +21 -34
  162. data/lib/rigor/inference/method_dispatcher/array_to_h_folding.rb +11 -18
  163. data/lib/rigor/inference/method_dispatcher/block_folding.rb +49 -83
  164. data/lib/rigor/inference/method_dispatcher/call_context.rb +16 -23
  165. data/lib/rigor/inference/method_dispatcher/cgi_folding.rb +13 -22
  166. data/lib/rigor/inference/method_dispatcher/constant_folding.rb +307 -454
  167. data/lib/rigor/inference/method_dispatcher/data_folding.rb +24 -34
  168. data/lib/rigor/inference/method_dispatcher/file_folding.rb +22 -37
  169. data/lib/rigor/inference/method_dispatcher/iterator_dispatch.rb +52 -81
  170. data/lib/rigor/inference/method_dispatcher/kernel_dispatch.rb +51 -78
  171. data/lib/rigor/inference/method_dispatcher/literal_string_folding.rb +60 -97
  172. data/lib/rigor/inference/method_dispatcher/math_folding.rb +23 -34
  173. data/lib/rigor/inference/method_dispatcher/member_shape_projection.rb +18 -26
  174. data/lib/rigor/inference/method_dispatcher/method_folding.rb +37 -71
  175. data/lib/rigor/inference/method_dispatcher/overload_selector.rb +114 -182
  176. data/lib/rigor/inference/method_dispatcher/rbs_dispatch.rb +122 -193
  177. data/lib/rigor/inference/method_dispatcher/receiver_affinity.rb +15 -24
  178. data/lib/rigor/inference/method_dispatcher/reduce_folding.rb +64 -93
  179. data/lib/rigor/inference/method_dispatcher/regexp_folding.rb +31 -45
  180. data/lib/rigor/inference/method_dispatcher/set_folding.rb +6 -9
  181. data/lib/rigor/inference/method_dispatcher/shape_dispatch.rb +356 -511
  182. data/lib/rigor/inference/method_dispatcher/shellwords_folding.rb +22 -32
  183. data/lib/rigor/inference/method_dispatcher/singleton_folding.rb +15 -21
  184. data/lib/rigor/inference/method_dispatcher/struct_folding.rb +66 -82
  185. data/lib/rigor/inference/method_dispatcher/time_folding.rb +10 -15
  186. data/lib/rigor/inference/method_dispatcher/uri_folding.rb +9 -13
  187. data/lib/rigor/inference/method_dispatcher.rb +378 -524
  188. data/lib/rigor/inference/method_parameter_binder.rb +81 -114
  189. data/lib/rigor/inference/multi_target_binder.rb +51 -68
  190. data/lib/rigor/inference/mutation_widening.rb +115 -158
  191. data/lib/rigor/inference/narrowing.rb +509 -727
  192. data/lib/rigor/inference/origin_lookup.rb +38 -0
  193. data/lib/rigor/inference/parameter_inference_collector.rb +100 -100
  194. data/lib/rigor/inference/precision_scanner.rb +26 -35
  195. data/lib/rigor/inference/project_patched_methods.rb +20 -32
  196. data/lib/rigor/inference/project_patched_scanner.rb +23 -37
  197. data/lib/rigor/inference/protection_scanner.rb +16 -18
  198. data/lib/rigor/inference/rbs_type_translator.rb +49 -66
  199. data/lib/rigor/inference/scope_indexer.rb +473 -817
  200. data/lib/rigor/inference/statement_evaluator.rb +706 -1111
  201. data/lib/rigor/inference/struct_fold_safety.rb +34 -48
  202. data/lib/rigor/inference/synthetic_method.rb +9 -16
  203. data/lib/rigor/inference/synthetic_method_index.rb +20 -35
  204. data/lib/rigor/inference/synthetic_method_scanner.rb +63 -103
  205. data/lib/rigor/language_server/buffer_resolution.rb +6 -9
  206. data/lib/rigor/language_server/buffer_table.rb +11 -18
  207. data/lib/rigor/language_server/completion_provider.rb +69 -116
  208. data/lib/rigor/language_server/debouncer.rb +16 -25
  209. data/lib/rigor/language_server/diagnostic_publisher.rb +26 -40
  210. data/lib/rigor/language_server/document_symbol_provider.rb +14 -19
  211. data/lib/rigor/language_server/folding_range_provider.rb +11 -19
  212. data/lib/rigor/language_server/hover_provider.rb +8 -11
  213. data/lib/rigor/language_server/hover_renderer.rb +56 -93
  214. data/lib/rigor/language_server/loop.rb +14 -19
  215. data/lib/rigor/language_server/project_context.rb +46 -74
  216. data/lib/rigor/language_server/selection_range_provider.rb +9 -12
  217. data/lib/rigor/language_server/server.rb +55 -83
  218. data/lib/rigor/language_server/signature_help_provider.rb +29 -46
  219. data/lib/rigor/language_server/synchronized_writer.rb +4 -7
  220. data/lib/rigor/language_server/uri.rb +8 -13
  221. data/lib/rigor/language_server.rb +4 -6
  222. data/lib/rigor/mcp/loop.rb +2 -3
  223. data/lib/rigor/mcp/server.rb +4 -7
  224. data/lib/rigor/mcp.rb +3 -6
  225. data/lib/rigor/plugin/access_denied_error.rb +5 -8
  226. data/lib/rigor/plugin/additional_initializer.rb +21 -31
  227. data/lib/rigor/plugin/base.rb +335 -518
  228. data/lib/rigor/plugin/blueprint.rb +14 -23
  229. data/lib/rigor/plugin/box.rb +18 -29
  230. data/lib/rigor/plugin/fact_store.rb +16 -26
  231. data/lib/rigor/plugin/inflector.rb +37 -53
  232. data/lib/rigor/plugin/io_boundary.rb +33 -56
  233. data/lib/rigor/plugin/isolation.rb +42 -55
  234. data/lib/rigor/plugin/load_error.rb +10 -15
  235. data/lib/rigor/plugin/loader.rb +30 -49
  236. data/lib/rigor/plugin/macro/block_as_method.rb +20 -32
  237. data/lib/rigor/plugin/macro/heredoc_template.rb +35 -58
  238. data/lib/rigor/plugin/macro/nested_class_template.rb +22 -36
  239. data/lib/rigor/plugin/macro/trait_registry.rb +34 -51
  240. data/lib/rigor/plugin/macro.rb +10 -15
  241. data/lib/rigor/plugin/manifest.rb +85 -144
  242. data/lib/rigor/plugin/node_context.rb +14 -22
  243. data/lib/rigor/plugin/node_rule_walk.rb +49 -74
  244. data/lib/rigor/plugin/protocol_contract.rb +25 -39
  245. data/lib/rigor/plugin/registry.rb +132 -205
  246. data/lib/rigor/plugin/services.rb +21 -33
  247. data/lib/rigor/plugin/source_rbs_synthesis_reporter.rb +11 -19
  248. data/lib/rigor/plugin/trust_policy.rb +24 -38
  249. data/lib/rigor/plugin/type_node_resolver.rb +15 -24
  250. data/lib/rigor/protection/diagnostic_oracle.rb +11 -13
  251. data/lib/rigor/protection/mutation_scanner.rb +27 -35
  252. data/lib/rigor/protection/mutator.rb +50 -70
  253. data/lib/rigor/protection/test_suite_oracle.rb +20 -27
  254. data/lib/rigor/rbs_extended/conformance_checker.rb +48 -68
  255. data/lib/rigor/rbs_extended/hkt_directives.rb +28 -54
  256. data/lib/rigor/rbs_extended/reporter.rb +24 -40
  257. data/lib/rigor/rbs_extended.rb +107 -197
  258. data/lib/rigor/reflection.rb +68 -86
  259. data/lib/rigor/scope/discovery_index.rb +14 -19
  260. data/lib/rigor/scope.rb +255 -310
  261. data/lib/rigor/sig_gen/classification.rb +6 -10
  262. data/lib/rigor/sig_gen/generator.rb +197 -323
  263. data/lib/rigor/sig_gen/layout_index.rb +12 -20
  264. data/lib/rigor/sig_gen/method_candidate.rb +12 -17
  265. data/lib/rigor/sig_gen/observation_collector.rb +38 -70
  266. data/lib/rigor/sig_gen/observed_call.rb +13 -23
  267. data/lib/rigor/sig_gen/path_mapper.rb +17 -29
  268. data/lib/rigor/sig_gen/renderer.rb +7 -13
  269. data/lib/rigor/sig_gen/type_elaborator.rb +15 -28
  270. data/lib/rigor/sig_gen/write_result.rb +8 -16
  271. data/lib/rigor/sig_gen/writer.rb +95 -174
  272. data/lib/rigor/sig_gen.rb +3 -6
  273. data/lib/rigor/signature_path_audit.rb +24 -30
  274. data/lib/rigor/source/constant_path.rb +10 -14
  275. data/lib/rigor/source/literals.rb +31 -45
  276. data/lib/rigor/source/node_locator.rb +9 -11
  277. data/lib/rigor/source/node_walker.rb +9 -13
  278. data/lib/rigor/source.rb +3 -4
  279. data/lib/rigor/testing.rb +16 -20
  280. data/lib/rigor/triage/catalogue.rb +38 -62
  281. data/lib/rigor/triage.rb +31 -52
  282. data/lib/rigor/trinary.rb +9 -13
  283. data/lib/rigor/type/acceptance_router.rb +4 -6
  284. data/lib/rigor/type/accepts_result.rb +10 -14
  285. data/lib/rigor/type/app.rb +19 -27
  286. data/lib/rigor/type/bot.rb +4 -6
  287. data/lib/rigor/type/bound_method.rb +10 -15
  288. data/lib/rigor/type/combinator.rb +165 -257
  289. data/lib/rigor/type/constant.rb +23 -34
  290. data/lib/rigor/type/data_class.rb +10 -15
  291. data/lib/rigor/type/data_instance.rb +14 -20
  292. data/lib/rigor/type/difference.rb +21 -32
  293. data/lib/rigor/type/dynamic.rb +3 -5
  294. data/lib/rigor/type/hash_shape.rb +32 -18
  295. data/lib/rigor/type/integer_range.rb +11 -16
  296. data/lib/rigor/type/intersection.rb +27 -42
  297. data/lib/rigor/type/nominal.rb +10 -15
  298. data/lib/rigor/type/plain_lattice.rb +9 -13
  299. data/lib/rigor/type/refined.rb +67 -114
  300. data/lib/rigor/type/singleton.rb +4 -6
  301. data/lib/rigor/type/struct_class.rb +11 -16
  302. data/lib/rigor/type/struct_instance.rb +15 -21
  303. data/lib/rigor/type/tuple.rb +14 -19
  304. data/lib/rigor/type/union.rb +30 -42
  305. data/lib/rigor/type_node/generic.rb +14 -26
  306. data/lib/rigor/type_node/identifier.rb +12 -19
  307. data/lib/rigor/type_node.rb +3 -12
  308. data/lib/rigor/value_semantics.rb +16 -21
  309. data/lib/rigor/version.rb +1 -1
  310. data/plugins/rigor-actioncable/lib/rigor/plugin/actioncable/analyzer.rb +18 -30
  311. data/plugins/rigor-actioncable/lib/rigor/plugin/actioncable/channel_discoverer.rb +16 -29
  312. data/plugins/rigor-actioncable/lib/rigor/plugin/actioncable/channel_index.rb +12 -23
  313. data/plugins/rigor-actioncable/lib/rigor/plugin/actioncable.rb +22 -39
  314. data/plugins/rigor-actionmailer/lib/rigor/plugin/actionmailer/analyzer.rb +26 -46
  315. data/plugins/rigor-actionmailer/lib/rigor/plugin/actionmailer/mailer_discoverer.rb +51 -87
  316. data/plugins/rigor-actionmailer/lib/rigor/plugin/actionmailer/mailer_index.rb +10 -17
  317. data/plugins/rigor-actionmailer/lib/rigor/plugin/actionmailer.rb +26 -39
  318. data/plugins/rigor-actionpack/lib/rigor/plugin/actionpack/analyzer.rb +118 -176
  319. data/plugins/rigor-actionpack/lib/rigor/plugin/actionpack/controller_discoverer.rb +30 -51
  320. data/plugins/rigor-actionpack/lib/rigor/plugin/actionpack/controller_index.rb +57 -92
  321. data/plugins/rigor-actionpack/lib/rigor/plugin/actionpack.rb +71 -108
  322. data/plugins/rigor-activejob/lib/rigor/plugin/activejob/analyzer.rb +13 -22
  323. data/plugins/rigor-activejob/lib/rigor/plugin/activejob/job_discoverer.rb +15 -25
  324. data/plugins/rigor-activejob/lib/rigor/plugin/activejob/job_index.rb +8 -13
  325. data/plugins/rigor-activejob/lib/rigor/plugin/activejob.rb +16 -24
  326. data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord/analyzer.rb +30 -47
  327. data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord/model_discoverer.rb +79 -129
  328. data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord/model_index.rb +36 -57
  329. data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord/schema_parser.rb +27 -41
  330. data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord/schema_table.rb +16 -24
  331. data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord.rb +148 -235
  332. data/plugins/rigor-activerecord/lib/rigor-activerecord.rb +3 -5
  333. data/plugins/rigor-activestorage/lib/rigor/plugin/activestorage/analyzer.rb +8 -14
  334. data/plugins/rigor-activestorage/lib/rigor/plugin/activestorage/attachment_discoverer.rb +9 -15
  335. data/plugins/rigor-activestorage/lib/rigor/plugin/activestorage/attachment_index.rb +4 -7
  336. data/plugins/rigor-activestorage/lib/rigor/plugin/activestorage.rb +23 -41
  337. data/plugins/rigor-activestorage/lib/rigor-activestorage.rb +3 -5
  338. data/plugins/rigor-activesupport-core-ext/lib/rigor/plugin/activesupport_core_ext.rb +6 -10
  339. data/plugins/rigor-activesupport-core-ext/lib/rigor-activesupport-core-ext.rb +8 -11
  340. data/plugins/rigor-devise/lib/rigor/plugin/devise.rb +30 -45
  341. data/plugins/rigor-devise/lib/rigor-devise.rb +3 -5
  342. data/plugins/rigor-dry-schema/lib/rigor/plugin/dry_schema/schema_scanner.rb +43 -69
  343. data/plugins/rigor-dry-schema/lib/rigor/plugin/dry_schema.rb +26 -35
  344. data/plugins/rigor-dry-schema/lib/rigor-dry-schema.rb +3 -5
  345. data/plugins/rigor-dry-struct/lib/rigor/plugin/dry_struct.rb +30 -47
  346. data/plugins/rigor-dry-struct/lib/rigor-dry-struct.rb +3 -5
  347. data/plugins/rigor-dry-types/lib/rigor/plugin/dry_types/alias_scanner.rb +67 -113
  348. data/plugins/rigor-dry-types/lib/rigor/plugin/dry_types.rb +29 -46
  349. data/plugins/rigor-dry-types/lib/rigor-dry-types.rb +3 -5
  350. data/plugins/rigor-dry-validation/lib/rigor/plugin/dry_validation/contract_scanner.rb +13 -20
  351. data/plugins/rigor-dry-validation/lib/rigor/plugin/dry_validation.rb +14 -23
  352. data/plugins/rigor-dry-validation/lib/rigor-dry-validation.rb +2 -3
  353. data/plugins/rigor-factorybot/lib/rigor/plugin/factorybot/analyzer.rb +22 -37
  354. data/plugins/rigor-factorybot/lib/rigor/plugin/factorybot/factory_discoverer.rb +28 -46
  355. data/plugins/rigor-factorybot/lib/rigor/plugin/factorybot/factory_index.rb +13 -17
  356. data/plugins/rigor-factorybot/lib/rigor/plugin/factorybot.rb +30 -47
  357. data/plugins/rigor-graphql/lib/rigor/plugin/graphql/type_scanner.rb +52 -80
  358. data/plugins/rigor-graphql/lib/rigor/plugin/graphql.rb +21 -31
  359. data/plugins/rigor-graphql/lib/rigor-graphql.rb +3 -5
  360. data/plugins/rigor-hanami/lib/rigor/plugin/hanami/action_checker.rb +11 -17
  361. data/plugins/rigor-hanami/lib/rigor/plugin/hanami.rb +20 -34
  362. data/plugins/rigor-mangrove/lib/rigor/plugin/mangrove.rb +45 -75
  363. data/plugins/rigor-minitest/lib/rigor/plugin/minitest/assertion_analyzer.rb +22 -36
  364. data/plugins/rigor-minitest/lib/rigor/plugin/minitest.rb +22 -41
  365. data/plugins/rigor-pundit/lib/rigor/plugin/pundit/analyzer.rb +17 -32
  366. data/plugins/rigor-pundit/lib/rigor/plugin/pundit/policy_discoverer.rb +6 -12
  367. data/plugins/rigor-pundit/lib/rigor/plugin/pundit/policy_index.rb +4 -8
  368. data/plugins/rigor-pundit/lib/rigor/plugin/pundit.rb +17 -32
  369. data/plugins/rigor-rails/lib/rigor-rails.rb +11 -18
  370. data/plugins/rigor-rails-i18n/lib/rigor/plugin/rails_i18n/analyzer.rb +58 -92
  371. data/plugins/rigor-rails-i18n/lib/rigor/plugin/rails_i18n/locale_index.rb +20 -34
  372. data/plugins/rigor-rails-i18n/lib/rigor/plugin/rails_i18n/locale_loader.rb +20 -31
  373. data/plugins/rigor-rails-i18n/lib/rigor/plugin/rails_i18n.rb +49 -82
  374. data/plugins/rigor-rails-routes/lib/rigor/plugin/rails_routes/analyzer.rb +55 -92
  375. data/plugins/rigor-rails-routes/lib/rigor/plugin/rails_routes/devise_routes.rb +51 -90
  376. data/plugins/rigor-rails-routes/lib/rigor/plugin/rails_routes/doorkeeper_routes.rb +17 -30
  377. data/plugins/rigor-rails-routes/lib/rigor/plugin/rails_routes/helper_discoverer.rb +38 -70
  378. data/plugins/rigor-rails-routes/lib/rigor/plugin/rails_routes/helper_table.rb +42 -74
  379. data/plugins/rigor-rails-routes/lib/rigor/plugin/rails_routes/routes_parser.rb +280 -497
  380. data/plugins/rigor-rails-routes/lib/rigor/plugin/rails_routes.rb +54 -89
  381. data/plugins/rigor-rbs-inline/lib/rigor/plugin/rbs_inline.rb +32 -54
  382. data/plugins/rigor-rbs-inline/lib/rigor-rbs-inline.rb +8 -13
  383. data/plugins/rigor-rspec/lib/rigor/plugin/rspec/analyzer.rb +9 -16
  384. data/plugins/rigor-rspec/lib/rigor/plugin/rspec/let_scope_index.rb +24 -39
  385. data/plugins/rigor-rspec/lib/rigor/plugin/rspec/let_type_resolver.rb +13 -22
  386. data/plugins/rigor-rspec/lib/rigor/plugin/rspec/matcher_analyzer.rb +34 -56
  387. data/plugins/rigor-rspec/lib/rigor/plugin/rspec/scope_walker.rb +22 -42
  388. data/plugins/rigor-rspec/lib/rigor/plugin/rspec.rb +43 -75
  389. data/plugins/rigor-rspec-rails/lib/rigor/plugin/rspec_rails/have_http_status_analyzer.rb +27 -39
  390. data/plugins/rigor-rspec-rails/lib/rigor/plugin/rspec_rails/http_status_codes.rb +14 -19
  391. data/plugins/rigor-rspec-rails/lib/rigor/plugin/rspec_rails.rb +20 -34
  392. data/plugins/rigor-shoulda-matchers/lib/rigor/plugin/shoulda_matchers/analyzer.rb +28 -47
  393. data/plugins/rigor-shoulda-matchers/lib/rigor/plugin/shoulda_matchers.rb +21 -37
  394. data/plugins/rigor-sidekiq/lib/rigor/plugin/sidekiq/analyzer.rb +13 -22
  395. data/plugins/rigor-sidekiq/lib/rigor/plugin/sidekiq/worker_discoverer.rb +18 -31
  396. data/plugins/rigor-sidekiq/lib/rigor/plugin/sidekiq/worker_index.rb +6 -10
  397. data/plugins/rigor-sidekiq/lib/rigor/plugin/sidekiq.rb +14 -29
  398. data/plugins/rigor-sinatra/lib/rigor/plugin/sinatra.rb +23 -41
  399. data/plugins/rigor-sinatra/lib/rigor-sinatra.rb +3 -5
  400. data/plugins/rigor-sorbet/lib/rigor/plugin/sorbet/absurd_recognizer.rb +23 -38
  401. data/plugins/rigor-sorbet/lib/rigor/plugin/sorbet/assertion_recognizer.rb +57 -96
  402. data/plugins/rigor-sorbet/lib/rigor/plugin/sorbet/catalog.rb +22 -35
  403. data/plugins/rigor-sorbet/lib/rigor/plugin/sorbet/catalog_walker.rb +41 -69
  404. data/plugins/rigor-sorbet/lib/rigor/plugin/sorbet/method_signature.rb +6 -10
  405. data/plugins/rigor-sorbet/lib/rigor/plugin/sorbet/sig_parser.rb +24 -41
  406. data/plugins/rigor-sorbet/lib/rigor/plugin/sorbet/sigil_detector.rb +29 -50
  407. data/plugins/rigor-sorbet/lib/rigor/plugin/sorbet/type_translator.rb +55 -88
  408. data/plugins/rigor-sorbet/lib/rigor/plugin/sorbet.rb +159 -262
  409. data/plugins/rigor-statesman/lib/rigor/plugin/statesman.rb +21 -29
  410. data/plugins/rigor-statesman/lib/rigor-statesman.rb +3 -5
  411. data/plugins/rigor-typescript-utility-types/lib/rigor/plugin/typescript_utility_types.rb +23 -37
  412. data/plugins/rigor-typescript-utility-types/lib/rigor-typescript-utility-types.rb +4 -6
  413. data/sig/rigor/scope.rbs +6 -0
  414. metadata +2 -1
@@ -12,21 +12,16 @@ require_relative "struct_fold_safety"
12
12
 
13
13
  module Rigor
14
14
  module Inference
15
- # Builds a per-node scope index for a Prism program by running
16
- # `Rigor::Inference::StatementEvaluator` over the root and recording
17
- # the entry scope visible at every node. Expression-interior nodes
18
- # the evaluator does not specialise (call receivers, arguments,
19
- # array/hash elements, ...) inherit their nearest statement-y
20
- # ancestor's recorded scope, so a downstream caller that looks up
21
- # the scope for any Prism node in the tree always gets the scope
22
- # that was effectively visible at that point.
15
+ # Builds a per-node scope index for a Prism program by running `Rigor::Inference::StatementEvaluator` over the root
16
+ # and recording the entry scope visible at every node. Expression-interior nodes the evaluator does not specialise
17
+ # (call receivers, arguments, array/hash elements, ...) inherit their nearest statement-y ancestor's recorded scope,
18
+ # so a downstream caller that looks up the scope for any Prism node in the tree always gets the scope that was
19
+ # effectively visible at that point.
23
20
  #
24
- # The CLI commands `rigor type-of` and `rigor type-scan` consume
25
- # the index so that local-variable bindings established earlier in
26
- # the program are visible to the typer when probing later nodes.
27
- # Without the index, both commands would type every node under an
28
- # empty scope and miss the constant-folding / dispatch precision
29
- # that Slice 3 phase 2's StatementEvaluator unlocks.
21
+ # The CLI commands `rigor type-of` and `rigor type-scan` consume the index so that local-variable bindings
22
+ # established earlier in the program are visible to the typer when probing later nodes. Without the index, both
23
+ # commands would type every node under an empty scope and miss the constant-folding / dispatch precision that Slice
24
+ # 3 phase 2's StatementEvaluator unlocks.
30
25
  #
31
26
  # The returned object is an identity-comparing Hash:
32
27
  #
@@ -35,11 +30,9 @@ module Rigor
35
30
  # index[some_prism_node] #=> the Rigor::Scope visible at that node
36
31
  # ```
37
32
  #
38
- # Nodes that are not part of the program subtree (e.g. synthesised
39
- # virtual nodes that the caller looks up after the fact) yield the
40
- # `default_scope`. The returned Hash is mutable in principle but
41
- # callers MUST treat it as read-only; the indexer itself never
42
- # exposes a way to update it past construction.
33
+ # Nodes that are not part of the program subtree (e.g. synthesised virtual nodes that the caller looks up after the
34
+ # fact) yield the `default_scope`. The returned Hash is mutable in principle but callers MUST treat it as read-only;
35
+ # the indexer itself never exposes a way to update it past construction.
43
36
  # rubocop:disable Metrics/ModuleLength
44
37
  module ScopeIndexer
45
38
  module_function
@@ -59,98 +52,68 @@ module Rigor
59
52
  # @return [Hash{Prism::Node => Rigor::Scope}] identity-comparing
60
53
  # table whose default value is `default_scope`.
61
54
  def index(root, default_scope:, converged_loop_recording: false) # rubocop:disable Metrics/AbcSize
62
- # Slice A-declarations. Build the declaration overrides
63
- # first so every scope handed to the StatementEvaluator
64
- # already carries the table; structural sharing through
65
- # `Scope#with_local` / `#with_fact` / `#with_self_type`
55
+ # Slice A-declarations. Build the declaration overrides first so every scope handed to the StatementEvaluator
56
+ # already carries the table; structural sharing through `Scope#with_local` / `#with_fact` / `#with_self_type`
66
57
  # propagates it across every derived scope.
67
58
  declared_types, discovered_classes = build_declaration_artifacts(root)
68
- # Merge the indexer's findings on top of whatever the
69
- # base scope already carries so callers that seed
70
- # cross-file class knowledge (e.g. the ADR-14
71
- # `SigGen::ObservationCollector` pre-walking project
72
- # `lib/` before scanning `spec/`) keep their seeds
73
- # alongside the per-file declarations the indexer
74
- # itself discovers. Indexer-found entries win on
75
- # collision — same-file declarations are the most
76
- # specific authority.
59
+ # Merge the indexer's findings on top of whatever the base scope already carries so callers that seed cross-file
60
+ # class knowledge (e.g. the ADR-14 `SigGen::ObservationCollector` pre-walking project `lib/` before scanning
61
+ # `spec/`) keep their seeds alongside the per-file declarations the indexer itself discovers. Indexer-found
62
+ # entries win on collision — same-file declarations are the most specific authority.
77
63
  merged_classes = default_scope.discovered_classes.merge(discovered_classes)
78
64
  seeded_scope = default_scope.with_discovery(
79
65
  default_scope.discovery.with(declared_types: declared_types,
80
66
  discovered_classes: merged_classes)
81
67
  )
82
68
 
83
- # Slice 7 phase 2. Pre-pass over every class/module body
84
- # to collect the per-class ivar accumulator. Seeded after
85
- # declared_types so the rvalue typer in the pre-pass can
86
- # see declaration overrides.
69
+ # Slice 7 phase 2. Pre-pass over every class/module body to collect the per-class ivar accumulator. Seeded after
70
+ # declared_types so the rvalue typer in the pre-pass can see declaration overrides.
87
71
  class_ivars = build_class_ivar_index(root, seeded_scope)
88
72
  seeded_scope = seeded_scope.with_discovery(seeded_scope.discovery.with(class_ivars: class_ivars))
89
73
 
90
- # Slice 7 phase 6. Same pre-pass shape for cvars (per
91
- # class) and globals (program-wide). Globals are also
92
- # materialised into the top-level scope's `globals` map
93
- # so reads at the top level (and in CLI probes that do
94
- # not enter a method body) observe the precise type
95
- # without consulting the accumulator on every lookup.
74
+ # Slice 7 phase 6. Same pre-pass shape for cvars (per class) and globals (program-wide). Globals are also
75
+ # materialised into the top-level scope's `globals` map so reads at the top level (and in CLI probes that do not
76
+ # enter a method body) observe the precise type without consulting the accumulator on every lookup.
96
77
  class_cvars = build_class_cvar_index(root, seeded_scope)
97
78
  seeded_scope = seeded_scope.with_discovery(seeded_scope.discovery.with(class_cvars: class_cvars))
98
79
  program_globals = build_program_global_index(root, seeded_scope)
99
80
  seeded_scope = seeded_scope.with_discovery(seeded_scope.discovery.with(program_globals: program_globals))
100
81
  program_globals.each { |name, type| seeded_scope = seeded_scope.with_global(name, type) }
101
82
 
102
- # Slice 7 phase 9. In-source constant value tracking.
103
- # Walks every ConstantWriteNode/ConstantPathWriteNode in
104
- # the program and types its rvalue under a scope that
105
- # carries the surrounding qualified prefix as
106
- # `self_type`, so the rvalue typer sees in-class
107
- # references resolve correctly. Multiple writes to the
108
- # same qualified name union via `Type::Combinator.union`.
83
+ # Slice 7 phase 9. In-source constant value tracking. Walks every ConstantWriteNode/ConstantPathWriteNode in the
84
+ # program and types its rvalue under a scope that carries the surrounding qualified prefix as `self_type`, so
85
+ # the rvalue typer sees in-class references resolve correctly. Multiple writes to the same qualified name union
86
+ # via `Type::Combinator.union`.
109
87
  in_source_constants = build_in_source_constants(root, seeded_scope)
110
88
  seeded_scope = seeded_scope.with_discovery(
111
89
  seeded_scope.discovery.with(in_source_constants: in_source_constants)
112
90
  )
113
91
 
114
- # Slice 7 phase 12. In-source method discovery. Walks
115
- # every class/module body for `Prism::DefNode` and
116
- # recognised `define_method` calls and records the
117
- # introduced method names. `rigor check` consults the
118
- # table to suppress false positives for methods the
119
- # user has defined but no RBS sig describes. Merged
120
- # UNDER the cross-file pre-pass seed; details: merge_project_method_indexes.
121
- # One combined descent yields both the discovered-methods existence
122
- # table and the instance def-node table — see
123
- # {#build_methods_and_def_nodes}. `seed_discovered_methods` seeds the
124
- # former onto the scope and returns the def-node table for
92
+ # Slice 7 phase 12. In-source method discovery. Walks every class/module body for `Prism::DefNode` and
93
+ # recognised `define_method` calls and records the introduced method names. `rigor check` consults the table to
94
+ # suppress false positives for methods the user has defined but no RBS sig describes. Merged UNDER the
95
+ # cross-file pre-pass seed; details: merge_project_method_indexes. One combined descent yields both the
96
+ # discovered-methods existence table and the instance def-node table see {#build_methods_and_def_nodes}.
97
+ # `seed_discovered_methods` seeds the former onto the scope and returns the def-node table for
125
98
  # `merge_project_method_indexes` below.
126
99
  seeded_scope, file_def_nodes = seed_discovered_methods(seeded_scope, default_scope, root)
127
100
 
128
- # v0.0.2 #5 + ADR-24 slice 2 — record per-instance-method
129
- # def nodes, the class -> superclass map, and the
130
- # class/module -> included-modules map, each merged under
131
- # the cross-file pre-pass seed (see below).
132
- # v0.1.2 per-class table of method visibilities
133
- # (`:public` / `:private` / `:protected`). The
134
- # `def.method-visibility-mismatch` and ADR-35
135
- # `def.override-visibility-reduced` CheckRules consult the
136
- # table. Seeded inside `merge_project_method_indexes` so the
137
- # per-file visibilities merge OVER the cross-file project seed
138
- # rather than overwriting it.
101
+ # v0.0.2 #5 + ADR-24 slice 2 — record per-instance-method def nodes, the class -> superclass map, and the
102
+ # class/module -> included-modules map, each merged under the cross-file pre-pass seed (see below). v0.1.2 —
103
+ # per-class table of method visibilities (`:public` / `:private` / `:protected`). The
104
+ # `def.method-visibility-mismatch` and ADR-35 `def.override-visibility-reduced` CheckRules consult the table.
105
+ # Seeded inside `merge_project_method_indexes` so the per-file visibilities merge OVER the cross-file project
106
+ # seed rather than overwriting it.
139
107
  seeded_scope = merge_project_method_indexes(seeded_scope, default_scope, root, file_def_nodes)
140
108
 
141
109
  table = {}.compare_by_identity
142
110
  table.default = seeded_scope
143
111
 
144
- # Last-visit-wins, not first: when `StatementEvaluator`
145
- # internally re-evaluates a subtree (notably `eval_begin`'s
146
- # retry-edge widening pass), the LATER visit carries the
147
- # corrected entry scope (e.g. a `tries` widened to
148
- # `Nominal[Integer]` after the rescue body's `tries += 1;
149
- # retry` is observed). The diagnostic layer reads
150
- # `table[node]` to type predicates; the second pass's
151
- # entry is the one that reflects all flow-derived
152
- # rebinds, so it MUST overwrite the first.
153
- # ADR-48 Struct slice 3 — install the top-level fold-safe-local set so
112
+ # Last-visit-wins, not first: when `StatementEvaluator` internally re-evaluates a subtree (notably
113
+ # `eval_begin`'s retry-edge widening pass), the LATER visit carries the corrected entry scope (e.g. a `tries`
114
+ # widened to `Nominal[Integer]` after the rescue body's `tries += 1; retry` is observed). The diagnostic layer
115
+ # reads `table[node]` to type predicates; the second pass's entry is the one that reflects all flow-derived
116
+ # rebinds, so it MUST overwrite the first. ADR-48 Struct slice 3 — install the top-level fold-safe-local set so
154
117
  # a member read off a mutation-free top-level struct binding folds.
155
118
  seeded_scope = seed_struct_fold_safe(seeded_scope, root)
156
119
 
@@ -162,12 +125,10 @@ module Rigor
162
125
  table
163
126
  end
164
127
 
165
- # Runs the combined methods/def-nodes descent (one walk of the file),
166
- # seeds the discovered-methods existence table onto `seeded_scope`
167
- # (merged UNDER the cross-file pre-pass seed `default_scope` carries),
168
- # and returns `[scope, file_def_nodes]` so the caller can thread the
169
- # def-node table into {#merge_project_method_indexes} without walking
170
- # the file a second time.
128
+ # Runs the combined methods/def-nodes descent (one walk of the file), seeds the discovered-methods existence table
129
+ # onto `seeded_scope` (merged UNDER the cross-file pre-pass seed `default_scope` carries), and returns `[scope,
130
+ # file_def_nodes]` so the caller can thread the def-node table into {#merge_project_method_indexes} without
131
+ # walking the file a second time.
171
132
  def seed_discovered_methods(seeded_scope, default_scope, root)
172
133
  file_methods, file_def_nodes = build_methods_and_def_nodes(root)
173
134
  discovered_methods = deep_merge_class_methods(default_scope.discovered_methods, file_methods)
@@ -175,9 +136,8 @@ module Rigor
175
136
  [scope, file_def_nodes]
176
137
  end
177
138
 
178
- # ADR-48 Struct slice 3 — installs the top-level fold-safe-local set
179
- # ({Inference::StructFoldSafety}). Struct member layouts of constant
180
- # receivers are resolved through the side-table the seeded scope carries.
139
+ # ADR-48 Struct slice 3 — installs the top-level fold-safe-local set ({Inference::StructFoldSafety}). Struct
140
+ # member layouts of constant receivers are resolved through the side-table the seeded scope carries.
181
141
  def seed_struct_fold_safe(seeded_scope, root)
182
142
  seeded_scope.with_struct_fold_safe(
183
143
  StructFoldSafety.fold_safe_locals(
@@ -186,14 +146,10 @@ module Rigor
186
146
  )
187
147
  end
188
148
 
189
- # v0.0.2 #5 + ADR-24 slice 2 — seeds the three
190
- # project-method indexes onto `seeded_scope`: the
191
- # per-instance-method def-node table, the class ->
192
- # superclass map, and the class/module -> included-modules
193
- # map. Each per-file table is merged UNDER the cross-file
194
- # `discovered_def_index_for_paths` seed carried on
195
- # `default_scope` — same-file declarations win per entry,
196
- # the cross-file seed supplies sibling-file ancestors.
149
+ # v0.0.2 #5 + ADR-24 slice 2 — seeds the three project-method indexes onto `seeded_scope`: the per-instance-method
150
+ # def-node table, the class -> superclass map, and the class/module -> included-modules map. Each per-file table
151
+ # is merged UNDER the cross-file `discovered_def_index_for_paths` seed carried on `default_scope` — same-file
152
+ # declarations win per entry, the cross-file seed supplies sibling-file ancestors.
197
153
  def merge_project_method_indexes(seeded_scope, default_scope, root, file_def_nodes)
198
154
  def_nodes = default_scope.discovered_def_nodes.merge(
199
155
  file_def_nodes
@@ -207,14 +163,13 @@ module Rigor
207
163
  includes = default_scope.discovered_includes.merge(
208
164
  build_discovered_includes(root)
209
165
  ) { |_class, cross_file, per_file| (cross_file + per_file).uniq }
210
- # ADR-35 — per-file visibilities merged OVER the cross-file
211
- # seed (the current file is authoritative for its own classes;
212
- # sibling-file ancestors are preserved from the project seed).
166
+ # ADR-35 — per-file visibilities merged OVER the cross-file seed (the current file is authoritative for its own
167
+ # classes; sibling-file ancestors are preserved from the project seed).
213
168
  method_visibilities = default_scope.discovered_method_visibilities.merge(
214
169
  build_discovered_method_visibilities(root)
215
170
  ) { |_class, cross_file, per_file| cross_file.merge(per_file) }
216
- # ADR-48 — per-file Data + Struct member layouts merged OVER the
217
- # cross-file seed (same-file declaration is authoritative).
171
+ # ADR-48 — per-file Data + Struct member layouts merged OVER the cross-file seed (same-file declaration is
172
+ # authoritative).
218
173
  data_member_layouts, struct_member_layouts = merge_member_layouts(default_scope, root)
219
174
 
220
175
  seeded_scope.with_discovery(
@@ -230,10 +185,9 @@ module Rigor
230
185
  )
231
186
  end
232
187
 
233
- # ADR-48 — the per-file Data + Struct member-layout tables, each merged
234
- # OVER the cross-file seed so a same-file declaration wins for its own
235
- # classes. Returned as a pair to keep {#merge_project_method_indexes}
236
- # under the method-size budget.
188
+ # ADR-48 — the per-file Data + Struct member-layout tables, each merged OVER the cross-file seed so a same-file
189
+ # declaration wins for its own classes. Returned as a pair to keep {#merge_project_method_indexes} under the
190
+ # method-size budget.
237
191
  def merge_member_layouts(default_scope, root)
238
192
  [
239
193
  default_scope.data_member_layouts.merge(build_data_member_layouts(root)),
@@ -241,29 +195,21 @@ module Rigor
241
195
  ]
242
196
  end
243
197
 
244
- # Slice 7 phase 2. Builds the class-level ivar accumulator
245
- # by walking every `Prism::ClassNode` / `Prism::ModuleNode`
246
- # body, descending into each nested `Prism::DefNode`, and
247
- # typing every `Prism::InstanceVariableWriteNode` rvalue
248
- # under a scope that carries the appropriate `self_type`
249
- # for that def (singleton vs instance). The rvalue is
250
- # typed with NO local bindings — the pre-pass lacks
251
- # statement-level threading — so `@x = 1` records
252
- # `Constant[1]` but `@x = some_local + 1` records
253
- # `Dynamic[Top]` (since `some_local` is unbound at
254
- # pre-pass time). Multiple writes to the same ivar union
255
- # via `Type::Combinator.union`.
198
+ # Slice 7 phase 2. Builds the class-level ivar accumulator by walking every `Prism::ClassNode` /
199
+ # `Prism::ModuleNode` body, descending into each nested `Prism::DefNode`, and typing every
200
+ # `Prism::InstanceVariableWriteNode` rvalue under a scope that carries the appropriate `self_type` for that def
201
+ # (singleton vs instance). The rvalue is typed with NO local bindings — the pre-pass lacks statement-level
202
+ # threading so `@x = 1` records `Constant[1]` but `@x = some_local + 1` records `Dynamic[Top]` (since
203
+ # `some_local` is unbound at pre-pass time). Multiple writes to the same ivar union via `Type::Combinator.union`.
256
204
  def build_class_ivar_index(root, default_scope)
257
205
  accumulator = {}
258
206
  mutated_ivars = {}
259
207
  read_before_write = {}
260
208
  init_writes = {}
261
- # WD3 — per-class summary of `{class_name => {method_name =>
262
- # Set<ivar names definitely assigned non-nil on every
263
- # completing path>}}`, consulted by `dead_transient_nil_writes`
264
- # so a ctor that reassigns `@x` indirectly through an
265
- # unconditional same-class method call (`mask!`) credits the
266
- # overwrite. Built once per program here, memoised by class.
209
+ # WD3 — per-class summary of `{class_name => {method_name => Set<ivar names definitely assigned non-nil on every
210
+ # completing path>}}`, consulted by `dead_transient_nil_writes` so a ctor that reassigns `@x` indirectly through
211
+ # an unconditional same-class method call (`mask!`) credits the overwrite. Built once per program here, memoised
212
+ # by class.
267
213
  method_assign_effects = build_method_assign_effects(root)
268
214
  walk_class_ivars(root, [], default_scope, accumulator, mutated_ivars,
269
215
  read_before_write, init_writes, method_assign_effects)
@@ -272,21 +218,15 @@ module Rigor
272
218
  accumulator.transform_values(&:freeze).freeze
273
219
  end
274
220
 
275
- # B2.3 — finalize the read-before-write nil contribution.
276
- # For each class, for each ivar where SOME method body
277
- # observed a read-before-write AND no `initialize` write
278
- # exists for that ivar, contribute `Constant[nil]` to the
221
+ # B2.3 — finalize the read-before-write nil contribution. For each class, for each ivar where SOME method body
222
+ # observed a read-before-write AND no `initialize` write exists for that ivar, contribute `Constant[nil]` to the
279
223
  # class-wide accumulator.
280
224
  #
281
- # The `initialize` filter is the soundness gate: Ruby
282
- # semantics guarantee `initialize` runs first (via
283
- # `Class.new`), so a write there reaches every other
284
- # method body's read. Read-before-write in a non-init
285
- # method is then NOT a nil-at-runtime case — it's just
286
- # AST-order coincidence. Without this filter a normal
287
- # `def initialize; @x = ... end` / `def use; @x.foo end`
288
- # class would have `@x` widened with nil, producing FPs
289
- # at every `@x.foo` call.
225
+ # The `initialize` filter is the soundness gate: Ruby semantics guarantee `initialize` runs first (via
226
+ # `Class.new`), so a write there reaches every other method body's read. Read-before-write in a non-init method is
227
+ # then NOT a nil-at-runtime case it's just AST-order coincidence. Without this filter a normal `def initialize;
228
+ # @x = ... end` / `def use; @x.foo end` class would have `@x` widened with nil, producing FPs at every `@x.foo`
229
+ # call.
290
230
  def contribute_read_before_write_nil!(accumulator, read_before_write, init_writes)
291
231
  nil_t = Type::Combinator.constant_of(nil)
292
232
  read_before_write.each do |class_name, ivar_set|
@@ -295,8 +235,7 @@ module Rigor
295
235
  next if per_class.nil?
296
236
 
297
237
  ivar_set.each do |ivar_name|
298
- # Soundness gates (in order):
299
- # (1) `initialize` writes the ivar → it's set
238
+ # Soundness gates (in order): (1) `initialize` writes the ivar → it's set
300
239
  # before any other method runs, so the
301
240
  # read-before-write in a sibling method is
302
241
  # NOT a runtime nil case.
@@ -315,28 +254,18 @@ module Rigor
315
254
  end
316
255
  end
317
256
 
318
- # Walks the post-collected accumulator and widens any Tuple
319
- # / HashShape entry for an ivar that observed a mutator call
320
- # anywhere in the same class body. The mutation evidence
321
- # comes from `gather_ivar_writes` recording every
322
- # `@ivar.<method>(...)` call whose method is in
323
- # `MutationWidening::ARRAY_MUTATORS` or `HASH_MUTATORS`.
257
+ # Walks the post-collected accumulator and widens any Tuple / HashShape entry for an ivar that observed a mutator
258
+ # call anywhere in the same class body. The mutation evidence comes from `gather_ivar_writes` recording every
259
+ # `@ivar.<method>(...)` call whose method is in `MutationWidening::ARRAY_MUTATORS` or `HASH_MUTATORS`.
324
260
  #
325
- # The widening uses `MutationWidening.widen_for_mutator` —
326
- # the same primitive `Inference::StatementEvaluator#eval_call`
327
- # applies for per-method-body widening on a local / ivar
328
- # receiver. The class-level pass extends that primitive's
329
- # reach so a `Tuple`-seeded ivar in `initialize` is observed
330
- # as `Nominal[Array]` at the entry of every OTHER method
331
- # body in the class — closing the cross-method gap noted in
332
- # ROADMAP § Future cycles / Type-language / engine
333
- # ("Tuple / HashShape widening for ivar-seeded literals
334
- # after mutation"; Redmine 6.1.2
335
- # `Redmine::Views::Builders::Structure` is the canonical
336
- # worked site).
261
+ # The widening uses `MutationWidening.widen_for_mutator` — the same primitive
262
+ # `Inference::StatementEvaluator#eval_call` applies for per-method-body widening on a local / ivar receiver. The
263
+ # class-level pass extends that primitive's reach so a `Tuple`-seeded ivar in `initialize` is observed as
264
+ # `Nominal[Array]` at the entry of every OTHER method body in the class — closing the cross-method gap noted in
265
+ # ROADMAP § Future cycles / Type-language / engine ("Tuple / HashShape widening for ivar-seeded literals after
266
+ # mutation"; Redmine 6.1.2 `Redmine::Views::Builders::Structure` is the canonical worked site).
337
267
  #
338
- # Always-safe: the widening can only LOSE precision; the
339
- # underlying nominal (`Array` / `Hash`) and the element
268
+ # Always-safe: the widening can only LOSE precision; the underlying nominal (`Array` / `Hash`) and the element
340
269
  # union are preserved.
341
270
  def widen_mutated_ivar_entries!(accumulator, mutated_ivars)
342
271
  accumulator.each do |class_name, ivars|
@@ -352,21 +281,13 @@ module Rigor
352
281
  end
353
282
  end
354
283
 
355
- # Walks a class-ivar accumulator entry (which may be a
356
- # `Union` of multiple write rvalues) and widens any
357
- # `Tuple` or `HashShape` member whose corresponding
358
- # mutator family was observed against the ivar somewhere
359
- # in the class. Class-level widening is more aggressive
360
- # than the per-method-body `MutationWidening` primitive:
361
- # it widens both the SHAPE carrier (Tuple Array,
362
- # HashShape → Hash) AND the element types to
363
- # `Dynamic[Top]`. The justification — once any method
364
- # mutates the ivar, its post-mutation contents are
365
- # statically unknown across method boundaries, so
366
- # preserving the seed-write's element precision would be
367
- # an unsound over-claim (e.g. `@struct = [{}]; somewhere:
368
- # @struct << []` makes the next read's element no longer
369
- # `Constant[{}]`).
284
+ # Walks a class-ivar accumulator entry (which may be a `Union` of multiple write rvalues) and widens any `Tuple`
285
+ # or `HashShape` member whose corresponding mutator family was observed against the ivar somewhere in the class.
286
+ # Class-level widening is more aggressive than the per-method-body `MutationWidening` primitive: it widens both
287
+ # the SHAPE carrier (Tuple Array, HashShape → Hash) AND the element types to `Dynamic[Top]`. The justification —
288
+ # once any method mutates the ivar, its post-mutation contents are statically unknown across method boundaries, so
289
+ # preserving the seed-write's element precision would be an unsound over-claim (e.g. `@struct = [{}]; somewhere:
290
+ # @struct << []` makes the next read's element no longer `Constant[{}]`).
370
291
  def widen_type_for_observed_mutators(type, observed_methods)
371
292
  members = type.is_a?(Type::Union) ? type.members : [type]
372
293
  widened = members.map { |m| widen_member_for_observed_mutators(m, observed_methods) }
@@ -399,21 +320,13 @@ module Rigor
399
320
  if name
400
321
  child_prefix = qualified_prefix + [name]
401
322
  if node.body
402
- # Class-body level `@x = nil` writes don't
403
- # initialise instance ivars at runtime (the
404
- # class's own singleton ivars and the instance's
405
- # ivars are separate stores), but they signal
406
- # "the author KNOWS @x could be nil" and extend
407
- # the B2.3 soundness gate: an ivar with a
408
- # class-body write is exempted from the
409
- # read-before-write nil contribution because the
410
- # seed already reflects the author's acknowledged
411
- # nullability via the def-body writes' union.
412
- # Without this exemption, code that explicitly
413
- # `@x = nil`s at class-body level then writes
414
- # `@x = SomeClass.new` inside an instance method
415
- # gains an unjustified nil widening at every
416
- # read.
323
+ # Class-body level `@x = nil` writes don't initialise instance ivars at runtime (the class's own singleton
324
+ # ivars and the instance's ivars are separate stores), but they signal "the author KNOWS @x could be nil"
325
+ # and extend the B2.3 soundness gate: an ivar with a class-body write is exempted from the
326
+ # read-before-write nil contribution because the seed already reflects the author's acknowledged
327
+ # nullability via the def-body writes' union. Without this exemption, code that explicitly `@x = nil`s at
328
+ # class-body level then writes `@x = SomeClass.new` inside an instance method gains an unjustified nil
329
+ # widening at every read.
417
330
  collect_class_body_ivar_writes(node.body, child_prefix.join("::"), init_writes) if init_writes
418
331
  walk_class_ivars(node.body, child_prefix, default_scope, accumulator,
419
332
  mutated_ivars, read_before_write, init_writes, method_assign_effects)
@@ -454,43 +367,30 @@ module Rigor
454
367
  end
455
368
  body_scope = default_scope.with_self_type(self_type)
456
369
 
457
- # C2 — transient `@x = nil` dead-write elimination. When a
458
- # method body opens with an unconditional `@x = nil`
459
- # (defensive init) and then *definitely* reassigns `@x` to a
460
- # non-nil value on every completing path (a later
461
- # unconditional statement-level write, OR an `if/else` whose
462
- # both branches write `@x`), the opening nil is dead it can
463
- # never be observed at method exit. Recording it anyway folds
464
- # a spurious `nil` constituent into the flow-insensitive
465
- # class-ivar union, which then poisons reads in OTHER methods
466
- # (e.g. ipaddr `IN4MASK ^ @mask_addr` rejects the resulting
467
- # `Integer | nil`). The set holds the `object_id`s of the
468
- # transient write nodes to skip; soundness is post-domination
469
- # at the top statement level, so dropping the nil never hides
470
- # a real runtime-nil read.
370
+ # C2 — transient `@x = nil` dead-write elimination. When a method body opens with an unconditional `@x = nil`
371
+ # (defensive init) and then *definitely* reassigns `@x` to a non-nil value on every completing path (a later
372
+ # unconditional statement-level write, OR an `if/else` whose both branches write `@x`), the opening nil is dead
373
+ # it can never be observed at method exit. Recording it anyway folds a spurious `nil` constituent into the
374
+ # flow-insensitive class-ivar union, which then poisons reads in OTHER methods (e.g. ipaddr `IN4MASK ^
375
+ # @mask_addr` rejects the resulting `Integer | nil`). The set holds the `object_id`s of the transient write
376
+ # nodes to skip; soundness is post-domination at the top statement level, so dropping the nil never hides a real
377
+ # runtime-nil read.
471
378
  dead_writes = dead_transient_nil_writes(def_node.body, class_name, method_assign_effects)
472
379
  gather_ivar_writes(def_node.body, body_scope, class_name, accumulator,
473
380
  EMPTY_GUARDED_IVARS, mutated_ivars, dead_writes)
474
381
 
475
- # B2.3 — collect per-method evidence for the read-before-
476
- # write nil contribution. The accumulator-level decision
477
- # ("is this ivar truly read-before-write across the
478
- # class lifetime?") is finalised at
479
- # `contribute_read_before_write_nil!` after the whole
480
- # class body has been walked, using `init_writes` as
481
- # the soundness gate (an ivar written in `initialize`
482
- # is initialised before any other method body runs).
382
+ # B2.3 — collect per-method evidence for the read-before- write nil contribution. The accumulator-level decision
383
+ # ("is this ivar truly read-before-write across the class lifetime?") is finalised at
384
+ # `contribute_read_before_write_nil!` after the whole class body has been walked, using `init_writes` as the
385
+ # soundness gate (an ivar written in `initialize` is initialised before any other method body runs).
483
386
  collect_read_before_write_evidence(def_node, class_name, read_before_write, init_writes, default_scope)
484
387
  end
485
388
 
486
- # ADR-38 block-form: collects ivar writes from a CallNode's
487
- # block body (e.g. RSpec `before { @x = }` / `let(:x) { … }`)
488
- # and folds them into `init_writes`, suppressing the
489
- # read-before-write nil contribution the same way a def-form
490
- # initializer does. The block body is always treated as an
491
- # initializer (the caller has already verified the method name
492
- # is declared as a block_method initializer), so there is no
493
- # read-before-write evidence collection step here.
389
+ # ADR-38 block-form: collects ivar writes from a CallNode's block body (e.g. RSpec `before { @x = … }` / `let(:x)
390
+ # { }`) and folds them into `init_writes`, suppressing the read-before-write nil contribution the same way a
391
+ # def-form initializer does. The block body is always treated as an initializer (the caller has already verified
392
+ # the method name is declared as a block_method initializer), so there is no read-before-write evidence collection
393
+ # step here.
494
394
  def collect_block_ivar_writes(block_node, qualified_prefix, default_scope, accumulator,
495
395
  mutated_ivars, init_writes)
496
396
  return if block_node.body.nil? || qualified_prefix.empty?
@@ -509,10 +409,9 @@ module Rigor
509
409
  seen_writes.each { |name| init_set << name }
510
410
  end
511
411
 
512
- # ADR-38 block-form gate: true when a loaded plugin declares
513
- # `method_name` a block-form initializer for `class_name` (or
514
- # an ancestor). Mirrors `additional_initializer?` but queries
515
- # `covers_block_method?` instead of `covers_method?`.
412
+ # ADR-38 block-form gate: true when a loaded plugin declares `method_name` a block-form initializer for
413
+ # `class_name` (or an ancestor). Mirrors `additional_initializer?` but queries `covers_block_method?` instead of
414
+ # `covers_method?`.
516
415
  def block_initializer?(class_name, method_name, default_scope)
517
416
  return false if class_name.nil? || default_scope.nil?
518
417
 
@@ -530,14 +429,10 @@ module Rigor
530
429
  false
531
430
  end
532
431
 
533
- # Walks the method body in AST (== execution) order
534
- # tracking ivar names whose first reference is a read.
535
- # The set is unioned into the class-wide
536
- # `read_before_write` accumulator. For `initialize` def
537
- # bodies, every write target is unioned into
538
- # `init_writes` instead — used by the finalisation step
539
- # to suppress nil contribution for ivars the constructor
540
- # guarantees are initialised.
432
+ # Walks the method body in AST (== execution) order tracking ivar names whose first reference is a read. The set
433
+ # is unioned into the class-wide `read_before_write` accumulator. For `initialize` def bodies, every write target
434
+ # is unioned into `init_writes` instead — used by the finalisation step to suppress nil contribution for ivars the
435
+ # constructor guarantees are initialised.
541
436
  def collect_read_before_write_evidence(def_node, class_name, read_before_write, init_writes, default_scope = nil)
542
437
  return if read_before_write.nil? || init_writes.nil?
543
438
 
@@ -545,12 +440,9 @@ module Rigor
545
440
  read_first = Set.new
546
441
  detect_read_before_write(def_node.body, seen_writes, read_first)
547
442
 
548
- # ADR-38 — `initialize` is the built-in initializer gate;
549
- # a plugin may declare additional `def`-form initializer
550
- # methods (minitest `setup`, Rails `after_initialize`, DI
551
- # setters) on a constrained class. Both fold their writes
552
- # into `init_writes`, suppressing the read-before-write nil
553
- # contribution for sibling readers.
443
+ # ADR-38 — `initialize` is the built-in initializer gate; a plugin may declare additional `def`-form initializer
444
+ # methods (minitest `setup`, Rails `after_initialize`, DI setters) on a constrained class. Both fold their
445
+ # writes into `init_writes`, suppressing the read-before-write nil contribution for sibling readers.
554
446
  if def_node.name == :initialize ||
555
447
  additional_initializer?(class_name, def_node.name, default_scope)
556
448
  init_set = (init_writes[class_name] ||= Set.new)
@@ -564,16 +456,11 @@ module Rigor
564
456
  read_first.each { |name| rbw_set << name }
565
457
  end
566
458
 
567
- # ADR-38 — true when a loaded plugin declares `method_name` an
568
- # additional initializer for `class_name` (or an ancestor).
569
- # Reads the plugin registry off the pre-pass scope's
570
- # environment; the receiver-constraint match reuses
571
- # `Environment#class_ordering` (the same mechanism ADR-16
572
- # Tier A's `MacroBlockSelfType` uses). The whole lookup is
573
- # wrapped so any resolution failure degrades to "no match" —
574
- # since the gate only ever SUPPRESSES a nil contribution, a
575
- # missed match is false-positive-safe (it merely leaves the
576
- # existing nil widening in place).
459
+ # ADR-38 — true when a loaded plugin declares `method_name` an additional initializer for `class_name` (or an
460
+ # ancestor). Reads the plugin registry off the pre-pass scope's environment; the receiver-constraint match reuses
461
+ # `Environment#class_ordering` (the same mechanism ADR-16 Tier A's `MacroBlockSelfType` uses). The whole lookup is
462
+ # wrapped so any resolution failure degrades to "no match" — since the gate only ever SUPPRESSES a nil
463
+ # contribution, a missed match is false-positive-safe (it merely leaves the existing nil widening in place).
577
464
  def additional_initializer?(class_name, method_name, default_scope)
578
465
  return false if class_name.nil? || default_scope.nil?
579
466
 
@@ -609,15 +496,10 @@ module Rigor
609
496
  ].freeze
610
497
  private_constant :IVAR_WRITE_NODES
611
498
 
612
- # Walks class-body level statements (i.e. NOT inside any
613
- # nested DefNode / ClassNode / ModuleNode) and records
614
- # every `@x = …` write target as a class-body init.
615
- # Consumed by `contribute_read_before_write_nil!` to
616
- # exempt ivars the author already knows might be nil
617
- # (the `@x = nil` at class-body level is the canonical
618
- # nullability acknowledgement; the instance @x is
619
- # technically a separate store, but the pragmatic intent
620
- # is unambiguous).
499
+ # Walks class-body level statements (i.e. NOT inside any nested DefNode / ClassNode / ModuleNode) and records
500
+ # every `@x = …` write target as a class-body init. Consumed by `contribute_read_before_write_nil!` to exempt
501
+ # ivars the author already knows might be nil (the `@x = nil` at class-body level is the canonical nullability
502
+ # acknowledgement; the instance @x is technically a separate store, but the pragmatic intent is unambiguous).
621
503
  def collect_class_body_ivar_writes(node, class_name, init_writes)
622
504
  return unless node.is_a?(Prism::Node)
623
505
  return if IVAR_BARRIER_NODES.any? { |klass| node.is_a?(klass) }
@@ -640,12 +522,9 @@ module Rigor
640
522
 
641
523
  read_first << node.name if node.is_a?(Prism::InstanceVariableReadNode) && !seen_writes.include?(node.name)
642
524
 
643
- # Descend BEFORE recording a write — `@x = @x + 1`'s
644
- # RHS is an `InstanceVariableReadNode` that runs before
645
- # the write is committed; the read is therefore
646
- # read-before-write semantically. Prism's
647
- # `compact_child_nodes` returns the value child before
648
- # the lvalue target, matching this order.
525
+ # Descend BEFORE recording a write — `@x = @x + 1`'s RHS is an `InstanceVariableReadNode` that runs before the
526
+ # write is committed; the read is therefore read-before-write semantically. Prism's `compact_child_nodes`
527
+ # returns the value child before the lvalue target, matching this order.
649
528
  node.compact_child_nodes.each do |c|
650
529
  detect_read_before_write(c, seen_writes, read_first)
651
530
  end
@@ -669,24 +548,18 @@ module Rigor
669
548
  guarded: guarded_ivars.include?(node.name))
670
549
  end
671
550
 
672
- # N1 — parallel / multiple assignment (`old, @cb = @cb, block`,
673
- # `@i, @o, @e, @thr = Open3.popen3(cmd)`). A direct
674
- # `InstanceVariableWriteNode` is the only write form this
675
- # collector handled, so an ivar appearing as a `MultiWriteNode`
676
- # target was silently dropped from the class-ivar union leaving
677
- # it to seed as pure `nil` (from a sibling `@cb = nil` ctor write,
678
- # or absent entirely) and false-fire `if @cb` always-falsey /
679
- # `@thr.alive?` undefined-for-nil. Record each ivar target with
680
- # its tuple-position RHS type where the RHS is array/tuple-shaped,
681
- # else the unanalyzable floor (the same `Dynamic[top]` a single
682
- # write to an unknown RHS records — an unanalyzable multi-write
683
- # means unknown, not nil).
551
+ # N1 — parallel / multiple assignment (`old, @cb = @cb, block`, `@i, @o, @e, @thr = Open3.popen3(cmd)`). A
552
+ # direct `InstanceVariableWriteNode` is the only write form this collector handled, so an ivar appearing as a
553
+ # `MultiWriteNode` target was silently dropped from the class-ivar union leaving it to seed as pure `nil`
554
+ # (from a sibling `@cb = nil` ctor write, or absent entirely) and false-fire `if @cb` always-falsey /
555
+ # `@thr.alive?` undefined-for-nil. Record each ivar target with its tuple-position RHS type where the RHS is
556
+ # array/tuple-shaped, else the unanalyzable floor (the same `Dynamic[top]` a single write to an unknown RHS
557
+ # records an unanalyzable multi-write means unknown, not nil).
684
558
  record_multi_write_ivars(node, scope, class_name, accumulator)
685
559
 
686
560
  record_ivar_mutator_call(node, class_name, mutated_ivars) if mutated_ivars && node.is_a?(Prism::CallNode)
687
561
 
688
- # Don't recurse into nested defs, classes, or modules; their
689
- # ivars belong to their own enclosing class.
562
+ # Don't recurse into nested defs, classes, or modules; their ivars belong to their own enclosing class.
690
563
  return if IVAR_BARRIER_NODES.any? { |klass| node.is_a?(klass) }
691
564
 
692
565
  if node.is_a?(Prism::IfNode) || node.is_a?(Prism::UnlessNode)
@@ -700,13 +573,10 @@ module Rigor
700
573
  end
701
574
  end
702
575
 
703
- # Records `@ivar.<method>(...)` calls whose method is in
704
- # `MutationWidening::ARRAY_MUTATORS` or `HASH_MUTATORS`.
705
- # The class-ivar pre-pass uses the resulting set to widen
706
- # the post-collected accumulator entries (see
707
- # {.widen_mutated_ivar_entries!}). Always-safe to over-
708
- # collect: any name that the widening primitive declines
709
- # is ignored at finalization.
576
+ # Records `@ivar.<method>(...)` calls whose method is in `MutationWidening::ARRAY_MUTATORS` or `HASH_MUTATORS`.
577
+ # The class-ivar pre-pass uses the resulting set to widen the post-collected accumulator entries (see
578
+ # {.widen_mutated_ivar_entries!}). Always-safe to over- collect: any name that the widening primitive declines is
579
+ # ignored at finalization.
710
580
  def record_ivar_mutator_call(node, class_name, mutated_ivars)
711
581
  receiver = node.receiver
712
582
  return unless receiver.is_a?(Prism::InstanceVariableReadNode)
@@ -718,22 +588,16 @@ module Rigor
718
588
  per_ivar << node.name
719
589
  end
720
590
 
721
- # Walk an `IfNode` / `UnlessNode` so writes inside the THEN body
722
- # that look like defensive ivar initialisation gain a `nil` union
723
- # in the seeded type. Without this, `@x = v unless @x` records
724
- # `Constant[v]` for `@x`, then the predicate folds to that same
725
- # constant and `flow.always-truthy-condition` fires against a
726
- # working program. Mirrors the existing skip for `@x ||= v`
727
- # (`Prism::InstanceVariableOrWriteNode`, which the pre-pass does
728
- # not seed at all).
591
+ # Walk an `IfNode` / `UnlessNode` so writes inside the THEN body that look like defensive ivar initialisation gain
592
+ # a `nil` union in the seeded type. Without this, `@x = v unless @x` records `Constant[v]` for `@x`, then the
593
+ # predicate folds to that same constant and `flow.always-truthy-condition` fires against a working program.
594
+ # Mirrors the existing skip for `@x ||= v` (`Prism::InstanceVariableOrWriteNode`, which the pre-pass does not seed
595
+ # at all).
729
596
  #
730
- # Polarity-aware on purpose: only the THEN body picks up the
731
- # guard. The ELSE branch of `if @x; ...; else; @x = init; end`
732
- # would otherwise be marked too but that pattern (write @x in
733
- # the else of `if @x`) is a separate idiom whose surrounding
734
- # reads of `@x` would then surface a nil-receiver FP. The
735
- # ELSE branch is left ungarded so those reads continue to type
736
- # as they did before this fix.
597
+ # Polarity-aware on purpose: only the THEN body picks up the guard. The ELSE branch of `if @x; ...; else; @x =
598
+ # init; end` would otherwise be marked too — but that pattern (write @x in the else of `if @x`) is a separate
599
+ # idiom whose surrounding reads of `@x` would then surface a nil-receiver FP. The ELSE branch is left ungarded so
600
+ # those reads continue to type as they did before this fix.
737
601
  def walk_conditional_ivar_writes(node, scope, class_name, accumulator, guarded_ivars,
738
602
  mutated_ivars = nil, dead_writes = nil)
739
603
  then_guards = then_body_guarded_ivars(node)
@@ -752,11 +616,9 @@ module Rigor
752
616
  mutated_ivars, dead_writes)
753
617
  end
754
618
 
755
- # Returns the set of ivar names that, in the THEN body of this
756
- # conditional, are statically known to be in a nil / unset state
757
- # i.e. the body really IS the defensive-init half of the
758
- # idiom. Conservative on purpose: only the shapes that
759
- # idiomatically express "the ivar is missing" qualify.
619
+ # Returns the set of ivar names that, in the THEN body of this conditional, are statically known to be in a nil /
620
+ # unset state i.e. the body really IS the defensive-init half of the idiom. Conservative on purpose: only the
621
+ # shapes that idiomatically express "the ivar is missing" qualify.
760
622
  #
761
623
  # For `unless P; body; end`, body runs when `P` is falsey:
762
624
  # - `P = @x` (or `@x && other` / `@x || other`) → @x is falsey
@@ -817,12 +679,9 @@ module Rigor
817
679
  end
818
680
  end
819
681
 
820
- # C2 — returns a Set of `object_id`s for transient `@x = nil`
821
- # writes that a later statement in the same method body
822
- # *definitely* overwrites with a non-nil value on every
823
- # completing path. Such a nil can never be the ivar's value at
824
- # method exit, so it must not contribute a `nil` constituent to
825
- # the (flow-insensitive) class-ivar union.
682
+ # C2 — returns a Set of `object_id`s for transient `@x = nil` writes that a later statement in the same method
683
+ # body *definitely* overwrites with a non-nil value on every completing path. Such a nil can never be the ivar's
684
+ # value at method exit, so it must not contribute a `nil` constituent to the (flow-insensitive) class-ivar union.
826
685
  #
827
686
  # Scope is deliberately narrow and post-domination-sound:
828
687
  # - only the top-level statement sequence of the body is
@@ -836,27 +695,21 @@ module Rigor
836
695
  # - only `@x = nil` literal writes are ever marked dead — a
837
696
  # non-nil transient is left untouched (it is already
838
697
  # precision-additive in the union).
839
- # WD3 — ADR-41-style hard cap on how deep the same-class-call
840
- # definite-assignment crediting recurses (the ctor calls
841
- # `mask!`, which could itself call another same-class helper).
842
- # Cycle-guarded independently; the cap bounds even acyclic
843
- # chains.
698
+ # WD3 — ADR-41-style hard cap on how deep the same-class-call definite-assignment crediting recurses (the ctor
699
+ # calls `mask!`, which could itself call another same-class helper). Cycle-guarded independently; the cap bounds
700
+ # even acyclic chains.
844
701
  SAME_CLASS_CALL_DEPTH_CAP = 3
845
702
  private_constant :SAME_CLASS_CALL_DEPTH_CAP
846
703
 
847
- # WD3 — builds the per-class definite-assignment summary
848
- # `{class_name => {method_name => Set<ivar names assigned
849
- # non-nil on every completing path>}}`. Used so a ctor's
850
- # `dead_transient_nil_writes` can credit an indirect overwrite
851
- # through an unconditionally-called same-class method (ipaddr's
852
- # `initialize` reassigns `@mask_addr` via `mask!`).
704
+ # WD3 — builds the per-class definite-assignment summary `{class_name => {method_name => Set<ivar names assigned
705
+ # non-nil on every completing path>}}`. Used so a ctor's `dead_transient_nil_writes` can credit an indirect
706
+ # overwrite through an unconditionally-called same-class method (ipaddr's `initialize` reassigns `@mask_addr` via
707
+ # `mask!`).
853
708
  #
854
- # Each method's set is computed by the same suffix
855
- # definite-assignment analysis used for the ctor seed, run from
856
- # the method body's first statement for every ivar the method
857
- # writes anywhere. Same-class calls inside a method are credited
858
- # transitively (depth-capped, cycle-guarded) so the resulting
859
- # FLAT table is correct at depth 0 for the ctor lookup.
709
+ # Each method's set is computed by the same suffix definite-assignment analysis used for the ctor seed, run from
710
+ # the method body's first statement for every ivar the method writes anywhere. Same-class calls inside a method
711
+ # are credited transitively (depth-capped, cycle-guarded) so the resulting FLAT table is correct at depth 0 for
712
+ # the ctor lookup.
860
713
  def build_method_assign_effects(root)
861
714
  defs = collect_class_method_defs(root)
862
715
  effects = {}
@@ -870,10 +723,9 @@ module Rigor
870
723
  effects.freeze
871
724
  end
872
725
 
873
- # Collects `{class_name => {method_name => DefNode}}` for every
874
- # instance-method def in the program. Singleton defs (`def
875
- # self.x`) are excluded — the ctor-call crediting only follows
876
- # instance-method calls on `self`. Last def wins on redefinition.
726
+ # Collects `{class_name => {method_name => DefNode}}` for every instance-method def in the program. Singleton defs
727
+ # (`def self.x`) are excluded — the ctor-call crediting only follows instance-method calls on `self`. Last def
728
+ # wins on redefinition.
877
729
  def collect_class_method_defs(root, prefix = [], acc = {})
878
730
  return acc unless root.is_a?(Prism::Node)
879
731
 
@@ -894,10 +746,9 @@ module Rigor
894
746
  acc
895
747
  end
896
748
 
897
- # Computes the definite-assignment set for one method, memoised
898
- # per def node. The `memo` cycle-guards: a method re-entered
899
- # while its own summary is in progress contributes nothing
900
- # (sound under-approximation), so mutual recursion terminates.
749
+ # Computes the definite-assignment set for one method, memoised per def node. The `memo` cycle-guards: a method
750
+ # re-entered while its own summary is in progress contributes nothing (sound under-approximation), so mutual
751
+ # recursion terminates.
901
752
  def method_definite_assigns(class_name, _method_name, def_node, defs, effects, memo, depth)
902
753
  return Set.new if def_node.body.nil?
903
754
  return memo[def_node] if memo.key?(def_node)
@@ -906,9 +757,8 @@ module Rigor
906
757
  memo[def_node] = Set.new # in-progress sentinel (cycle guard)
907
758
  statements = top_level_statements(def_node.body)
908
759
  candidates = ivar_write_targets(def_node.body)
909
- # A transient `@x = nil` opener whose own method reassigns it
910
- # later must still count `@x` as assigned for callers, so the
911
- # crediting is computed at the BUILD-time depth.
760
+ # A transient `@x = nil` opener whose own method reassigns it later must still count `@x` as assigned for
761
+ # callers, so the crediting is computed at the BUILD-time depth.
912
762
  resolver = MethodEffectResolver.new(self, class_name, defs, effects, memo, depth)
913
763
  assigns = Set.new
914
764
  candidates.each do |ivar|
@@ -917,8 +767,8 @@ module Rigor
917
767
  memo[def_node] = assigns
918
768
  end
919
769
 
920
- # Every ivar this body assigns a non-nil value to ANYWHERE (the
921
- # candidate set for the method's definite-assignment scan).
770
+ # Every ivar this body assigns a non-nil value to ANYWHERE (the candidate set for the method's definite-assignment
771
+ # scan).
922
772
  def ivar_write_targets(node, acc = Set.new)
923
773
  return acc unless node.is_a?(Prism::Node)
924
774
 
@@ -927,10 +777,9 @@ module Rigor
927
777
  acc
928
778
  end
929
779
 
930
- # Build-time variant of `suffix_definitely_assigns?` that resolves
931
- # same-class calls through the lazy `resolver` (which recurses
932
- # into `method_definite_assigns` for not-yet-computed callees)
933
- # rather than the finished flat table.
780
+ # Build-time variant of `suffix_definitely_assigns?` that resolves same-class calls through the lazy `resolver`
781
+ # (which recurses into `method_definite_assigns` for not-yet-computed callees) rather than the finished flat
782
+ # table.
934
783
  def suffix_definitely_assigns_with_resolver?(statements, from, target, class_name, resolver, depth)
935
784
  statements[from..].each do |stmt|
936
785
  outcome = statement_assignment_outcome(stmt, target, class_name, resolver, depth, nil)
@@ -940,9 +789,8 @@ module Rigor
940
789
  false
941
790
  end
942
791
 
943
- # Adapts `effects.dig(class, method)` for build-time crediting:
944
- # when the callee summary is not yet in the flat table, compute
945
- # it on demand (depth+1) via `method_definite_assigns`.
792
+ # Adapts `effects.dig(class, method)` for build-time crediting: when the callee summary is not yet in the flat
793
+ # table, compute it on demand (depth+1) via `method_definite_assigns`.
946
794
  class MethodEffectResolver
947
795
  def initialize(indexer, class_name, defs, effects, memo, depth)
948
796
  @indexer = indexer
@@ -974,12 +822,9 @@ module Rigor
974
822
  statements.each_with_index do |stmt, i|
975
823
  next unless stmt.is_a?(Prism::InstanceVariableWriteNode) && nil_literal_value?(stmt.value)
976
824
 
977
- # The opening `@x = nil` is dead when every completing path
978
- # of the SUFFIX after it (normal end OR early `return`,
979
- # never a `raise`-terminated path) definitely reassigns
980
- # `@x` non-nil. The suffix analysis credits an
981
- # unconditionally-called same-class method's own definite
982
- # assignments via `method_assign_effects`.
825
+ # The opening `@x = nil` is dead when every completing path of the SUFFIX after it (normal end OR early
826
+ # `return`, never a `raise`-terminated path) definitely reassigns `@x` non-nil. The suffix analysis credits an
827
+ # unconditionally-called same-class method's own definite assignments via `method_assign_effects`.
983
828
  if suffix_definitely_assigns?(statements, i + 1, stmt.name, class_name, method_assign_effects)
984
829
  (dead ||= Set.new) << stmt.object_id
985
830
  end
@@ -999,24 +844,19 @@ module Rigor
999
844
  node.is_a?(Prism::NilNode)
1000
845
  end
1001
846
 
1002
- # True when, starting from `statements[from]`, EVERY path that
1003
- # completes the method (falls off the end OR hits an early
1004
- # `return`) definitely assigns `target` a non-nil value first.
1005
- # Paths terminated by `raise` are not completing paths and are
1006
- # ignored (they never observe the ivar at method exit). A path
1007
- # that can fall through `statements` without assigning fails.
847
+ # True when, starting from `statements[from]`, EVERY path that completes the method (falls off the end OR hits an
848
+ # early `return`) definitely assigns `target` a non-nil value first. Paths terminated by `raise` are not
849
+ # completing paths and are ignored (they never observe the ivar at method exit). A path that can fall through
850
+ # `statements` without assigning fails.
1008
851
  def suffix_definitely_assigns?(statements, from, target, class_name, effects)
1009
852
  statements[from..].each do |stmt|
1010
853
  outcome = statement_assignment_outcome(stmt, target, class_name, effects, 0, nil)
1011
- # The statement assigned on every continuing path -> the
1012
- # suffix is satisfied no matter what follows.
854
+ # The statement assigned on every continuing path -> the suffix is satisfied no matter what follows.
1013
855
  return true if outcome == :assigned
1014
- # The statement terminates control here (return/raise) and
1015
- # the value it carried did not assign on every path -> some
1016
- # completing path reached exit without the assignment.
856
+ # The statement terminates control here (return/raise) and the value it carried did not assign on every path
857
+ # -> some completing path reached exit without the assignment.
1017
858
  return false if outcome == :terminates_unassigned
1018
- # Otherwise (:falls_through_unassigned) keep scanning the
1019
- # remaining statements.
859
+ # Otherwise (:falls_through_unassigned) keep scanning the remaining statements.
1020
860
  end
1021
861
  # Fell off the end with no definite assignment.
1022
862
  false
@@ -1052,20 +892,16 @@ module Rigor
1052
892
  when Prism::ReturnNode
1053
893
  :terminates_unassigned
1054
894
  else
1055
- # Any other statement — including a bare `raise`/`fail`,
1056
- # which terminates without a completing path that observes
1057
- # the seed nil — is neutral: control either continues or the
1058
- # path never reaches method exit. Keep scanning the suffix.
895
+ # Any other statement — including a bare `raise`/`fail`, which terminates without a completing path that
896
+ # observes the seed nil is neutral: control either continues or the path never reaches method exit. Keep
897
+ # scanning the suffix.
1059
898
  :falls_through_unassigned
1060
899
  end
1061
900
  end
1062
901
 
1063
- # True when a branch body (a StatementsNode / single node)
1064
- # definitely assigns `target` non-nil on every path that
1065
- # completes the method through it, OR terminates every path by
1066
- # raise (vacuously safe — no completing path observes the seed
1067
- # nil). Returns false if any path can complete/return without the
1068
- # assignment.
902
+ # True when a branch body (a StatementsNode / single node) definitely assigns `target` non-nil on every path that
903
+ # completes the method through it, OR terminates every path by raise (vacuously safe — no completing path observes
904
+ # the seed nil). Returns false if any path can complete/return without the assignment.
1069
905
  def branch_definitely_assigns?(branch, target, class_name, effects, depth, visiting)
1070
906
  stmts = top_level_statements(branch)
1071
907
  return false if stmts.empty?
@@ -1075,17 +911,14 @@ module Rigor
1075
911
  return true if outcome == :assigned
1076
912
  return false if outcome == :terminates_unassigned
1077
913
  end
1078
- # Reached the end of the branch without a definite assignment;
1079
- # safe only if the branch's last statement always raises (no
1080
- # completing path falls out of it).
914
+ # Reached the end of the branch without a definite assignment; safe only if the branch's last statement always
915
+ # raises (no completing path falls out of it).
1081
916
  always_raises?(stmts.last)
1082
917
  end
1083
918
 
1084
- # `if`/`unless` is a definite assignment of `target` only when
1085
- # BOTH the then and else arms definitely assign (or raise-out).
1086
- # A missing else arm means the fall-through path skips the
1087
- # assignment -> not definite. Modifier-form `if`/`unless` (no
1088
- # else, single predicate'd statement) likewise.
919
+ # `if`/`unless` is a definite assignment of `target` only when BOTH the then and else arms definitely assign (or
920
+ # raise-out). A missing else arm means the fall-through path skips the assignment -> not definite. Modifier-form
921
+ # `if`/`unless` (no else, single predicate'd statement) likewise.
1089
922
  def conditional_assignment_outcome(node, target, class_name, effects, depth, visiting)
1090
923
  else_branch = node.is_a?(Prism::IfNode) ? node.subsequent : node.else_clause
1091
924
  return :falls_through_unassigned unless else_branch.is_a?(Prism::ElseNode)
@@ -1096,10 +929,8 @@ module Rigor
1096
929
  then_ok && else_ok ? :assigned : :falls_through_unassigned
1097
930
  end
1098
931
 
1099
- # `case` is a definite assignment only when there is a real
1100
- # `else` clause AND every `when`/`in` body plus the else body
1101
- # definitely assigns (or raises-out). A missing else lets an
1102
- # unmatched subject fall through unassigned.
932
+ # `case` is a definite assignment only when there is a real `else` clause AND every `when`/`in` body plus the else
933
+ # body definitely assigns (or raises-out). A missing else lets an unmatched subject fall through unassigned.
1103
934
  def case_assignment_outcome(node, target, class_name, effects, depth, visiting)
1104
935
  else_clause = node.else_clause
1105
936
  return :falls_through_unassigned unless else_clause.is_a?(Prism::ElseNode)
@@ -1112,10 +943,9 @@ module Rigor
1112
943
  all_ok ? :assigned : :falls_through_unassigned
1113
944
  end
1114
945
 
1115
- # True when `node` (a single statement or its last statement) is
1116
- # an unconditional `raise`/`fail` call that always terminates the
1117
- # path — used to treat raise-terminated branches as
1118
- # non-completing (they never observe the seed nil).
946
+ # True when `node` (a single statement or its last statement) is an unconditional `raise`/`fail` call that always
947
+ # terminates the path — used to treat raise-terminated branches as non-completing (they never observe the seed
948
+ # nil).
1119
949
  def always_raises?(node)
1120
950
  node = top_level_statements(node).last if node.is_a?(Prism::StatementsNode)
1121
951
  return false unless node.is_a?(Prism::CallNode)
@@ -1124,11 +954,9 @@ module Rigor
1124
954
  %i[raise fail].include?(node.name)
1125
955
  end
1126
956
 
1127
- # True when `call` is an unconditional, statement-level,
1128
- # implicit-self (or `self.`) call to a SAME-CLASS method whose
1129
- # definite-assignment summary includes `target`. Calls through a
1130
- # block, on another receiver, or to an unresolved name contribute
1131
- # nothing (the seed nil stays).
957
+ # True when `call` is an unconditional, statement-level, implicit-self (or `self.`) call to a SAME-CLASS method
958
+ # whose definite-assignment summary includes `target`. Calls through a block, on another receiver, or to an
959
+ # unresolved name contribute nothing (the seed nil stays).
1132
960
  def unconditional_call_assigns?(call, target, class_name, effects, depth, _visiting)
1133
961
  return false if effects.nil? || class_name.nil?
1134
962
  return false if depth >= SAME_CLASS_CALL_DEPTH_CAP
@@ -1146,23 +974,14 @@ module Rigor
1146
974
  def record_ivar_write(node, scope, class_name, accumulator, guarded: false)
1147
975
  rvalue_type = scope.type_of(node.value)
1148
976
 
1149
- # `@x = nil unless @x` / `@y = false unless @y` —
1150
- # follow-up to the polarity-aware defensive-init guard
1151
- # fix (ROADMAP § Future cycles "Defensive ivar-init
1152
- # with nil / false rvalue"). When the rvalue is itself a
1153
- # falsey Constant, `union(rvalue, Constant[nil])`
1154
- # collapses (for `nil`) or doesn't widen the type's
1155
- # truthiness profile (for `false`) the predicate
1156
- # `unless @x` then folds to a single `Constant[nil]` /
1157
- # `Constant[false]` and the
1158
- # `flow.always-truthy-condition` / `-always-falsey-`
1159
- # rule false-fires on the no-op-but-documented-default
1160
- # idiom. Skip the seed contribution for this write
1161
- # (matches the existing skip for `@x ||= v`, which the
1162
- # pre-pass also does not seed). Other writes to the
1163
- # same ivar still contribute; the falsey-default write
1164
- # carries no useful precision the predicate hasn't
1165
- # already given us. See tdiary-core HEAD `ee40c2b`
977
+ # `@x = nil unless @x` / `@y = false unless @y` — follow-up to the polarity-aware defensive-init guard fix
978
+ # (ROADMAP § Future cycles — "Defensive ivar-init with nil / false rvalue"). When the rvalue is itself a falsey
979
+ # Constant, `union(rvalue, Constant[nil])` collapses (for `nil`) or doesn't widen the type's truthiness profile
980
+ # (for `false`) the predicate `unless @x` then folds to a single `Constant[nil]` / `Constant[false]` and the
981
+ # `flow.always-truthy-condition` / `-always-falsey-` rule false-fires on the no-op-but-documented-default idiom.
982
+ # Skip the seed contribution for this write (matches the existing skip for `@x ||= v`, which the pre-pass also
983
+ # does not seed). Other writes to the same ivar still contribute; the falsey-default write carries no useful
984
+ # precision the predicate hasn't already given us. See tdiary-core HEAD `ee40c2b`
1166
985
  # `lib/tdiary/configuration.rb:157` for the worked site.
1167
986
  return if guarded && falsey_constant?(rvalue_type)
1168
987
 
@@ -1170,9 +989,8 @@ module Rigor
1170
989
  accumulate_ivar_type(accumulator, class_name, node.name, rvalue_type)
1171
990
  end
1172
991
 
1173
- # Unions `type` into the class-ivar accumulator for `(class_name,
1174
- # ivar_name)`. Shared by the single-write and multi-write
1175
- # (parallel-assignment) collectors.
992
+ # Unions `type` into the class-ivar accumulator for `(class_name, ivar_name)`. Shared by the single-write and
993
+ # multi-write (parallel-assignment) collectors.
1176
994
  def accumulate_ivar_type(accumulator, class_name, ivar_name, type)
1177
995
  accumulator[class_name] ||= {}
1178
996
  existing = accumulator[class_name][ivar_name]
@@ -1180,17 +998,13 @@ module Rigor
1180
998
  existing ? Type::Combinator.union(existing, type) : type
1181
999
  end
1182
1000
 
1183
- # N1 — records each `InstanceVariableTargetNode` of a
1184
- # `MultiWriteNode` (parallel / multiple assignment) into the
1185
- # class-ivar union, with the best cheap per-slot type. When the RHS
1186
- # is array/tuple-shaped (`Type::Tuple`) the ivar at position `i`
1187
- # records the type of element `i`; otherwise an unanalyzable RHS
1188
- # such as `Open3.popen3(cmd)` typing to `Dynamic[top]` every ivar
1189
- # slot records that unanalyzable floor (NOT `nil`: a multi-write we
1190
- # cannot decompose means the value is *unknown*, and `Dynamic[top]`
1191
- # is the sound union constituent, mirroring what a single write to
1192
- # an unknown RHS records). Nested targets (`(@a, @b), @c = …`)
1193
- # recurse with the slot's type as the new RHS type.
1001
+ # N1 — records each `InstanceVariableTargetNode` of a `MultiWriteNode` (parallel / multiple assignment) into the
1002
+ # class-ivar union, with the best cheap per-slot type. When the RHS is array/tuple-shaped (`Type::Tuple`) the ivar
1003
+ # at position `i` records the type of element `i`; otherwise an unanalyzable RHS such as `Open3.popen3(cmd)`
1004
+ # typing to `Dynamic[top]` every ivar slot records that unanalyzable floor (NOT `nil`: a multi-write we cannot
1005
+ # decompose means the value is *unknown*, and `Dynamic[top]` is the sound union constituent, mirroring what a
1006
+ # single write to an unknown RHS records). Nested targets (`(@a, @b), @c = …`) recurse with the slot's type as the
1007
+ # new RHS type.
1194
1008
  def record_multi_write_ivars(node, scope, class_name, accumulator)
1195
1009
  return unless node.is_a?(Prism::MultiWriteNode)
1196
1010
 
@@ -1198,10 +1012,8 @@ module Rigor
1198
1012
  record_multi_target_ivars(node, rhs_type, class_name, accumulator)
1199
1013
  end
1200
1014
 
1201
- # Walks a `MultiWriteNode` / `MultiTargetNode` target tree against
1202
- # `rhs_type`, recording ivar targets per slot. Mirrors
1203
- # `MultiTargetBinder`'s tuple decomposition but for ivar (rather
1204
- # than local-variable) targets.
1015
+ # Walks a `MultiWriteNode` / `MultiTargetNode` target tree against `rhs_type`, recording ivar targets per slot.
1016
+ # Mirrors `MultiTargetBinder`'s tuple decomposition but for ivar (rather than local-variable) targets.
1205
1017
  def record_multi_target_ivars(node, rhs_type, class_name, accumulator)
1206
1018
  lefts = node.lefts || []
1207
1019
  rest = node.rest
@@ -1233,12 +1045,10 @@ module Rigor
1233
1045
  end
1234
1046
  end
1235
1047
 
1236
- # The per-slot type for index `i` of a tuple RHS. A missing slot
1237
- # (over-destructure) is `nil` at runtime; a present slot keeps its
1238
- # type. Unlike the local-variable binder we do NOT soften an
1239
- # optional slot here a class-ivar seed deliberately preserves a
1240
- # genuine `T | nil`, and any spurious nil is removed by the
1241
- # flow-side narrowing, not by dropping it at collection time.
1048
+ # The per-slot type for index `i` of a tuple RHS. A missing slot (over-destructure) is `nil` at runtime; a present
1049
+ # slot keeps its type. Unlike the local-variable binder we do NOT soften an optional slot here — a class-ivar seed
1050
+ # deliberately preserves a genuine `T | nil`, and any spurious nil is removed by the flow-side narrowing, not by
1051
+ # dropping it at collection time.
1242
1052
  def multi_write_slot_type(elements, index)
1243
1053
  element = elements[index]
1244
1054
  return Type::Combinator.constant_of(nil) if element.nil?
@@ -1261,9 +1071,8 @@ module Rigor
1261
1071
  expression = splat_node.expression
1262
1072
  return unless expression.is_a?(Prism::InstanceVariableTargetNode)
1263
1073
 
1264
- # A splat collects the middle slots into an Array; the precise
1265
- # element type is not worth recovering here. Record the
1266
- # unanalyzable floor (an Array of unknown), never nil.
1074
+ # A splat collects the middle slots into an Array; the precise element type is not worth recovering here. Record
1075
+ # the unanalyzable floor (an Array of unknown), never nil.
1267
1076
  accumulate_ivar_type(accumulator, class_name, expression.name, Type::Combinator.untyped)
1268
1077
  end
1269
1078
 
@@ -1271,13 +1080,10 @@ module Rigor
1271
1080
  type.is_a?(Type::Constant) && (type.value.nil? || type.value == false)
1272
1081
  end
1273
1082
 
1274
- # Slice 7 phase 6 — class-cvar pre-pass. Same shape as the
1275
- # ivar pre-pass but collects `Prism::ClassVariableWriteNode`
1276
- # writes inside ANY def body (instance or singleton) of the
1277
- # enclosing class, because Ruby cvars are shared across both
1278
- # facets. The resulting table is seeded into both instance
1279
- # and singleton method bodies through
1280
- # `Scope#class_cvars_for`.
1083
+ # Slice 7 phase 6 — class-cvar pre-pass. Same shape as the ivar pre-pass but collects
1084
+ # `Prism::ClassVariableWriteNode` writes inside ANY def body (instance or singleton) of the enclosing class,
1085
+ # because Ruby cvars are shared across both facets. The resulting table is seeded into both instance and singleton
1086
+ # method bodies through `Scope#class_cvars_for`.
1281
1087
  def build_class_cvar_index(root, default_scope)
1282
1088
  accumulator = {}
1283
1089
  walk_class_cvars(root, [], default_scope, accumulator)
@@ -1330,12 +1136,9 @@ module Rigor
1330
1136
  existing ? Type::Combinator.union(existing, rvalue_type) : rvalue_type
1331
1137
  end
1332
1138
 
1333
- # Slice 7 phase 6 — program-global pre-pass. Globals are
1334
- # process-wide so the accumulator is a flat
1335
- # `Hash[Symbol, Type::t]` populated from every
1336
- # `Prism::GlobalVariableWriteNode` in the program (top-level
1337
- # AND inside method bodies). The same accumulator is
1338
- # seeded into every method body and the top-level scope.
1139
+ # Slice 7 phase 6 — program-global pre-pass. Globals are process-wide so the accumulator is a flat `Hash[Symbol,
1140
+ # Type::t]` populated from every `Prism::GlobalVariableWriteNode` in the program (top-level AND inside method
1141
+ # bodies). The same accumulator is seeded into every method body and the top-level scope.
1339
1142
  def build_program_global_index(root, default_scope)
1340
1143
  accumulator = {}
1341
1144
  gather_global_writes(root, default_scope, accumulator)
@@ -1356,14 +1159,10 @@ module Rigor
1356
1159
  existing ? Type::Combinator.union(existing, rvalue_type) : rvalue_type
1357
1160
  end
1358
1161
 
1359
- # Slice 7 phase 9 — in-source constant value pre-pass.
1360
- # Walks the entire program (top-level AND inside class /
1361
- # module / def bodies) for `Prism::ConstantWriteNode` and
1362
- # `Prism::ConstantPathWriteNode`, types each rvalue, and
1363
- # accumulates by qualified name. Constants defined inside
1364
- # a class body are qualified with the surrounding class
1365
- # path; constants written via a path (`Foo::BAR = ...`)
1366
- # use the rendered path as-is.
1162
+ # Slice 7 phase 9 — in-source constant value pre-pass. Walks the entire program (top-level AND inside class /
1163
+ # module / def bodies) for `Prism::ConstantWriteNode` and `Prism::ConstantPathWriteNode`, types each rvalue, and
1164
+ # accumulates by qualified name. Constants defined inside a class body are qualified with the surrounding class
1165
+ # path; constants written via a path (`Foo::BAR = ...`) use the rendered path as-is.
1367
1166
  def build_in_source_constants(root, default_scope)
1368
1167
  accumulator = {}
1369
1168
  walk_constant_writes(root, [], default_scope, accumulator)
@@ -1406,14 +1205,10 @@ module Rigor
1406
1205
  accumulator[full] = existing ? Type::Combinator.union(existing, rvalue_type) : rvalue_type
1407
1206
  end
1408
1207
 
1409
- # Survey item (e): when the rvalue is a recognised
1410
- # `Module.new do ... end` / `Class.new do ... end` /
1411
- # `Struct.new(*sym) do ... end` / `Data.define(*sym) do
1412
- # ... end` form, type the named constant as
1413
- # `Singleton[<full>]` so the discovered-method table
1414
- # registered under `full` becomes reachable through
1415
- # singleton-side dispatch (`Const.[]=` etc.). Returns nil
1416
- # for non-meta-new rvalues so the caller falls back to the
1208
+ # Survey item (e): when the rvalue is a recognised `Module.new do ... end` / `Class.new do ... end` /
1209
+ # `Struct.new(*sym) do ... end` / `Data.define(*sym) do ... end` form, type the named constant as
1210
+ # `Singleton[<full>]` so the discovered-method table registered under `full` becomes reachable through
1211
+ # singleton-side dispatch (`Const.[]=` etc.). Returns nil for non-meta-new rvalues so the caller falls back to the
1417
1212
  # default `body_scope.type_of(node.value)` shape.
1418
1213
  def meta_new_constant_type(node, full)
1419
1214
  return nil unless meta_new_block_body(node)
@@ -1421,10 +1216,8 @@ module Rigor
1421
1216
  Type::Combinator.singleton_of(full)
1422
1217
  end
1423
1218
 
1424
- # Slice 7 phase 12 — in-source method discovery pre-pass, fused with
1425
- # the instance-method def-node pre-pass (v0.0.2 #5). One descent
1426
- # produces BOTH tables the per-file `index` and the cross-file
1427
- # pre-pass each need together:
1219
+ # Slice 7 phase 12 — in-source method discovery pre-pass, fused with the instance-method def-node pre-pass (v0.0.2
1220
+ # #5). One descent produces BOTH tables the per-file `index` and the cross-file pre-pass each need together:
1428
1221
  #
1429
1222
  # - `methods` : `{class_name => {method => :instance | :singleton}}`
1430
1223
  # for every `def` / `define_method(:name)` / `attr_*` / `alias` /
@@ -1434,10 +1227,9 @@ module Rigor
1434
1227
  # table; singleton defs and `define_method` are intentionally
1435
1228
  # skipped — `record_def_node` filters them).
1436
1229
  #
1437
- # `walk_methods` and `walk_def_nodes` had byte-identical class /
1438
- # module / singleton / meta-block descents (both stop at `DefNode`),
1439
- # so a single combined walk records both accumulators at once instead
1440
- # of traversing every file twice.
1230
+ # `walk_methods` and `walk_def_nodes` had byte-identical class / module / singleton / meta-block descents (both
1231
+ # stop at `DefNode`), so a single combined walk records both accumulators at once instead of traversing every file
1232
+ # twice.
1441
1233
  def build_methods_and_def_nodes(root)
1442
1234
  methods = {}
1443
1235
  def_nodes = {}
@@ -1446,9 +1238,8 @@ module Rigor
1446
1238
  [methods.transform_values(&:freeze).freeze, def_nodes.transform_values(&:freeze).freeze]
1447
1239
  end
1448
1240
 
1449
- # Merges two `class_name => { method => kind }` tables, unioning
1450
- # the per-class method maps (so a seeded cross-file table and the
1451
- # current file's table combine instead of clobbering).
1241
+ # Merges two `class_name => { method => kind }` tables, unioning the per-class method maps (so a seeded cross-file
1242
+ # table and the current file's table combine instead of clobbering).
1452
1243
  def deep_merge_class_methods(base, overlay)
1453
1244
  return overlay if base.nil? || base.empty?
1454
1245
  return base if overlay.empty?
@@ -1459,13 +1250,11 @@ module Rigor
1459
1250
  end
1460
1251
 
1461
1252
  # rubocop:disable Metrics/CyclomaticComplexity, Metrics/MethodLength, Metrics/AbcSize, Metrics/PerceivedComplexity
1462
- # Combined `walk_methods` + `walk_def_nodes` descent. The two walks
1463
- # had identical class / module / singleton-class / meta-block
1464
- # traversals and both stopped at `DefNode`; the only divergences are
1465
- # leaf actions (recorded into the right accumulator) and the original
1466
- # `walk_methods` returning at `AliasMethodNode` (its symbol-only
1467
- # children carry no def / class node, so not descending them is
1468
- # byte-identical for `def_nodes` too). See {#build_methods_and_def_nodes}.
1253
+ # Combined `walk_methods` + `walk_def_nodes` descent. The two walks had identical class / module / singleton-class
1254
+ # / meta-block traversals and both stopped at `DefNode`; the only divergences are leaf actions (recorded into the
1255
+ # right accumulator) and the original `walk_methods` returning at `AliasMethodNode` (its symbol-only children
1256
+ # carry no def / class node, so not descending them is byte-identical for `def_nodes` too). See
1257
+ # {#build_methods_and_def_nodes}.
1469
1258
  def walk_methods_and_def_nodes(node, qualified_prefix, in_singleton_class, methods_acc, def_nodes_acc)
1470
1259
  return unless node.is_a?(Prism::Node)
1471
1260
 
@@ -1536,16 +1325,11 @@ module Rigor
1536
1325
  end
1537
1326
  # rubocop:enable Metrics/CyclomaticComplexity, Metrics/MethodLength, Metrics/AbcSize, Metrics/PerceivedComplexity
1538
1327
 
1539
- # v0.1.2 — when a `Const = Data.define(*sym) do ... end`
1540
- # / `Const = Struct.new(*sym) do ... end` constant write
1541
- # carries a block, the block body holds method overrides
1542
- # whose canonical class is `Const`. Survey item (e) extended
1543
- # the recognition to `Const = Module.new do ... end` and
1544
- # `Const = Class.new(?super) do ... end` — the
1545
- # ADR-16 Tier A "block-as-method" idiom at constant-write
1546
- # position. Returns the block body node (a
1547
- # `Prism::StatementsNode`) when the rvalue matches; nil
1548
- # otherwise. Used by `walk_methods` / `walk_def_nodes` to
1328
+ # v0.1.2 — when a `Const = Data.define(*sym) do ... end` / `Const = Struct.new(*sym) do ... end` constant write
1329
+ # carries a block, the block body holds method overrides whose canonical class is `Const`. Survey item (e)
1330
+ # extended the recognition to `Const = Module.new do ... end` and `Const = Class.new(?super) do ... end` — the
1331
+ # ADR-16 Tier A "block-as-method" idiom at constant-write position. Returns the block body node (a
1332
+ # `Prism::StatementsNode`) when the rvalue matches; nil otherwise. Used by `walk_methods` / `walk_def_nodes` to
1549
1333
  # push `Const` onto the qualified prefix before recursing.
1550
1334
  def meta_new_block_body(node)
1551
1335
  return nil unless node.is_a?(Prism::ConstantWriteNode)
@@ -1559,17 +1343,12 @@ module Rigor
1559
1343
  rvalue.block&.body
1560
1344
  end
1561
1345
 
1562
- # `class Foo < Data.define(:a, :b)` / `class Bar < Struct.new(:x)`
1563
- # synthesizes reader methods (`a`, `b`, `x`) on the subclass that no
1564
- # `def` / `attr_*` declares. Register them in the discovered-methods
1565
- # existence table so an implicit-self read of a member inside the
1566
- # class body is known to exist both for the existing
1567
- # undefined-method suppression and for the ADR-24 slice-4 self-call
1568
- # recorder, which must treat a synthesized member as an existing
1569
- # method, not an unresolved call. The block-form
1570
- # (`Const = Data.define(:a) do ... end`) is handled by the
1571
- # `ConstantWriteNode` branch's block recursion; its members type
1572
- # `self` as `Object`, out of scope here.
1346
+ # `class Foo < Data.define(:a, :b)` / `class Bar < Struct.new(:x)` synthesizes reader methods (`a`, `b`, `x`) on
1347
+ # the subclass that no `def` / `attr_*` declares. Register them in the discovered-methods existence table so an
1348
+ # implicit-self read of a member inside the class body is known to exist — both for the existing undefined-method
1349
+ # suppression and for the ADR-24 slice-4 self-call recorder, which must treat a synthesized member as an existing
1350
+ # method, not an unresolved call. The block-form (`Const = Data.define(:a) do ... end`) is handled by the
1351
+ # `ConstantWriteNode` branch's block recursion; its members type `self` as `Object`, out of scope here.
1573
1352
  def record_meta_superclass_members(class_node, qualified_prefix, accumulator)
1574
1353
  superclass = class_node.superclass
1575
1354
  return unless data_define_call?(superclass) || struct_new_call?(superclass)
@@ -1582,11 +1361,9 @@ module Rigor
1582
1361
  members.each { |member| table[member] ||= :instance }
1583
1362
  end
1584
1363
 
1585
- # The Symbol member names of a `Data.define(*Symbol)` /
1586
- # `Struct.new(*Symbol [, keyword_init:])` call. For `Struct.new` the
1587
- # optional leading String name and trailing `keyword_init:` hash are
1588
- # stripped by {#struct_new_positionals}; `Data.define` args are all
1589
- # Symbols already.
1364
+ # The Symbol member names of a `Data.define(*Symbol)` / `Struct.new(*Symbol [, keyword_init:])` call. For
1365
+ # `Struct.new` the optional leading String name and trailing `keyword_init:` hash are stripped by
1366
+ # {#struct_new_positionals}; `Data.define` args are all Symbols already.
1590
1367
  def meta_member_names(call_node)
1591
1368
  raw = call_node.arguments&.arguments || []
1592
1369
  symbols = struct_new_call?(call_node) ? (struct_new_positionals(raw) || []) : raw
@@ -1603,16 +1380,12 @@ module Rigor
1603
1380
  accumulator[class_name][def_node.name] = kind
1604
1381
  end
1605
1382
 
1606
- # `def Foo.bar` inside `module Foo` (or `def Meta.init` inside
1607
- # `module Meta`) is semantically equivalent to `def self.bar`:
1608
- # at the def-site, the runtime value of the constant `Foo` is
1609
- # the module itself (== `self`). Recognise the form so the
1610
- # method registers as singleton on the enclosing class.
1383
+ # `def Foo.bar` inside `module Foo` (or `def Meta.init` inside `module Meta`) is semantically equivalent to `def
1384
+ # self.bar`: at the def-site, the runtime value of the constant `Foo` is the module itself (== `self`). Recognise
1385
+ # the form so the method registers as singleton on the enclosing class.
1611
1386
  #
1612
- # The cross-class form `def Bar.baz` inside `module Foo` —
1613
- # where the receiver names a constant other than the
1614
- # enclosing class — is not supported at this slice; falls
1615
- # through to `:instance` (current behaviour) rather than
1387
+ # The cross-class form `def Bar.baz` inside `module Foo` — where the receiver names a constant other than the
1388
+ # enclosing class is not supported at this slice; falls through to `:instance` (current behaviour) rather than
1616
1389
  # silently re-routing the registration.
1617
1390
  def def_singleton?(def_node, qualified_prefix, in_singleton_class)
1618
1391
  return true if def_node.receiver.is_a?(Prism::SelfNode) || in_singleton_class
@@ -1620,10 +1393,9 @@ module Rigor
1620
1393
  def_receiver_targets_lexical_self?(def_node.receiver, qualified_prefix)
1621
1394
  end
1622
1395
 
1623
- # Only `Prism::ConstantReadNode` is observed in real Ruby —
1624
- # Prism mis-parses `def C::P.method` as `def C.P` (Ruby itself
1625
- # rejects the form as a SyntaxError). The ConstantPathNode
1626
- # branch stays defensive in case Prism's grammar widens.
1396
+ # Only `Prism::ConstantReadNode` is observed in real Ruby — Prism mis-parses `def C::P.method` as `def C.P` (Ruby
1397
+ # itself rejects the form as a SyntaxError). The ConstantPathNode branch stays defensive in case Prism's grammar
1398
+ # widens.
1627
1399
  def def_receiver_targets_lexical_self?(receiver, qualified_prefix)
1628
1400
  return false if qualified_prefix.empty?
1629
1401
 
@@ -1641,13 +1413,10 @@ module Rigor
1641
1413
  end
1642
1414
  end
1643
1415
 
1644
- # v0.0.3 A — sentinel key under which `record_def_node`
1645
- # files DefNodes that live outside any class / module
1646
- # body (top-level helpers, `def`s nested inside DSL
1647
- # blocks like `RSpec.describe ... do; def helper; end`).
1648
- # Looked up by `Scope#top_level_def_for` to give
1649
- # implicit-self calls priority over RBS dispatch when
1650
- # the file defines a same-named local method.
1416
+ # v0.0.3 A — sentinel key under which `record_def_node` files DefNodes that live outside any class / module body
1417
+ # (top-level helpers, `def`s nested inside DSL blocks like `RSpec.describe ... do; def helper; end`). Looked up by
1418
+ # `Scope#top_level_def_for` to give implicit-self calls priority over RBS dispatch when the file defines a
1419
+ # same-named local method.
1651
1420
  TOP_LEVEL_DEF_KEY = "<toplevel>"
1652
1421
 
1653
1422
  def record_def_node(def_node, qualified_prefix, in_singleton_class, accumulator)
@@ -1658,18 +1427,13 @@ module Rigor
1658
1427
  accumulator[class_name][def_node.name] = def_node
1659
1428
  end
1660
1429
 
1661
- # Module-singleton call resolution (ADR-57 follow-up) — the
1662
- # SINGLETON-side mirror of `build_discovered_def_nodes`. Records the
1663
- # `Prism::DefNode` for every singleton-side method (`def self.x`,
1664
- # `def Foo.x`, a `class << self` body, and a `module_function`
1665
- # method) keyed by qualified class/module name method node, so
1666
- # `ExpressionTyper` can re-type the body when a `Singleton[Foo]`
1667
- # receiver dispatches `Foo.x`. The instance-side table is kept
1668
- # singleton-free on purpose (its ancestor walk binds `self` as
1669
- # `Nominal`), so the two never overlap except for `module_function`
1670
- # defs, which are genuinely callable on both sides and so appear in
1671
- # both tables. Top-level singleton defs (`def self.x` outside any
1672
- # class — `self` is `main`) are not recorded; they have no constant
1430
+ # Module-singleton call resolution (ADR-57 follow-up) — the SINGLETON-side mirror of `build_discovered_def_nodes`.
1431
+ # Records the `Prism::DefNode` for every singleton-side method (`def self.x`, `def Foo.x`, a `class << self` body,
1432
+ # and a `module_function` method) keyed by qualified class/module name → method → node, so `ExpressionTyper` can
1433
+ # re-type the body when a `Singleton[Foo]` receiver dispatches `Foo.x`. The instance-side table is kept
1434
+ # singleton-free on purpose (its ancestor walk binds `self` as `Nominal`), so the two never overlap except for
1435
+ # `module_function` defs, which are genuinely callable on both sides and so appear in both tables. Top-level
1436
+ # singleton defs (`def self.x` outside any class — `self` is `main`) are not recorded; they have no constant
1673
1437
  # receiver to dispatch through.
1674
1438
  def build_discovered_singleton_def_nodes(root)
1675
1439
  accumulator = {}
@@ -1677,12 +1441,10 @@ module Rigor
1677
1441
  accumulator.transform_values(&:freeze).freeze
1678
1442
  end
1679
1443
 
1680
- # Walks every node, entering class/module/singleton-class bodies via
1681
- # {#walk_singleton_body} so a bare `module_function` toggle threads
1682
- # correctly across the body's *sibling* statements (a child-by-child
1683
- # recursion would reset it). At the top level / inside an arbitrary
1684
- # node there is no `module_function` state to carry, so descent is a
1685
- # plain per-child walk.
1444
+ # Walks every node, entering class/module/singleton-class bodies via {#walk_singleton_body} so a bare
1445
+ # `module_function` toggle threads correctly across the body's *sibling* statements (a child-by-child recursion
1446
+ # would reset it). At the top level / inside an arbitrary node there is no `module_function` state to carry, so
1447
+ # descent is a plain per-child walk.
1686
1448
  def walk_singleton_def_nodes(node, qualified_prefix, in_singleton_class, accumulator)
1687
1449
  return unless node.is_a?(Prism::Node)
1688
1450
 
@@ -1717,12 +1479,10 @@ module Rigor
1717
1479
  end
1718
1480
  end
1719
1481
 
1720
- # Walks a class/module/singleton-class body's direct statements in
1721
- # source order, threading the bare-`module_function` toggle: once a
1722
- # bare `module_function` is seen, every subsequent `def` in the body
1723
- # registers as a singleton method. Nested classes/modules/defs and
1724
- # `module_function :a, :b` named forms recurse / record through the
1725
- # general walker so the toggle stays scoped to its own body.
1482
+ # Walks a class/module/singleton-class body's direct statements in source order, threading the
1483
+ # bare-`module_function` toggle: once a bare `module_function` is seen, every subsequent `def` in the body
1484
+ # registers as a singleton method. Nested classes/modules/defs and `module_function :a, :b` named forms recurse /
1485
+ # record through the general walker so the toggle stays scoped to its own body.
1726
1486
  def walk_singleton_body(body, qualified_prefix, in_singleton_class, accumulator)
1727
1487
  module_function_on = false
1728
1488
  statements_of(body).each do |stmt|
@@ -1742,9 +1502,8 @@ module Rigor
1742
1502
  end
1743
1503
  end
1744
1504
 
1745
- # Direct statement children of a class/module body node (a
1746
- # `Prism::StatementsNode`, a `Prism::BeginNode` wrapping one, or a
1747
- # lone statement). Returns an empty list for an empty body.
1505
+ # Direct statement children of a class/module body node (a `Prism::StatementsNode`, a `Prism::BeginNode` wrapping
1506
+ # one, or a lone statement). Returns an empty list for an empty body.
1748
1507
  def statements_of(body)
1749
1508
  case body
1750
1509
  when Prism::StatementsNode then body.body
@@ -1763,8 +1522,8 @@ module Rigor
1763
1522
  (accumulator[class_name] ||= {})[def_node.name] = def_node
1764
1523
  end
1765
1524
 
1766
- # A bare `module_function` (no arguments) flips every following `def`
1767
- # in the module body to module-function (instance + singleton) mode.
1525
+ # A bare `module_function` (no arguments) flips every following `def` in the module body to module-function
1526
+ # (instance + singleton) mode.
1768
1527
  def module_function_toggle?(node)
1769
1528
  node.name == :module_function && node.receiver.nil?
1770
1529
  end
@@ -1773,12 +1532,10 @@ module Rigor
1773
1532
  node.arguments.nil? || node.arguments.arguments.empty?
1774
1533
  end
1775
1534
 
1776
- # `module_function :a, :b` retro-marks named siblings (defined
1777
- # earlier OR later in the same body) as module-functions. Resolves
1778
- # each symbol-literal argument against the body's own `def`s and
1779
- # registers the matching `DefNode` on the module's singleton side.
1780
- # Non-symbol arguments and names with no matching `def` are skipped
1781
- # (a miss degrades to today's `Dynamic`, never a false resolution).
1535
+ # `module_function :a, :b` retro-marks named siblings (defined earlier OR later in the same body) as
1536
+ # module-functions. Resolves each symbol-literal argument against the body's own `def`s and registers the matching
1537
+ # `DefNode` on the module's singleton side. Non-symbol arguments and names with no matching `def` are skipped (a
1538
+ # miss degrades to today's `Dynamic`, never a false resolution).
1782
1539
  def record_module_function_names(node, qualified_prefix, body, accumulator)
1783
1540
  return if qualified_prefix.empty?
1784
1541
 
@@ -1798,14 +1555,10 @@ module Rigor
1798
1555
  arg.unescaped.to_sym if arg.is_a?(Prism::SymbolNode) || arg.is_a?(Prism::StringNode)
1799
1556
  end
1800
1557
 
1801
- # ADR-24 slice 2 — per-class table mapping a fully
1802
- # qualified user class to its superclass name AS WRITTEN
1803
- # at the `class Foo < Bar` declaration. Only constant
1804
- # superclasses are recorded (`class Foo < Struct.new(...)`
1805
- # and other non-constant superclasses produce no entry).
1806
- # The as-written name is resolved to a qualified class at
1807
- # the call site against the subclass's lexical nesting —
1808
- # see `ExpressionTyper#resolve_ancestor_class_name`.
1558
+ # ADR-24 slice 2 — per-class table mapping a fully qualified user class to its superclass name AS WRITTEN at the
1559
+ # `class Foo < Bar` declaration. Only constant superclasses are recorded (`class Foo < Struct.new(...)` and other
1560
+ # non-constant superclasses produce no entry). The as-written name is resolved to a qualified class at the call
1561
+ # site against the subclass's lexical nesting — see `ExpressionTyper#resolve_ancestor_class_name`.
1809
1562
  def build_discovered_superclasses(root)
1810
1563
  accumulator = {}
1811
1564
  walk_class_superclasses(root, [], accumulator)
@@ -1838,15 +1591,11 @@ module Rigor
1838
1591
  end
1839
1592
  end
1840
1593
 
1841
- # ADR-48 — per qualified class name -> ordered `Data.define`
1842
- # member-name list, for both the named-subclass form
1843
- # (`class Point < Data.define(:x, :y)`) and the constant-assigned
1844
- # form (`Point = Data.define(:x, :y)`). Only `Data.define` is
1845
- # recorded: `Struct.new` instances are mutable, so member-value
1846
- # folding would be unsound (the Struct follow-up is deferred — see
1847
- # ADR-48 § "Struct follow-up"). Consumed by
1848
- # {Inference::MethodDispatcher::DataFolding} via
1849
- # {Scope#data_member_layout}.
1594
+ # ADR-48 — per qualified class name -> ordered `Data.define` member-name list, for both the named-subclass form
1595
+ # (`class Point < Data.define(:x, :y)`) and the constant-assigned form (`Point = Data.define(:x, :y)`). Only
1596
+ # `Data.define` is recorded: `Struct.new` instances are mutable, so member-value folding would be unsound (the
1597
+ # Struct follow-up is deferred — see ADR-48 § "Struct follow-up"). Consumed by
1598
+ # {Inference::MethodDispatcher::DataFolding} via {Scope#data_member_layout}.
1850
1599
  def build_data_member_layouts(root)
1851
1600
  accumulator = {}
1852
1601
  walk_data_member_layouts(root, [], accumulator)
@@ -1879,8 +1628,8 @@ module Rigor
1879
1628
  end
1880
1629
  end
1881
1630
 
1882
- # Records `qualified -> [members]` when `expr` is a
1883
- # `Data.define(*Symbol)` call with at least one literal-Symbol member.
1631
+ # Records `qualified -> [members]` when `expr` is a `Data.define(*Symbol)` call with at least one literal-Symbol
1632
+ # member.
1884
1633
  def record_data_member_layout(accumulator, qualified_parts, expr)
1885
1634
  return unless data_define_call?(expr)
1886
1635
 
@@ -1890,12 +1639,10 @@ module Rigor
1890
1639
  accumulator[qualified_parts.join("::")] = members.freeze
1891
1640
  end
1892
1641
 
1893
- # ADR-48 Struct follow-up — the `Struct.new(...)` sibling of
1894
- # {#build_data_member_layouts}. A separate, additive table so the
1895
- # existing `Data.define` value-shape contract (a bare `[Symbol]`) is
1896
- # untouched: a Struct entry carries `{ members:, keyword_init: }`
1897
- # because the dispatcher needs the flag to fold the matching `.new`
1898
- # call form (positional vs keyword) without manufacturing a wrong map.
1642
+ # ADR-48 Struct follow-up — the `Struct.new(...)` sibling of {#build_data_member_layouts}. A separate, additive
1643
+ # table so the existing `Data.define` value-shape contract (a bare `[Symbol]`) is untouched: a Struct entry
1644
+ # carries `{ members:, keyword_init: }` because the dispatcher needs the flag to fold the matching `.new` call
1645
+ # form (positional vs keyword) without manufacturing a wrong map.
1899
1646
  def build_struct_member_layouts(root)
1900
1647
  accumulator = {}
1901
1648
  walk_struct_member_layouts(root, [], accumulator)
@@ -1928,9 +1675,8 @@ module Rigor
1928
1675
  end
1929
1676
  end
1930
1677
 
1931
- # Records `qualified -> { members:, keyword_init: }` when `expr` is a
1932
- # `Struct.new(*Symbol [, keyword_init: <bool>])` call with at least one
1933
- # literal-Symbol member.
1678
+ # Records `qualified -> { members:, keyword_init: }` when `expr` is a `Struct.new(*Symbol [, keyword_init:
1679
+ # <bool>])` call with at least one literal-Symbol member.
1934
1680
  def record_struct_member_layout(accumulator, qualified_parts, expr)
1935
1681
  return unless struct_new_call?(expr)
1936
1682
 
@@ -1943,9 +1689,8 @@ module Rigor
1943
1689
  }.freeze
1944
1690
  end
1945
1691
 
1946
- # True when a `Struct.new` call carries `keyword_init: true` as a
1947
- # literal in its trailing keyword hash. A non-literal value (or its
1948
- # absence) reads as `false` — the conservative positional default.
1692
+ # True when a `Struct.new` call carries `keyword_init: true` as a literal in its trailing keyword hash. A
1693
+ # non-literal value (or its absence) reads as `false` the conservative positional default.
1949
1694
  def struct_new_keyword_init?(call_node)
1950
1695
  args = call_node.arguments&.arguments || []
1951
1696
  last = args.last
@@ -1960,16 +1705,11 @@ module Rigor
1960
1705
 
1961
1706
  MIXIN_CALL_NAMES = %i[include prepend].freeze
1962
1707
 
1963
- # ADR-24 slice 2 — per-class/module table mapping a fully
1964
- # qualified user class or module to the list of module
1965
- # names it `include`s / `prepend`s, AS WRITTEN at the
1966
- # mixin call (`include Foo` / `include Foo::Bar`). Only
1967
- # constant arguments are recorded; dynamic mixins
1968
- # (`include some_method`) produce no entry. `prepend` is
1969
- # bucketed with `include` — both contribute instance
1970
- # methods to the ancestor chain. `extend` is NOT tracked
1971
- # (it adds singleton methods; ADR-24 slice 2 resolves the
1972
- # instance-side chain).
1708
+ # ADR-24 slice 2 — per-class/module table mapping a fully qualified user class or module to the list of module
1709
+ # names it `include`s / `prepend`s, AS WRITTEN at the mixin call (`include Foo` / `include Foo::Bar`). Only
1710
+ # constant arguments are recorded; dynamic mixins (`include some_method`) produce no entry. `prepend` is bucketed
1711
+ # with `include` both contribute instance methods to the ancestor chain. `extend` is NOT tracked (it adds
1712
+ # singleton methods; ADR-24 slice 2 resolves the instance-side chain).
1973
1713
  def build_discovered_includes(root)
1974
1714
  accumulator = {}
1975
1715
  walk_class_includes(root, [], nil, accumulator)
@@ -2008,8 +1748,7 @@ module Rigor
2008
1748
 
2009
1749
  VISIBILITY_MODIFIERS = %i[public private protected].freeze
2010
1750
 
2011
- # v0.1.2 — per-class method-visibility table for the
2012
- # `def.method-visibility-mismatch` CheckRule.
1751
+ # v0.1.2 — per-class method-visibility table for the `def.method-visibility-mismatch` CheckRule.
2013
1752
  #
2014
1753
  # Tracks two visibility-changing forms:
2015
1754
  #
@@ -2025,10 +1764,8 @@ module Rigor
2025
1764
  # tracking the def-call's return-value visibility,
2026
1765
  # which is a separate slice.
2027
1766
  #
2028
- # Top-level (no surrounding class) defs do not contribute
2029
- # — Ruby's top-level visibility nuances (private at
2030
- # top-level marks the method on `Object`) are out of
2031
- # scope for v0.1.2.
1767
+ # Top-level (no surrounding class) defs do not contribute — Ruby's top-level visibility nuances (private at
1768
+ # top-level marks the method on `Object`) are out of scope for v0.1.2.
2032
1769
  def build_discovered_method_visibilities(root)
2033
1770
  accumulator = {}
2034
1771
  walk_method_visibilities(root, [], false, :public, accumulator)
@@ -2069,9 +1806,8 @@ module Rigor
2069
1806
  return updated unless updated.equal?(current_visibility)
2070
1807
  end
2071
1808
 
2072
- # Statement-position StatementsNode preserves
2073
- # left-to-right visibility flow; everything else
2074
- # recurses with the entry visibility unchanged.
1809
+ # Statement-position StatementsNode preserves left-to-right visibility flow; everything else recurses with the
1810
+ # entry visibility unchanged.
2075
1811
  if node.is_a?(Prism::StatementsNode)
2076
1812
  local_visibility = current_visibility
2077
1813
  node.compact_child_nodes.each do |child|
@@ -2096,8 +1832,7 @@ module Rigor
2096
1832
  accumulator[class_name][def_node.name] = current_visibility
2097
1833
  end
2098
1834
 
2099
- # Recognises modifier calls on the implicit-self receiver
2100
- # inside a class body. Returns the (possibly updated)
1835
+ # Recognises modifier calls on the implicit-self receiver inside a class body. Returns the (possibly updated)
2101
1836
  # current visibility:
2102
1837
  #
2103
1838
  # - `private` / `public` / `protected` (no args) —
@@ -2137,11 +1872,9 @@ module Rigor
2137
1872
  nil
2138
1873
  end
2139
1874
 
2140
- # Registers the alias name in the `discovered_methods` table so
2141
- # `undefined-method` diagnostics are not emitted for calls to the
2142
- # aliased name. The kind mirrors the surrounding class context
2143
- # (instance inside a regular class body, singleton inside
2144
- # `class << self`).
1875
+ # Registers the alias name in the `discovered_methods` table so `undefined-method` diagnostics are not emitted for
1876
+ # calls to the aliased name. The kind mirrors the surrounding class context (instance inside a regular class body,
1877
+ # singleton inside `class << self`).
2145
1878
  def record_alias_method(alias_node, qualified_prefix, in_singleton_class, accumulator)
2146
1879
  return if qualified_prefix.empty?
2147
1880
  return unless alias_node.new_name.is_a?(Prism::SymbolNode)
@@ -2152,11 +1885,9 @@ module Rigor
2152
1885
  (accumulator[class_name] ||= {})[new_name] = kind
2153
1886
  end
2154
1887
 
2155
- # Post-pass over the `def_nodes` accumulator: for every `alias`
2156
- # declaration inside a class body, if the original method name
2157
- # maps to a `Prism::DefNode`, register the new name pointing to
2158
- # the same node so inter-procedural return-type inference works
2159
- # for the aliased name.
1888
+ # Post-pass over the `def_nodes` accumulator: for every `alias` declaration inside a class body, if the original
1889
+ # method name maps to a `Prism::DefNode`, register the new name pointing to the same node so inter-procedural
1890
+ # return-type inference works for the aliased name.
2160
1891
  def apply_alias_def_nodes(root, accumulator)
2161
1892
  alias_map = collect_class_alias_map(root, [], {})
2162
1893
  alias_map.each do |class_name, aliases|
@@ -2172,8 +1903,8 @@ module Rigor
2172
1903
  end
2173
1904
  end
2174
1905
 
2175
- # Builds a map `{class_name => {new_name_sym => old_name_sym}}` by
2176
- # walking the tree for `AliasMethodNode` nodes inside class bodies.
1906
+ # Builds a map `{class_name => {new_name_sym => old_name_sym}}` by walking the tree for `AliasMethodNode` nodes
1907
+ # inside class bodies.
2177
1908
  def collect_class_alias_map(node, qualified_prefix, accumulator)
2178
1909
  return accumulator unless node.is_a?(Prism::Node)
2179
1910
 
@@ -2218,14 +1949,11 @@ module Rigor
2218
1949
  accumulator[class_name][method_name] = in_singleton_class ? :singleton : :instance
2219
1950
  end
2220
1951
 
2221
- # The `attr_*` accessor macros that introduce methods Rigor must
2222
- # treat as source-declared. Without this, a class that defines an
2223
- # accessor with `attr_reader :x` AND carries RBS that omits `x`
2224
- # (a common gap the project ships an incomplete `sig/`) fires a
2225
- # false `call.undefined-method` on `obj.x`, because the
2226
- # undefined-method rule only suppressed `def` / `define_method` /
2227
- # `alias_method`-discovered methods. `attr_reader` defines
2228
- # readers, `attr_writer` writers (`x=`), `attr_accessor` both.
1952
+ # The `attr_*` accessor macros that introduce methods Rigor must treat as source-declared. Without this, a class
1953
+ # that defines an accessor with `attr_reader :x` AND carries RBS that omits `x` (a common gap — the project ships
1954
+ # an incomplete `sig/`) fires a false `call.undefined-method` on `obj.x`, because the undefined-method rule only
1955
+ # suppressed `def` / `define_method` / `alias_method`-discovered methods. `attr_reader` defines readers,
1956
+ # `attr_writer` writers (`x=`), `attr_accessor` both.
2229
1957
  ATTR_MACROS = %i[attr_reader attr_writer attr_accessor].freeze
2230
1958
 
2231
1959
  def record_attr_methods(call_node, qualified_prefix, in_singleton_class, accumulator)
@@ -2253,36 +1981,23 @@ module Rigor
2253
1981
  node.unescaped&.to_sym
2254
1982
  end
2255
1983
 
2256
- # Walks every file in `paths` (each path is parsed once with
2257
- # `Prism.parse_file`) and returns the unioned project-wide
2258
- # `discovered_classes` Hash: `{qualified_name => Singleton[…]}`.
2259
- # Used by `Analysis::Runner` to seed each file's
2260
- # `default_scope.discovered_classes` so that lexical
2261
- # constant lookup in one file resolves a `class Foo`
2262
- # declared in a sibling file. Per-file collisions are
2263
- # last-write-wins (matches the existing in-file merge
2264
- # semantics). Parse failures fail-soft to an empty
2265
- # contribution. The `buffer` argument, when present,
2266
- # redirects reads for the bound logical path to the
2267
- # buffer's physical path so editor-mode pre-passes see
2268
- # the in-flight bytes.
1984
+ # Walks every file in `paths` (each path is parsed once with `Prism.parse_file`) and returns the unioned
1985
+ # project-wide `discovered_classes` Hash: `{qualified_name => Singleton[…]}`. Used by `Analysis::Runner` to seed
1986
+ # each file's `default_scope.discovered_classes` so that lexical constant lookup in one file resolves a `class
1987
+ # Foo` declared in a sibling file. Per-file collisions are last-write-wins (matches the existing in-file merge
1988
+ # semantics). Parse failures fail-soft to an empty contribution. The `buffer` argument, when present, redirects
1989
+ # reads for the bound logical path to the buffer's physical path so editor-mode pre-passes see the in-flight
1990
+ # bytes.
2269
1991
  #
2270
- # **Modules are intentionally excluded** from the
2271
- # project-wide seed: a `module M; module_function; def x; end; end`
2272
- # body, when surfaced as `singleton(M)` to the dispatcher,
2273
- # falls through to `Kernel#x` (or any Module ancestor
2274
- # method) when the project's per-file
2275
- # `discovered_methods` doesn't know `M.x` — leading to
2276
- # surprising types like `Kernel.select Array[String]`.
2277
- # Until cross-file `discovered_methods` follows the same
2278
- # project-wide seed, registering modules here would
2279
- # introduce regressions in modules-with-module_function
2280
- # idioms that previously resolved to `Dynamic[Top]`.
2281
- # Class declarations are safe because per-file
2282
- # `discovered_methods` already tracks `def self.x` /
2283
- # `def x` instance and singleton methods consistently.
1992
+ # **Modules are intentionally excluded** from the project-wide seed: a `module M; module_function; def x; end;
1993
+ # end` body, when surfaced as `singleton(M)` to the dispatcher, falls through to `Kernel#x` (or any Module
1994
+ # ancestor method) when the project's per-file `discovered_methods` doesn't know `M.x` — leading to surprising
1995
+ # types like `Kernel.select → Array[String]`. Until cross-file `discovered_methods` follows the same project-wide
1996
+ # seed, registering modules here would introduce regressions in modules-with-module_function idioms that
1997
+ # previously resolved to `Dynamic[Top]`. Class declarations are safe because per-file `discovered_methods` already
1998
+ # tracks `def self.x` / `def x` instance and singleton methods consistently.
2284
1999
  #
2285
- # @param paths [Array<String>] project file paths.
2000
+ # @param paths [Array<String>] project file paths.
2286
2001
  # @param buffer [Rigor::Analysis::BufferBinding, nil]
2287
2002
  # @return [Hash{String => Rigor::Type::Singleton}]
2288
2003
  def discovered_classes_for_paths(paths, buffer: nil)
@@ -2293,35 +2008,26 @@ module Rigor
2293
2008
  root = Prism.parse(source, filepath: path).value
2294
2009
  collect_class_decls(root, [], accumulator)
2295
2010
  rescue StandardError
2296
- # Skip files that fail to parse or read; the per-file
2297
- # analyzer surfaces the parse error separately.
2011
+ # Skip files that fail to parse or read; the per-file analyzer surfaces the parse error separately.
2298
2012
  next
2299
2013
  end
2300
2014
  accumulator.freeze
2301
2015
  end
2302
2016
 
2303
- # ADR-24 slice 2 — cross-file companion to
2304
- # `discovered_classes_for_paths`. Walks every project
2305
- # file once and returns both the merged
2306
- # `discovered_def_nodes` table (a class reopened across
2307
- # files has its method tables merged) and the merged
2308
- # class -> superclass-name map. The engine consults these
2309
- # so an implicit-self call inside a subclass resolves
2310
- # against a superclass `def` declared in a sibling file
2311
- # (`Mastodon::CLI::Accounts` calling a helper defined in
2312
- # `Mastodon::CLI::Base`).
2017
+ # ADR-24 slice 2 — cross-file companion to `discovered_classes_for_paths`. Walks every project file once and
2018
+ # returns both the merged `discovered_def_nodes` table (a class reopened across files has its method tables
2019
+ # merged) and the merged class -> superclass-name map. The engine consults these so an implicit-self call inside a
2020
+ # subclass resolves against a superclass `def` declared in a sibling file (`Mastodon::CLI::Accounts` calling a
2021
+ # helper defined in `Mastodon::CLI::Base`).
2313
2022
  #
2314
- # The returned `def_sources` map mirrors `def_nodes` but stores
2315
- # a `"path:line"` String per `(class_name, method_name)` instead
2316
- # of the `Prism::DefNode`. A `Prism::Location` does not expose
2317
- # its source file through public API, so the source site is
2318
- # captured here, in the pre-pass loop that still holds `path`.
2319
- # `CheckRules#undefined_method_diagnostic` consults the seeded
2320
- # copy to name the defining file when a project monkey-patch on
2321
- # a core/stdlib/gem class is called cross-file (ADR-17). First
2322
- # write wins, matching `def_nodes`' own merge order.
2023
+ # The returned `def_sources` map mirrors `def_nodes` but stores a `"path:line"` String per `(class_name,
2024
+ # method_name)` instead of the `Prism::DefNode`. A `Prism::Location` does not expose its source file through
2025
+ # public API, so the source site is captured here, in the pre-pass loop that still holds `path`.
2026
+ # `CheckRules#undefined_method_diagnostic` consults the seeded copy to name the defining file when a project
2027
+ # monkey-patch on a core/stdlib/gem class is called cross-file (ADR-17). First write wins, matching `def_nodes`'
2028
+ # own merge order.
2323
2029
  #
2324
- # @param paths [Array<String>] project file paths.
2030
+ # @param paths [Array<String>] project file paths.
2325
2031
  # @param buffer [Rigor::Analysis::BufferBinding, nil]
2326
2032
  # @return [Hash{Symbol => Hash}]
2327
2033
  # `{ def_nodes:, def_sources:, superclasses:, includes:, class_sources: }`
@@ -2334,18 +2040,14 @@ module Rigor
2334
2040
  root = Prism.parse(File.read(physical), filepath: path).value
2335
2041
  accumulate_project_index(acc, path, root)
2336
2042
  rescue StandardError
2337
- # Skip files that fail to parse or read; the per-file
2338
- # analyzer surfaces the parse error separately.
2043
+ # Skip files that fail to parse or read; the per-file analyzer surfaces the parse error separately.
2339
2044
  next
2340
2045
  end
2341
- # Cross-file method suppression is for the project's OWN
2342
- # accessors (attr_* / define_method / alias) NOT for plain
2343
- # `def`s. A cross-file `def` on a class is exactly the ADR-17
2344
- # monkey-patch case the undefined-method rule deliberately
2345
- # surfaces (fire + def-site annotation, nudging `pre_eval:`),
2346
- # so dropping the `def`-declared names keeps that contract
2347
- # intact while still letting `attr_reader :x` in one file
2348
- # suppress a false undefined-method for `obj.x` in another.
2046
+ # Cross-file method suppression is for the project's OWN accessors (attr_* / define_method / alias) — NOT for
2047
+ # plain `def`s. A cross-file `def` on a class is exactly the ADR-17 monkey-patch case the undefined-method rule
2048
+ # deliberately surfaces (fire + def-site annotation, nudging `pre_eval:`), so dropping the `def`-declared names
2049
+ # keeps that contract intact while still letting `attr_reader :x` in one file suppress a false undefined-method
2050
+ # for `obj.x` in another.
2349
2051
  acc[:methods] = subtract_def_methods(acc[:methods], acc[:def_nodes])
2350
2052
  %i[def_nodes singleton_def_nodes def_sources includes method_visibilities methods class_sources].each do |key|
2351
2053
  acc[key].each_value(&:freeze)
@@ -2353,9 +2055,8 @@ module Rigor
2353
2055
  acc.transform_values(&:freeze)
2354
2056
  end
2355
2057
 
2356
- # Removes, per class, the method names that have a project `def`
2357
- # node, leaving only accessor/alias/define_method-introduced
2358
- # methods in the cross-file suppression table.
2058
+ # Removes, per class, the method names that have a project `def` node, leaving only
2059
+ # accessor/alias/define_method-introduced methods in the cross-file suppression table.
2359
2060
  def subtract_def_methods(methods, def_nodes)
2360
2061
  methods.each_with_object({}) do |(class_name, table), out|
2361
2062
  defs = def_nodes[class_name] || {}
@@ -2364,16 +2065,13 @@ module Rigor
2364
2065
  end
2365
2066
  end
2366
2067
 
2367
- # Folds one file's class-keyed indexes into the cross-file
2368
- # accumulator. `method_visibilities` (ADR-35) is collected here so
2369
- # the override-visibility-reduced rule can read an ancestor's
2370
- # visibility declared in a sibling file.
2068
+ # Folds one file's class-keyed indexes into the cross-file accumulator. `method_visibilities` (ADR-35) is
2069
+ # collected here so the override-visibility-reduced rule can read an ancestor's visibility declared in a sibling
2070
+ # file.
2371
2071
  def accumulate_project_index(acc, path, root)
2372
- # One combined descent yields both the methods existence table and
2373
- # the def-node table; the latter is also consumed by
2374
- # `record_class_sources`, so a def-dense file is walked once here
2375
- # instead of three times (methods + def-nodes ×2). See
2376
- # {#build_methods_and_def_nodes}.
2072
+ # One combined descent yields both the methods existence table and the def-node table; the latter is also
2073
+ # consumed by `record_class_sources`, so a def-dense file is walked once here instead of three times (methods +
2074
+ # def-nodes ×2). See {#build_methods_and_def_nodes}.
2377
2075
  file_methods, file_def_nodes = build_methods_and_def_nodes(root)
2378
2076
  merge_discovered_defs(acc[:def_nodes], acc[:def_sources], path, file_def_nodes)
2379
2077
  build_discovered_singleton_def_nodes(root).each do |class_name, methods|
@@ -2390,18 +2088,16 @@ module Rigor
2390
2088
  merge_member_layout_tables(acc, root)
2391
2089
  end
2392
2090
 
2393
- # Folds one file's Data + Struct member-layout tables into the
2394
- # cross-file accumulator (kept out of {#accumulate_project_index} to
2395
- # hold its ABC budget).
2091
+ # Folds one file's Data + Struct member-layout tables into the cross-file accumulator (kept out of
2092
+ # {#accumulate_project_index} to hold its ABC budget).
2396
2093
  def merge_member_layout_tables(acc, root)
2397
2094
  acc[:data_member_layouts].merge!(build_data_member_layouts(root))
2398
2095
  acc[:struct_member_layouts].merge!(build_struct_member_layouts(root))
2399
2096
  end
2400
2097
 
2401
- # Folds the per-class method-visibility and method-existence tables of
2402
- # one file into the cross-file accumulator (kept out of
2403
- # {#accumulate_project_index} to hold its ABC budget). `file_methods`
2404
- # is the existence table from the combined methods/def-nodes descent.
2098
+ # Folds the per-class method-visibility and method-existence tables of one file into the cross-file accumulator
2099
+ # (kept out of {#accumulate_project_index} to hold its ABC budget). `file_methods` is the existence table from the
2100
+ # combined methods/def-nodes descent.
2405
2101
  def merge_class_keyed_index_tables(acc, root, file_methods)
2406
2102
  build_discovered_method_visibilities(root).each do |class_name, table|
2407
2103
  (acc[:method_visibilities][class_name] ||= {}).merge!(table)
@@ -2411,16 +2107,12 @@ module Rigor
2411
2107
  end
2412
2108
  end
2413
2109
 
2414
- # ADR-46 slice 1 — accumulates, per qualified user class/module
2415
- # name, the set of files that declare it. A class's declaration
2416
- # shape (its body `def`s, its `class Foo < Bar` superclass, its
2417
- # `include`s) lives wherever the class is opened, so every file that
2418
- # contributes a def / superclass / include for a name is a source of
2419
- # that name's ancestry edges. {Scope#superclass_of} /
2420
- # {Scope#includes_of} record this set when resolving the edge during
2421
- # dependency recording (ADR-46). The class-declaration walk
2422
- # (`collect_class_decls`) catches bodyless / def-less reopenings the
2423
- # other three builders miss.
2110
+ # ADR-46 slice 1 — accumulates, per qualified user class/module name, the set of files that declare it. A class's
2111
+ # declaration shape (its body `def`s, its `class Foo < Bar` superclass, its `include`s) lives wherever the class
2112
+ # is opened, so every file that contributes a def / superclass / include for a name is a source of that name's
2113
+ # ancestry edges. {Scope#superclass_of} / {Scope#includes_of} record this set when resolving the edge during
2114
+ # dependency recording (ADR-46). The class-declaration walk (`collect_class_decls`) catches bodyless / def-less
2115
+ # reopenings the other three builders miss.
2424
2116
  def record_class_sources(class_sources, path, root, superclasses, includes, file_def_nodes)
2425
2117
  names = Set.new
2426
2118
  collect_class_decls(root, [], decls = {})
@@ -2431,11 +2123,9 @@ module Rigor
2431
2123
  names.each { |name| (class_sources[name] ||= Set.new) << path }
2432
2124
  end
2433
2125
 
2434
- # Merges one file's `class → method → DefNode` map into the
2435
- # cross-file `def_nodes` index and records each method's first-
2436
- # seen `"path:line"` definition site in `def_sources` (ADR-17
2437
- # the un-registered-project-patch signal `call.undefined-method`
2438
- # and `rigor triage` key on).
2126
+ # Merges one file's `class → method → DefNode` map into the cross-file `def_nodes` index and records each method's
2127
+ # first- seen `"path:line"` definition site in `def_sources` (ADR-17 — the un-registered-project-patch signal
2128
+ # `call.undefined-method` and `rigor triage` key on).
2439
2129
  def merge_discovered_defs(def_nodes, def_sources, path, file_def_nodes)
2440
2130
  file_def_nodes.each do |class_name, methods|
2441
2131
  (def_nodes[class_name] ||= {}).merge!(methods)
@@ -2446,10 +2136,8 @@ module Rigor
2446
2136
  end
2447
2137
  end
2448
2138
 
2449
- # Class-only variant of `record_declarations` — descends
2450
- # into nested module bodies (so `module Foo; class Bar`
2451
- # registers `Foo::Bar`) but never registers the module
2452
- # itself in `accumulator`.
2139
+ # Class-only variant of `record_declarations` — descends into nested module bodies (so `module Foo; class Bar`
2140
+ # registers `Foo::Bar`) but never registers the module itself in `accumulator`.
2453
2141
  def collect_class_decls(node, qualified_prefix, accumulator)
2454
2142
  return unless node.is_a?(Prism::Node)
2455
2143
 
@@ -2471,22 +2159,16 @@ module Rigor
2471
2159
  node.compact_child_nodes.each { |child| collect_class_decls(child, qualified_prefix, accumulator) }
2472
2160
  end
2473
2161
 
2474
- # T1 (template-corpora survey) — record a `Const = Class.new(Super)`
2475
- # (and the bare `Class.new` / `Module.new`) class-creating constant
2476
- # in the cross-file discovery table so a reference to `Const` from
2477
- # ANOTHER file under the same namespace resolves to the project
2478
- # class instead of falling through to a core same-named class
2479
- # (`Liquid::SyntaxError = Class.new(Error)` referenced in a sibling
2480
- # file's `rescue SyntaxError => e`, which otherwise resolved to core
2481
- # `::SyntaxError`). Mirrors the single-file `in_source_constants`
2482
- # answer, which types `Class.new(Super)` as `Singleton[Super]` (the
2483
- # constructed class answers method lookups through Super's chain).
2484
- # The superclass name is resolved lexically against the enclosing
2485
- # prefix; a bare `Class.new` with no superclass (or `Module.new`)
2486
- # types as `Singleton[Const]` itself. The block form is left to the
2487
- # existing `meta_new_block_body` machinery — only the plain
2488
- # `Class.new(Super)` constant (the namespaced-sibling-error idiom)
2489
- # is added here.
2162
+ # T1 (template-corpora survey) — record a `Const = Class.new(Super)` (and the bare `Class.new` / `Module.new`)
2163
+ # class-creating constant in the cross-file discovery table so a reference to `Const` from ANOTHER file under the
2164
+ # same namespace resolves to the project class instead of falling through to a core same-named class
2165
+ # (`Liquid::SyntaxError = Class.new(Error)` referenced in a sibling file's `rescue SyntaxError => e`, which
2166
+ # otherwise resolved to core `::SyntaxError`). Mirrors the single-file `in_source_constants` answer, which types
2167
+ # `Class.new(Super)` as `Singleton[Super]` (the constructed class answers method lookups through Super's chain).
2168
+ # The superclass name is resolved lexically against the enclosing prefix; a bare `Class.new` with no superclass
2169
+ # (or `Module.new`) types as `Singleton[Const]` itself. The block form is left to the existing
2170
+ # `meta_new_block_body` machinery only the plain `Class.new(Super)` constant (the namespaced-sibling-error
2171
+ # idiom) is added here.
2490
2172
  def record_class_new_constant_decl(node, qualified_prefix, accumulator)
2491
2173
  rvalue = node.value
2492
2174
  return unless class_new_call?(rvalue) || module_new_call?(rvalue)
@@ -2497,13 +2179,11 @@ module Rigor
2497
2179
  accumulator[full] = Type::Combinator.singleton_of(super_name || full)
2498
2180
  end
2499
2181
 
2500
- # Lexically-qualified name of a `Class.new(Super)` superclass
2501
- # argument, or nil when there is no positional superclass (a bare
2502
- # `Class.new` / `Module.new`). When the unqualified super name is a
2503
- # class already discovered under an enclosing-prefix segment, the
2504
- # qualified form is returned (so `Class.new(Error)` inside `module M`
2505
- # resolves to `M::Error`); otherwise the literal name is returned
2506
- # (covering a core / RBS-known superclass spelled bare).
2182
+ # Lexically-qualified name of a `Class.new(Super)` superclass argument, or nil when there is no positional
2183
+ # superclass (a bare `Class.new` / `Module.new`). When the unqualified super name is a class already discovered
2184
+ # under an enclosing-prefix segment, the qualified form is returned (so `Class.new(Error)` inside `module M`
2185
+ # resolves to `M::Error`); otherwise the literal name is returned (covering a core / RBS-known superclass spelled
2186
+ # bare).
2507
2187
  def class_new_superclass_name(call_node, qualified_prefix, accumulator)
2508
2188
  arg = call_node.arguments&.arguments&.first
2509
2189
  return nil if arg.nil?
@@ -2521,15 +2201,11 @@ module Rigor
2521
2201
  raw
2522
2202
  end
2523
2203
 
2524
- # Walks the program once for `Prism::ModuleNode` and
2525
- # `Prism::ClassNode`, recording the `Singleton[<qualified>]`
2526
- # type for the outermost `constant_path` node of each
2527
- # declaration. Inner segments of a `class Foo::Bar::Baz`
2528
- # path remain real references (resolved through the
2529
- # ordinary lexical walk), so we annotate ONLY the topmost
2530
- # path node. Nested declarations contribute their fully
2531
- # qualified path: `class A::B; class C; ...` produces
2532
- # `A::B` for the outer and `A::B::C` for the inner.
2204
+ # Walks the program once for `Prism::ModuleNode` and `Prism::ClassNode`, recording the `Singleton[<qualified>]`
2205
+ # type for the outermost `constant_path` node of each declaration. Inner segments of a `class Foo::Bar::Baz` path
2206
+ # remain real references (resolved through the ordinary lexical walk), so we annotate ONLY the topmost path node.
2207
+ # Nested declarations contribute their fully qualified path: `class A::B; class C; ...` produces `A::B` for the
2208
+ # outer and `A::B::C` for the inner.
2533
2209
  def build_declaration_artifacts(root)
2534
2210
  identity_table = {}.compare_by_identity
2535
2211
  discovered = {}
@@ -2565,21 +2241,17 @@ module Rigor
2565
2241
  true
2566
2242
  end
2567
2243
 
2568
- # Recognises class-creating meta calls at constant-write rvalue
2569
- # position and registers `Const` (qualified by the surrounding
2570
- # class/module path) as a discovered class. `Const.new(...)`
2571
- # then resolves to a fresh `Nominal[Const]` via `meta_new`,
2572
- # instead of the un-narrowed `Dynamic[top]` returned by the
2573
- # default `Class#new` envelope.
2244
+ # Recognises class-creating meta calls at constant-write rvalue position and registers `Const` (qualified by the
2245
+ # surrounding class/module path) as a discovered class. `Const.new(...)` then resolves to a fresh `Nominal[Const]`
2246
+ # via `meta_new`, instead of the un-narrowed `Dynamic[top]` returned by the default `Class#new` envelope.
2574
2247
  #
2575
2248
  # Two recognised meta forms:
2576
2249
  #
2577
2250
  # - `Const = Data.define(*Symbol) [do ... end]`
2578
2251
  # - `Const = Struct.new(*Symbol [, keyword_init: ...]) [do ... end]`
2579
2252
  #
2580
- # The block body, if present, is recursed into so any nested
2581
- # class/module declarations in the override block (rare but legal)
2582
- # still feed the discovered table.
2253
+ # The block body, if present, is recursed into so any nested class/module declarations in the override block (rare
2254
+ # but legal) still feed the discovered table.
2583
2255
  def record_meta_new_constant?(node, qualified_prefix, identity_table, discovered)
2584
2256
  return false unless data_define_call?(node.value) || struct_new_call?(node.value)
2585
2257
 
@@ -2589,11 +2261,9 @@ module Rigor
2589
2261
  true
2590
2262
  end
2591
2263
 
2592
- # Recognises `Data.define(*Symbol)` and `Data.define(*Symbol) do ... end`
2593
- # at constant-write rvalue position. The receiver MUST be the bare
2594
- # `Data` constant (or `::Data`); other receivers (a local variable, a
2595
- # method call return) are rejected because their identity is not
2596
- # statically known.
2264
+ # Recognises `Data.define(*Symbol)` and `Data.define(*Symbol) do ... end` at constant-write rvalue position. The
2265
+ # receiver MUST be the bare `Data` constant (or `::Data`); other receivers (a local variable, a method call
2266
+ # return) are rejected because their identity is not statically known.
2597
2267
  def data_define_call?(node)
2598
2268
  return false unless node.is_a?(Prism::CallNode)
2599
2269
  return false unless node.name == :define
@@ -2603,12 +2273,9 @@ module Rigor
2603
2273
  args.all?(Prism::SymbolNode)
2604
2274
  end
2605
2275
 
2606
- # Recognises `Struct.new(*Symbol)` and
2607
- # `Struct.new(*Symbol, keyword_init: <expr>)` at constant-write
2608
- # rvalue position. A trailing `KeywordHashNode` (the
2609
- # `keyword_init: ...` form) is accepted but does not contribute
2610
- # to member discovery; every other argument MUST be a
2611
- # `Prism::SymbolNode`. At least one Symbol member is required —
2276
+ # Recognises `Struct.new(*Symbol)` and `Struct.new(*Symbol, keyword_init: <expr>)` at constant-write rvalue
2277
+ # position. A trailing `KeywordHashNode` (the `keyword_init: ...` form) is accepted but does not contribute to
2278
+ # member discovery; every other argument MUST be a `Prism::SymbolNode`. At least one Symbol member is required —
2612
2279
  # `Struct.new()` is a degenerate form callers don't typically use.
2613
2280
  def struct_new_call?(node)
2614
2281
  return false unless meta_call_with_name?(node, :Struct, :new)
@@ -2620,27 +2287,19 @@ module Rigor
2620
2287
  positional.all?(Prism::SymbolNode)
2621
2288
  end
2622
2289
 
2623
- # Recognises `Module.new` and `Module.new(&block)` /
2624
- # `Module.new do ... end` at constant-write rvalue
2625
- # position. The block body is the anonymous module's
2626
- # `module_eval` body; defs inside it bind methods on the
2627
- # named constant (`Const = Module.new do ...; def foo; ...; end; end`).
2628
- # Arguments are NOT inspected because `Module.new` accepts
2629
- # no positionals — Ruby raises ArgumentError if any are
2630
- # passed — so a malformed call falls through the walker
2290
+ # Recognises `Module.new` and `Module.new(&block)` / `Module.new do ... end` at constant-write rvalue position.
2291
+ # The block body is the anonymous module's `module_eval` body; defs inside it bind methods on the named constant
2292
+ # (`Const = Module.new do ...; def foo; ...; end; end`). Arguments are NOT inspected because `Module.new` accepts
2293
+ # no positionals Ruby raises ArgumentError if any are passed — so a malformed call falls through the walker
2631
2294
  # without affecting analysis.
2632
2295
  def module_new_call?(node)
2633
2296
  meta_call_with_name?(node, :Module, :new)
2634
2297
  end
2635
2298
 
2636
- # Recognises `Class.new`, `Class.new(super_class)`, and the
2637
- # block form `Class.new { ... }`. Like `module_new_call?`,
2638
- # the block body is walked as the anonymous class's body.
2639
- # The optional `super_class` positional is accepted but does
2640
- # NOT route through `ancestor` discovery in this slice — the
2641
- # synthesised class still answers method lookups via its
2642
- # own body's defs, mirroring how `Struct.new` / `Data.define`
2643
- # are handled.
2299
+ # Recognises `Class.new`, `Class.new(super_class)`, and the block form `Class.new { ... }`. Like
2300
+ # `module_new_call?`, the block body is walked as the anonymous class's body. The optional `super_class`
2301
+ # positional is accepted but does NOT route through `ancestor` discovery in this slice — the synthesised class
2302
+ # still answers method lookups via its own body's defs, mirroring how `Struct.new` / `Data.define` are handled.
2644
2303
  def class_new_call?(node)
2645
2304
  meta_call_with_name?(node, :Class, :new)
2646
2305
  end
@@ -2665,17 +2324,14 @@ module Rigor
2665
2324
  end
2666
2325
  end
2667
2326
 
2668
- # Walks `node`'s subtree DFS and fills in scope entries for every
2669
- # Prism node the StatementEvaluator did not visit (i.e. expression-
2670
- # interior nodes like the receiver/args of a CallNode). Those
2671
- # nodes inherit their nearest recorded ancestor's scope.
2327
+ # Walks `node`'s subtree DFS and fills in scope entries for every Prism node the StatementEvaluator did not visit
2328
+ # (i.e. expression- interior nodes like the receiver/args of a CallNode). Those nodes inherit their nearest
2329
+ # recorded ancestor's scope.
2672
2330
  #
2673
- # `IfNode` / `UnlessNode` are special-cased: the truthy and falsey
2674
- # branches each get their predicate's narrowed scope before
2675
- # recursing. This handles expression-position conditionals
2676
- # (e.g. `cache[k] = if cond; t; else; e; end` and conditionals
2677
- # nested as call arguments) which are typed by ExpressionTyper
2678
- # without going through `eval_if`'s narrowing path.
2331
+ # `IfNode` / `UnlessNode` are special-cased: the truthy and falsey branches each get their predicate's narrowed
2332
+ # scope before recursing. This handles expression-position conditionals (e.g. `cache[k] = if cond; t; else; e;
2333
+ # end` and conditionals nested as call arguments) which are typed by ExpressionTyper without going through
2334
+ # `eval_if`'s narrowing path.
2679
2335
  def propagate(node, table, parent_scope)
2680
2336
  return unless node.is_a?(Prism::Node)
2681
2337