rigortype 0.4.5 → 0.4.7

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (452) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +2 -2
  3. data/data/gem_overlay/activesupport/core_ext.rbs +16 -13
  4. data/data/vendored_gem_sigs/bundler/bundler.rbs +1 -1
  5. data/data/vendored_gem_sigs/rubygems/rubygems_extras.rbs +4 -3
  6. data/docs/handbook/01-getting-started.md +5 -2
  7. data/docs/handbook/11-sig-gen.md +61 -5
  8. data/docs/handbook/appendix-phpstan.md +4 -4
  9. data/docs/manual/02-cli-reference.md +192 -27
  10. data/docs/manual/03-configuration.md +38 -4
  11. data/docs/manual/04-diagnostics.md +1 -1
  12. data/docs/manual/06-baseline.md +6 -4
  13. data/docs/manual/09-editor-integration.md +13 -2
  14. data/docs/manual/10-mcp-server.md +47 -3
  15. data/docs/manual/12-caching.md +9 -1
  16. data/docs/manual/plugins/rigor-actionmailer.md +30 -0
  17. data/docs/manual/plugins/rigor-actionpack.md +49 -11
  18. data/docs/manual/plugins/rigor-activesupport-core-ext.md +20 -1
  19. data/docs/manual/plugins/rigor-devise.md +15 -0
  20. data/docs/manual/plugins/rigor-rspec.md +2 -1
  21. data/lib/rigor/analysis/baseline.rb +24 -27
  22. data/lib/rigor/analysis/buffer_binding.rb +14 -0
  23. data/lib/rigor/analysis/check_rules/always_truthy_condition_collector.rb +5 -6
  24. data/lib/rigor/analysis/check_rules/dead_assignment_collector.rb +2 -2
  25. data/lib/rigor/analysis/check_rules/declaration_sourced_guard.rb +4 -4
  26. data/lib/rigor/analysis/check_rules/duplicate_hash_key_collector.rb +2 -2
  27. data/lib/rigor/analysis/check_rules/inferred_param_guard.rb +105 -12
  28. data/lib/rigor/analysis/check_rules/ivar_write_collector.rb +143 -15
  29. data/lib/rigor/analysis/check_rules/lexical_method_sites.rb +4 -1
  30. data/lib/rigor/analysis/check_rules/main_pass_collector.rb +5 -8
  31. data/lib/rigor/analysis/check_rules/published_constant_guard.rb +1 -1
  32. data/lib/rigor/analysis/check_rules/rbs_receiver_surface.rb +66 -3
  33. data/lib/rigor/analysis/check_rules/rebinding_block_callees/dispatch.rb +112 -0
  34. data/lib/rigor/analysis/check_rules/rebinding_block_callees.rb +394 -0
  35. data/lib/rigor/analysis/check_rules/return_in_ensure_collector.rb +1 -2
  36. data/lib/rigor/analysis/check_rules/ruby_deprecations.rb +7 -0
  37. data/lib/rigor/analysis/check_rules/rule_ids.rb +1 -1
  38. data/lib/rigor/analysis/check_rules/rule_walk.rb +9 -7
  39. data/lib/rigor/analysis/check_rules/shadowed_rescue_collector.rb +3 -4
  40. data/lib/rigor/analysis/check_rules/special_global_setters.rb +3 -3
  41. data/lib/rigor/analysis/check_rules/unreachable_clause_collector.rb +53 -2
  42. data/lib/rigor/analysis/check_rules/void_value_use_collector.rb +1 -1
  43. data/lib/rigor/analysis/check_rules.rb +217 -52
  44. data/lib/rigor/analysis/crash_signature.rb +2 -2
  45. data/lib/rigor/analysis/dependency_recorder.rb +15 -4
  46. data/lib/rigor/analysis/dependency_source_inference/boundary_cross_reporter.rb +14 -3
  47. data/lib/rigor/analysis/dependency_source_inference/gem_resolver.rb +1 -1
  48. data/lib/rigor/analysis/dependency_source_inference/index.rb +6 -8
  49. data/lib/rigor/analysis/dependency_source_inference/walker.rb +4 -4
  50. data/lib/rigor/analysis/edit_replay.rb +154 -0
  51. data/lib/rigor/analysis/fact_store.rb +3 -3
  52. data/lib/rigor/analysis/incremental.rb +37 -0
  53. data/lib/rigor/analysis/incremental_run_slot/write_guard.rb +2 -2
  54. data/lib/rigor/analysis/incremental_run_slot.rb +2 -2
  55. data/lib/rigor/analysis/incremental_session.rb +202 -43
  56. data/lib/rigor/analysis/path_expansion.rb +41 -0
  57. data/lib/rigor/analysis/require_relative_closure.rb +100 -0
  58. data/lib/rigor/analysis/rule_catalog.rb +7 -3
  59. data/lib/rigor/analysis/run_disclosure_credit.rb +80 -0
  60. data/lib/rigor/analysis/run_disclosure_ledger.rb +155 -0
  61. data/lib/rigor/analysis/run_stats.rb +2 -2
  62. data/lib/rigor/analysis/runner/buffer_pool_dispatcher.rb +8 -4
  63. data/lib/rigor/analysis/runner/declaration_position.rb +1 -1
  64. data/lib/rigor/analysis/runner/diagnostic_aggregator.rb +204 -96
  65. data/lib/rigor/analysis/runner/effect_envelope_pass.rb +1 -1
  66. data/lib/rigor/analysis/runner/pool_coordinator.rb +157 -320
  67. data/lib/rigor/analysis/runner/project_pre_passes.rb +48 -6
  68. data/lib/rigor/analysis/runner/run_level_facts.rb +304 -0
  69. data/lib/rigor/analysis/runner/run_snapshots.rb +44 -54
  70. data/lib/rigor/analysis/runner/scan_environment.rb +51 -0
  71. data/lib/rigor/analysis/runner/worker_payload.rb +36 -0
  72. data/lib/rigor/analysis/runner.rb +233 -120
  73. data/lib/rigor/analysis/self_call_resolution_recorder.rb +1 -1
  74. data/lib/rigor/analysis/template_unit_positions.rb +2 -3
  75. data/lib/rigor/analysis/template_units.rb +9 -1
  76. data/lib/rigor/analysis/worker_session.rb +38 -33
  77. data/lib/rigor/bleeding_edge.rb +3 -6
  78. data/lib/rigor/builtins/imported_refinements.rb +6 -8
  79. data/lib/rigor/builtins/predefined_constant_refinements.rb +1 -1
  80. data/lib/rigor/builtins/regex_refinement.rb +4 -6
  81. data/lib/rigor/cache/descriptor.rb +6 -5
  82. data/lib/rigor/cache/incremental_snapshot.rb +60 -18
  83. data/lib/rigor/cache/rbs_cache_producer.rb +2 -2
  84. data/lib/rigor/cache/rbs_class_ancestor_table.rb +1 -1
  85. data/lib/rigor/cache/rbs_class_type_param_names.rb +2 -2
  86. data/lib/rigor/cache/rbs_constant_table.rb +1 -1
  87. data/lib/rigor/cache/rbs_descriptor.rb +54 -3
  88. data/lib/rigor/cache/rbs_environment.rb +4 -1
  89. data/lib/rigor/cache/rbs_environment_marshal_patch.rb +2 -2
  90. data/lib/rigor/cache/rbs_known_class_names.rb +1 -1
  91. data/lib/rigor/cache/store.rb +11 -10
  92. data/lib/rigor/ci_detector.rb +1 -1
  93. data/lib/rigor/cli/annotate_command.rb +24 -19
  94. data/lib/rigor/cli/baseline_command.rb +145 -55
  95. data/lib/rigor/cli/catalogue.rb +256 -0
  96. data/lib/rigor/cli/check_command.rb +188 -93
  97. data/lib/rigor/cli/check_runner_factory.rb +2 -2
  98. data/lib/rigor/cli/command.rb +15 -1
  99. data/lib/rigor/cli/coverage_command.rb +21 -8
  100. data/lib/rigor/cli/coverage_mutation.rb +5 -7
  101. data/lib/rigor/cli/coverage_renderer.rb +2 -1
  102. data/lib/rigor/cli/coverage_scan.rb +3 -5
  103. data/lib/rigor/cli/diff_command.rb +27 -21
  104. data/lib/rigor/cli/docs_command.rb +1 -2
  105. data/lib/rigor/cli/doctor_command.rb +12 -12
  106. data/lib/rigor/cli/effects_command.rb +50 -40
  107. data/lib/rigor/cli/effects_diff_renderer.rb +18 -17
  108. data/lib/rigor/cli/effects_explain_renderer.rb +13 -12
  109. data/lib/rigor/cli/effects_renderer.rb +2 -1
  110. data/lib/rigor/cli/effects_snapshot_command.rb +18 -17
  111. data/lib/rigor/cli/explain_command.rb +18 -8
  112. data/lib/rigor/cli/fused_protection_renderer.rb +2 -1
  113. data/lib/rigor/cli/json_document.rb +41 -0
  114. data/lib/rigor/cli/lens_command.rb +17 -13
  115. data/lib/rigor/cli/lsp_command.rb +250 -23
  116. data/lib/rigor/cli/mcp_command.rb +14 -8
  117. data/lib/rigor/cli/mutation_fork_scan.rb +1 -1
  118. data/lib/rigor/cli/mutation_protection_renderer.rb +3 -2
  119. data/lib/rigor/cli/options.rb +60 -4
  120. data/lib/rigor/cli/plugin_command.rb +93 -46
  121. data/lib/rigor/cli/plugins_command.rb +22 -18
  122. data/lib/rigor/cli/plugins_renderer.rb +34 -8
  123. data/lib/rigor/cli/protection_fork_scan.rb +2 -2
  124. data/lib/rigor/cli/protection_renderer.rb +2 -1
  125. data/lib/rigor/cli/show_bleedingedge_command.rb +23 -16
  126. data/lib/rigor/cli/sig_gen_command.rb +130 -34
  127. data/lib/rigor/cli/skill_command.rb +32 -3
  128. data/lib/rigor/cli/skill_deep_probe.rb +1 -1
  129. data/lib/rigor/cli/skill_describe.rb +1 -1
  130. data/lib/rigor/cli/trace_command.rb +14 -9
  131. data/lib/rigor/cli/triage_command.rb +10 -11
  132. data/lib/rigor/cli/triage_renderer.rb +2 -1
  133. data/lib/rigor/cli/type_of_command.rb +18 -12
  134. data/lib/rigor/cli/type_of_project_scope.rb +24 -0
  135. data/lib/rigor/cli/type_of_renderer.rb +4 -1
  136. data/lib/rigor/cli/type_scan_command.rb +14 -9
  137. data/lib/rigor/cli/type_scan_renderer.rb +2 -1
  138. data/lib/rigor/cli/unused_command.rb +27 -17
  139. data/lib/rigor/cli/upgrade_command.rb +5 -0
  140. data/lib/rigor/cli.rb +209 -54
  141. data/lib/rigor/config_audit.rb +17 -6
  142. data/lib/rigor/configuration/severity_profile.rb +5 -5
  143. data/lib/rigor/configuration.rb +86 -34
  144. data/lib/rigor/effects/definition_context.rb +0 -2
  145. data/lib/rigor/effects/envelope.rb +1 -1
  146. data/lib/rigor/effects/framework_units.rb +1 -1
  147. data/lib/rigor/effects/mutation_classifier.rb +4 -7
  148. data/lib/rigor/effects/propagator.rb +1 -1
  149. data/lib/rigor/effects/registry.rb +2 -2
  150. data/lib/rigor/effects/snapshot.rb +1 -1
  151. data/lib/rigor/effects/taint_cause.rb +0 -2
  152. data/lib/rigor/effects/unit_scan.rb +9 -13
  153. data/lib/rigor/environment/bundle_sig_discovery.rb +5 -9
  154. data/lib/rigor/environment/class_registry.rb +3 -3
  155. data/lib/rigor/environment/core_root.rb +77 -0
  156. data/lib/rigor/environment/default_libraries.rb +1 -1
  157. data/lib/rigor/environment/deferred_signature_plan.rb +283 -0
  158. data/lib/rigor/environment/gated_signature_guard.rb +1 -1
  159. data/lib/rigor/environment/gated_signature_plan.rb +5 -2
  160. data/lib/rigor/environment/lockfile_resolver.rb +28 -12
  161. data/lib/rigor/environment/member_consistency/comparator.rb +18 -1
  162. data/lib/rigor/environment/member_consistency.rb +56 -7
  163. data/lib/rigor/environment/missing_gem_constant_index.rb +3 -2
  164. data/lib/rigor/environment/project_path_gems.rb +124 -0
  165. data/lib/rigor/environment/rbs_hierarchy.rb +6 -15
  166. data/lib/rigor/environment/rbs_loader.rb +592 -70
  167. data/lib/rigor/environment/reflection.rb +7 -7
  168. data/lib/rigor/environment/required_features.rb +136 -32
  169. data/lib/rigor/environment/ruby_library_roots.rb +122 -0
  170. data/lib/rigor/environment/vendored_signature_plan.rb +153 -0
  171. data/lib/rigor/environment.rb +44 -11
  172. data/lib/rigor/flow_contribution/merger.rb +4 -4
  173. data/lib/rigor/inference/acceptance.rb +93 -32
  174. data/lib/rigor/inference/block_parameter_binder.rb +2 -3
  175. data/lib/rigor/inference/block_run_count.rb +178 -0
  176. data/lib/rigor/inference/budget_trace.rb +8 -2
  177. data/lib/rigor/inference/builtins/exception_catalog.rb +2 -2
  178. data/lib/rigor/inference/builtins/numeric_catalog.rb +2 -5
  179. data/lib/rigor/inference/builtins/random_catalog.rb +2 -2
  180. data/lib/rigor/inference/callee_mutation_chain.rb +126 -0
  181. data/lib/rigor/inference/callee_write_census.rb +815 -0
  182. data/lib/rigor/inference/construction_escape.rb +217 -0
  183. data/lib/rigor/inference/declaration_walk/traversal.rb +1 -1
  184. data/lib/rigor/inference/deep_erasure.rb +170 -0
  185. data/lib/rigor/inference/def_handle.rb +1 -1
  186. data/lib/rigor/inference/def_node_resolver.rb +1 -1
  187. data/lib/rigor/inference/def_return_typer.rb +204 -12
  188. data/lib/rigor/inference/definer_resolution.rb +21 -5
  189. data/lib/rigor/inference/definers_index.rb +106 -0
  190. data/lib/rigor/inference/direct_ivar_writes.rb +300 -0
  191. data/lib/rigor/inference/element_read_widening.rb +49 -3
  192. data/lib/rigor/inference/error_info.rb +17 -3
  193. data/lib/rigor/inference/expression_typer.rb +173 -57
  194. data/lib/rigor/inference/external_ancestor_resolution.rb +3 -4
  195. data/lib/rigor/inference/global_write_census.rb +110 -1
  196. data/lib/rigor/inference/global_writer_reach.rb +260 -0
  197. data/lib/rigor/inference/hkt_body.rb +1 -2
  198. data/lib/rigor/inference/hkt_body_parser.rb +3 -5
  199. data/lib/rigor/inference/hkt_reducer.rb +2 -2
  200. data/lib/rigor/inference/hkt_registry.rb +3 -5
  201. data/lib/rigor/inference/indexed_narrowing.rb +4 -7
  202. data/lib/rigor/inference/intervening_ivar_reset.rb +26 -4
  203. data/lib/rigor/inference/ivar_writer_census.rb +246 -0
  204. data/lib/rigor/inference/key_presence_guard.rb +2 -1
  205. data/lib/rigor/inference/loop_value_record.rb +57 -0
  206. data/lib/rigor/inference/match_rebinding/frame.rb +65 -0
  207. data/lib/rigor/inference/match_rebinding/yielding_match.rb +45 -0
  208. data/lib/rigor/inference/match_rebinding.rb +11 -1
  209. data/lib/rigor/inference/memo_audit.rb +55 -0
  210. data/lib/rigor/inference/method_dispatcher/alias_strict_nominals.rb +1 -1
  211. data/lib/rigor/inference/method_dispatcher/argument_distribution.rb +40 -0
  212. data/lib/rigor/inference/method_dispatcher/block_folding.rb +1 -1
  213. data/lib/rigor/inference/method_dispatcher/constant_folding.rb +113 -12
  214. data/lib/rigor/inference/method_dispatcher/contested_overloads.rb +89 -0
  215. data/lib/rigor/inference/method_dispatcher/iterator_dispatch.rb +1 -1
  216. data/lib/rigor/inference/method_dispatcher/kernel_dispatch.rb +7 -3
  217. data/lib/rigor/inference/method_dispatcher/no_match_fallback.rb +54 -0
  218. data/lib/rigor/inference/method_dispatcher/overload_selector.rb +39 -27
  219. data/lib/rigor/inference/method_dispatcher/random_folding.rb +1 -1
  220. data/lib/rigor/inference/method_dispatcher/rbs_dispatch.rb +203 -17
  221. data/lib/rigor/inference/method_dispatcher/reduce_folding.rb +1 -1
  222. data/lib/rigor/inference/method_dispatcher/regexp_folding.rb +2 -2
  223. data/lib/rigor/inference/method_dispatcher/self_substitute.rb +2 -2
  224. data/lib/rigor/inference/method_dispatcher/set_folding.rb +59 -24
  225. data/lib/rigor/inference/method_dispatcher/shape_dispatch.rb +83 -20
  226. data/lib/rigor/inference/method_dispatcher/singleton_folding.rb +0 -1
  227. data/lib/rigor/inference/method_dispatcher/singleton_mixin_dispatch.rb +50 -17
  228. data/lib/rigor/inference/method_dispatcher/struct_folding.rb +5 -5
  229. data/lib/rigor/inference/method_dispatcher/struct_materialization.rb +4 -6
  230. data/lib/rigor/inference/method_dispatcher/uri_folding.rb +1 -1
  231. data/lib/rigor/inference/method_dispatcher.rb +109 -29
  232. data/lib/rigor/inference/module_function_state/spellings.rb +64 -0
  233. data/lib/rigor/inference/module_function_state.rb +23 -1
  234. data/lib/rigor/inference/multi_target_binder.rb +1 -1
  235. data/lib/rigor/inference/mutation_rejoin.rb +240 -4
  236. data/lib/rigor/inference/mutation_widening.rb +162 -29
  237. data/lib/rigor/inference/narrowing.rb +113 -38
  238. data/lib/rigor/inference/object_mixins.rb +13 -1
  239. data/lib/rigor/inference/optimistic_origin.rb +1 -1
  240. data/lib/rigor/inference/out_buffer_mutation.rb +72 -0
  241. data/lib/rigor/inference/parameter_inference_collector.rb +5 -6
  242. data/lib/rigor/inference/pre_eval_constants.rb +1 -1
  243. data/lib/rigor/inference/project_method_ownership.rb +6 -3
  244. data/lib/rigor/inference/project_seed_tables.rb +2 -1
  245. data/lib/rigor/inference/project_yielder.rb +122 -0
  246. data/lib/rigor/inference/receiver_alias.rb +20 -0
  247. data/lib/rigor/inference/refined_dispatch/constructed_class.rb +3 -1
  248. data/lib/rigor/inference/refined_dispatch/own_method.rb +2 -1
  249. data/lib/rigor/inference/remover_holder_scan.rb +233 -0
  250. data/lib/rigor/inference/remover_narrowing.rb +224 -0
  251. data/lib/rigor/inference/rewrite_mutation.rb +53 -2
  252. data/lib/rigor/inference/scope_indexer/certainty.rb +9 -1
  253. data/lib/rigor/inference/scope_indexer/constant_ivar_value.rb +93 -0
  254. data/lib/rigor/inference/scope_indexer/declared_ivar_types.rb +64 -0
  255. data/lib/rigor/inference/scope_indexer/declared_param_writes.rb +205 -0
  256. data/lib/rigor/inference/scope_indexer/derived_param_value.rb +112 -0
  257. data/lib/rigor/inference/scope_indexer/empty_seed.rb +99 -0
  258. data/lib/rigor/inference/scope_indexer/ivar_contributions.rb +135 -0
  259. data/lib/rigor/inference/scope_indexer/unseen_ivar_writers.rb +133 -0
  260. data/lib/rigor/inference/scope_indexer.rb +614 -146
  261. data/lib/rigor/inference/self_call_ivar_writes.rb +101 -17
  262. data/lib/rigor/inference/set_mutation.rb +189 -0
  263. data/lib/rigor/inference/singleton_hook_decline.rb +1 -2
  264. data/lib/rigor/inference/statement_evaluator.rb +3036 -278
  265. data/lib/rigor/inference/struct_fold_safety.rb +4 -6
  266. data/lib/rigor/inference/synthetic_method_index.rb +23 -0
  267. data/lib/rigor/inference/synthetic_method_scanner/project_members.rb +130 -0
  268. data/lib/rigor/inference/synthetic_method_scanner.rb +73 -14
  269. data/lib/rigor/inference/toplevel_def_census.rb +59 -0
  270. data/lib/rigor/inference/unknown_store_widening.rb +103 -4
  271. data/lib/rigor/inference/unthreaded_rebinds.rb +2 -3
  272. data/lib/rigor/inference/void_tail_summary.rb +2 -1
  273. data/lib/rigor/language_server/buffer_table.rb +9 -3
  274. data/lib/rigor/language_server/completion_provider.rb +26 -20
  275. data/lib/rigor/language_server/config_warning_announcer.rb +28 -0
  276. data/lib/rigor/language_server/diagnostic_publisher.rb +28 -9
  277. data/lib/rigor/language_server/document_symbol_provider.rb +1 -1
  278. data/lib/rigor/language_server/folding_range_provider.rb +1 -2
  279. data/lib/rigor/language_server/hover_provider.rb +1 -7
  280. data/lib/rigor/language_server/hover_renderer.rb +6 -8
  281. data/lib/rigor/language_server/project_context.rb +98 -4
  282. data/lib/rigor/language_server/publish_batcher.rb +3 -4
  283. data/lib/rigor/language_server/selection_range_provider.rb +2 -4
  284. data/lib/rigor/language_server/server.rb +12 -6
  285. data/lib/rigor/language_server/signature_help_provider.rb +4 -4
  286. data/lib/rigor/lens/plugins.rb +1 -1
  287. data/lib/rigor/mcp/server.rb +82 -3
  288. data/lib/rigor/mcp.rb +1 -1
  289. data/lib/rigor/plugin/access_denied_error.rb +1 -2
  290. data/lib/rigor/plugin/base.rb +26 -11
  291. data/lib/rigor/plugin/bundled_catalog.rb +1 -12
  292. data/lib/rigor/plugin/io_boundary.rb +1 -1
  293. data/lib/rigor/plugin/loader.rb +25 -8
  294. data/lib/rigor/plugin/macro/heredoc_template.rb +7 -8
  295. data/lib/rigor/plugin/macro/trait_registry.rb +6 -9
  296. data/lib/rigor/plugin/manifest.rb +38 -16
  297. data/lib/rigor/plugin/registry.rb +41 -3
  298. data/lib/rigor/plugin/services.rb +2 -3
  299. data/lib/rigor/plugin/source_rbs_synthesis_reporter.rb +11 -0
  300. data/lib/rigor/plugin/trust_policy.rb +2 -2
  301. data/lib/rigor/plugin/type_node_resolver.rb +2 -4
  302. data/lib/rigor/plugin.rb +6 -8
  303. data/lib/rigor/plugin_gap_advisory.rb +5 -1
  304. data/lib/rigor/project_environment.rb +16 -2
  305. data/lib/rigor/protection/analysis_guard.rb +1 -1
  306. data/lib/rigor/protection/discovery_seed.rb +96 -2
  307. data/lib/rigor/protection/mutator.rb +1 -1
  308. data/lib/rigor/rbs_extended/envelope_scanner.rb +4 -6
  309. data/lib/rigor/rbs_extended/hkt_directives.rb +6 -10
  310. data/lib/rigor/rbs_extended/reporter.rb +27 -3
  311. data/lib/rigor/rbs_extended.rb +2 -3
  312. data/lib/rigor/reflection/constant_ancestors.rb +5 -3
  313. data/lib/rigor/rooted_glob.rb +25 -2
  314. data/lib/rigor/run_scoped_slots.rb +38 -0
  315. data/lib/rigor/runtime/jit.rb +1 -1
  316. data/lib/rigor/scope/discovery_index.rb +19 -6
  317. data/lib/rigor/scope/resolution_chain.rb +3 -1
  318. data/lib/rigor/scope.rb +379 -36
  319. data/lib/rigor/sig_gen/classification.rb +4 -0
  320. data/lib/rigor/sig_gen/effect_annotation.rb +3 -3
  321. data/lib/rigor/sig_gen/generator.rb +668 -84
  322. data/lib/rigor/sig_gen/generic_header_scan.rb +189 -0
  323. data/lib/rigor/sig_gen/inline_declarations.rb +2 -2
  324. data/lib/rigor/sig_gen/layout_index.rb +3 -3
  325. data/lib/rigor/sig_gen/method_candidate.rb +23 -4
  326. data/lib/rigor/sig_gen/observation_collector.rb +7 -6
  327. data/lib/rigor/sig_gen/observed_call.rb +1 -1
  328. data/lib/rigor/sig_gen/path_mapper.rb +6 -0
  329. data/lib/rigor/sig_gen/rbs_validity.rb +1 -1
  330. data/lib/rigor/sig_gen/renderer.rb +21 -12
  331. data/lib/rigor/sig_gen/skip_reason_catalog.rb +15 -2
  332. data/lib/rigor/sig_gen/type_elaborator.rb +21 -5
  333. data/lib/rigor/sig_gen/type_name_qualification.rb +69 -0
  334. data/lib/rigor/sig_gen/write_reporter.rb +63 -14
  335. data/lib/rigor/sig_gen/write_result.rb +23 -12
  336. data/lib/rigor/sig_gen/writer.rb +217 -37
  337. data/lib/rigor/type/acceptance_router.rb +2 -2
  338. data/lib/rigor/type/accepts_result.rb +1 -1
  339. data/lib/rigor/type/app.rb +4 -6
  340. data/lib/rigor/type/combinator.rb +13 -16
  341. data/lib/rigor/type/constant.rb +48 -7
  342. data/lib/rigor/type/hash_shape.rb +26 -40
  343. data/lib/rigor/type/struct_instance.rb +3 -2
  344. data/lib/rigor/type/union.rb +1 -1
  345. data/lib/rigor/type_node/identifier.rb +1 -2
  346. data/lib/rigor/type_node/resolver_chain.rb +1 -1
  347. data/lib/rigor/type_node/string_literal.rb +1 -2
  348. data/lib/rigor/type_node/symbol_literal.rb +1 -2
  349. data/lib/rigor/type_node/union.rb +3 -6
  350. data/lib/rigor/value_semantics.rb +1 -10
  351. data/lib/rigor/version.rb +1 -1
  352. data/plugins/rigor-actioncable/lib/rigor/plugin/actioncable/channel_discoverer.rb +2 -2
  353. data/plugins/rigor-actioncable/lib/rigor/plugin/actioncable/channel_index.rb +2 -3
  354. data/plugins/rigor-actioncable/lib/rigor/plugin/actioncable.rb +1 -1
  355. data/plugins/rigor-actionmailer/lib/rigor/plugin/actionmailer/mailer_discoverer.rb +89 -70
  356. data/plugins/rigor-actionmailer/lib/rigor/plugin/actionmailer/mailer_index.rb +4 -2
  357. data/plugins/rigor-actionmailer/lib/rigor/plugin/actionmailer.rb +25 -4
  358. data/plugins/rigor-actionpack/lib/rigor/plugin/actionpack/analyzer.rb +1 -1
  359. data/plugins/rigor-actionpack/lib/rigor/plugin/actionpack/controller_index.rb +1 -5
  360. data/plugins/rigor-actionpack/lib/rigor/plugin/actionpack/controller_scan.rb +0 -1
  361. data/plugins/rigor-actionpack/lib/rigor/plugin/actionpack/erb_compiler.rb +2 -1
  362. data/plugins/rigor-actionpack/lib/rigor/plugin/actionpack/view_assigns.rb +1 -1
  363. data/plugins/rigor-actionpack/lib/rigor/plugin/actionpack.rb +79 -19
  364. data/plugins/rigor-activejob/lib/rigor/plugin/activejob/effects.rb +1 -1
  365. data/plugins/rigor-activejob/lib/rigor/plugin/activejob.rb +6 -3
  366. data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord/association_roots.rb +3 -2
  367. data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord/effects.rb +2 -2
  368. data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord/model_discoverer.rb +21 -31
  369. data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord/model_index.rb +10 -16
  370. data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord/schema_parser.rb +2 -3
  371. data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord/schema_table.rb +0 -1
  372. data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord.rb +5 -4
  373. data/plugins/rigor-activestorage/lib/rigor/plugin/activestorage/attachment_discoverer.rb +3 -4
  374. data/plugins/rigor-activestorage/lib/rigor/plugin/activestorage/attachment_index.rb +1 -1
  375. data/plugins/rigor-activestorage/lib/rigor/plugin/activestorage.rb +7 -12
  376. data/plugins/rigor-activestorage/lib/rigor-activestorage.rb +1 -1
  377. data/plugins/rigor-activesupport-core-ext/lib/rigor/plugin/activesupport_core_ext/effects.rb +1 -1
  378. data/plugins/rigor-activesupport-core-ext/lib/rigor/plugin/activesupport_core_ext/symbolize_keys.rb +102 -0
  379. data/plugins/rigor-activesupport-core-ext/lib/rigor/plugin/activesupport_core_ext.rb +19 -6
  380. data/plugins/rigor-activesupport-core-ext/sig/active_support/core_ext.rbs +26 -13
  381. data/plugins/rigor-alba/lib/rigor/plugin/alba/resource_index.rb +3 -3
  382. data/plugins/rigor-devise/lib/rigor/plugin/devise.rb +5 -3
  383. data/plugins/rigor-dry-schema/lib/rigor/plugin/dry_schema/schema_scanner.rb +1 -3
  384. data/plugins/rigor-dry-struct/lib/rigor/plugin/dry_struct.rb +6 -6
  385. data/plugins/rigor-dry-types/lib/rigor/plugin/dry_types/alias_scanner.rb +5 -6
  386. data/plugins/rigor-dry-types/lib/rigor/plugin/dry_types.rb +7 -6
  387. data/plugins/rigor-dry-validation/lib/rigor/plugin/dry_validation/params_shape.rb +2 -3
  388. data/plugins/rigor-dry-validation/lib/rigor/plugin/dry_validation.rb +5 -8
  389. data/plugins/rigor-ethon/lib/rigor/plugin/ethon.rb +0 -1
  390. data/plugins/rigor-factorybot/lib/rigor/plugin/factorybot/factory_discoverer.rb +1 -1
  391. data/plugins/rigor-factorybot/lib/rigor/plugin/factorybot.rb +10 -18
  392. data/plugins/rigor-ffi/lib/rigor/plugin/ffi/analyzer.rb +0 -1
  393. data/plugins/rigor-ffi/lib/rigor/plugin/ffi/discoverer.rb +0 -2
  394. data/plugins/rigor-ffi/lib/rigor/plugin/ffi.rb +4 -5
  395. data/plugins/rigor-grape/lib/rigor/plugin/grape.rb +0 -2
  396. data/plugins/rigor-graphql/lib/rigor/plugin/graphql/type_scanner.rb +6 -7
  397. data/plugins/rigor-hanami/lib/rigor/plugin/hanami/action_checker.rb +2 -4
  398. data/plugins/rigor-hanami/lib/rigor/plugin/hanami.rb +2 -5
  399. data/plugins/rigor-mangrove/lib/rigor/plugin/mangrove.rb +2 -2
  400. data/plugins/rigor-minitest/lib/rigor/plugin/minitest/assertion_analyzer.rb +6 -6
  401. data/plugins/rigor-pundit/lib/rigor/plugin/pundit/policy_discoverer.rb +2 -1
  402. data/plugins/rigor-rails-i18n/lib/rigor/plugin/rails_i18n/analyzer.rb +3 -8
  403. data/plugins/rigor-rails-i18n/lib/rigor/plugin/rails_i18n/locale_index.rb +2 -3
  404. data/plugins/rigor-rails-i18n/lib/rigor/plugin/rails_i18n/locale_loader.rb +5 -8
  405. data/plugins/rigor-rails-routes/lib/rigor/plugin/rails_routes/analyzer.rb +1 -3
  406. data/plugins/rigor-rails-routes/lib/rigor/plugin/rails_routes/devise_routes.rb +2 -3
  407. data/plugins/rigor-rails-routes/lib/rigor/plugin/rails_routes/helper_discoverer.rb +1 -1
  408. data/plugins/rigor-rails-routes/lib/rigor/plugin/rails_routes/routes_parser.rb +4 -11
  409. data/plugins/rigor-rails-routes/lib/rigor/plugin/rails_routes.rb +1 -1
  410. data/plugins/rigor-rbs-inline/lib/rigor/plugin/rbs_inline.rb +2 -1
  411. data/plugins/rigor-rspec/lib/rigor/plugin/rspec/matcher_analyzer.rb +102 -11
  412. data/plugins/rigor-rspec/lib/rigor/plugin/rspec.rb +2 -2
  413. data/plugins/rigor-rspec-rails/lib/rigor/plugin/rspec_rails.rb +3 -4
  414. data/plugins/rigor-shoulda-matchers/lib/rigor/plugin/shoulda_matchers/analyzer.rb +6 -8
  415. data/plugins/rigor-sidekiq/lib/rigor/plugin/sidekiq.rb +5 -2
  416. data/plugins/rigor-sorbet/lib/rigor/plugin/sorbet/absurd_recognizer.rb +4 -5
  417. data/plugins/rigor-sorbet/lib/rigor/plugin/sorbet/assertion_recognizer.rb +6 -7
  418. data/plugins/rigor-sorbet/lib/rigor/plugin/sorbet/catalog.rb +1 -1
  419. data/plugins/rigor-sorbet/lib/rigor/plugin/sorbet/sig_parser.rb +2 -2
  420. data/plugins/rigor-sorbet/lib/rigor/plugin/sorbet.rb +17 -21
  421. data/plugins/rigor-statesman/lib/rigor/plugin/statesman.rb +2 -3
  422. data/plugins/rigor-typelizer/lib/rigor/plugin/typelizer/serializer_discoverer.rb +3 -2
  423. data/plugins/rigor-typelizer/lib/rigor/plugin/typelizer/serializer_index.rb +4 -4
  424. data/plugins/rigor-typelizer/lib/rigor/plugin/typelizer.rb +1 -1
  425. data/sig/rigor/analysis/baseline.rbs +0 -2
  426. data/sig/rigor/analysis/fact_store.rbs +1 -0
  427. data/sig/rigor/analysis/reachability/scan_cache.rbs +1 -1
  428. data/sig/rigor/cli/diff_command.rbs +1 -1
  429. data/sig/rigor/cli/explain_command.rbs +1 -1
  430. data/sig/rigor/effects/file_collection.rbs +5 -6
  431. data/sig/rigor/effects/registry.rbs +8 -6
  432. data/sig/rigor/effects/summary.rbs +6 -5
  433. data/sig/rigor/environment.rbs +5 -3
  434. data/sig/rigor/inference/optimistic_origin.rbs +11 -0
  435. data/sig/rigor/inference.rbs +10 -1
  436. data/sig/rigor/plugin/additional_initializer.rbs +0 -5
  437. data/sig/rigor/plugin/base.rbs +1 -0
  438. data/sig/rigor/plugin/effect_ancestry.rbs +6 -4
  439. data/sig/rigor/plugin/effect_attribution.rbs +25 -10
  440. data/sig/rigor/plugin/effect_edge.rbs +12 -5
  441. data/sig/rigor/plugin/effect_entry_points.rbs +8 -3
  442. data/sig/rigor/plugin/io_boundary.rbs +6 -5
  443. data/sig/rigor/plugin/manifest.rbs +16 -18
  444. data/sig/rigor/plugin/protocol_contract.rbs +6 -9
  445. data/sig/rigor/plugin/registry.rbs +1 -1
  446. data/sig/rigor/scope.rbs +51 -50
  447. data/sig/rigor/type.rbs +0 -6
  448. data/skills/rigor-plugin-author/SKILL.md +4 -4
  449. data/skills/rigor-plugin-author/references/02-walker-and-types.md +5 -5
  450. data/skills/rigor-plugin-review/SKILL.md +3 -3
  451. data/skills/rigor-project-init/references/03-baseline-and-bugs.md +12 -1
  452. metadata +55 -3
@@ -10,8 +10,10 @@ require_relative "../source/constant_path"
10
10
  require_relative "../source/node_children"
11
11
  require_relative "../source/node_walker"
12
12
  require_relative "../analysis/self_call_resolution_recorder"
13
+ require_relative "../run_scoped_slots"
13
14
  require_relative "block_call_timing"
14
15
  require_relative "block_repetition"
16
+ require_relative "project_yielder"
15
17
  require_relative "block_parameter_binder"
16
18
  require_relative "method_parameter_binder"
17
19
  require_relative "body_fixpoint"
@@ -344,7 +346,7 @@ module Rigor
344
346
  self.class.recorded_inject_accumulator(block, scope)
345
347
  end
346
348
 
347
- INJECT_ACCUMULATOR_KEY = :__rigor_inject_accumulator__
349
+ INJECT_ACCUMULATOR_KEY = RunScopedSlots.register(:__rigor_inject_accumulator__)
348
350
  private_constant :INJECT_ACCUMULATOR_KEY
349
351
 
350
352
  # The accumulator type {#record_inject_accumulator} stored for `block` when the call was typed from `scope`
@@ -359,13 +361,18 @@ module Rigor
359
361
  # recording run to harvest each analyzed callee's observed call keys → return descriptors. Returns an
360
362
  # empty hash when no bucket exists (a run that memoised nothing). Class method: the bucket lives on
361
363
  # `Thread.current`, independent of any one `ExpressionTyper` instance.
362
- def self.harvest_return_memo
363
- slot = Thread.current[RETURN_MEMO_KEY]
364
- return {} if slot.nil?
364
+ #
365
+ # `bucket` defaults to the calling thread's current one; `Runner#run` keeps the bucket its run filled
366
+ # ({.current_return_memo}) and passes it, since the run releases the thread's slot when it ends.
367
+ def self.harvest_return_memo(bucket = current_return_memo)
368
+ return {} if bucket.nil?
365
369
 
366
- slot[1].transform_values(&:values)
370
+ bucket.transform_values(&:values)
367
371
  end
368
372
 
373
+ # The calling thread's return memo bucket (`{ def_node => { descriptor => MemoEntry } }`), or nil.
374
+ def self.current_return_memo = Thread.current[RETURN_MEMO_KEY]&.last
375
+
369
376
  # Issue #720 — the value the block passed by the call site currently being re-typed evaluates to, or
370
377
  # `nil` outside such a frame. Read by {#type_of_yield}.
371
378
  #
@@ -1051,7 +1058,7 @@ module Rigor
1051
1058
  # optimistic-carrier decline) and one terminating-arm rule, and cannot drift apart again. Only the
1052
1059
  # value is read; the post-scope belongs to whichever walk owns the statement.
1053
1060
  def type_of_conditional(node)
1054
- scope.evaluate(node, tracer: tracer).first
1061
+ LoopValueRecord.fetch(node, scope) || scope.evaluate(node, tracer: tracer).first
1055
1062
  end
1056
1063
 
1057
1064
  def type_of_else(node)
@@ -1099,7 +1106,6 @@ module Rigor
1099
1106
  conditions = when_node.respond_to?(:conditions) ? when_node.conditions : []
1100
1107
  body_scope, next_scope = Narrowing.case_when_scopes(node.predicate, conditions, clause_scope)
1101
1108
  certainty = case_when_branch_certainty(subject_type, when_node)
1102
- # :no — drop the branch
1103
1109
  candidates << case_arm_type(when_node, body_scope) unless certainty == :no
1104
1110
  if certainty == :yes
1105
1111
  reached_yes = true
@@ -1272,11 +1278,22 @@ module Rigor
1272
1278
  Type::Combinator.bot
1273
1279
  end
1274
1280
 
1275
- # `while` and `until` loops produce nil unless interrupted by `break VALUE`; the expression value of
1276
- # `break VALUE` is not yet modeled (scope break-path propagation landed in `eval_loop`). Returning
1277
- # Constant[nil] is safe and matches Ruby semantics for the common case.
1278
- def type_of_loop(_node)
1279
- Type::Combinator.constant_of(nil)
1281
+ # A `while` / `until` / `for` loop in value position. Like {#type_of_conditional}, it delegates to the
1282
+ # statement evaluator, which owns the loop's value: `nil` or a `while` / `until` loop's `break` values
1283
+ # (issue #2031), and a `for` loop's collection or its `break` values (issue #1216).
1284
+ #
1285
+ # A `while` / `until` loop no `break` leaves is `nil` without evaluating its body, as
1286
+ # {StatementEvaluator#loop_value} answers it: a loop that is a call's receiver or argument is typed again by
1287
+ # every narrowing and rule that types that operand, and evaluating it each time re-ran the passes of every loop
1288
+ # nested in it.
1289
+ def type_of_loop(node)
1290
+ return Type::Combinator.constant_of(nil) if break_free_condition_loop?(node)
1291
+
1292
+ LoopValueRecord.fetch(node, scope) || scope.evaluate(node, tracer: tracer).first
1293
+ end
1294
+
1295
+ def break_free_condition_loop?(node)
1296
+ !node.is_a?(Prism::ForNode) && !block_level_jump?(node.statements, Prism::BreakNode)
1280
1297
  end
1281
1298
 
1282
1299
  def type_of_lambda(_node)
@@ -1507,9 +1524,8 @@ module Rigor
1507
1524
 
1508
1525
  # Stable single-hop chain narrowing — `receiver.method` after an `is_a?` / `kind_of?` / `instance_of?`
1509
1526
  # predicate established the narrowing on the dominated edge. The call MUST be no-arg + no-block +
1510
- # rooted at a local-var / ivar read; everything else falls through to the standard dispatcher. ROADMAP
1511
- # § Future cycles — "Method-call receiver narrowing across stable receivers" — Law-of-Demeter-justified
1512
- # single-hop scope.
1527
+ # rooted at a local-var / ivar read; everything else falls through to the standard dispatcher. The
1528
+ # single hop is Law-of-Demeter-justified.
1513
1529
  def method_chain_narrowing_for(node)
1514
1530
  return nil unless node.is_a?(Prism::CallNode)
1515
1531
  return nil unless node.block.nil?
@@ -1759,7 +1775,23 @@ module Rigor
1759
1775
  def call_type_for(node)
1760
1776
  return safe_navigation_call_type(node) if node.safe_navigation?
1761
1777
 
1762
- attribute_write_value(node, call_dispatch_type_for(node))
1778
+ memo_object_value(node, attribute_write_value(node, call_dispatch_type_for(node)))
1779
+ end
1780
+
1781
+ # Issue #2411 — `recv.each_with_object(memo) { |x, acc| acc[k] = v }` returns the memo as the block's
1782
+ # stores leave it. The statement evaluator owns that join ({StatementEvaluator#each_with_object_value}),
1783
+ # and reading it here gives every value position the statement's answer: a method's tail is typed through
1784
+ # this typer, and read off the dispatch alone it was the memo argument's literal `{}` / `[]`. A `break` ends
1785
+ # the call with its own value, so the block's break arms ({#call_break_arm_types}) join the memo: the join
1786
+ # alone read `break :stop if x == 2; a << x` as `Array[1 | 2]`, where Ruby answers `:stop`.
1787
+ def memo_object_value(node, result)
1788
+ return result unless node.name == :each_with_object && node.block.is_a?(Prism::BlockNode)
1789
+
1790
+ memo = StatementEvaluator.new(scope: scope, tracer: tracer).each_with_object_value(node, result)
1791
+ return memo if memo.equal?(result)
1792
+
1793
+ breaks = call_break_arm_types(node)
1794
+ breaks.empty? ? memo : Type::Combinator.union(memo, *breaks)
1763
1795
  end
1764
1796
 
1765
1797
  # The RHS-value override for plain attribute / index writes (#520); a non-write call keeps the
@@ -2376,14 +2408,12 @@ module Rigor
2376
2408
  #
2377
2409
  # - the receiver is not `Nominal[T]` for some T;
2378
2410
  # - no def_node is recorded for that class/method (the receiver is foreign or has only an RBS sig);
2379
- # - the def has no body, or has a parameter shape we cannot bind from the call's positional args;
2380
- # - the inference is already in progress for this (class, method, signature) tuple — recursion safety
2381
- # net.
2411
+ # - the def has no body, or has a parameter shape we cannot bind from the call's positional args.
2382
2412
  # v0.0.3 A — re-types a top-level (or DSL-block-nested) `def` discovered by `ScopeIndexer` under the
2383
2413
  # `TOP_LEVEL_DEF_KEY` sentinel. Mirrors the `infer_user_method_return` shape but uses the current
2384
2414
  # `scope.self_type` (or implicit `Object`) as the receiver carrier so the body's own self is
2385
2415
  # consistent with the call site's. Returns nil when the parameter shape disqualifies the def, when the
2386
- # body is empty, or when a recursion cycle is detected.
2416
+ # body is empty, or when the inference raises.
2387
2417
  def infer_top_level_user_method(def_node, receiver, arg_types, block_type = nil)
2388
2418
  infer_user_method_return(def_node, receiver, arg_types, yield_type: block_type)
2389
2419
  rescue StandardError
@@ -2430,9 +2460,9 @@ module Rigor
2430
2460
 
2431
2461
  # True when `type` is a concrete value — a `Type::Constant` or a `Type::Tuple` whose elements are
2432
2462
  # (recursively) all value-pinned. ADR-55 slice 1: a value-pinned self-call result is adopted even
2433
- # inside a class/method body (where WD3 otherwise keeps non-`Bot` returns as `Dynamic[top]`). A
2434
- # concrete value at a call site is strictly more precise and can never enable an undefined-method or
2435
- # argument-type false positive — it is FP-neutral by construction.
2463
+ # inside a class/method body (where WD3 otherwise keeps non-`Bot` returns as `Dynamic[top]`). A concrete
2464
+ # value at a call site is strictly more precise and can never enable an undefined-method or argument-type
2465
+ # false positive — it is FP-neutral by construction.
2436
2466
  def fully_value_pinned?(type)
2437
2467
  case type
2438
2468
  when Type::Constant then true
@@ -2634,9 +2664,63 @@ module Rigor
2634
2664
  found = scope.singleton_def_through_ancestors(class_name, method_name)
2635
2665
  owner = found.last
2636
2666
  found = [nil, nil] if owner && SingletonExternalAhead.withhold?(scope, class_name, method_name, owner)
2667
+ found = [nil, nil] if found.last && dynamic_singleton_definer_ahead?(class_name, method_name, found.last)
2637
2668
  table[key] = found
2638
2669
  end
2639
2670
 
2671
+ SINGLETON_WILDCARD_SLOT = %i[singleton *].freeze
2672
+ private_constant :SINGLETON_WILDCARD_SLOT
2673
+
2674
+ # Issue #2284 — a project entry nearer than the `def`'s owner on the singleton chain may define the name
2675
+ # without a `def`, which the def tables do not hold: `class S < B; define_singleton_method(:x) { "s" }; end`
2676
+ # calls S's block, not `B.x`. The farther `def` is then withheld and the call types `Dynamic`. A class's own
2677
+ # singleton side is read from the visibility-contest census: `define_singleton_method(:x)`,
2678
+ # `singleton_class.define_method(:x)` / `attr_*` and a computed name contest the entry's slot whether or not a
2679
+ # `def` is recorded (#1984, #2013). An `extend`ed module's side is read from `discovered_methods`, which holds
2680
+ # its `attr_*` and `define_method` rows, and the module's `def self.extended` hook may define any name on the
2681
+ # extender, so it withholds too.
2682
+ #
2683
+ # An `inherited` hook anywhere on the chain past the receiver's own entry runs when the receiver's class is
2684
+ # created and may define any singleton method on it (`def self.inherited(s) = s.define_singleton_method(:x)`),
2685
+ # so a `def` the receiver only inherits is withheld then as well: a class's `def self.inherited`, or an
2686
+ # `inherited` an extended module defines.
2687
+ def dynamic_singleton_definer_ahead?(class_name, method_name, owner)
2688
+ name = method_name.to_sym
2689
+ chain = Scope::ResolutionChain.for(scope, class_name.to_s, :singleton, :methods)
2690
+ return true if class_name.to_s != owner.to_s && inherited_hook_on_chain?(chain, class_name.to_s)
2691
+
2692
+ chain.entries.each do |entry|
2693
+ next if entry.external?
2694
+ return false if entry.name == owner.to_s
2695
+ return true if entry_defines_without_def?(entry, name)
2696
+ end
2697
+ false
2698
+ end
2699
+
2700
+ def inherited_hook_on_chain?(chain, receiver_name)
2701
+ chain.entries.any? do |entry|
2702
+ next false if entry.external? || entry.name == receiver_name
2703
+
2704
+ if entry.side == :singleton
2705
+ !scope.singleton_def_for(entry.name, :inherited).nil?
2706
+ else
2707
+ scope.discovered_method?(entry.name, :inherited, :instance)
2708
+ end
2709
+ end
2710
+ end
2711
+
2712
+ def entry_defines_without_def?(entry, name)
2713
+ unless entry.side == :singleton
2714
+ return scope.discovered_method?(entry.name, name, :instance) ||
2715
+ !scope.singleton_def_for(entry.name, :extended).nil?
2716
+ end
2717
+
2718
+ discovery = scope.discovery
2719
+ # No edge of its own: `singleton_def_through_ancestors`' chain search filed every entry ahead of the owner.
2720
+ discovery.contested?(:discovered_method_visibilities, [entry.name, [:singleton, name]]) ||
2721
+ discovery.contested?(:discovered_method_visibilities, [entry.name, SINGLETON_WILDCARD_SLOT])
2722
+ end
2723
+
2640
2724
  # The project-side singleton-method band: a `Foo.bar` call resolved against a `class << …` / `def self.…`
2641
2725
  # body the project itself wrote. Which of the two tiers applies is decided by the receiver carrier —
2642
2726
  # `Singleton[Foo]` when the constant names a class or module, `Nominal[…]` when it holds an ordinary
@@ -2672,7 +2756,7 @@ module Rigor
2672
2756
  # external entries this lookup passes over. Cross-file: the chain is built from the tables the runner
2673
2757
  # seeds from the project-wide pre-pass, cycle-guarded and capped at `Scope::ANCESTOR_WALK_LIMIT`.
2674
2758
 
2675
- CLASS_GRAPH_CACHE_KEY = :__rigor_class_graph_cache__
2759
+ CLASS_GRAPH_CACHE_KEY = RunScopedSlots.register(:__rigor_class_graph_cache__)
2676
2760
  private_constant :CLASS_GRAPH_CACHE_KEY
2677
2761
 
2678
2762
  # Memo for the static class-graph resolvers below. They are pure functions of the scope's *frozen*
@@ -2873,7 +2957,7 @@ module Rigor
2873
2957
  dynamic_top
2874
2958
  end
2875
2959
 
2876
- OVERRIDE_GATE_CACHE_KEY = :__rigor_overridable_method_gate__
2960
+ OVERRIDE_GATE_CACHE_KEY = RunScopedSlots.register(:__rigor_overridable_method_gate__)
2877
2961
  private_constant :OVERRIDE_GATE_CACHE_KEY
2878
2962
 
2879
2963
  # Memo for {#overridden_in_project?}, nested `kind → owner → method_name`. The predicate is a pure
@@ -2946,7 +3030,7 @@ module Rigor
2946
3030
  EMPTY_REDEFINERS = [].freeze
2947
3031
  private_constant :EMPTY_REDEFINERS
2948
3032
 
2949
- METHOD_DEFINERS_INDEX_KEY = :__rigor_method_definers_index__
3033
+ METHOD_DEFINERS_INDEX_KEY = RunScopedSlots.register(:__rigor_method_definers_index__)
2950
3034
  private_constant :METHOD_DEFINERS_INDEX_KEY
2951
3035
 
2952
3036
  # The second way. Two flat thread slots rather than one slot holding an array of ways: a
@@ -2954,7 +3038,7 @@ module Rigor
2954
3038
  # elements are pinned, and the engine then reads the `way[0].equal?(…)` guards below as always-falsey
2955
3039
  # (`make check` fires `flow.always-truthy-condition` on this very file). Two keys read as two plain
2956
3040
  # `Thread.current` reads and keep the guards analysable.
2957
- METHOD_DEFINERS_INDEX_ALT_KEY = :__rigor_method_definers_index_alt__
3041
+ METHOD_DEFINERS_INDEX_ALT_KEY = RunScopedSlots.register(:__rigor_method_definers_index_alt__)
2958
3042
  private_constant :METHOD_DEFINERS_INDEX_ALT_KEY
2959
3043
 
2960
3044
  # `method_name (Symbol) → [owner names]` inverted index over the instance / singleton def tables,
@@ -2988,8 +3072,9 @@ module Rigor
2988
3072
  # the singleton index.
2989
3073
  #
2990
3074
  # DELIBERATELY not keyed on the whole {Scope::DiscoveryIndex}, unlike {#class_graph_buckets} and
2991
- # {#override_gate_buckets} above. {#build_method_definers_index} reads the ONE table it is handed and
2992
- # nothing else, so the def tables are already the complete key, and the index object is a strictly
3075
+ # {#override_gate_buckets} above. {DefinersIndex.for} reads the ONE table it is handed, and the seed that
3076
+ # table was merged over, and nothing else, so the def tables are already the complete key, and the index
3077
+ # object is a strictly
2993
3078
  # narrower one: the project-seed scope carries a fresh index per file while its def tables stay the
2994
3079
  # same object. Over the 200 synthetic files above, an index-keyed memo misses all 400 times where a
2995
3080
  # def-table-keyed one misses 201 — it would rebuild the seed's 934-entry table once per file and hand
@@ -3007,17 +3092,7 @@ module Rigor
3007
3092
  Thread.current[METHOD_DEFINERS_INDEX_KEY] = way
3008
3093
  end
3009
3094
  singleton = kind == :singleton
3010
- way[singleton ? 3 : 2] ||= build_method_definers_index(singleton ? singleton_def_nodes : def_nodes)
3011
- end
3012
-
3013
- def build_method_definers_index(table)
3014
- index = {}
3015
- table.each do |class_name, methods|
3016
- next if class_name == Inference::ScopeIndexer::TOP_LEVEL_DEF_KEY
3017
-
3018
- methods.each_key { |method_name| (index[method_name] ||= []) << class_name }
3019
- end
3020
- index
3095
+ way[singleton ? 3 : 2] ||= DefinersIndex.for(singleton ? singleton_def_nodes : def_nodes)
3021
3096
  end
3022
3097
 
3023
3098
  # True when `candidate`'s ancestor chain (superclasses + included/prepended modules) reaches `owner` —
@@ -3098,10 +3173,11 @@ module Rigor
3098
3173
  # `[receiver, *args]` descriptor tuple; a callee may appear under at most two node identities per run
3099
3174
  # (the project-index parse every OTHER file resolves through, plus the defining file's own analysis
3100
3175
  # parse). Only the current generation's bucket is retained (single slot) — a rolled-over run's entries
3101
- # become garbage instead of accumulating across LSP runs. `ExpressionTyper` is rebuilt per
3102
- # `Scope#type_of`, so the store lives on `Thread.current`; fork-pool workers are separate processes,
3103
- # so it never crosses a project boundary (ADR-84 WD5).
3104
- RETURN_MEMO_KEY = :__rigor_user_method_return_memo__
3176
+ # become garbage instead of accumulating across LSP runs, and the run that filled the slot releases it
3177
+ # when it ends ({RunScopedSlots}; `Runner#run` keeps its bucket for `#return_summaries`).
3178
+ # `ExpressionTyper` is rebuilt per `Scope#type_of`, so the store lives on `Thread.current`; fork-pool
3179
+ # workers are separate processes, so it never crosses a project boundary (ADR-84 WD5).
3180
+ RETURN_MEMO_KEY = RunScopedSlots.register(:__rigor_user_method_return_memo__)
3105
3181
  private_constant :RETURN_MEMO_KEY
3106
3182
 
3107
3183
  # ADR-84 WD2 — one memo entry: the final inferred return plus, when the entry was computed under
@@ -3318,21 +3394,34 @@ module Rigor
3318
3394
  cache = class_graph_buckets[:yields]
3319
3395
  return cache[def_node] if cache.key?(def_node)
3320
3396
 
3321
- cache[def_node] = yield_reachable?(def_node.body)
3397
+ cache[def_node] = yield_reachable?(def_node.body, def_node.parameters&.block&.name)
3322
3398
  end
3323
3399
 
3324
- def yield_reachable?(node)
3400
+ def yield_reachable?(node, block_name)
3325
3401
  return false if node.nil?
3326
- return true if node.is_a?(Prism::YieldNode)
3402
+ return true if node.is_a?(Prism::YieldNode) || forwards_block_argument?(node, block_name)
3327
3403
 
3328
3404
  node.rigor_each_child do |child|
3329
3405
  next if YIELD_BOUNDARY_NODES.include?(child.class)
3330
3406
 
3331
- return true if yield_reachable?(child)
3407
+ return true if yield_reachable?(child, block_name)
3332
3408
  end
3333
3409
  false
3334
3410
  end
3335
3411
 
3412
+ # Issue #1157 — a call that hands on its method's block (`&`, or `&blk` naming the block parameter) reaches the
3413
+ # caller's block as a `yield` does. Whether `blk` is still the method's own block (not rebound or shadowed) is
3414
+ # decided where it is typed ({#forwarded_block_return_type}); this only keeps the call-site dimension for a def
3415
+ # that might.
3416
+ def forwards_block_argument?(node, block_name)
3417
+ block = node.respond_to?(:block) ? node.block : nil
3418
+ return false unless block.is_a?(Prism::BlockArgumentNode)
3419
+
3420
+ expression = block.expression
3421
+ expression.nil? || (!block_name.nil? && expression.is_a?(Prism::LocalVariableReadNode) &&
3422
+ expression.name == block_name)
3423
+ end
3424
+
3336
3425
  # The candidate-frame memo path: consult the current run generation's bucket, and on a miss compute
3337
3426
  # and store a FINAL result. Reached only when `memo_candidate?` held (see `infer_user_method_return`).
3338
3427
  # The store gate (ADR-84 WD3): a result is stored when no transient-machinery event during the
@@ -3873,7 +3962,7 @@ module Rigor
3873
3962
  # Consumes one unit from the thread-local unroll-fuel counter and returns the units that were
3874
3963
  # available *before* this consumption (so a positive return means the extended value-key may be used).
3875
3964
  # Fuel is per-outermost inference entry: at the top level (empty guard stack) it seeds to
3876
- # `RECURSION_UNROLL_FUEL`, and the `ensure` in `infer_user_method_return` clears it once the stack
3965
+ # `RECURSION_UNROLL_FUEL`, and the `ensure` in `evaluate_guarded_user_method_body` clears it once the stack
3877
3966
  # drains back to empty. On exhaustion (return 0) it records a `RECURSION_UNROLL_FUEL` hit so the caller
3878
3967
  # keeps the plain `(receiver, method)` signature — today's behaviour.
3879
3968
  def unroll_fuel_remaining(stack)
@@ -3936,7 +4025,7 @@ module Rigor
3936
4025
  # to the receiver's nominal type, the project-wide accumulators inherited (so the body sees the same
3937
4026
  # `discovered_classes` / `class_ivars` / etc. the caller does), and the call's `arg_types` bound to
3938
4027
  # the parameters. Returns nil when the binding is truly ambiguous (arity mismatch, trailing `post`
3939
- # params, `...` forwarding).
4028
+ # params).
3940
4029
  #
3941
4030
  # Issue #524 — the binder is per-PARAMETER, not per-signature. The first iteration declined any def
3942
4031
  # with an optional / rest / keyword / block parameter outright, so ONE `options = {}` default zeroed
@@ -3984,7 +4073,7 @@ module Rigor
3984
4073
  body_scope.with_repeated_or_writes(RepeatedOrWrites.straight_line_sites(def_node.body, body_scope))
3985
4074
  end
3986
4075
 
3987
- # Issue #681 / #707 — the ONE place in `lib/` that answers "what `Module.nesting` was recorded for this
4076
+ # Issue #681 / #707 — the typer's answer to "what `Module.nesting` was recorded for this
3988
4077
  # def node". `Scope::DiscoveryIndex#discovered_def_nestings` is the owner; the resolver memo is that
3989
4078
  # table's REHYDRATION for the one population it cannot hold — a def served from an ADR-85 seed bundle,
3990
4079
  # whose node {Inference::DefNodeResolver} mints from its own parse, so no table keyed by the identities
@@ -3999,7 +4088,7 @@ module Rigor
3999
4088
  # run and an `--incremental` recheck answering the same constant for the same body; the rehydration half
4000
4089
  # is populated only inside `DefNodeResolver.with_run`, and outside it this falls back to the peel.
4001
4090
  def recorded_def_nesting(def_node)
4002
- scope.discovery.discovered_def_nestings[def_node] || DefNodeResolver.rehydrated_nesting(def_node)
4091
+ scope.discovery.recorded_def_nesting(def_node)
4003
4092
  end
4004
4093
 
4005
4094
  # The body scope's fold-safe set: the body's own struct locals, plus issue #525's `:self` sentinel
@@ -4328,9 +4417,15 @@ module Rigor
4328
4417
 
4329
4418
  # Whether the call may run its block more than once, so a later run reads a captured binding an earlier
4330
4419
  # run rebound — the premise of laying the #587 (b) binding ({#captured_block_bindings}) under the block.
4331
- # {BlockRepetition.may_repeat?} holds the rule, which the statement pass shares (issue #1412).
4420
+ # {BlockRepetition.may_repeat?} holds the rule, which the statement pass shares (issue #1412). A project callee
4421
+ # whose body may run the block more than once on one path ({ProjectYielder.block_value_may_repeat?}) also
4422
+ # counts: a `yield` in a loop or a core iterator's block (`xs.each { |x| v = yield(x) }`, issue #2303), or a
4423
+ # hand-off (`g(x, &)`, issue #2301), whose target may run it many times, so a captured local the block rebinds
4424
+ # must not read its entry binding. A callee that runs it once per path (`f ? yield(1) : yield(2)`,
4425
+ # `then { yield }`) keeps the single-run value.
4332
4426
  def block_may_repeat?(call_node, receiver_type)
4333
- BlockRepetition.may_repeat?(method_name: call_node.name, receiver_type: receiver_type, scope: scope)
4427
+ BlockRepetition.may_repeat?(method_name: call_node.name, receiver_type: receiver_type, scope: scope) ||
4428
+ ProjectYielder.block_value_may_repeat?(call_node, receiver_type, scope)
4334
4429
  end
4335
4430
 
4336
4431
  EMPTY_BREAK_ARMS = [].freeze
@@ -4426,10 +4521,22 @@ module Rigor
4426
4521
  )
4427
4522
  type_block_body(block_arg, entry, captured: captured)
4428
4523
  when Prism::BlockArgumentNode
4429
- symbol_block_return_type(block_arg, expected)
4524
+ forwarded_block_return_type(block_arg) || symbol_block_return_type(block_arg, expected)
4430
4525
  end
4431
4526
  end
4432
4527
 
4528
+ # Issue #1157 — `&` / `&blk` hands the callee the block this method was itself called with, so the callee's
4529
+ # `yield` is worth what the CALLER's block returns: the value {.current_yield_value_type} already holds for
4530
+ # this frame (the one a `yield` written here reads). Without it a name resolver that passes its block on
4531
+ # (`def walk(x, &) = first_hit(x, &)`) read as `Dynamic[top]` through the forward and was skipped by
4532
+ # `sig-gen`. Nil when the argument is not a forwarded block or the call site passed no block.
4533
+ def forwarded_block_return_type(block_arg)
4534
+ frame = scope.match_frame
4535
+ return nil unless frame&.forwards_block?(block_arg)
4536
+
4537
+ ExpressionTyper.current_yield_value_type
4538
+ end
4539
+
4433
4540
  # The scope a block body is typed under: the surrounding scope plus the parameter bindings the receiving
4434
4541
  # method's signature implies, with the #587 (b) `captured` binding ({#repeating_captured_bindings}) laid
4435
4542
  # between the two, so the body reads what a captured local, instance variable, class variable or global
@@ -4524,6 +4631,14 @@ module Rigor
4524
4631
  # A failure in the re-answer falls back to the plain tail-only answer, for the reason
4525
4632
  # {#threaded_block_body_type} gives: a raise reaching `block_return_type_for` would report "no block".
4526
4633
  def tail_only_block_body_type(body, block_scope, captured)
4634
+ StatementEvaluator.provisional_returns { tail_only_block_body_type_unrecorded(body, block_scope, captured) }
4635
+ end
4636
+
4637
+ # Issue #2299 — the tail alone, typed from `block_scope`, is not the walk the method's returns are collected
4638
+ # from: a `return` in it reads a name the skipped prefix binds as unbound. Its records are provisional
4639
+ # ({StatementEvaluator.provisional_returns}): the statement walk's record of the same `return` replaces one,
4640
+ # and one stands only for a block the walk never enters (a block in expression position).
4641
+ def tail_only_block_body_type_unrecorded(body, block_scope, captured)
4527
4642
  return block_scope.type_of(body) unless block_body_threading_suppressed? && body.is_a?(Prism::StatementsNode)
4528
4643
 
4529
4644
  answered =
@@ -5520,8 +5635,9 @@ module Rigor
5520
5635
  end
5521
5636
 
5522
5637
  # Records `type` as the accumulator `block` receives when typed from this typer's scope
5523
- # ({.recorded_inject_accumulator}). The keys are weak, so a finished file's nodes do not stay reachable, and
5524
- # the values strong, so a record outlives a collection while its node does. Prism nodes compare by identity.
5638
+ # ({.recorded_inject_accumulator}). Prism nodes compare by identity. The keys are weak but the values strong,
5639
+ # and a recorded scope reaches the parse tree that holds the key, so a record never goes on its own (issue
5640
+ # #2412): the map is a run-scoped slot ({RunScopedSlots}), emptied when the run that filled it ends.
5525
5641
  def record_inject_accumulator(block, type)
5526
5642
  (Thread.current[INJECT_ACCUMULATOR_KEY] ||= ObjectSpace::WeakKeyMap.new)[block] = [scope, type].freeze
5527
5643
  end
@@ -28,13 +28,12 @@ module Rigor
28
28
  # a flag threaded into `Scope`: an ADR-2 plugin-facing bypass of dependency recording would produce
29
29
  # a silently stale warm cache that no diagnostic diff can show.
30
30
  #
31
- # There is deliberately NO memo here. The dispatch consumer this module is being extracted FOR does
32
- # not exist yet, and a memo keyed on the tables this slice happens to name would be keyed on less
31
+ # There is deliberately NO memo here. A memo keyed on the tables the walk names would be keyed on less
33
32
  # than the walk reads — the ancestor chain resolves names through `known_user_class?`
34
33
  # (`discovered_def_nodes`, `discovered_methods`) and `ancestor_name_candidates`
35
34
  # (`discovered_header_nestings`). The chain itself is memoised per discovery index
36
- # ({Scope::ResolutionChain}); the slice that brings the hot path brings this module's memo, keyed on
37
- # what that consumer actually reads.
35
+ # ({Scope::ResolutionChain}); the dispatch callers in {MethodDispatcher::RbsDispatch} memoise their
36
+ # own answers, keyed on what they read.
38
37
  module ExternalAncestorResolution
39
38
  # The owner a name must PRECEDE for its declaration to win an MRO. See {declared_before_object?}.
40
39
  OBJECT_OWNER = "Object"
@@ -4,6 +4,8 @@ require "prism"
4
4
 
5
5
  require_relative "../source/constant_path"
6
6
  require_relative "../source/node_children"
7
+ require_relative "ivar_writer_census"
8
+ require_relative "stored_block_call"
7
9
 
8
10
  module Rigor
9
11
  module Inference
@@ -26,6 +28,16 @@ module Rigor
26
28
  # kept apart from `[:defines, …]`.
27
29
  # - `[:main_mixin, name]` — a module a top-level `include`, `prepend` or `extend` mixes into `Object` (or
28
30
  # `main`), as written. A module RBS does not know may carry any method, so it reaches every class.
31
+ # - the instance-variable writer entries of {IvarWriterCensus} (issue #1154): `[:ivar_set, name]`,
32
+ # `[:ivar_set_class, class_name]`, `[:own_write, class_name, name, path]` and, issue #2079,
33
+ # `[:own_write_type, class_name, name, value, path]`.
34
+ # - `[:global_writer, name, method]` — issue #2314: a write of the global `name` inside a method body, keyed by
35
+ # the `def`'s name (or the literal name a `define_method` block defines), or by nil inside a lambda or a block
36
+ # its call keeps to run later ({StoredBlockCall}). A write in a block its call runs at once belongs to the
37
+ # enclosing body. A write at the top level or in a class body records nothing, and neither does a write of
38
+ # the frame-local `$_` or `$~`. {GlobalWriterReach} reads these.
39
+ # - `[:method_alias, new, old]` — issue #2314: `alias new old`, `alias_method :new, :old` or `def_delegator
40
+ # :@o, :old, :new`, whose new name runs a method named old. {GlobalWriterReach} follows it.
29
41
  #
30
42
  # The census is a `Set` of those frozen entries, merged by union across files, the project pre-pass and the
31
43
  # `pre_eval:` files. It is recorded inside descents that already visit every node ({Collector#visit}), never
@@ -74,12 +86,41 @@ module Rigor
74
86
  census.include?(DEFINES_ANY) || names.any? { |name| census.include?([:defines, name]) }
75
87
  end
76
88
 
89
+ # #1154 — the {IvarWriterCensus} entries of `census`, for the incremental diff and its readers.
90
+ IVAR_KINDS = %i[ivar_set ivar_set_class own_write own_write_type].to_set.freeze
91
+
92
+ def ivar_set_entries(census) = census.select { |entry| IVAR_KINDS.include?(entry.first) }.to_set
93
+
94
+ # The negative-dependency key an {IvarWriterCensus} entry fires, the one `UnseenIvarWriters` records.
95
+ def ivar_key(entry)
96
+ case entry.first
97
+ when :ivar_set then "ivar_set:#{entry[1]}"
98
+ when :ivar_set_class then "ivar_set_class:#{entry[1]}"
99
+ when :own_write_type then "own_write_type:#{entry[1]}##{entry[2]}"
100
+ else "own_write:#{entry[1]}##{entry[2]}"
101
+ end
102
+ end
103
+
77
104
  # Whether some refinement may add `write`.
78
105
  def refines_write?(census) = census.include?(REFINES_ANY) || census.include?(%i[refines write])
79
106
 
80
107
  # The module names the top-level mixins spell.
81
108
  def main_mixins(census) = census.filter_map { |entry| entry[1] if entry[0] == :main_mixin }
82
109
 
110
+ # Issue #2158 — the `global_census:` dependency key a census entry moves, without the kind prefix: `alias:$x`,
111
+ # `defines:write`, `refines:write`, `defines_any`, `refines_any`, and `main_mixin` for every top-level mixin: its
112
+ # reader asks about each, and one shared key is a deliberate precision trade-off (a mixin moving anywhere
113
+ # re-checks every write that read them). The rules that read the census record these keys
114
+ # (`CheckRules#record_global_census`), and `Analysis::Incremental.changed_global_census_keys` fires them for
115
+ # every entry that appeared in or vanished from a file's census.
116
+ def dependency_key(entry)
117
+ case entry
118
+ when DEFINES_ANY then "defines_any"
119
+ when REFINES_ANY then "refines_any"
120
+ else entry[0] == :main_mixin ? "main_mixin" : "#{entry[0]}:#{entry[1]}"
121
+ end
122
+ end
123
+
83
124
  # A standalone walk, for a file no existing descent visits (a `pre_eval:` entry).
84
125
  def scan(root)
85
126
  collector = Collector.new(InEffectRefinements.new(root))
@@ -107,12 +148,18 @@ module Rigor
107
148
  def initialize(in_effect)
108
149
  @census = Set.new
109
150
  @refine_ranges = []
151
+ @ivar_writers = IvarWriterCensus.new(@census)
110
152
  @in_effect = in_effect
153
+ @bodies = []
111
154
  end
112
155
 
113
156
  # A node the collector cannot read must not fail the analysis of its file, nor of every file the pre-pass
114
157
  # feeds: the census then counts it as a definition of every name, which only declines a report.
115
- def visit(node, top_level:)
158
+ # `owner` is the qualified name of the class whose body the node sits in, for {IvarWriterCensus} (nil where
159
+ # the host walk does not track it, or no class owns an instance there).
160
+ def visit(node, top_level:, owner: nil)
161
+ @ivar_writers.visit(node, owner)
162
+ visit_global_writer(node)
116
163
  case node
117
164
  when Prism::AliasGlobalVariableNode then visit_global_alias(node)
118
165
  when Prism::DefNode then record(node, node.name)
@@ -125,6 +172,68 @@ module Rigor
125
172
 
126
173
  private
127
174
 
175
+ # Issue #2314 — tracks the method bodies, lambdas and stored blocks of a pre-order descent as flat
176
+ # `start, end, label` triples (`@bodies`, innermost last), and records a global write inside one
177
+ # ({GlobalWriterReach}). A body ending before the write starts is closed. Only those nodes read their location.
178
+ def visit_global_writer(node)
179
+ case node
180
+ when Prism::DefNode then open_body(node, node.name)
181
+ when Prism::LambdaNode then open_body(node, nil)
182
+ when Prism::AliasMethodNode then record_method_alias(node.new_name, node.old_name)
183
+ when Prism::CallNode
184
+ open_stored_block(node)
185
+ visit_alias_call(node)
186
+ when Prism::GlobalVariableWriteNode, Prism::GlobalVariableOrWriteNode, Prism::GlobalVariableAndWriteNode,
187
+ Prism::GlobalVariableOperatorWriteNode, Prism::GlobalVariableTargetNode
188
+ record_global_writer(node)
189
+ end
190
+ end
191
+
192
+ # `$_` and `$~` are the frame's own: a method's write to one never reaches its caller.
193
+ FRAME_LOCAL_GLOBALS = %i[$_ $~].freeze
194
+ private_constant :FRAME_LOCAL_GLOBALS
195
+
196
+ def record_global_writer(node)
197
+ return if FRAME_LOCAL_GLOBALS.include?(node.name)
198
+
199
+ start = node.location.start_offset
200
+ bodies = @bodies
201
+ bodies.pop(3) while !bodies.empty? && bodies[-2] <= start
202
+ index = bodies.size - 3
203
+ index -= 3 while index >= 0 && !(bodies[index] <= start && start < bodies[index + 1])
204
+ @census << [:global_writer, node.name, bodies[index + 2]].freeze if index >= 0
205
+ end
206
+
207
+ def open_body(node, label)
208
+ location = node.location
209
+ @bodies.push(location.start_offset, location.end_offset, label)
210
+ end
211
+
212
+ # `alias_method :new, :old`, and `def_delegator :@o, :old, :new` (and its instance and single spellings), whose
213
+ # new name runs a method of the old name.
214
+ def visit_alias_call(node)
215
+ arguments = node.arguments&.arguments
216
+ case node.name
217
+ when :alias_method then record_method_alias(arguments[0], arguments[1]) if arguments&.size == 2
218
+ when :def_delegator, :def_instance_delegator, :def_single_delegator
219
+ record_method_alias(arguments[2], arguments[1]) if arguments&.size == 3
220
+ end
221
+ end
222
+
223
+ def record_method_alias(new_node, old_node)
224
+ new_name = literal_name(new_node)
225
+ old_name = literal_name(old_node)
226
+ @census << [:method_alias, new_name, old_name].freeze if new_name && old_name && new_name != old_name
227
+ end
228
+
229
+ def open_stored_block(node)
230
+ block = node.block
231
+ return unless block.is_a?(Prism::BlockNode) && StoredBlockCall.stores_block?(node)
232
+
233
+ definer = %i[define_method define_singleton_method].include?(node.name)
234
+ open_body(block, definer ? literal_name(node.arguments&.arguments&.first) : nil)
235
+ end
236
+
128
237
  def visit_global_alias(node)
129
238
  [node.new_name, node.old_name].each do |side|
130
239
  @census << GlobalWriteCensus.alias_entry(side.name) if side.is_a?(Prism::GlobalVariableReadNode)