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
data/lib/rigor/scope.rb CHANGED
@@ -10,11 +10,9 @@ require_relative "inference/flow_tracer"
10
10
  require_relative "inference/statement_evaluator"
11
11
 
12
12
  module Rigor
13
- # Immutable analyzer scope: holds local-variable bindings and a reference
14
- # to the surrounding Environment. State changes return new scopes through
15
- # explicit transition methods (#with_local). The central query is
16
- # #type_of(node), the Rigor counterpart of PHPStan's
17
- # $scope->getType($node).
13
+ # Immutable analyzer scope: holds local-variable bindings and a reference to the surrounding Environment. State
14
+ # changes return new scopes through explicit transition methods (#with_local). The central query is
15
+ # #type_of(node), the Rigor counterpart of PHPStan's $scope->getType($node).
18
16
  #
19
17
  # See docs/internal-spec/inference-engine.md for the binding contract.
20
18
  # rubocop:disable Metrics/ClassLength,Metrics/ParameterLists
@@ -24,19 +22,15 @@ module Rigor
24
22
  :indexed_narrowings, :method_chain_narrowings,
25
23
  :declaration_sourced,
26
24
  :source_path, :discovery, :struct_fold_safe_locals,
27
- :dynamic_origins
25
+ :dynamic_origins, :local_origins, :ivar_origins
28
26
 
29
- # ADR-53 Track A — the seed-time discovery tables live on the
30
- # {DiscoveryIndex} the scope carries by a single reference; the
31
- # per-table readers stay on Scope so engine call sites and plugins
32
- # are unaffected by the extraction. The whole index is swapped in
33
- # one transition through {#with_discovery}.
27
+ # ADR-53 Track A — the seed-time discovery tables live on the {DiscoveryIndex} the scope carries by a single
28
+ # reference; the per-table readers stay on Scope so engine call sites and plugins are unaffected by the
29
+ # extraction. The whole index is swapped in one transition through {#with_discovery}.
34
30
  #
35
- # `declared_types` carries the identity-comparing
36
- # `Prism::Node => Rigor::Type` declaration overrides
37
- # `ExpressionTyper#type_of(node)` MUST consult before any other
38
- # dispatch (a `module Foo` / `class Bar` header types as
39
- # `Singleton[<qualified path>]` rather than `Dynamic[Top]`).
31
+ # `declared_types` carries the identity-comparing `Prism::Node => Rigor::Type` declaration overrides
32
+ # `ExpressionTyper#type_of(node)` MUST consult before any other dispatch (a `module Foo` / `class Bar` header
33
+ # types as `Singleton[<qualified path>]` rather than `Dynamic[Top]`).
40
34
  def declared_types = @discovery.declared_types
41
35
  def class_ivars = @discovery.class_ivars
42
36
  def class_cvars = @discovery.class_cvars
@@ -53,68 +47,63 @@ module Rigor
53
47
  def discovered_class_sources = @discovery.discovered_class_sources
54
48
  def data_member_layouts = @discovery.data_member_layouts
55
49
  def struct_member_layouts = @discovery.struct_member_layouts
56
- # ADR-67 WD3 — call-site-inferred parameter types, keyed by
57
- # `[class_name, method_name, kind]`. `build_method_entry_scope` consults
58
- # this to seed an undeclared `def` parameter with the union of its
59
- # resolved call-site argument types (precision-additive; an RBS-declared
60
- # parameter always wins). Empty unless a collection pass seeded it.
50
+ # ADR-67 WD3 — call-site-inferred parameter types, keyed by `[class_name, method_name, kind]`.
51
+ # `build_method_entry_scope` consults this to seed an undeclared `def` parameter with the union of its resolved
52
+ # call-site argument types (precision-additive; an RBS-declared parameter always wins). Empty unless a
53
+ # collection pass seeded it.
61
54
  def param_inferred_types = @discovery.param_inferred_types
62
55
 
63
- # Narrowing key for an indexed read `receiver[key]` where both
64
- # the receiver and the key are stable enough to address. The
65
- # value of the map at this key is the narrowed type the next
66
- # read at the same address MUST observe.
56
+ # Narrowing key for an indexed read `receiver[key]` where both the receiver and the key are stable enough to
57
+ # address. The value of the map at this key is the narrowed type the next read at the same address MUST
58
+ # observe.
67
59
  #
68
- # - `receiver_kind` ∈ `{:local, :ivar}` — the analyzer only
69
- # tracks reads against a local or instance variable today.
60
+ # - `receiver_kind` ∈ `{:local, :ivar}` — the analyzer only tracks reads against a local or instance variable
61
+ # today.
70
62
  # - `receiver_name` is the variable's Symbol.
71
- # - `key` is the Ruby value of the literal index (Symbol /
72
- # String / Integer). Non-literal keys (`params[field]`) are
73
- # not recorded; they have no stable address.
63
+ # - `key` is the Ruby value of the literal index (Symbol / String / Integer). Non-literal keys
64
+ # (`params[field]`) are not recorded; they have no stable address.
74
65
  IndexedKey = Data.define(:receiver_kind, :receiver_name, :key)
75
66
 
76
- # Narrowing key for a no-arg / no-block method-call chain
77
- # `receiver.method_name` (a "single-hop" chain per A1 from the
78
- # ROADMAP § Future cycles slice). The value of the map at this
79
- # key is the narrowed type the next read of the same chain
80
- # MUST observe — typically the post-`is_a?(C)` narrowing
81
- # established on a predicate edge.
67
+ # Narrowing key for a no-arg / no-block method-call chain `receiver.method_name` (a "single-hop" chain per A1
68
+ # from the ROADMAP § Future cycles slice). The value of the map at this key is the narrowed type the next read
69
+ # of the same chain MUST observe typically the post-`is_a?(C)` narrowing established on a predicate edge.
82
70
  #
83
- # - `receiver_kind` ∈ `{:local, :ivar}` — the analyzer only
84
- # tracks chains rooted at a local or instance variable
85
- # today (Law-of-Demeter-style single-hop).
71
+ # - `receiver_kind` ∈ `{:local, :ivar}` — the analyzer only tracks chains rooted at a local or instance
72
+ # variable today (Law-of-Demeter-style single-hop).
86
73
  # - `receiver_name` is the root variable's Symbol.
87
74
  # - `method_name` is the no-arg method invoked on the root.
88
75
  #
89
- # Chains with arguments (`x.first(3)`), with a block
90
- # (`x.detect { ... }`), or with intermediate links
91
- # (`x.foo.bar`) are NOT recorded; each loses stability for
92
- # different reasons (args / block alter the call's return;
93
- # multi-hop loses the LoD guarantee).
76
+ # Chains with arguments (`x.first(3)`), with a block (`x.detect { ... }`), or with intermediate links
77
+ # (`x.foo.bar`) are NOT recorded; each loses stability for different reasons (args / block alter the call's
78
+ # return; multi-hop loses the LoD guarantee).
94
79
  ChainKey = Data.define(:receiver_kind, :receiver_name, :method_name)
95
80
 
96
81
  EMPTY_VAR_BINDINGS = {}.freeze
97
82
  EMPTY_INDEXED_NARROWINGS = {}.freeze
98
83
  EMPTY_CHAIN_NARROWINGS = {}.freeze
99
- # ADR-58 WD1 — the set of variable references whose binding's `nil`
100
- # constituent is *declaration-sourced*: it arrives only via the
101
- # class-ivar index seed (a ctor `@x = nil` written in another method),
102
- # never through a method-local write, narrowing, or parameter. Members
103
- # are frozen `[kind, name]` pairs (`[:ivar, :@x]`, `[:local, :r]`).
104
- # `possible-nil-receiver` consults this set and declines to fire when the
105
- # receiver's optionality is purely declaration-sourced — the working
106
- # program's cross-method invariant is assumed per the robustness
107
- # principle. Any flow-live touch (write / narrowing) drops the mark, so
108
- # the diagnostic keeps firing exactly as before on flow-observed nil.
84
+ # ADR-58 WD1 — the set of variable references whose binding's `nil` constituent is *declaration-sourced*: it
85
+ # arrives only via the class-ivar index seed (a ctor `@x = nil` written in another method), never through a
86
+ # method-local write, narrowing, or parameter. Members are frozen `[kind, name]` pairs (`[:ivar, :@x]`,
87
+ # `[:local, :r]`). `possible-nil-receiver` consults this set and declines to fire when the receiver's
88
+ # optionality is purely declaration-sourced the working program's cross-method invariant is assumed per the
89
+ # robustness principle. Any flow-live touch (write / narrowing) drops the mark, so the diagnostic keeps firing
90
+ # exactly as before on flow-observed nil.
109
91
  EMPTY_DECLARATION_SOURCED = Set.new.freeze
110
- # ADR-48 Struct slice 3 — the per-body set of local names whose struct
111
- # member reads are fold-safe (provably never mutated / aliased / escaped).
112
- # A static per-scope context like {#source_path}: inherited unchanged
92
+ # ADR-48 Struct slice 3 — the per-body set of local names whose struct member reads are fold-safe (provably
93
+ # never mutated / aliased / escaped). A static per-scope context like {#source_path}: inherited unchanged
113
94
  # through flow transitions and ignored by `==` / `hash`.
114
95
  EMPTY_FOLD_SAFE = Set.new.freeze
96
+ # ADR-82 WD1 — provenance propagation across the receiver-node lookup. `local_origins` / `ivar_origins`
97
+ # map a `name` (Symbol) to the {Inference::DynamicOrigin} cause of the `Dynamic` value currently bound to
98
+ # it, so a downstream dispatch whose receiver is a bare `x` / `@x` read resolves to why that value is
99
+ # dynamic (the cause was recorded on the *assignment*'s rhs node, which the receiver-read node is not).
100
+ # Advisory metadata like {#dynamic_origins}: ignored by `==` / `hash` (never varies a flow decision) and
101
+ # threaded by reference through transitions; reset per method body (a fresh entry scope drops it), so the
102
+ # name keys never collide across bodies.
103
+ EMPTY_ORIGINS = {}.freeze
115
104
  private_constant :EMPTY_VAR_BINDINGS, :EMPTY_INDEXED_NARROWINGS,
116
105
  :EMPTY_CHAIN_NARROWINGS, :EMPTY_DECLARATION_SOURCED,
117
- :EMPTY_FOLD_SAFE
106
+ :EMPTY_FOLD_SAFE, :EMPTY_ORIGINS
118
107
 
119
108
  class << self
120
109
  def empty(environment: Environment.default, source_path: nil)
@@ -141,7 +130,9 @@ module Rigor
141
130
  declaration_sourced: EMPTY_DECLARATION_SOURCED,
142
131
  source_path: nil,
143
132
  struct_fold_safe_locals: EMPTY_FOLD_SAFE,
144
- dynamic_origins: {}.compare_by_identity
133
+ dynamic_origins: {}.compare_by_identity,
134
+ local_origins: EMPTY_ORIGINS,
135
+ ivar_origins: EMPTY_ORIGINS
145
136
  )
146
137
  @environment = environment
147
138
  @locals = locals
@@ -157,9 +148,31 @@ module Rigor
157
148
  @source_path = source_path
158
149
  @struct_fold_safe_locals = struct_fold_safe_locals
159
150
  @dynamic_origins = dynamic_origins
151
+ @local_origins = local_origins
152
+ @ivar_origins = ivar_origins
160
153
  freeze
161
154
  end
162
155
 
156
+ # ADR-82 WD1 — the propagated origin of the `Dynamic` value currently bound to a local / instance
157
+ # variable, or `nil` when none is tracked. Consulted by `Inference::ProtectionScanner` when a dispatch's
158
+ # receiver is a bare `x` / `@x` read whose own node carries no origin.
159
+ def local_origin(name) = @local_origins[name.to_sym]
160
+ def ivar_origin(name) = @ivar_origins[name.to_sym]
161
+
162
+ # Records the cause of the `Dynamic` value being bound to `name`. A `nil` cause is a no-op (the common
163
+ # case — most bindings are concrete or have no recorded origin), so callers need not pre-check.
164
+ def with_local_origin(name, cause)
165
+ return self if cause.nil?
166
+
167
+ rebuild(local_origins: @local_origins.merge(name.to_sym => cause).freeze)
168
+ end
169
+
170
+ def with_ivar_origin(name, cause)
171
+ return self if cause.nil?
172
+
173
+ rebuild(ivar_origins: @ivar_origins.merge(name.to_sym => cause).freeze)
174
+ end
175
+
163
176
  def local(name)
164
177
  @locals[name.to_sym]
165
178
  end
@@ -169,79 +182,65 @@ module Rigor
169
182
  Inference::FlowTracer.bind(name, type) if Inference::FlowTracer.active?
170
183
  new_locals = @locals.merge(name.to_sym => type).freeze
171
184
  new_fact_store = fact_store.invalidate_target(Analysis::FactStore::Target.local(name))
172
- # Rebinding `name` invalidates every "after `receiver[key]
173
- # ||= default`" narrowing keyed on it the slot at `name[*]`
174
- # is reachable through the old binding only, so the
175
- # next read against the new binding does not inherit the
176
- # earlier non-nil guarantee. The same logic applies to
177
- # method-chain narrowings: `x.last` after `x = something_new`
178
- # is a call on the new binding and any prior `is_a?`-driven
179
- # narrowing keyed on `(local, :x, :last)` no longer holds.
185
+ # Rebinding `name` invalidates every "after `receiver[key] ||= default`" narrowing keyed on it — the slot at
186
+ # `name[*]` is reachable through the old binding only, so the next read against the new binding does not
187
+ # inherit the earlier non-nil guarantee. The same logic applies to method-chain narrowings: `x.last` after
188
+ # `x = something_new` is a call on the new binding and any prior `is_a?`-driven narrowing keyed on
189
+ # `(local, :x, :last)` no longer holds.
180
190
  new_indexed_narrowings = drop_indexed_narrowings_for(:local, name)
181
191
  new_chain_narrowings = drop_chain_narrowings_for(:local, name)
182
- # ADR-58 WD1 — rebinding a local is a flow-live touch: any prior
183
- # declaration-sourced mark on `name` no longer holds (the new value
184
- # may carry a method-local nil). `with_declaration_sourced_local`
185
- # re-establishes the mark afterward when the RHS is a pure copy of a
186
- # declaration-sourced ivar read; the default is to drop it.
192
+ # ADR-58 WD1 — rebinding a local is a flow-live touch: any prior declaration-sourced mark on `name` no
193
+ # longer holds (the new value may carry a method-local nil). `with_declaration_sourced_local` re-establishes
194
+ # the mark afterward when the RHS is a pure copy of a declaration-sourced ivar read; the default is to drop
195
+ # it.
187
196
  rebuild(locals: new_locals, fact_store: new_fact_store,
188
197
  indexed_narrowings: new_indexed_narrowings,
189
198
  method_chain_narrowings: new_chain_narrowings,
190
- declaration_sourced: drop_declaration_sourced_for(:local, name))
199
+ declaration_sourced: drop_declaration_sourced_for(:local, name),
200
+ local_origins: drop_origin(@local_origins, name))
191
201
  end
192
202
 
193
203
  def with_fact(fact)
194
204
  rebuild(fact_store: fact_store.with_fact(fact))
195
205
  end
196
206
 
197
- # Slice A-engine. Returns a scope with `self_type` set to `type`,
198
- # preserving locals and facts. `StatementEvaluator` injects this
199
- # at class-body and method-body boundaries; `ExpressionTyper`
200
- # consults it when typing `Prism::SelfNode` and implicit-self
201
- # `Prism::CallNode` receivers.
207
+ # Slice A-engine. Returns a scope with `self_type` set to `type`, preserving locals and facts.
208
+ # `StatementEvaluator` injects this at class-body and method-body boundaries; `ExpressionTyper` consults it
209
+ # when typing `Prism::SelfNode` and implicit-self `Prism::CallNode` receivers.
202
210
  def with_self_type(type)
203
211
  rebuild(self_type: type)
204
212
  end
205
213
 
206
- # ADR-28 / ADR-52 slice 5a — per-file source path carried on
207
- # the scope. The analyzer stamps the current file's path onto
208
- # the seed scope; nested rebuilds propagate it so plugin rules
209
- # (`dynamic_return`'s `file_methods:` gate, sigil checks) can
210
- # resolve "which file does this call site belong to?" without
211
- # thread-locals.
214
+ # ADR-28 / ADR-52 slice 5a — per-file source path carried on the scope. The analyzer stamps the current file's
215
+ # path onto the seed scope; nested rebuilds propagate it so plugin rules (`dynamic_return`'s `file_methods:`
216
+ # gate, sigil checks) can resolve "which file does this call site belong to?" without thread-locals.
212
217
  def with_source_path(path)
213
218
  rebuild(source_path: path)
214
219
  end
215
220
 
216
- # ADR-48 Struct slice 3 — installs the per-body fold-safe-local set
217
- # ({Inference::StructFoldSafety}). Set once at body entry; inherited
218
- # unchanged through subsequent flow transitions.
221
+ # ADR-48 Struct slice 3 — installs the per-body fold-safe-local set ({Inference::StructFoldSafety}). Set once
222
+ # at body entry; inherited unchanged through subsequent flow transitions.
219
223
  def with_struct_fold_safe(locals)
220
224
  rebuild(struct_fold_safe_locals: locals)
221
225
  end
222
226
 
223
- # True when `name`'s `Struct` member reads are fold-safe in this body
224
- # (the local is provably never mutated / aliased / escaped).
227
+ # True when `name`'s `Struct` member reads are fold-safe in this body (the local is provably never mutated /
228
+ # aliased / escaped).
225
229
  def struct_fold_safe?(name)
226
230
  @struct_fold_safe_locals.include?(name.to_sym)
227
231
  end
228
232
 
229
- # ADR-53 Track A — swaps the whole discovery index in one transition.
230
- # The sole seeding path; the per-table writers it replaced are derived
231
- # off-`Scope` through `scope.discovery.with(table_name: table)`.
233
+ # ADR-53 Track A — swaps the whole discovery index in one transition. The sole seeding path; the per-table
234
+ # writers it replaced are derived off-`Scope` through `scope.discovery.with(table_name: table)`.
232
235
  def with_discovery(index)
233
236
  rebuild(discovery: index)
234
237
  end
235
238
 
236
- # Slice 7 phase 1 — instance/class/global variable bindings.
237
- # `ivar(name)` / `cvar(name)` / `global(name)` return the
238
- # type currently bound for the named variable, or `nil` when
239
- # the variable has not been written in the analyzed slice of
240
- # the program. The first cut tracks bindings only within a
241
- # single method body (each `def` enters with a fresh binding
242
- # map), so reads in other methods of the same class fall
243
- # through to `Dynamic[Top]`. Cross-method ivar/cvar inference
244
- # is a follow-up slice.
239
+ # Slice 7 phase 1 — instance/class/global variable bindings. `ivar(name)` / `cvar(name)` / `global(name)`
240
+ # return the type currently bound for the named variable, or `nil` when the variable has not been written in
241
+ # the analyzed slice of the program. The first cut tracks bindings only within a single method body (each
242
+ # `def` enters with a fresh binding map), so reads in other methods of the same class fall through to
243
+ # `Dynamic[Top]`. Cross-method ivar/cvar inference is a follow-up slice.
245
244
  def ivar(name)
246
245
  @ivars[name.to_sym]
247
246
  end
@@ -257,44 +256,40 @@ module Rigor
257
256
  def with_ivar(name, type)
258
257
  new_indexed_narrowings = drop_indexed_narrowings_for(:ivar, name)
259
258
  new_chain_narrowings = drop_chain_narrowings_for(:ivar, name)
260
- # ADR-58 WD1 — a method-local ivar write or narrowing is flow-live:
261
- # drop any declaration-sourced mark so subsequent reads of `@name`
262
- # observe flow-live provenance and fire as before. The seed path uses
259
+ # ADR-58 WD1 — a method-local ivar write or narrowing is flow-live: drop any declaration-sourced mark so
260
+ # subsequent reads of `@name` observe flow-live provenance and fire as before. The seed path uses
263
261
  # `seed_declaration_sourced_ivar` to (re-)establish the mark.
264
262
  rebuild(ivars: @ivars.merge(name.to_sym => type).freeze,
265
263
  indexed_narrowings: new_indexed_narrowings,
266
264
  method_chain_narrowings: new_chain_narrowings,
267
- declaration_sourced: drop_declaration_sourced_for(:ivar, name))
265
+ declaration_sourced: drop_declaration_sourced_for(:ivar, name),
266
+ ivar_origins: drop_origin(@ivar_origins, name))
268
267
  end
269
268
 
270
- # ADR-58 WD1 — used by the method-entry seed to mark an ivar whose only
271
- # provenance is the class-ivar index. Unlike `with_ivar` this binds the
272
- # type AND records the declaration-sourced mark in one transition.
269
+ # ADR-58 WD1 — used by the method-entry seed to mark an ivar whose only provenance is the class-ivar index.
270
+ # Unlike `with_ivar` this binds the type AND records the declaration-sourced mark in one transition.
273
271
  def seed_declaration_sourced_ivar(name, type)
274
272
  rebuild(ivars: @ivars.merge(name.to_sym => type).freeze,
275
273
  declaration_sourced: add_declaration_sourced(:ivar, name))
276
274
  end
277
275
 
278
- # ADR-58 WD1 — a local assignment `r = @right` whose RHS is a pure read
279
- # of a declaration-sourced ivar inherits the mark, so the survey's exact
280
- # rotation/traversal shape (`r = @right; r.key`) does not fire. Binds the
281
- # type and stamps the local's mark in one transition (the plain
282
- # `with_local` would have dropped it).
276
+ # ADR-58 WD1 — a local assignment `r = @right` whose RHS is a pure read of a declaration-sourced ivar inherits
277
+ # the mark, so the survey's exact rotation/traversal shape (`r = @right; r.key`) does not fire. Binds the type
278
+ # and stamps the local's mark in one transition (the plain `with_local` would have dropped it).
283
279
  def with_declaration_sourced_local(name, type)
284
280
  written = with_local(name, type)
285
281
  written.with_local_declaration_mark(name)
286
282
  end
287
283
 
288
- # ADR-58 WD1 — re-stamp the local mark on a scope produced by
289
- # `with_local` (which always drops it). Public so the sibling
290
- # `with_declaration_sourced_local` can call it across the new
291
- # post-write receiver without reaching into a private method.
284
+ # ADR-58 WD1 — re-stamp the local mark on a scope produced by `with_local` (which always drops it). Public so
285
+ # the sibling `with_declaration_sourced_local` can call it across the new post-write receiver without reaching
286
+ # into a private method.
292
287
  def with_local_declaration_mark(name)
293
288
  rebuild(declaration_sourced: add_declaration_sourced(:local, name))
294
289
  end
295
290
 
296
- # ADR-58 WD1 — true when `(kind, name)`'s binding optionality is purely
297
- # declaration-sourced (no flow-live write/narrowing has touched it).
291
+ # ADR-58 WD1 — true when `(kind, name)`'s binding optionality is purely declaration-sourced (no flow-live
292
+ # write/narrowing has touched it).
298
293
  def declaration_sourced?(kind, name)
299
294
  @declaration_sourced.include?([kind.to_sym, name.to_sym])
300
295
  end
@@ -307,14 +302,11 @@ module Rigor
307
302
  rebuild(globals: @globals.merge(name.to_sym => type).freeze)
308
303
  end
309
304
 
310
- # Regex match-data globals (`$~`, `$&`, `$1..$9`, the pre/post-match
311
- # and last-paren back-references). Narrowed on a successful-`=~` /
312
- # `case`-`when` match edge (see `Narrowing#regex_match_predicate_scopes`);
313
- # any subsequent method call could run another match and rebind every
314
- # one of them, so `eval_call` forgets the narrowed facts here. Always
315
- # safe — only drops facts, so a subsequent read falls back to the
316
- # default `String | nil`. Program-level `$GLOBAL = ...` seeds use other
317
- # names and are untouched.
305
+ # Regex match-data globals (`$~`, `$&`, `$1..$9`, the pre/post-match and last-paren back-references). Narrowed
306
+ # on a successful-`=~` / `case`-`when` match edge (see `Narrowing#regex_match_predicate_scopes`); any
307
+ # subsequent method call could run another match and rebind every one of them, so `eval_call` forgets the
308
+ # narrowed facts here. Always safe only drops facts, so a subsequent read falls back to the default
309
+ # `String | nil`. Program-level `$GLOBAL = ...` seeds use other names and are untouched.
318
310
  MATCH_DATA_GLOBALS = %i[$~ $& $` $' $+ $1 $2 $3 $4 $5 $6 $7 $8 $9].freeze
319
311
  private_constant :MATCH_DATA_GLOBALS
320
312
 
@@ -324,44 +316,33 @@ module Rigor
324
316
  rebuild(globals: @globals.except(*MATCH_DATA_GLOBALS).freeze)
325
317
  end
326
318
 
327
- # Slice 7 phase 2 — class-level ivar accumulator. Keyed by
328
- # the qualified class name (e.g. `"Rigor::Scope"`); the
329
- # value is a `Hash[Symbol, Type::t]` of every ivar that
330
- # appears as a write target inside any def body of that
331
- # class. `StatementEvaluator#build_method_entry_scope`
332
- # seeds the method body's `ivars` map from this table so a
333
- # `def get; @x; end` reads the type written in a sibling
334
- # `def init; @x = 1; end`.
319
+ # Slice 7 phase 2 — class-level ivar accumulator. Keyed by the qualified class name (e.g. `"Rigor::Scope"`);
320
+ # the value is a `Hash[Symbol, Type::t]` of every ivar that appears as a write target inside any def body of
321
+ # that class. `StatementEvaluator#build_method_entry_scope` seeds the method body's `ivars` map from this
322
+ # table so a `def get; @x; end` reads the type written in a sibling `def init; @x = 1; end`.
335
323
  #
336
- # `ScopeIndexer` populates the table once at index time
337
- # through a separate pre-pass over the program. The map is
338
- # frozen and shared by structural reference across every
339
- # derived scope.
324
+ # `ScopeIndexer` populates the table once at index time through a separate pre-pass over the program. The map
325
+ # is frozen and shared by structural reference across every derived scope.
340
326
  def class_ivars_for(class_name)
341
327
  return EMPTY_VAR_BINDINGS if class_name.nil?
342
328
 
343
329
  @discovery.class_ivars[class_name.to_s] || EMPTY_VAR_BINDINGS
344
330
  end
345
331
 
346
- # Slice 7 phase 6 — class-level cvar accumulator (same shape
347
- # as `class_ivars` but populated from `Prism::ClassVariableWriteNode`
348
- # writes, and seeded on BOTH instance and singleton method
349
- # bodies because Ruby cvars are visible from each).
332
+ # Slice 7 phase 6 — class-level cvar accumulator (same shape as `class_ivars` but populated from
333
+ # `Prism::ClassVariableWriteNode` writes, and seeded on BOTH instance and singleton method bodies because Ruby
334
+ # cvars are visible from each).
350
335
  def class_cvars_for(class_name)
351
336
  return EMPTY_VAR_BINDINGS if class_name.nil?
352
337
 
353
338
  @discovery.class_cvars[class_name.to_s] || EMPTY_VAR_BINDINGS
354
339
  end
355
340
 
356
- # Slice 7 phase 12 — in-source method discovery. Maps a
357
- # qualified class name to a `Hash[Symbol, Symbol]` of
358
- # `method_name => :instance | :singleton`. Populated by
359
- # `ScopeIndexer` from every `Prism::DefNode` and recognised
360
- # `define_method` invocation inside class/module bodies. The
361
- # `rigor check` undefined-method and wrong-arity rules
362
- # consult this map to suppress diagnostics for methods the
363
- # user has defined dynamically, even when no RBS sig
364
- # describes them.
341
+ # Slice 7 phase 12 — in-source method discovery. Maps a qualified class name to a `Hash[Symbol, Symbol]` of
342
+ # `method_name => :instance | :singleton`. Populated by `ScopeIndexer` from every `Prism::DefNode` and
343
+ # recognised `define_method` invocation inside class/module bodies. The `rigor check` undefined-method and
344
+ # wrong-arity rules consult this map to suppress diagnostics for methods the user has defined dynamically,
345
+ # even when no RBS sig describes them.
365
346
  def discovered_method?(class_name, method_name, kind)
366
347
  table = @discovery.discovered_methods[class_name.to_s]
367
348
  return false unless table
@@ -369,30 +350,20 @@ module Rigor
369
350
  table[method_name.to_sym] == kind
370
351
  end
371
352
 
372
- # ADR-34 § "Decision" — predicate identifying a toplevel-shaped
373
- # scope (no enclosing `class` / `module` body). True at the top
374
- # of a file AND inside a top-level `def` body (since toplevel
375
- # defs leave `self_type` nil per the existing scope-construction
376
- # contract the same nil-`self_type` signal ADR-24's self-call
377
- # return adoption historically keyed on before ADR-57 opened the
378
- # gate unconditionally). Used by
379
- # `CheckRules#unresolved_toplevel_diagnostic` to gate the
380
- # `call.unresolved-toplevel` rule so it fires only outside
381
- # class / module bodies, where Rails-DSL metaprogramming
382
- # leniency (ADR-24 WD3 → WD4) does not apply.
353
+ # ADR-34 § "Decision" — predicate identifying a toplevel-shaped scope (no enclosing `class` / `module` body).
354
+ # True at the top of a file AND inside a top-level `def` body (since toplevel defs leave `self_type` nil per
355
+ # the existing scope-construction contract the same nil-`self_type` signal ADR-24's self-call return
356
+ # adoption historically keyed on before ADR-57 opened the gate unconditionally). Used by
357
+ # `CheckRules#unresolved_toplevel_diagnostic` to gate the `call.unresolved-toplevel` rule so it fires only
358
+ # outside class / module bodies, where Rails-DSL metaprogramming leniency (ADR-24 WD3 → WD4) does not apply.
383
359
  def toplevel?
384
360
  @self_type.nil?
385
361
  end
386
362
 
387
- # v0.0.2 #5 — per-class table mapping
388
- # `method_name (Symbol) Prism::DefNode`. Populated by
389
- # `ScopeIndexer` alongside `discovered_methods` for
390
- # instance-side defs only (singleton-side and
391
- # `define_method`-introduced methods do not contribute a
392
- # static body the engine can re-type). Consumed by
393
- # `ExpressionTyper` to do inter-procedural return-type
394
- # inference when the receiver class is user-defined and
395
- # has no RBS sig.
363
+ # v0.0.2 #5 — per-class table mapping `method_name (Symbol) → Prism::DefNode`. Populated by `ScopeIndexer`
364
+ # alongside `discovered_methods` for instance-side defs only (singleton-side and `define_method`-introduced
365
+ # methods do not contribute a static body the engine can re-type). Consumed by `ExpressionTyper` to do
366
+ # inter-procedural return-type inference when the receiver class is user-defined and has no RBS sig.
396
367
  def user_def_for(class_name, method_name)
397
368
  table = @discovery.discovered_def_nodes[class_name.to_s]
398
369
  node = table && table[method_name.to_sym]
@@ -400,15 +371,12 @@ module Rigor
400
371
  node
401
372
  end
402
373
 
403
- # Module-singleton call resolution (ADR-57 follow-up) — companion of
404
- # {#user_def_for} for SINGLETON-side defs (`def self.x`, `def Foo.x`,
405
- # `class << self` bodies, and `module_function` defs). Returns the
406
- # `Prism::DefNode` for `class_name.method_name` invoked on the
407
- # module/class constant itself, or nil. The `discovered_def_nodes`
408
- # table is deliberately instance-side only (its ancestor walk binds
409
- # `self` as `Nominal`), so singleton bodies live in a parallel table
410
- # the `ScopeIndexer` populates alongside it. Records the same
411
- # cross-file dependency edge as the instance path (ADR-46).
374
+ # Module-singleton call resolution (ADR-57 follow-up) — companion of {#user_def_for} for SINGLETON-side defs
375
+ # (`def self.x`, `def Foo.x`, `class << self` bodies, and `module_function` defs). Returns the
376
+ # `Prism::DefNode` for `class_name.method_name` invoked on the module/class constant itself, or nil. The
377
+ # `discovered_def_nodes` table is deliberately instance-side only (its ancestor walk binds `self` as
378
+ # `Nominal`), so singleton bodies live in a parallel table the `ScopeIndexer` populates alongside it. Records
379
+ # the same cross-file dependency edge as the instance path (ADR-46).
412
380
  def singleton_def_for(class_name, method_name)
413
381
  table = @discovery.discovered_singleton_def_nodes[class_name.to_s]
414
382
  node = table && table[method_name.to_sym]
@@ -416,15 +384,14 @@ module Rigor
416
384
  node
417
385
  end
418
386
 
419
- # ADR-46 slice 1 — note the cross-file dependency this resolution
420
- # creates: the file defining `class_name#method_name` (the consumer's
421
- # analysis reads its body via `infer_user_method_return`), or, when
422
- # unresolved, a negative edge so a later definition re-checks the
423
- # consumer. Gated on the recorder being active — no-op on a normal run.
387
+ # ADR-46 slice 1 — note the cross-file dependency this resolution creates: the file defining
388
+ # `class_name#method_name` (the consumer's analysis reads its body via `infer_user_method_return`), or, when
389
+ # unresolved, a negative edge so a later definition re-checks the consumer. Gated on the recorder being
390
+ # active no-op on a normal run.
424
391
  def record_cross_file_method(class_name, method_name, node)
425
392
  if node
426
- # ADR-46 slice 4 — pass the symbol so the recorder tracks this as a
427
- # method-call (symbol-granularity) edge rather than a file-level edge.
393
+ # ADR-46 slice 4 — pass the symbol so the recorder tracks this as a method-call (symbol-granularity) edge
394
+ # rather than a file-level edge.
428
395
  Analysis::DependencyRecorder.read_site(
429
396
  @discovery.discovered_def_sources.dig(class_name.to_s, method_name.to_sym),
430
397
  "#{class_name}##{method_name}"
@@ -435,12 +402,9 @@ module Rigor
435
402
  end
436
403
  private :record_cross_file_method
437
404
 
438
- # v0.0.3 A — top-level def lookup for implicit-self
439
- # calls. Returns the `Prism::DefNode` for a top-level
440
- # (or DSL-block-nested, outside any class body) `def
441
- # <method_name>` in the file, or nil. The sentinel key
442
- # is owned by `Inference::ScopeIndexer::TOP_LEVEL_DEF_KEY`;
443
- # consumers should treat its presence as an opaque
405
+ # v0.0.3 A — top-level def lookup for implicit-self calls. Returns the `Prism::DefNode` for a top-level (or
406
+ # DSL-block-nested, outside any class body) `def <method_name>` in the file, or nil. The sentinel key is owned
407
+ # by `Inference::ScopeIndexer::TOP_LEVEL_DEF_KEY`; consumers should treat its presence as an opaque
444
408
  # implementation detail and go through this accessor.
445
409
  def top_level_def_for(method_name)
446
410
  table = @discovery.discovered_def_nodes[Inference::ScopeIndexer::TOP_LEVEL_DEF_KEY]
@@ -449,14 +413,12 @@ module Rigor
449
413
  node
450
414
  end
451
415
 
452
- # ADR-46 slice 3 — a top-level (`def helper` outside any class) call has
453
- # NO class ancestry to walk, so unlike {#user_def_for} a miss here records
454
- # no positive ancestry edge that would re-check the consumer when the
455
- # method later appears. Record the cross-file edge explicitly: the file
456
- # defining the top-level method (symbol-granularity, so a body / removal
457
- # edit re-checks the caller), or, on a miss, a negative `toplevel:` edge
458
- # so a later top-level definition re-checks this consumer (the
459
- # `call.unresolved-toplevel` stale-diagnostic gap).
416
+ # ADR-46 slice 3 — a top-level (`def helper` outside any class) call has NO class ancestry to walk, so unlike
417
+ # {#user_def_for} a miss here records no positive ancestry edge that would re-check the consumer when the
418
+ # method later appears. Record the cross-file edge explicitly: the file defining the top-level method
419
+ # (symbol-granularity, so a body / removal edit re-checks the caller), or, on a miss, a negative `toplevel:`
420
+ # edge so a later top-level definition re-checks this consumer (the `call.unresolved-toplevel`
421
+ # stale-diagnostic gap).
460
422
  def record_cross_file_toplevel(method_name, node)
461
423
  key = Inference::ScopeIndexer::TOP_LEVEL_DEF_KEY
462
424
  if node
@@ -470,16 +432,12 @@ module Rigor
470
432
  end
471
433
  private :record_cross_file_toplevel
472
434
 
473
- # Companion to {#user_def_for}: returns the `"path:line"` where
474
- # the project defines `class_name#method_name` (instance-side),
475
- # or nil. Populated only by the cross-file project pre-pass
476
- # ({Inference::ScopeIndexer.discovered_def_index_for_paths}) a
477
- # `Prism::Location` hides its source file, so the site is recorded
478
- # at scan time. `CheckRules#undefined_method_diagnostic` consults
479
- # this to name the defining file when a project monkey-patch on a
480
- # core/stdlib/gem class is called cross-file, so the diagnostic
481
- # can point at `pre_eval:` (ADR-17) instead of reading as a bare
482
- # unresolved call.
435
+ # Companion to {#user_def_for}: returns the `"path:line"` where the project defines `class_name#method_name`
436
+ # (instance-side), or nil. Populated only by the cross-file project pre-pass
437
+ # ({Inference::ScopeIndexer.discovered_def_index_for_paths}) a `Prism::Location` hides its source file, so
438
+ # the site is recorded at scan time. `CheckRules#undefined_method_diagnostic` consults this to name the
439
+ # defining file when a project monkey-patch on a core/stdlib/gem class is called cross-file, so the diagnostic
440
+ # can point at `pre_eval:` (ADR-17) instead of reading as a bare unresolved call.
483
441
  def user_def_site_for(class_name, method_name)
484
442
  table = @discovery.discovered_def_sources[class_name.to_s]
485
443
  return nil unless table
@@ -487,74 +445,56 @@ module Rigor
487
445
  table[method_name.to_sym]
488
446
  end
489
447
 
490
- # ADR-24 slice 2 — per-class table mapping a fully
491
- # qualified user-class name to its superclass name AS
492
- # WRITTEN at the `class Foo < Bar` declaration (`"Bar"`,
493
- # possibly a qualified `"A::B"`). Populated by `ScopeIndexer`
494
- # per-file plus the cross-file project pre-pass and
495
- # consumed by `ExpressionTyper#try_user_method_inference`
496
- # to walk the superclass chain when an implicit-self call
497
- # does not resolve against the enclosing class's own defs.
498
- # The as-written name is resolved to a qualified class at
499
- # walk time against the call's lexical nesting.
448
+ # ADR-24 slice 2 — per-class table mapping a fully qualified user-class name to its superclass name AS WRITTEN
449
+ # at the `class Foo < Bar` declaration (`"Bar"`, possibly a qualified `"A::B"`). Populated by `ScopeIndexer` —
450
+ # per-file plus the cross-file project pre-pass and consumed by
451
+ # `ExpressionTyper#try_user_method_inference` to walk the superclass chain when an implicit-self call does not
452
+ # resolve against the enclosing class's own defs. The as-written name is resolved to a qualified class at walk
453
+ # time against the call's lexical nesting.
500
454
  def superclass_of(class_name)
501
455
  record_class_dependency(class_name) if Analysis::DependencyRecorder.active?
502
456
  @discovery.discovered_superclasses[class_name.to_s]
503
457
  end
504
458
 
505
- # ADR-48 — per-class table mapping a fully qualified class name to its
506
- # ordered `Data.define` / `Struct.new` member-name list. Populated by
507
- # `ScopeIndexer` for both the constant-assigned form
508
- # (`Point = Data.define(:x, :y)`) and the named-subclass form
509
- # (`class Point < Data.define(:x, :y)`). Consumed by
510
- # {Inference::MethodDispatcher::DataFolding} so `Point.new(...)` on a
511
- # `Singleton[Point]` receiver materialises a precise member instance.
512
- # Returns nil when the class has no recorded layout.
459
+ # ADR-48 — per-class table mapping a fully qualified class name to its ordered `Data.define` / `Struct.new`
460
+ # member-name list. Populated by `ScopeIndexer` for both the constant-assigned form
461
+ # (`Point = Data.define(:x, :y)`) and the named-subclass form (`class Point < Data.define(:x, :y)`). Consumed
462
+ # by {Inference::MethodDispatcher::DataFolding} so `Point.new(...)` on a `Singleton[Point]` receiver
463
+ # materialises a precise member instance. Returns nil when the class has no recorded layout.
513
464
  def data_member_layout(class_name)
514
465
  layout = @discovery.data_member_layouts[class_name.to_s]
515
- # Record the ancestry dependency only on a hit — DataFolding consults
516
- # this for every `Singleton[*].new`, and a miss (the common case: an
517
- # ordinary class) must not manufacture a spurious cross-file edge.
466
+ # Record the ancestry dependency only on a hit — DataFolding consults this for every `Singleton[*].new`,
467
+ # and a miss (the common case: an ordinary class) must not manufacture a spurious cross-file edge.
518
468
  record_class_dependency(class_name) if layout && Analysis::DependencyRecorder.active?
519
469
  layout
520
470
  end
521
471
 
522
- # ADR-48 Struct follow-up — the `{ members:, keyword_init: }` layout
523
- # recorded for a `Struct.new(...)`-defined class, in the constant form
524
- # (`Point = Struct.new(:x, :y)`) and the named-subclass form
525
- # (`class Point < Struct.new(:x, :y)`). Consumed by
526
- # {Inference::MethodDispatcher::StructFolding} so `Point.new(...)` on a
527
- # `Singleton[Point]` receiver materialises a member instance. Returns nil
528
- # when the class has no recorded struct layout. Mirrors
529
- # {#data_member_layout}'s dependency-recording contract.
472
+ # ADR-48 Struct follow-up — the `{ members:, keyword_init: }` layout recorded for a `Struct.new(...)`-defined
473
+ # class, in the constant form (`Point = Struct.new(:x, :y)`) and the named-subclass form
474
+ # (`class Point < Struct.new(:x, :y)`). Consumed by {Inference::MethodDispatcher::StructFolding} so
475
+ # `Point.new(...)` on a `Singleton[Point]` receiver materialises a member instance. Returns nil when the class
476
+ # has no recorded struct layout. Mirrors {#data_member_layout}'s dependency-recording contract.
530
477
  def struct_member_layout(class_name)
531
478
  layout = @discovery.struct_member_layouts[class_name.to_s]
532
479
  record_class_dependency(class_name) if layout && Analysis::DependencyRecorder.active?
533
480
  layout
534
481
  end
535
482
 
536
- # ADR-24 slice 2 — per-class/module table mapping a fully
537
- # qualified user class or module to the list of module
538
- # names it `include`s / `prepend`s, AS WRITTEN at the
539
- # mixin call. Populated by `ScopeIndexer` (per-file plus
540
- # the cross-file pre-pass) and consumed by
541
- # `ExpressionTyper#resolve_user_def_through_ancestors` so an
542
- # implicit-self call resolves against an included module's
543
- # `def`s, not just the superclass chain. As-written names
544
- # are resolved to qualified classes at walk time.
483
+ # ADR-24 slice 2 — per-class/module table mapping a fully qualified user class or module to the list of
484
+ # module names it `include`s / `prepend`s, AS WRITTEN at the mixin call. Populated by `ScopeIndexer` (per-file
485
+ # plus the cross-file pre-pass) and consumed by `ExpressionTyper#resolve_user_def_through_ancestors` so an
486
+ # implicit-self call resolves against an included module's `def`s, not just the superclass chain. As-written
487
+ # names are resolved to qualified classes at walk time.
545
488
  def includes_of(class_name)
546
489
  record_class_dependency(class_name) if Analysis::DependencyRecorder.active?
547
490
  @discovery.discovered_includes[class_name.to_s] || []
548
491
  end
549
492
 
550
- # Records, for a resolved cross-class ancestry read, every file that
551
- # declares `class_name` (its declaration / reopening / superclass /
552
- # include sites). The `discovered_class_sources` table it reads is
553
- # populated only by the cross-file project pre-pass
554
- # ({Inference::ScopeIndexer.discovered_def_index_for_paths}) and only
555
- # when dependency recording is active. No-op when the class is not a
556
- # project class (core / stdlib / gem names never appear in the source
557
- # map). Gated by the caller on the recorder being active.
493
+ # Records, for a resolved cross-class ancestry read, every file that declares `class_name` (its declaration /
494
+ # reopening / superclass / include sites). The `discovered_class_sources` table it reads is populated only by
495
+ # the cross-file project pre-pass ({Inference::ScopeIndexer.discovered_def_index_for_paths}) and only when
496
+ # dependency recording is active. No-op when the class is not a project class (core / stdlib / gem names
497
+ # never appear in the source map). Gated by the caller on the recorder being active.
558
498
  def record_class_dependency(class_name)
559
499
  sites = @discovery.discovered_class_sources[class_name.to_s]
560
500
  return if sites.nil?
@@ -563,15 +503,11 @@ module Rigor
563
503
  end
564
504
  private :record_class_dependency
565
505
 
566
- # v0.1.2 — per-class table mapping `method_name (Symbol) →
567
- # :public | :private | :protected`. Populated by
568
- # `ScopeIndexer` for every `def` it sees inside a class
569
- # body, with the visibility taken from the surrounding
570
- # `private` / `protected` / `public` modifier state plus
571
- # any post-hoc `private :name, ...` named-argument calls.
572
- # Consumed by the `def.method-visibility-mismatch` rule
573
- # so explicit-non-self calls to a private method surface
574
- # a diagnostic.
506
+ # v0.1.2 — per-class table mapping `method_name (Symbol) → :public | :private | :protected`. Populated by
507
+ # `ScopeIndexer` for every `def` it sees inside a class body, with the visibility taken from the surrounding
508
+ # `private` / `protected` / `public` modifier state plus any post-hoc `private :name, ...` named-argument
509
+ # calls. Consumed by the `def.method-visibility-mismatch` rule so explicit-non-self calls to a private method
510
+ # surface a diagnostic.
575
511
  def discovered_method_visibility(class_name, method_name)
576
512
  table = @discovery.discovered_method_visibilities[class_name.to_s]
577
513
  return nil unless table
@@ -579,15 +515,11 @@ module Rigor
579
515
  table[method_name.to_sym]
580
516
  end
581
517
 
582
- # Closes the "`params[:f] ||= []; params[:f] << x`" precision
583
- # gap (ROADMAP § Type-language / engine indexed-collection
584
- # narrowing through `Hash[k] ||= default`). After
585
- # `receiver[key] ||= default`, the next read at `receiver[key]`
586
- # is known non-nil; recording the post-`||=` type keyed on
587
- # `(receiver_kind, receiver_name, literal_key)` lets the
588
- # ExpressionTyper's `[]` dispatch hand back the narrowed
589
- # type. Receiver-rebind and `[]=`/mutator invalidation rules
590
- # are documented at the call sites in
518
+ # Closes the "`params[:f] ||= []; params[:f] << x`" precision gap (ROADMAP § Type-language / engine —
519
+ # indexed-collection narrowing through `Hash[k] ||= default`). After `receiver[key] ||= default`, the next
520
+ # read at `receiver[key]` is known non-nil; recording the post-`||=` type keyed on
521
+ # `(receiver_kind, receiver_name, literal_key)` lets the ExpressionTyper's `[]` dispatch hand back the
522
+ # narrowed type. Receiver-rebind and `[]=`/mutator invalidation rules are documented at the call sites in
591
523
  # `Inference::StatementEvaluator`.
592
524
  def indexed_narrowing(receiver_kind, receiver_name, key)
593
525
  @indexed_narrowings[indexed_key(receiver_kind, receiver_name, key)]
@@ -615,16 +547,12 @@ module Rigor
615
547
  rebuild(indexed_narrowings: new_table)
616
548
  end
617
549
 
618
- # Closes the "stable receiver method-chain narrowing" gap
619
- # (ROADMAP § Future cycles / Type-language / engine
620
- # "Method-call receiver narrowing across stable receivers";
621
- # 2026-05-28 Redmine survey). After `if x.last.is_a?(Array)`
622
- # the dominated body's `x.last` reads MUST observe the
623
- # truthy-narrowed type; the same chain reaching the falsey
624
- # edge observes the negative narrowing.
550
+ # Closes the "stable receiver method-chain narrowing" gap (ROADMAP § Future cycles / Type-language / engine —
551
+ # "Method-call receiver narrowing across stable receivers"; 2026-05-28 Redmine survey). After
552
+ # `if x.last.is_a?(Array)` the dominated body's `x.last` reads MUST observe the truthy-narrowed type; the same
553
+ # chain reaching the falsey edge observes the negative narrowing.
625
554
  #
626
- # Address shape mirrors {.indexed_narrowing}: stable root
627
- # variable + no-arg single-hop method name. See
555
+ # Address shape mirrors {.indexed_narrowing}: stable root variable + no-arg single-hop method name. See
628
556
  # {ChainKey} for the precise contract.
629
557
  def method_chain_narrowing(receiver_kind, receiver_name, method_name)
630
558
  @method_chain_narrowings[chain_key(receiver_kind, receiver_name, method_name)]
@@ -664,23 +592,19 @@ module Rigor
664
592
  Inference::ExpressionTyper.new(scope: self, tracer: tracer).type_of(node)
665
593
  end
666
594
 
667
- # Statement-level evaluation: returns the pair `[type, scope']`
668
- # where `type` is what the node produces and `scope'` is the
669
- # scope observable after the node has run. The receiver scope is
670
- # never mutated. See {Rigor::Inference::StatementEvaluator} for
671
- # the catalogue of nodes that thread scope; everything else
672
- # defers to {#type_of} and returns the receiver scope unchanged.
595
+ # Statement-level evaluation: returns the pair `[type, scope']` where `type` is what the node produces and
596
+ # `scope'` is the scope observable after the node has run. The receiver scope is never mutated. See
597
+ # {Rigor::Inference::StatementEvaluator} for the catalogue of nodes that thread scope; everything else defers
598
+ # to {#type_of} and returns the receiver scope unchanged.
673
599
  def evaluate(node, tracer: nil)
674
600
  Inference::StatementEvaluator.new(scope: self, tracer: tracer).evaluate(node)
675
601
  end
676
602
 
677
- # Joins this scope with another at a control-flow merge point. The
678
- # joined scope is bound to every local that BOTH branches bind, with
679
- # the type widened to the union of both sides. Names bound in only
680
- # one branch are dropped from the joined scope; the eventual
681
- # statement-level evaluator (Slice 3 phase 2) is responsible for
682
- # nil-injecting half-bound names where the language semantics demand
683
- # it. The two scopes MUST share the same Environment.
603
+ # Joins this scope with another at a control-flow merge point. The joined scope is bound to every local that
604
+ # BOTH branches bind, with the type widened to the union of both sides. Names bound in only one branch are
605
+ # dropped from the joined scope; the eventual statement-level evaluator (Slice 3 phase 2) is responsible for
606
+ # nil-injecting half-bound names where the language semantics demand it. The two scopes MUST share the same
607
+ # Environment.
684
608
  def join(other)
685
609
  raise ArgumentError, "join requires a Rigor::Scope, got #{other.class}" unless other.is_a?(Scope)
686
610
 
@@ -725,7 +649,9 @@ module Rigor
725
649
  declaration_sourced: @declaration_sourced,
726
650
  source_path: @source_path,
727
651
  struct_fold_safe_locals: @struct_fold_safe_locals,
728
- dynamic_origins: @dynamic_origins
652
+ dynamic_origins: @dynamic_origins,
653
+ local_origins: @local_origins,
654
+ ivar_origins: @ivar_origins
729
655
  )
730
656
  self.class.new(
731
657
  environment: environment, locals: locals,
@@ -737,16 +663,16 @@ module Rigor
737
663
  declaration_sourced: declaration_sourced,
738
664
  source_path: source_path,
739
665
  struct_fold_safe_locals: struct_fold_safe_locals,
740
- dynamic_origins: dynamic_origins
666
+ dynamic_origins: dynamic_origins,
667
+ local_origins: local_origins,
668
+ ivar_origins: ivar_origins
741
669
  )
742
670
  end
743
671
 
744
672
  def join_bindings(left, right)
745
- # Keys present in both, unioned. Iterating `left` and probing
746
- # `right.key?` yields the same keys in the same order as the prior
747
- # `(left.keys & right.keys)` while avoiding the two key arrays and
748
- # the intersection array — this is the control-flow join, run at
749
- # every branch merge, and was a top allocation site (~75% of
673
+ # Keys present in both, unioned. Iterating `left` and probing `right.key?` yields the same keys in the same
674
+ # order as the prior `(left.keys & right.keys)` while avoiding the two key arrays and the intersection
675
+ # array this is the control-flow join, run at every branch merge, and was a top allocation site (~75% of
750
676
  # `Hash#keys`).
751
677
  result = {}
752
678
  left.each do |name, ltype|
@@ -769,16 +695,35 @@ module Rigor
769
695
  discovery: @discovery,
770
696
  indexed_narrowings: join_bindings(@indexed_narrowings, other.indexed_narrowings),
771
697
  method_chain_narrowings: join_bindings(@method_chain_narrowings, other.method_chain_narrowings),
772
- # ADR-58 WD1 — a ref is declaration-sourced after a join only when
773
- # BOTH branches agree it is. If either path made the binding
774
- # flow-live (a method-local nil write / failed-guard narrowing), the
775
- # merge is flow-live and `possible-nil-receiver` fires as before.
698
+ # ADR-58 WD1 — a ref is declaration-sourced after a join only when BOTH branches agree it is. If either
699
+ # path made the binding flow-live (a method-local nil write / failed-guard narrowing), the merge is
700
+ # flow-live and `possible-nil-receiver` fires as before.
776
701
  declaration_sourced: join_declaration_sourced(other),
777
702
  source_path: source_path,
778
- dynamic_origins: @dynamic_origins
703
+ dynamic_origins: @dynamic_origins,
704
+ local_origins: join_origins(@local_origins, other.local_origins),
705
+ ivar_origins: join_origins(@ivar_origins, other.ivar_origins)
779
706
  )
780
707
  end
781
708
 
709
+ # ADR-82 WD1 — merge two branches' propagated origins (self wins on a name conflict; advisory metadata, so
710
+ # a disagreement is harmless). Zero-alloc when either side is empty — the common case, keeping the join
711
+ # hot-path cost negligible.
712
+ def join_origins(mine, theirs)
713
+ return mine if mine.equal?(theirs) || theirs.empty?
714
+ return theirs if mine.empty?
715
+
716
+ theirs.merge(mine).freeze
717
+ end
718
+
719
+ # ADR-82 WD1 — rebinding drops any propagated origin for the name (the new value's provenance is set
720
+ # afterward by `with_local_origin` when it is a `Dynamic` with a recorded cause). Zero-alloc when the name
721
+ # carries no origin — the overwhelming common case, so this stays off the with_local hot-path budget.
722
+ def drop_origin(origins, name)
723
+ key = name.to_sym
724
+ origins.key?(key) ? origins.reject { |k, _| k == key }.freeze : origins
725
+ end
726
+
782
727
  def join_declaration_sourced(other)
783
728
  return @declaration_sourced if @declaration_sourced.equal?(other.declaration_sourced)
784
729
  return EMPTY_DECLARATION_SOURCED if @declaration_sourced.empty? || other.declaration_sourced.empty?