rigortype 0.4.6 → 0.4.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 (550) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +4 -2
  3. data/data/core_overlay/bigdecimal.rbs +33 -0
  4. data/data/core_overlay/resolv.rbs +30 -0
  5. data/data/gem_overlay/activesupport/core_ext.rbs +45 -13
  6. data/data/gem_overlay/addressable/addressable.rbs +40 -0
  7. data/data/vendored_gem_sigs/bundler/bundler.rbs +1 -1
  8. data/data/vendored_gem_sigs/rubygems/rubygems_extras.rbs +4 -3
  9. data/docs/handbook/01-getting-started.md +6 -3
  10. data/docs/handbook/11-sig-gen.md +85 -3
  11. data/docs/handbook/appendix-phpstan.md +4 -4
  12. data/docs/install.md +20 -0
  13. data/docs/manual/01-installation.md +67 -26
  14. data/docs/manual/02-cli-reference.md +455 -113
  15. data/docs/manual/03-configuration.md +76 -5
  16. data/docs/manual/04-diagnostics.md +5 -4
  17. data/docs/manual/06-baseline.md +6 -4
  18. data/docs/manual/08-skills.md +11 -0
  19. data/docs/manual/09-editor-integration.md +21 -2
  20. data/docs/manual/10-mcp-server.md +38 -2
  21. data/docs/manual/12-caching.md +4 -3
  22. data/docs/manual/14-rails-quickstart.md +1 -1
  23. data/docs/manual/19-effect-labels.md +3 -4
  24. data/docs/manual/plugins/rigor-actionpack.md +32 -15
  25. data/docs/manual/plugins/rigor-activesupport-core-ext.md +23 -1
  26. data/docs/manual/plugins/rigor-sorbet.md +17 -3
  27. data/lib/rigor/analysis/baseline.rb +24 -27
  28. data/lib/rigor/analysis/check_rules/always_raises_shield.rb +147 -0
  29. data/lib/rigor/analysis/check_rules/always_truthy_condition_collector.rb +5 -6
  30. data/lib/rigor/analysis/check_rules/call_facts.rb +58 -0
  31. data/lib/rigor/analysis/check_rules/dead_assignment_collector.rb +2 -2
  32. data/lib/rigor/analysis/check_rules/declaration_sourced_guard.rb +12 -4
  33. data/lib/rigor/analysis/check_rules/duplicate_hash_key_collector.rb +2 -2
  34. data/lib/rigor/analysis/check_rules/inferred_param_guard.rb +85 -13
  35. data/lib/rigor/analysis/check_rules/ivar_write_collector.rb +16 -4
  36. data/lib/rigor/analysis/check_rules/lexical_method_sites.rb +6 -1
  37. data/lib/rigor/analysis/check_rules/main_pass_collector.rb +5 -8
  38. data/lib/rigor/analysis/check_rules/plugin_api_surface.rb +195 -0
  39. data/lib/rigor/analysis/check_rules/published_constant_guard.rb +41 -12
  40. data/lib/rigor/analysis/check_rules/rbs_receiver_surface.rb +27 -1
  41. data/lib/rigor/analysis/check_rules/rebinding_block_callees.rb +3 -3
  42. data/lib/rigor/analysis/check_rules/replaced_singleton_defs.rb +96 -0
  43. data/lib/rigor/analysis/check_rules/return_in_ensure_collector.rb +1 -2
  44. data/lib/rigor/analysis/check_rules/rule_ids.rb +9 -2
  45. data/lib/rigor/analysis/check_rules/rule_walk.rb +9 -7
  46. data/lib/rigor/analysis/check_rules/shadowed_rescue_collector.rb +3 -4
  47. data/lib/rigor/analysis/check_rules/special_global_setters.rb +3 -3
  48. data/lib/rigor/analysis/check_rules/unreachable_clause_collector.rb +2 -2
  49. data/lib/rigor/analysis/check_rules/void_value_use_collector.rb +2 -2
  50. data/lib/rigor/analysis/check_rules.rb +720 -141
  51. data/lib/rigor/analysis/crash_signature.rb +45 -3
  52. data/lib/rigor/analysis/dependency_recorder.rb +237 -80
  53. data/lib/rigor/analysis/dependency_source_inference/boundary_cross_reporter.rb +14 -3
  54. data/lib/rigor/analysis/dependency_source_inference/gem_resolver.rb +1 -1
  55. data/lib/rigor/analysis/dependency_source_inference/index.rb +6 -8
  56. data/lib/rigor/analysis/dependency_source_inference/walker.rb +4 -4
  57. data/lib/rigor/analysis/edit_replay.rb +93 -6
  58. data/lib/rigor/analysis/fact_store.rb +3 -3
  59. data/lib/rigor/analysis/incremental.rb +11 -1
  60. data/lib/rigor/analysis/incremental_run_slot/write_guard.rb +4 -3
  61. data/lib/rigor/analysis/incremental_run_slot.rb +2 -2
  62. data/lib/rigor/analysis/incremental_session.rb +306 -53
  63. data/lib/rigor/analysis/reachability/plugin_roots.rb +6 -3
  64. data/lib/rigor/analysis/rule_catalog.rb +39 -3
  65. data/lib/rigor/analysis/run_cache_key.rb +27 -3
  66. data/lib/rigor/analysis/run_disclosure_credit.rb +26 -6
  67. data/lib/rigor/analysis/run_stats.rb +2 -2
  68. data/lib/rigor/analysis/runner/buffer_pool_dispatcher.rb +6 -2
  69. data/lib/rigor/analysis/runner/declaration_position.rb +8 -8
  70. data/lib/rigor/analysis/runner/diagnostic_aggregator.rb +264 -112
  71. data/lib/rigor/analysis/runner/effect_envelope_pass.rb +4 -9
  72. data/lib/rigor/analysis/runner/pool_coordinator.rb +173 -328
  73. data/lib/rigor/analysis/runner/project_pre_passes.rb +4 -37
  74. data/lib/rigor/analysis/runner/run_level_facts.rb +304 -0
  75. data/lib/rigor/analysis/runner/run_snapshots.rb +44 -54
  76. data/lib/rigor/analysis/runner/worker_payload.rb +36 -0
  77. data/lib/rigor/analysis/runner.rb +298 -109
  78. data/lib/rigor/analysis/self_call_resolution_recorder.rb +62 -23
  79. data/lib/rigor/analysis/template_unit_positions.rb +2 -3
  80. data/lib/rigor/analysis/template_units.rb +9 -1
  81. data/lib/rigor/analysis/worker_session.rb +65 -71
  82. data/lib/rigor/bleeding_edge.rb +22 -10
  83. data/lib/rigor/builtins/imported_refinements.rb +6 -8
  84. data/lib/rigor/builtins/predefined_constant_refinements.rb +1 -1
  85. data/lib/rigor/builtins/regex_refinement.rb +4 -6
  86. data/lib/rigor/cache/descriptor.rb +6 -5
  87. data/lib/rigor/cache/file_digest.rb +39 -31
  88. data/lib/rigor/cache/incremental_snapshot.rb +95 -17
  89. data/lib/rigor/cache/rbs_cache_producer.rb +2 -2
  90. data/lib/rigor/cache/rbs_class_ancestor_table.rb +1 -1
  91. data/lib/rigor/cache/rbs_class_type_param_names.rb +2 -2
  92. data/lib/rigor/cache/rbs_constant_table.rb +1 -1
  93. data/lib/rigor/cache/rbs_descriptor.rb +90 -4
  94. data/lib/rigor/cache/rbs_environment.rb +8 -1
  95. data/lib/rigor/cache/rbs_environment_marshal_patch.rb +2 -2
  96. data/lib/rigor/cache/rbs_known_class_names.rb +1 -1
  97. data/lib/rigor/cache/store.rb +11 -10
  98. data/lib/rigor/ci_detector.rb +1 -1
  99. data/lib/rigor/cli/annotate_command.rb +24 -19
  100. data/lib/rigor/cli/baseline_command.rb +160 -57
  101. data/lib/rigor/cli/catalogue.rb +256 -0
  102. data/lib/rigor/cli/check_command.rb +112 -104
  103. data/lib/rigor/cli/check_runner_factory.rb +2 -2
  104. data/lib/rigor/cli/command.rb +15 -1
  105. data/lib/rigor/cli/coverage_command.rb +21 -8
  106. data/lib/rigor/cli/coverage_mutation.rb +5 -7
  107. data/lib/rigor/cli/coverage_renderer.rb +2 -1
  108. data/lib/rigor/cli/coverage_scan.rb +3 -5
  109. data/lib/rigor/cli/diff_command.rb +25 -20
  110. data/lib/rigor/cli/docs_command.rb +1 -2
  111. data/lib/rigor/cli/doctor_command.rb +12 -12
  112. data/lib/rigor/cli/effects_command.rb +50 -40
  113. data/lib/rigor/cli/effects_diff_renderer.rb +18 -17
  114. data/lib/rigor/cli/effects_explain_renderer.rb +13 -12
  115. data/lib/rigor/cli/effects_renderer.rb +2 -1
  116. data/lib/rigor/cli/effects_snapshot_command.rb +26 -18
  117. data/lib/rigor/cli/explain_command.rb +18 -8
  118. data/lib/rigor/cli/fused_protection_renderer.rb +2 -1
  119. data/lib/rigor/cli/json_document.rb +41 -0
  120. data/lib/rigor/cli/lens_command.rb +29 -10
  121. data/lib/rigor/cli/lsp_command.rb +250 -23
  122. data/lib/rigor/cli/mcp_command.rb +12 -7
  123. data/lib/rigor/cli/mutation_fork_scan.rb +1 -1
  124. data/lib/rigor/cli/mutation_protection_renderer.rb +3 -2
  125. data/lib/rigor/cli/options.rb +40 -1
  126. data/lib/rigor/cli/plugin_command.rb +93 -46
  127. data/lib/rigor/cli/plugins_command.rb +17 -18
  128. data/lib/rigor/cli/plugins_renderer.rb +10 -6
  129. data/lib/rigor/cli/protection_fork_scan.rb +2 -2
  130. data/lib/rigor/cli/protection_renderer.rb +2 -1
  131. data/lib/rigor/cli/show_bleedingedge_command.rb +21 -15
  132. data/lib/rigor/cli/sig_gen_command.rb +92 -27
  133. data/lib/rigor/cli/skill_command.rb +32 -3
  134. data/lib/rigor/cli/skill_deep_probe.rb +1 -1
  135. data/lib/rigor/cli/skill_describe.rb +2 -1
  136. data/lib/rigor/cli/trace_command.rb +14 -9
  137. data/lib/rigor/cli/triage_command.rb +10 -11
  138. data/lib/rigor/cli/triage_renderer.rb +2 -1
  139. data/lib/rigor/cli/type_of_command.rb +16 -11
  140. data/lib/rigor/cli/type_of_project_scope.rb +1 -4
  141. data/lib/rigor/cli/type_of_renderer.rb +4 -1
  142. data/lib/rigor/cli/type_scan_command.rb +14 -9
  143. data/lib/rigor/cli/type_scan_renderer.rb +2 -1
  144. data/lib/rigor/cli/unused_command.rb +27 -17
  145. data/lib/rigor/cli/upgrade_command.rb +5 -0
  146. data/lib/rigor/cli.rb +214 -56
  147. data/lib/rigor/config_audit.rb +33 -19
  148. data/lib/rigor/configuration/severity_profile.rb +14 -5
  149. data/lib/rigor/configuration.rb +104 -35
  150. data/lib/rigor/effects/collector.rb +53 -21
  151. data/lib/rigor/effects/definition_context.rb +0 -2
  152. data/lib/rigor/effects/envelope.rb +1 -1
  153. data/lib/rigor/effects/framework_units.rb +1 -1
  154. data/lib/rigor/effects/mutation_classifier.rb +4 -7
  155. data/lib/rigor/effects/propagator.rb +1 -1
  156. data/lib/rigor/effects/registry.rb +2 -2
  157. data/lib/rigor/effects/snapshot.rb +1 -1
  158. data/lib/rigor/effects/taint_cause.rb +0 -2
  159. data/lib/rigor/effects/unit_scan.rb +9 -13
  160. data/lib/rigor/environment/bundle_sig_discovery.rb +64 -33
  161. data/lib/rigor/environment/class_registry.rb +3 -3
  162. data/lib/rigor/environment/constant_type_cache_holder.rb +5 -0
  163. data/lib/rigor/environment/core_root.rb +77 -0
  164. data/lib/rigor/environment/default_libraries.rb +18 -1
  165. data/lib/rigor/environment/deferred_signature_plan.rb +381 -0
  166. data/lib/rigor/environment/gated_signature_guard.rb +1 -1
  167. data/lib/rigor/environment/gated_signature_plan.rb +5 -2
  168. data/lib/rigor/environment/hkt_registry_holder.rb +6 -0
  169. data/lib/rigor/environment/lockfile_resolver.rb +28 -12
  170. data/lib/rigor/environment/member_consistency/comparator.rb +18 -1
  171. data/lib/rigor/environment/member_consistency.rb +56 -7
  172. data/lib/rigor/environment/missing_gem_constant_index.rb +3 -2
  173. data/lib/rigor/environment/project_path_gems.rb +124 -0
  174. data/lib/rigor/environment/rbs_collection_discovery.rb +123 -8
  175. data/lib/rigor/environment/rbs_coverage_report.rb +12 -11
  176. data/lib/rigor/environment/rbs_hierarchy.rb +12 -15
  177. data/lib/rigor/environment/rbs_loader.rb +772 -110
  178. data/lib/rigor/environment/reflection.rb +7 -7
  179. data/lib/rigor/environment/required_features.rb +0 -3
  180. data/lib/rigor/environment/ruby_library_roots.rb +1 -1
  181. data/lib/rigor/environment/vendored_signature_plan.rb +153 -0
  182. data/lib/rigor/environment.rb +169 -42
  183. data/lib/rigor/flow_contribution/merger.rb +4 -4
  184. data/lib/rigor/inference/acceptance.rb +136 -40
  185. data/lib/rigor/inference/alias_mutation_floor/foreign_locals.rb +117 -0
  186. data/lib/rigor/inference/alias_mutation_floor/memos.rb +85 -0
  187. data/lib/rigor/inference/alias_mutation_floor/reach.rb +119 -0
  188. data/lib/rigor/inference/alias_mutation_floor/site_collector.rb +252 -0
  189. data/lib/rigor/inference/alias_mutation_floor.rb +339 -0
  190. data/lib/rigor/inference/anonymous_meta_class.rb +18 -0
  191. data/lib/rigor/inference/block_auto_splat.rb +1 -1
  192. data/lib/rigor/inference/block_parameter_binder.rb +2 -3
  193. data/lib/rigor/inference/block_run_count.rb +185 -0
  194. data/lib/rigor/inference/budget_trace.rb +8 -2
  195. data/lib/rigor/inference/builtins/array_catalog.rb +2 -2
  196. data/lib/rigor/inference/builtins/exception_catalog.rb +2 -2
  197. data/lib/rigor/inference/builtins/numeric_catalog.rb +2 -5
  198. data/lib/rigor/inference/builtins/random_catalog.rb +2 -2
  199. data/lib/rigor/inference/callee_mutation_chain.rb +88 -77
  200. data/lib/rigor/inference/callee_write_census.rb +815 -0
  201. data/lib/rigor/inference/captured_locals.rb +2 -0
  202. data/lib/rigor/inference/census_foreign_block.rb +354 -0
  203. data/lib/rigor/inference/clause_widening.rb +1 -1
  204. data/lib/rigor/inference/closure_escape_analyzer.rb +11 -4
  205. data/lib/rigor/inference/construction_escape.rb +258 -0
  206. data/lib/rigor/inference/declaration_walk/traversal.rb +1 -1
  207. data/lib/rigor/inference/declaration_walk.rb +4 -0
  208. data/lib/rigor/inference/deep_erasure.rb +39 -7
  209. data/lib/rigor/inference/def_handle.rb +1 -1
  210. data/lib/rigor/inference/def_node_resolver.rb +33 -15
  211. data/lib/rigor/inference/def_return_typer.rb +47 -5
  212. data/lib/rigor/inference/definer_resolution.rb +19 -9
  213. data/lib/rigor/inference/definers_index.rb +162 -0
  214. data/lib/rigor/inference/direct_ivar_writes.rb +300 -0
  215. data/lib/rigor/inference/element_read_widening.rb +49 -3
  216. data/lib/rigor/inference/error_info.rb +16 -0
  217. data/lib/rigor/inference/expression_typer.rb +1559 -320
  218. data/lib/rigor/inference/external_ancestor_resolution.rb +3 -4
  219. data/lib/rigor/inference/flow_tracer.rb +31 -14
  220. data/lib/rigor/inference/fork_map.rb +3 -0
  221. data/lib/rigor/inference/fresh_frame_blocks.rb +52 -1
  222. data/lib/rigor/inference/global_write_census.rb +81 -5
  223. data/lib/rigor/inference/global_writer_reach.rb +260 -0
  224. data/lib/rigor/inference/hkt_body.rb +1 -2
  225. data/lib/rigor/inference/hkt_body_parser.rb +3 -5
  226. data/lib/rigor/inference/hkt_reducer.rb +2 -2
  227. data/lib/rigor/inference/hkt_registry.rb +3 -5
  228. data/lib/rigor/inference/in_effect_refinements.rb +7 -0
  229. data/lib/rigor/inference/indexed_narrowing.rb +61 -15
  230. data/lib/rigor/inference/intervening_ivar_reset.rb +26 -4
  231. data/lib/rigor/inference/ivar_writer_census.rb +154 -8
  232. data/lib/rigor/inference/key_presence_guard.rb +51 -24
  233. data/lib/rigor/inference/last_line/self_evidence.rb +30 -0
  234. data/lib/rigor/inference/last_line.rb +5 -2
  235. data/lib/rigor/inference/loop_value_record.rb +66 -14
  236. data/lib/rigor/inference/match_rebinding/frame.rb +57 -0
  237. data/lib/rigor/inference/match_rebinding/yielding_match.rb +101 -0
  238. data/lib/rigor/inference/match_rebinding.rb +28 -5
  239. data/lib/rigor/inference/method_dispatcher/alias_strict_nominals.rb +1 -1
  240. data/lib/rigor/inference/method_dispatcher/argument_distribution.rb +40 -0
  241. data/lib/rigor/inference/method_dispatcher/block_folding.rb +44 -30
  242. data/lib/rigor/inference/method_dispatcher/class_keyed_block_params.rb +65 -0
  243. data/lib/rigor/inference/method_dispatcher/constant_folding.rb +3 -3
  244. data/lib/rigor/inference/method_dispatcher/contested_overloads.rb +89 -0
  245. data/lib/rigor/inference/method_dispatcher/core_enumerable_receiver.rb +44 -0
  246. data/lib/rigor/inference/method_dispatcher/declared_constructor.rb +151 -0
  247. data/lib/rigor/inference/method_dispatcher/facet_distribution.rb +1 -1
  248. data/lib/rigor/inference/method_dispatcher/imprecise_argument.rb +2 -2
  249. data/lib/rigor/inference/method_dispatcher/interface_shape.rb +181 -0
  250. data/lib/rigor/inference/method_dispatcher/iterator_dispatch.rb +1 -1
  251. data/lib/rigor/inference/method_dispatcher/kernel_dispatch.rb +1 -1
  252. data/lib/rigor/inference/method_dispatcher/no_match_fallback.rb +54 -0
  253. data/lib/rigor/inference/method_dispatcher/overload_selector.rb +51 -27
  254. data/lib/rigor/inference/method_dispatcher/random_folding.rb +1 -1
  255. data/lib/rigor/inference/method_dispatcher/rbs_dispatch.rb +606 -78
  256. data/lib/rigor/inference/method_dispatcher/reduce_folding.rb +1 -1
  257. data/lib/rigor/inference/method_dispatcher/regexp_folding.rb +2 -2
  258. data/lib/rigor/inference/method_dispatcher/self_substitute.rb +3 -3
  259. data/lib/rigor/inference/method_dispatcher/set_folding.rb +9 -7
  260. data/lib/rigor/inference/method_dispatcher/shape_dispatch.rb +467 -84
  261. data/lib/rigor/inference/method_dispatcher/singleton_folding.rb +0 -1
  262. data/lib/rigor/inference/method_dispatcher/singleton_mixin_dispatch.rb +50 -17
  263. data/lib/rigor/inference/method_dispatcher/singleton_type_params.rb +104 -0
  264. data/lib/rigor/inference/method_dispatcher/struct_folding.rb +5 -5
  265. data/lib/rigor/inference/method_dispatcher/struct_materialization.rb +4 -6
  266. data/lib/rigor/inference/method_dispatcher/uri_folding.rb +1 -1
  267. data/lib/rigor/inference/method_dispatcher.rb +123 -25
  268. data/lib/rigor/inference/method_parameter_binder.rb +136 -56
  269. data/lib/rigor/inference/module_function_state/spellings.rb +2 -2
  270. data/lib/rigor/inference/module_function_state.rb +24 -3
  271. data/lib/rigor/inference/multi_target_binder.rb +2 -2
  272. data/lib/rigor/inference/mutation_rejoin.rb +240 -4
  273. data/lib/rigor/inference/mutation_widening.rb +199 -29
  274. data/lib/rigor/inference/narrowing.rb +238 -50
  275. data/lib/rigor/inference/operand_effects.rb +43 -2
  276. data/lib/rigor/inference/optimistic_origin.rb +134 -9
  277. data/lib/rigor/inference/origin_lookup.rb +1 -1
  278. data/lib/rigor/inference/out_buffer_mutation.rb +72 -0
  279. data/lib/rigor/inference/parameter_inference_collector.rb +5 -6
  280. data/lib/rigor/inference/pre_eval_constants.rb +1 -1
  281. data/lib/rigor/inference/precision_scanner.rb +1 -1
  282. data/lib/rigor/inference/project_method_ownership.rb +30 -9
  283. data/lib/rigor/inference/project_patched_methods.rb +65 -13
  284. data/lib/rigor/inference/project_patched_scanner.rb +120 -25
  285. data/lib/rigor/inference/project_patched_visibility.rb +39 -3
  286. data/lib/rigor/inference/project_yielder.rb +11 -17
  287. data/lib/rigor/inference/rbs_type_translator.rb +68 -15
  288. data/lib/rigor/inference/receiver_alias.rb +20 -0
  289. data/lib/rigor/inference/receiver_writer_call.rb +67 -0
  290. data/lib/rigor/inference/recursion_rebinding.rb +157 -0
  291. data/lib/rigor/inference/refined_dispatch/constructed_class.rb +57 -17
  292. data/lib/rigor/inference/refined_dispatch/own_method.rb +51 -16
  293. data/lib/rigor/inference/refined_dispatch.rb +13 -0
  294. data/lib/rigor/inference/reflective_ivar_write.rb +19 -11
  295. data/lib/rigor/inference/remover_holder_scan.rb +287 -0
  296. data/lib/rigor/inference/remover_narrowing.rb +224 -0
  297. data/lib/rigor/inference/rewrite_mutation.rb +53 -2
  298. data/lib/rigor/inference/scope_indexer/certainty.rb +0 -1
  299. data/lib/rigor/inference/scope_indexer/class_cvars_collector.rb +13 -6
  300. data/lib/rigor/inference/scope_indexer/constant_ivar_value.rb +93 -0
  301. data/lib/rigor/inference/scope_indexer/declared_param_writes.rb +70 -26
  302. data/lib/rigor/inference/scope_indexer/derived_param_value.rb +112 -0
  303. data/lib/rigor/inference/scope_indexer/empty_seed.rb +99 -0
  304. data/lib/rigor/inference/scope_indexer/ivar_contributions.rb +295 -0
  305. data/lib/rigor/inference/scope_indexer/mixin_accumulator.rb +136 -9
  306. data/lib/rigor/inference/scope_indexer.rb +2213 -226
  307. data/lib/rigor/inference/self_call_ivar_writes.rb +103 -12
  308. data/lib/rigor/inference/self_preserving_block.rb +87 -0
  309. data/lib/rigor/inference/set_mutation.rb +189 -0
  310. data/lib/rigor/inference/singleton_hook_decline.rb +42 -8
  311. data/lib/rigor/inference/singleton_object_constant.rb +32 -2
  312. data/lib/rigor/inference/statement_evaluator.rb +3268 -358
  313. data/lib/rigor/inference/string_mutation.rb +14 -0
  314. data/lib/rigor/inference/struct_fold_safety.rb +4 -6
  315. data/lib/rigor/inference/synthetic_method_scanner.rb +40 -17
  316. data/lib/rigor/inference/toplevel_def_census.rb +67 -0
  317. data/lib/rigor/inference/unknown_store_widening.rb +103 -4
  318. data/lib/rigor/inference/unthreaded_rebinds.rb +2 -3
  319. data/lib/rigor/inference/untyped_rejoin.rb +1 -1
  320. data/lib/rigor/inference/void_tail_summary.rb +29 -9
  321. data/lib/rigor/language_server/buffer_table.rb +3 -3
  322. data/lib/rigor/language_server/completion_provider.rb +26 -20
  323. data/lib/rigor/language_server/config_warning_announcer.rb +28 -0
  324. data/lib/rigor/language_server/diagnostic_publisher.rb +8 -3
  325. data/lib/rigor/language_server/document_symbol_provider.rb +1 -1
  326. data/lib/rigor/language_server/folding_range_provider.rb +1 -2
  327. data/lib/rigor/language_server/hover_provider.rb +1 -7
  328. data/lib/rigor/language_server/hover_renderer.rb +6 -8
  329. data/lib/rigor/language_server/project_context.rb +39 -1
  330. data/lib/rigor/language_server/publish_batcher.rb +3 -4
  331. data/lib/rigor/language_server/selection_range_provider.rb +2 -4
  332. data/lib/rigor/language_server/server.rb +7 -3
  333. data/lib/rigor/language_server/signature_help_provider.rb +4 -4
  334. data/lib/rigor/lens/claims/comparison.rb +78 -0
  335. data/lib/rigor/lens/claims/request.rb +131 -0
  336. data/lib/rigor/lens/claims/type_reader.rb +216 -0
  337. data/lib/rigor/lens/claims.rb +208 -0
  338. data/lib/rigor/lens/contract_reader.rb +5 -0
  339. data/lib/rigor/lens/document.rb +1 -1
  340. data/lib/rigor/lens/file_reader.rb +21 -8
  341. data/lib/rigor/lens/plugins.rb +1 -1
  342. data/lib/rigor/lens/run.rb +8 -0
  343. data/lib/rigor/lens.rb +1 -1
  344. data/lib/rigor/library_probe.rb +68 -0
  345. data/lib/rigor/mcp/server.rb +73 -2
  346. data/lib/rigor/mcp.rb +1 -1
  347. data/lib/rigor/memo_audit.rb +282 -0
  348. data/lib/rigor/plugin/access_denied_error.rb +1 -2
  349. data/lib/rigor/plugin/base.rb +15 -2
  350. data/lib/rigor/plugin/bundled_catalog.rb +1 -12
  351. data/lib/rigor/plugin/io_boundary.rb +26 -4
  352. data/lib/rigor/plugin/loader.rb +25 -8
  353. data/lib/rigor/plugin/macro/heredoc_template.rb +7 -8
  354. data/lib/rigor/plugin/macro/trait_registry.rb +6 -9
  355. data/lib/rigor/plugin/manifest.rb +16 -16
  356. data/lib/rigor/plugin/registry.rb +16 -5
  357. data/lib/rigor/plugin/services.rb +2 -3
  358. data/lib/rigor/plugin/source_rbs_synthesis_reporter.rb +11 -0
  359. data/lib/rigor/plugin/trust_policy.rb +48 -2
  360. data/lib/rigor/plugin/type_node_resolver.rb +2 -4
  361. data/lib/rigor/plugin.rb +6 -8
  362. data/lib/rigor/plugin_gap_advisory.rb +5 -1
  363. data/lib/rigor/project_environment.rb +16 -2
  364. data/lib/rigor/protection/analysis_guard.rb +1 -1
  365. data/lib/rigor/protection/discovery_seed.rb +44 -5
  366. data/lib/rigor/protection/mutation_cache.rb +4 -1
  367. data/lib/rigor/protection/mutator.rb +1 -1
  368. data/lib/rigor/rbs_extended/envelope_scanner.rb +4 -6
  369. data/lib/rigor/rbs_extended/hkt_directives.rb +6 -10
  370. data/lib/rigor/rbs_extended/reporter.rb +27 -3
  371. data/lib/rigor/rbs_extended.rb +2 -3
  372. data/lib/rigor/reflection/constant_ancestors.rb +81 -22
  373. data/lib/rigor/reflection/declaration_collectors.rb +55 -0
  374. data/lib/rigor/reflection/declarations.rb +58 -0
  375. data/lib/rigor/reflection/project_method.rb +177 -0
  376. data/lib/rigor/reflection.rb +2 -0
  377. data/lib/rigor/rooted_glob.rb +32 -1
  378. data/lib/rigor/run_scoped_slots.rb +38 -0
  379. data/lib/rigor/runtime/jit.rb +1 -1
  380. data/lib/rigor/scope/discovery_index.rb +168 -8
  381. data/lib/rigor/scope/resolution_chain/relevance.rb +14 -1
  382. data/lib/rigor/scope/resolution_chain.rb +901 -54
  383. data/lib/rigor/scope.rb +644 -37
  384. data/lib/rigor/sig_gen/bot_proposal_guard.rb +306 -0
  385. data/lib/rigor/sig_gen/classification.rb +18 -1
  386. data/lib/rigor/sig_gen/effect_annotation.rb +3 -3
  387. data/lib/rigor/sig_gen/generator.rb +588 -66
  388. data/lib/rigor/sig_gen/generic_header_scan.rb +189 -0
  389. data/lib/rigor/sig_gen/inline_declarations.rb +2 -2
  390. data/lib/rigor/sig_gen/layout_index.rb +3 -3
  391. data/lib/rigor/sig_gen/method_candidate.rb +37 -4
  392. data/lib/rigor/sig_gen/observation_collector.rb +14 -7
  393. data/lib/rigor/sig_gen/observed_call.rb +1 -1
  394. data/lib/rigor/sig_gen/path_mapper.rb +6 -0
  395. data/lib/rigor/sig_gen/platform_guarded_files.rb +184 -0
  396. data/lib/rigor/sig_gen/renderer.rb +15 -9
  397. data/lib/rigor/sig_gen/skip_reason_catalog.rb +62 -2
  398. data/lib/rigor/sig_gen/type_name_qualification.rb +69 -0
  399. data/lib/rigor/sig_gen/write_reporter.rb +63 -14
  400. data/lib/rigor/sig_gen/write_result.rb +23 -12
  401. data/lib/rigor/sig_gen/writer.rb +238 -37
  402. data/lib/rigor/sig_gen.rb +1 -0
  403. data/lib/rigor/source/forwardable_delegators.rb +122 -0
  404. data/lib/rigor/source/node_children.rb +13 -0
  405. data/lib/rigor/type/acceptance_router.rb +2 -2
  406. data/lib/rigor/type/accepts_result.rb +1 -1
  407. data/lib/rigor/type/app.rb +4 -6
  408. data/lib/rigor/type/combinator.rb +173 -21
  409. data/lib/rigor/type/constant.rb +4 -5
  410. data/lib/rigor/type/hash_shape.rb +21 -40
  411. data/lib/rigor/type/interface.rb +60 -0
  412. data/lib/rigor/type/union.rb +1 -1
  413. data/lib/rigor/type.rb +1 -0
  414. data/lib/rigor/type_node/identifier.rb +1 -2
  415. data/lib/rigor/type_node/resolver_chain.rb +1 -1
  416. data/lib/rigor/type_node/string_literal.rb +1 -2
  417. data/lib/rigor/type_node/symbol_literal.rb +1 -2
  418. data/lib/rigor/type_node/union.rb +3 -6
  419. data/lib/rigor/type_of_memo/serving.rb +99 -0
  420. data/lib/rigor/type_of_memo/stats.rb +43 -0
  421. data/lib/rigor/type_of_memo.rb +337 -0
  422. data/lib/rigor/typing_state.rb +1094 -0
  423. data/lib/rigor/value_semantics.rb +1 -10
  424. data/lib/rigor/version.rb +1 -1
  425. data/plugins/rigor-actioncable/lib/rigor/plugin/actioncable/channel_discoverer.rb +2 -2
  426. data/plugins/rigor-actioncable/lib/rigor/plugin/actioncable/channel_index.rb +2 -3
  427. data/plugins/rigor-actioncable/lib/rigor/plugin/actioncable.rb +1 -1
  428. data/plugins/rigor-actionmailer/lib/rigor/plugin/actionmailer/mailer_discoverer.rb +38 -36
  429. data/plugins/rigor-actionmailer/lib/rigor/plugin/actionmailer/mailer_index.rb +4 -2
  430. data/plugins/rigor-actionmailer/lib/rigor/plugin/actionmailer.rb +6 -4
  431. data/plugins/rigor-actionpack/lib/rigor/plugin/actionpack/analyzer.rb +1 -1
  432. data/plugins/rigor-actionpack/lib/rigor/plugin/actionpack/controller_index.rb +1 -5
  433. data/plugins/rigor-actionpack/lib/rigor/plugin/actionpack/controller_scan.rb +0 -1
  434. data/plugins/rigor-actionpack/lib/rigor/plugin/actionpack/erb_compiler.rb +2 -1
  435. data/plugins/rigor-actionpack/lib/rigor/plugin/actionpack/view_assigns.rb +3 -3
  436. data/plugins/rigor-actionpack/lib/rigor/plugin/actionpack.rb +115 -36
  437. data/plugins/rigor-active-model-serializers/lib/rigor/plugin/active_model_serializers.rb +3 -6
  438. data/plugins/rigor-activejob/lib/rigor/plugin/activejob/analyzer.rb +25 -1
  439. data/plugins/rigor-activejob/lib/rigor/plugin/activejob/effects.rb +1 -1
  440. data/plugins/rigor-activejob/lib/rigor/plugin/activejob/job_discoverer.rb +16 -6
  441. data/plugins/rigor-activejob/lib/rigor/plugin/activejob/job_index.rb +4 -2
  442. data/plugins/rigor-activejob/lib/rigor/plugin/activejob.rb +4 -3
  443. data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord/analyzer.rb +2 -1
  444. data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord/association_roots.rb +3 -2
  445. data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord/effects.rb +2 -2
  446. data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord/model_discoverer.rb +21 -31
  447. data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord/model_index.rb +10 -16
  448. data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord/schema_parser.rb +132 -33
  449. data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord/schema_table.rb +20 -6
  450. data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord/structure_sql_parser.rb +7 -4
  451. data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord.rb +54 -17
  452. data/plugins/rigor-activerecord/sig/active_record/relation.rbs +19 -5
  453. data/plugins/rigor-activestorage/lib/rigor/plugin/activestorage/attachment_discoverer.rb +3 -4
  454. data/plugins/rigor-activestorage/lib/rigor/plugin/activestorage/attachment_index.rb +1 -1
  455. data/plugins/rigor-activestorage/lib/rigor/plugin/activestorage.rb +11 -14
  456. data/plugins/rigor-activestorage/lib/rigor-activestorage.rb +1 -1
  457. data/plugins/rigor-activesupport-core-ext/lib/rigor/plugin/activesupport_core_ext/effects.rb +1 -1
  458. data/plugins/rigor-activesupport-core-ext/lib/rigor/plugin/activesupport_core_ext/symbolize_keys.rb +102 -0
  459. data/plugins/rigor-activesupport-core-ext/lib/rigor/plugin/activesupport_core_ext.rb +30 -9
  460. data/plugins/rigor-activesupport-core-ext/sig/active_support/core_ext.rbs +55 -13
  461. data/plugins/rigor-alba/lib/rigor/plugin/alba/resource_collector.rb +15 -21
  462. data/plugins/rigor-alba/lib/rigor/plugin/alba/resource_discoverer.rb +1 -6
  463. data/plugins/rigor-alba/lib/rigor/plugin/alba/resource_index.rb +3 -3
  464. data/plugins/rigor-dry-schema/lib/rigor/plugin/dry_schema/schema_scanner.rb +1 -3
  465. data/plugins/rigor-dry-struct/lib/rigor/plugin/dry_struct.rb +6 -6
  466. data/plugins/rigor-dry-types/lib/rigor/plugin/dry_types/alias_scanner.rb +5 -6
  467. data/plugins/rigor-dry-types/lib/rigor/plugin/dry_types.rb +1 -1
  468. data/plugins/rigor-dry-validation/lib/rigor/plugin/dry_validation/params_shape.rb +2 -3
  469. data/plugins/rigor-dry-validation/lib/rigor/plugin/dry_validation.rb +5 -8
  470. data/plugins/rigor-ethon/lib/rigor/plugin/ethon.rb +0 -1
  471. data/plugins/rigor-factorybot/lib/rigor/plugin/factorybot/factory_discoverer.rb +1 -1
  472. data/plugins/rigor-factorybot/lib/rigor/plugin/factorybot.rb +10 -18
  473. data/plugins/rigor-ffi/lib/rigor/plugin/ffi/analyzer.rb +0 -1
  474. data/plugins/rigor-ffi/lib/rigor/plugin/ffi/discoverer.rb +0 -2
  475. data/plugins/rigor-ffi/lib/rigor/plugin/ffi.rb +8 -7
  476. data/plugins/rigor-grape/lib/rigor/plugin/grape.rb +0 -2
  477. data/plugins/rigor-graphql/lib/rigor/plugin/graphql/type_scanner.rb +6 -7
  478. data/plugins/rigor-graphql/lib/rigor/plugin/graphql.rb +12 -2
  479. data/plugins/rigor-hanami/lib/rigor/plugin/hanami/action_checker.rb +2 -4
  480. data/plugins/rigor-hanami/lib/rigor/plugin/hanami.rb +2 -5
  481. data/plugins/rigor-mangrove/lib/rigor/plugin/mangrove.rb +8 -2
  482. data/plugins/rigor-minitest/lib/rigor/plugin/minitest/assertion_analyzer.rb +6 -6
  483. data/plugins/rigor-pundit/lib/rigor/plugin/pundit/policy_discoverer.rb +2 -1
  484. data/plugins/rigor-rails-i18n/lib/rigor/plugin/rails_i18n/analyzer.rb +3 -8
  485. data/plugins/rigor-rails-i18n/lib/rigor/plugin/rails_i18n/locale_index.rb +2 -3
  486. data/plugins/rigor-rails-i18n/lib/rigor/plugin/rails_i18n/locale_loader.rb +5 -8
  487. data/plugins/rigor-rails-routes/lib/rigor/plugin/rails_routes/analyzer.rb +200 -4
  488. data/plugins/rigor-rails-routes/lib/rigor/plugin/rails_routes/devise_routes.rb +2 -3
  489. data/plugins/rigor-rails-routes/lib/rigor/plugin/rails_routes/helper_discoverer.rb +1 -1
  490. data/plugins/rigor-rails-routes/lib/rigor/plugin/rails_routes/routes_parser.rb +4 -11
  491. data/plugins/rigor-rails-routes/lib/rigor/plugin/rails_routes.rb +4 -3
  492. data/plugins/rigor-rbs-inline/lib/rigor/plugin/rbs_inline.rb +2 -1
  493. data/plugins/rigor-rspec/lib/rigor/plugin/rspec/matcher_analyzer.rb +5 -5
  494. data/plugins/rigor-rspec/lib/rigor/plugin/rspec.rb +2 -2
  495. data/plugins/rigor-rspec-rails/lib/rigor/plugin/rspec_rails.rb +3 -4
  496. data/plugins/rigor-shoulda-matchers/lib/rigor/plugin/shoulda_matchers/analyzer.rb +6 -8
  497. data/plugins/rigor-sidekiq/lib/rigor/plugin/sidekiq.rb +3 -2
  498. data/plugins/rigor-sorbet/lib/rigor/plugin/sorbet/absurd_recognizer.rb +4 -5
  499. data/plugins/rigor-sorbet/lib/rigor/plugin/sorbet/assertion_recognizer.rb +6 -7
  500. data/plugins/rigor-sorbet/lib/rigor/plugin/sorbet/catalog.rb +210 -3
  501. data/plugins/rigor-sorbet/lib/rigor/plugin/sorbet/catalog_walker.rb +146 -7
  502. data/plugins/rigor-sorbet/lib/rigor/plugin/sorbet/sig_parser.rb +2 -2
  503. data/plugins/rigor-sorbet/lib/rigor/plugin/sorbet/type_translator.rb +52 -34
  504. data/plugins/rigor-sorbet/lib/rigor/plugin/sorbet.rb +97 -24
  505. data/plugins/rigor-statesman/lib/rigor/plugin/statesman.rb +2 -3
  506. data/plugins/rigor-typelizer/lib/rigor/plugin/typelizer/serializer_collector.rb +16 -16
  507. data/plugins/rigor-typelizer/lib/rigor/plugin/typelizer/serializer_discoverer.rb +4 -8
  508. data/plugins/rigor-typelizer/lib/rigor/plugin/typelizer/serializer_index.rb +4 -4
  509. data/sig/rigor/analysis/baseline.rbs +0 -3
  510. data/sig/rigor/analysis/check_rules/rule_walk.rbs +6 -0
  511. data/sig/rigor/analysis/fact_store.rbs +2 -2
  512. data/sig/rigor/analysis/reachability/scan_cache.rbs +1 -1
  513. data/sig/rigor/cache/descriptor.rbs +14 -0
  514. data/sig/rigor/cli/diff_command.rbs +1 -1
  515. data/sig/rigor/cli/explain_command.rbs +1 -1
  516. data/sig/rigor/effects/registry.rbs +2 -2
  517. data/sig/rigor/environment.rbs +4 -1
  518. data/sig/rigor/inference.rbs +4 -1
  519. data/sig/rigor/plugin/additional_initializer.rbs +0 -5
  520. data/sig/rigor/plugin/base.rbs +53 -18
  521. data/sig/rigor/plugin/effect_attribution.rbs +3 -3
  522. data/sig/rigor/plugin/effect_edge.rbs +1 -1
  523. data/sig/rigor/plugin/effect_entry_points.rbs +3 -3
  524. data/sig/rigor/plugin/interfaces.rbs +51 -0
  525. data/sig/rigor/plugin/io_boundary.rbs +3 -3
  526. data/sig/rigor/plugin/manifest.rbs +9 -9
  527. data/sig/rigor/plugin/node_context.rbs +21 -0
  528. data/sig/rigor/plugin/protocol_contract.rbs +3 -7
  529. data/sig/rigor/reflection.rbs +37 -0
  530. data/sig/rigor/scope.rbs +43 -4
  531. data/sig/rigor/trinary.rbs +1 -0
  532. data/sig/rigor/type.rbs +9 -3
  533. data/sig/rigor.rbs +3 -2
  534. data/skills/rigor-ask/SKILL.md +1 -0
  535. data/skills/rigor-plugin-author/SKILL.md +4 -4
  536. data/skills/rigor-plugin-author/references/02-walker-and-types.md +5 -5
  537. data/skills/rigor-plugin-review/SKILL.md +3 -3
  538. data/skills/rigor-project-init/references/01-detect.md +1 -0
  539. data/skills/rigor-project-init/references/03-baseline-and-bugs.md +17 -2
  540. data/skills/rigor-project-init/references/06-agent-contract.md +4 -4
  541. data/skills/rigor-rbs-contract/SKILL.md +147 -0
  542. data/skills/rigor-rbs-contract/references/01-finding-candidates.md +212 -0
  543. data/skills/rigor-rbs-contract/references/02-contract-rules.md +157 -0
  544. data/skills/rigor-rbs-contract/references/03-target-own-code.md +90 -0
  545. data/skills/rigor-rbs-contract/references/04-target-gem-rbs-collection.md +210 -0
  546. data/skills/rigor-rbs-contract/references/05-declaration-mutants.md +182 -0
  547. data/skills/rigor-rbs-setup/SKILL.md +3 -1
  548. data/skills/rigor-type-oracle/SKILL.md +26 -1
  549. data/skills/rigor-type-oracle/references/02-agents-md-fragment.md +4 -3
  550. metadata +82 -3
@@ -5,6 +5,7 @@ require "prism"
5
5
  require_relative "../scope"
6
6
  require_relative "../type"
7
7
  require_relative "../source/alias_names"
8
+ require_relative "../source/forwardable_delegators"
8
9
  require_relative "../source/literals"
9
10
  require_relative "../source/constant_path"
10
11
  require_relative "object_mixins"
@@ -12,9 +13,13 @@ require_relative "../source/node_children"
12
13
  require_relative "../source/node_walker"
13
14
  require_relative "../source/parameter_envelope"
14
15
  require_relative "../cache/file_digest"
16
+ require_relative "../typing_state"
17
+ require_relative "../memo_audit"
15
18
  require_relative "../analysis/check_rules/published_constant_guard"
16
19
  require_relative "anonymous_meta_class"
20
+ require_relative "census_foreign_block"
17
21
  require_relative "def_handle"
22
+ require_relative "definers_index"
18
23
  require_relative "fresh_frame_blocks"
19
24
  require_relative "global_write_census"
20
25
  require_relative "guard_rebinding"
@@ -25,6 +30,7 @@ require_relative "hash_lookup_mutation"
25
30
  require_relative "in_effect_refinements"
26
31
  require_relative "refine_census"
27
32
  require_relative "refine_self"
33
+ require_relative "receiver_writer_call"
28
34
  require_relative "reflective_ivar_write"
29
35
  require_relative "index_write_widening"
30
36
  require_relative "module_function_state"
@@ -45,6 +51,9 @@ require_relative "scope_indexer/certainty"
45
51
  require_relative "scope_indexer/member_layouts_collector"
46
52
  require_relative "scope_indexer/declared_ivar_types"
47
53
  require_relative "scope_indexer/declared_param_writes"
54
+ require_relative "scope_indexer/constant_ivar_value"
55
+ require_relative "scope_indexer/ivar_contributions"
56
+ require_relative "scope_indexer/empty_seed"
48
57
 
49
58
  module Rigor
50
59
  module Inference
@@ -67,8 +76,8 @@ module Rigor
67
76
  # ```
68
77
  #
69
78
  # Nodes that are not part of the program subtree (e.g. synthesised virtual nodes that the caller looks up after the
70
- # fact) yield the `default_scope`. The returned Hash is mutable in principle but callers MUST treat it as read-only;
71
- # the indexer itself never exposes a way to update it past construction.
79
+ # fact) yield the seeded `default_scope`. The returned Hash is mutable in principle but callers MUST treat it as
80
+ # read-only; the indexer itself never exposes a way to update it past construction.
72
81
  # rubocop:disable-next Metrics/ModuleLength
73
82
  module ScopeIndexer
74
83
  # Issue #644 — the census descriptor for a constant write that publishes no value: an unfoldable or
@@ -109,13 +118,15 @@ module Rigor
109
118
  # `Dynamic[top]`. Off for every other reader, so parameter inference
110
119
  # and sig-gen observe the clause's plain entry.
111
120
  # @return identity-comparing
112
- # table whose default value is `default_scope`.
121
+ # table whose default value is the seeded `default_scope`.
113
122
  # The no-op re-anchoring table (#722 residue 2): shared so the overwhelmingly common
114
123
  # nothing-to-re-anchor walk allocates none.
115
124
  EMPTY_RENAMES = {}.freeze
116
125
 
117
126
  # No path whose contributions are all `possible` (issue #2021), the cross-file walks' default.
118
127
  EMPTY_PATH_SET = Set.new.freeze
128
+ # The {IvarWriterCensus} entry kinds the project pre-pass tags with the file that records them.
129
+ PATH_TAGGED_KINDS = %i[own_write own_write_type].to_set.freeze
119
130
 
120
131
  # `declared_ivar_types:` — whether an RBS-declared ivar type replaces the census's join of the writes (#1406).
121
132
  # `rigor sig-gen` passes false: it proposes what the code says, and reading a declaration back would make a row
@@ -222,20 +233,21 @@ module Rigor
222
233
  # nodes in `excluded`: the file's own pass, and each construct {#evaluate_unentered_flow} evaluates in the walk
223
234
  # after it.
224
235
  def recording_evaluator(table, converged_loop_recording, widen_clauses)
225
- lambda do |node, scope, excluded = nil|
236
+ lambda do |node, scope, excluded = nil, block_only: false|
226
237
  on_enter =
227
238
  if excluded.nil? || excluded.empty? then ->(visited, at) { table[visited] = at }
228
239
  else ->(visited, at) { table[visited] = at unless excluded.include?(visited) }
229
240
  end
230
- StatementEvaluator.new(scope: scope, on_enter: on_enter, converged_loop_recording: converged_loop_recording,
231
- widen_clauses: widen_clauses).evaluate(node)
241
+ evaluator = StatementEvaluator.new(scope: scope, on_enter: on_enter, widen_clauses: widen_clauses,
242
+ converged_loop_recording: converged_loop_recording)
243
+ block_only ? evaluator.evaluate_block_of(node) : evaluator.evaluate(node)
232
244
  end
233
245
  end
234
246
 
235
247
  # Runs the combined methods/def-nodes descent (one walk of the file), seeds the discovered-methods existence table
236
- # onto `seeded_scope` (merged UNDER the cross-file pre-pass seed `default_scope` carries), and returns `[scope,
237
- # file_def_nodes, file_envelopes]` so the caller can thread the def-node and issue #992 envelope tables into
238
- # {#merge_project_method_indexes} without walking the file a second time.
248
+ # onto `seeded_scope` (merged UNDER the cross-file pre-pass seed `default_scope` carries), and returns
249
+ # `[scope, [file_def_nodes, file_envelopes, file_siblings]]` so the caller can thread the def-node and issue #992
250
+ # envelope tables into {#merge_project_method_indexes} without walking the file a second time.
239
251
  #
240
252
  # ADR-119 WD3 — the file's `possible_discovered_methods` folds over the seed's by {#fold_possible_methods}, so a
241
253
  # name the file defines certainly is no longer possible and a name only the file's possible contribution
@@ -306,8 +318,8 @@ module Rigor
306
318
  siblings = per_file_siblings(seeded_scope.discovery.sibling_tables, file_tables[0], file_tables[2],
307
319
  file_singleton, file_visibilities)
308
320
  extends, methods_table, unpositioned =
309
- merge_and_fold_extends(default_scope, root, def_nodes, singleton_def_nodes, seeded_scope, siblings,
310
- mixin_unpositioned)
321
+ merge_and_fold_extends(default_scope, root, [def_nodes, file_tables[0]],
322
+ [singleton_def_nodes, file_singleton[0]], seeded_scope, siblings, mixin_unpositioned)
311
323
 
312
324
  seeded_scope.with_discovery(
313
325
  seeded_scope.discovery.with(
@@ -387,8 +399,9 @@ module Rigor
387
399
  default_scope.discovered_includes.merge(file[:includes]) do |_class, cross_file, per_file|
388
400
  (per_file + cross_file).uniq
389
401
  end,
402
+ # ADR-129 G4 — each merged list is frozen as the per-file walk's lists are ({#freeze_mixin_lists}).
390
403
  default_scope.discovered_prepends.merge(file[:prepends]) do |_class, cross_file, per_file|
391
- (per_file + cross_file).uniq
404
+ (per_file + cross_file).uniq.freeze
392
405
  end,
393
406
  file[:unpositioned]
394
407
  ]
@@ -421,13 +434,23 @@ module Rigor
421
434
  # ADR-119 WD1 — the unpositioned union (the seed's, the mixin walks' and this file's extend side) is settled
422
435
  # first, because the fold reads it to tell a certain extend edge from a listed one. `siblings` is written by
423
436
  # the fold.
424
- def merge_and_fold_extends(default_scope, root, def_nodes, singleton_def_nodes, seeded_scope, siblings,
437
+ # `def_pair` and `singleton_pair` are `[merged table, this file's own]`; the merged tables are frozen once the
438
+ # fold, their last in-place writer, is done ({#freeze_def_tables}).
439
+ def merge_and_fold_extends(default_scope, root, def_pair, singleton_pair, seeded_scope, siblings,
425
440
  mixin_unpositioned)
441
+ def_nodes = def_pair.first
442
+ singleton_def_nodes = singleton_pair.first
426
443
  file_extends, extends, extend_unpositioned = merge_extend_tables(default_scope, root)
427
444
  unpositioned = union_unpositioned(default_scope.discovery.unpositioned_mixins, *mixin_unpositioned,
428
445
  extend_unpositioned)
429
446
  methods_table = fold_per_file_extends(file_extends, def_nodes, singleton_def_nodes, seeded_scope, siblings,
430
447
  unpositioned)
448
+ # The fold also writes the singleton hash of each class this file `extend`s, declared there or not.
449
+ file_extends.each_key { |owner| singleton_def_nodes[owner]&.freeze }
450
+ freeze_def_tables(def_pair, singleton_pair)
451
+ # #2300 — the override gate's definers index then inverts the seed once per run, not once per file.
452
+ DefinersIndex.record_layers(def_nodes, default_scope.discovered_def_nodes)
453
+ DefinersIndex.record_layers(singleton_def_nodes, default_scope.discovered_singleton_def_nodes)
431
454
  [extends, methods_table, unpositioned]
432
455
  end
433
456
 
@@ -458,6 +481,20 @@ module Rigor
458
481
  walked.fetch(:header_nestings)).freeze]
459
482
  end
460
483
 
484
+ # Issue #2330 — the per-file def table is read by identity: the callee-chain census memo is keyed by the
485
+ # {Scope::DiscoveryIndex} that carries it, which is only sound while the table is never written in place.
486
+ # Frozen after the extends fold, the last in-place writer; the seed's per-class hashes are frozen already.
487
+ #
488
+ # Only the per-class hashes of the classes this file declares are frozen here (the merge made or took
489
+ # them); a seed-only class's hash was frozen by `finalize_def_index`, so walking the whole table per file is
490
+ # needless.
491
+ def freeze_def_tables(*pairs)
492
+ pairs.each do |table, file_table|
493
+ file_table.each_key { |owner| table[owner].freeze }
494
+ table.freeze
495
+ end
496
+ end
497
+
461
498
  # The instance-side def-node table and its issue #681 nesting twin, each merged over the cross-file seed.
462
499
  # Returned as a pair so the two stay written together: a node the merge keeps must be the same object the
463
500
  # nesting table keys, and they are only that if both halves take the same file's walk.
@@ -529,25 +566,45 @@ module Rigor
529
566
  mutated_ivars = {}
530
567
  read_before_write = {}
531
568
  init_writes = {}
532
- # WD3 — per-class summary of `{class_name => {method_name => Set<ivar names definitely assigned non-nil on every
533
- # completing path>}}`, consulted by `dead_transient_nil_writes` so a ctor that reassigns `@x` indirectly through
534
- # an unconditional same-class method call (`mask!`) credits the overwrite. Built once per program here, memoised
535
- # by class.
569
+ # WD3 — per class, then per method, the ivars the method definitely assigns non-nil on every completing path,
570
+ # consulted by `dead_transient_nil_writes` so a ctor that reassigns `@x` indirectly through an unconditional
571
+ # same-class method call (`mask!`) credits the overwrite. Built once per program here, memoised by class.
536
572
  method_assign_effects = build_method_assign_effects(root)
573
+ # #2291 — an `@x = LIT` write reads the file's constant table, built on the first such write.
574
+ accumulator[CONSTANT_IVAR_VALUES] = ConstantIvarValue.new(root, default_scope)
537
575
  walk_class_ivars(root, [], default_scope, accumulator, mutated_ivars,
538
576
  read_before_write, init_writes, method_assign_effects)
577
+ accumulator.delete(CONSTANT_IVAR_VALUES)
539
578
  reflective_opaque = accumulator.delete(REFLECTIVE_OPAQUE_CLASSES)
579
+ singleton_writes = accumulator.delete(SINGLETON_IVAR_WRITES)
540
580
  exempt_class_body_writes!(init_writes, accumulator.delete(DEFINE_METHOD_WRITES) || {})
541
581
  merge_held_ivar_writes!(accumulator)
542
582
  record_aliased_ivar_mutations!(root, mutated_ivars)
543
583
  widen_mutated_ivar_entries!(accumulator, mutated_ivars)
584
+ # #2170 — an ancestor's writes as this file's census types them, before its read-before-write `nil`.
585
+ writes = instance_side_writes(accumulator, singleton_writes)
544
586
  contribute_read_before_write_nil!(accumulator, read_before_write, init_writes)
545
587
  make_reflective_classes_gradual!(accumulator, reflective_opaque)
588
+ # #2079 / #2170 — another file's writes, and the chain's modules' and superclasses', join existing entries.
589
+ IvarContributions.join!(accumulator, writes, default_scope)
590
+ # An in-place mutation the file observes widens a joined value too (a same-file ancestor's String literal).
591
+ widen_mutated_ivar_entries!(accumulator, mutated_ivars)
546
592
  # #1406 — an RBS `@x: T` or `attr_* x: T` declaration replaces the census's join of the writes.
547
593
  DeclaredIvarTypes.apply!(accumulator, default_scope) if declared_ivar_types
548
594
  accumulator.transform_values(&:freeze).freeze
549
595
  end
550
596
 
597
+ # #2170 — a copy of the census entries for {IvarContributions} to join as an ancestor's own. #2931 — an ivar a
598
+ # singleton-side `def` of the class writes is left out: its entry joins that write, which runs on the module or
599
+ # class object and never on an includer's instance, so the join reads the file's instance-side census entries
600
+ # for it, as another file's reader does.
601
+ def instance_side_writes(accumulator, singleton_writes)
602
+ accumulator.to_h do |class_name, entries|
603
+ skipped = singleton_writes&.[](class_name)
604
+ [class_name, skipped ? entries.except(*skipped) : entries.dup]
605
+ end
606
+ end
607
+
551
608
  # Folds the class-body writes {#collect_class_body_ivar_writes} held under {CLASS_BODY_WRITES} into the
552
609
  # `init_writes` exemption, except for an ivar a `define_method` block, or a `block_as_methods:` block bound to the
553
610
  # receiver's instance ({#method_body_block_call?}), of the class writes (#1768). Such an ivar had no census entry
@@ -628,8 +685,8 @@ module Rigor
628
685
  end
629
686
  end
630
687
 
631
- # `{class_name => {method_name => Set<ivar name>}}` for methods that hand an ivar back, dropping the empty sets
632
- # so the caller's `next if per_class.nil?` skips a whole class in one lookup.
688
+ # Per class, then per method, the ivars each method hands back, dropping the empty sets so the caller's
689
+ # `next if per_class.nil?` skips a whole class in one lookup.
633
690
  def returned_ivars_by_method(defs)
634
691
  defs.each_with_object({}) do |(class_name, methods), acc|
635
692
  methods.each do |method_name, def_node|
@@ -718,9 +775,8 @@ module Rigor
718
775
  # The widening uses `MutationWidening.widen_for_mutator` — the same primitive
719
776
  # `Inference::StatementEvaluator#eval_call` applies for per-method-body widening on a local / ivar receiver. The
720
777
  # class-level pass extends that primitive's reach so a `Tuple`-seeded ivar in `initialize` is observed as
721
- # `Nominal[Array]` at the entry of every OTHER method body in the class — closing the cross-method gap noted in
722
- # ROADMAP § Future cycles / Type-language / engine ("Tuple / HashShape widening for ivar-seeded literals after
723
- # mutation"; Redmine 6.1.2 `Redmine::Views::Builders::Structure` is the canonical worked site).
778
+ # `Nominal[Array]` at the entry of every OTHER method body in the class — closing the cross-method gap
779
+ # (Redmine 6.1.2 `Redmine::Views::Builders::Structure` is the canonical worked site).
724
780
  #
725
781
  # Always-safe: the widening can only LOSE precision; the underlying nominal (`Array` / `Hash`) and the element
726
782
  # union are preserved.
@@ -765,18 +821,27 @@ module Rigor
765
821
  end
766
822
 
767
823
  widen_member_for_lookup_mutators(member, observed_methods)
768
- when Type::Constant
769
- # `@s = +"ab"` in one method and `@s << "c"` in another: without this arm the seed stayed pinned at every
770
- # other method's entry, and `@s == "ab"` folded always-truthy on a receiver that holds `"abc"`.
771
- return member unless StringMutation.constant?(member) &&
772
- observed_methods.any? { |m| StringMutation::MUTATORS.include?(m) }
773
-
774
- Type::Combinator.nominal_of("String")
824
+ when Type::Constant then widen_constant_for_observed_mutators(member, observed_methods)
775
825
  else
776
826
  member
777
827
  end
778
828
  end
779
829
 
830
+ # `@s = +"ab"` in one method and `@s << "c"` in another: without this arm the seed stayed pinned at every other
831
+ # method's entry, and `@s == "ab"` folded always-truthy on a receiver that holds `"abc"`. A constant-folded Set
832
+ # (`@s = Set["a"]`, then `@s.clear; @s << 1` elsewhere — issue #2294) takes the untyped floor the Tuple arm takes.
833
+ def widen_constant_for_observed_mutators(member, observed_methods)
834
+ if SetMutation.constant?(member)
835
+ return member unless observed_methods.any? { |m| SetMutation::MUTATORS.include?(m) }
836
+
837
+ return Type::Combinator.nominal_of("Set", type_args: [Type::Combinator.untyped])
838
+ end
839
+ return member unless StringMutation.constant?(member) &&
840
+ observed_methods.any? { |m| StringMutation::MUTATORS.include?(m) }
841
+
842
+ Type::Combinator.nominal_of("String")
843
+ end
844
+
780
845
  # A `HashShape` ivar seed some method gave a default, a default proc or identity keys, and no method stored
781
846
  # into, removed from or rewrote: its pairs are the seed's own, so it takes the per-method {HashLookupMutation}
782
847
  # widening rather than the untyped floor above — a present key keeps its value across methods, a missing one
@@ -903,6 +968,7 @@ module Rigor
903
968
  collect_initializer_block_ivars(node, owner, default_scope, accumulator, mutated_ivars, init_writes)
904
969
  record_attr_writer_ivars(node, owner, accumulator) if ATTR_WRITER_MACROS.include?(attr_macro(node))
905
970
  collect_define_method_block_ivars(node, owner, default_scope, accumulator, mutated_ivars)
971
+ collect_receiver_writer_ivars(node, owner, qualified_prefix, default_scope, accumulator, mutated_ivars)
906
972
  false
907
973
  else
908
974
  walk_ivars_meta_new?(node, qualified_prefix, default_scope, accumulator,
@@ -1031,6 +1097,8 @@ module Rigor
1031
1097
  read_before_write = nil, init_writes = nil, method_assign_effects = nil,
1032
1098
  singleton_self: false)
1033
1099
  return if def_node.body.nil? || qualified_prefix.empty?
1100
+ # Issue #3155 — `def String.q` writes the ivars of another object.
1101
+ return if foreign_def?(def_node, qualified_prefix, false)
1034
1102
 
1035
1103
  class_name = qualified_prefix.join("::")
1036
1104
  singleton = singleton_self || def_node.receiver.is_a?(Prism::SelfNode) ||
@@ -1057,6 +1125,10 @@ module Rigor
1057
1125
  declared = DeclaredParamWrites.collect(def_node, class_name, body_scope) unless singleton
1058
1126
  gather_ivar_writes(def_node.body, body_scope, class_name, accumulator,
1059
1127
  EMPTY_GUARDED_IVARS, mutated_ivars, dead_writes, declared)
1128
+ if singleton
1129
+ singleton_written = (accumulator[SINGLETON_IVAR_WRITES] ||= {})[class_name] ||= Set.new
1130
+ define_method_written_ivars(def_node.body, singleton_written)
1131
+ end
1060
1132
 
1061
1133
  # B2.3 — collect per-method evidence for the read-before- write nil contribution. The accumulator-level decision
1062
1134
  # ("is this ivar truly read-before-write across the class lifetime?") is finalised at
@@ -1083,7 +1155,7 @@ module Rigor
1083
1155
 
1084
1156
  seen_writes = Set.new
1085
1157
  read_first = Set.new
1086
- detect_read_before_write(block_node.body, seen_writes, read_first)
1158
+ detect_read_before_write(block_node.body, seen_writes, read_first, class_name)
1087
1159
  init_set = (init_writes[class_name] ||= Set.new)
1088
1160
  seen_writes.each { |name| init_set << name }
1089
1161
  end
@@ -1146,6 +1218,29 @@ module Rigor
1146
1218
  define_method_written_ivars(block.body, written)
1147
1219
  end
1148
1220
 
1221
+ # Issue #2488 — the explicit-receiver arm of {#walk_class_ivars}: `E.attr_writer :w` (or through `send`) records
1222
+ # `@w` as `Dynamic[top]` on `E`, as an `attr_writer` in `E`'s body does ({#record_attr_writer_ivars}), and an
1223
+ # `E.define_method(:m) { … }` block is walked as an instance body of `E`, as one in `E`'s body is
1224
+ # ({#collect_define_method_block_ivars}). `E` resolves as a `class_eval` receiver does; an unnameable one
1225
+ # contributes nothing. A call inside a `def` is that body's to walk.
1226
+ def collect_receiver_writer_ivars(node, owner, qualified_prefix, default_scope, accumulator, mutated_ivars)
1227
+ return if ReceiverWriterCall.receiver(node).nil?
1228
+
1229
+ target = eval_receiver_prefix(node, owner, qualified_prefix, unnameable_self: owner.empty?)
1230
+ return if target.nil? || target.empty?
1231
+
1232
+ class_name = target.join("::")
1233
+ ReceiverWriterCall.writer_ivars(node).each do |ivar|
1234
+ accumulate_ivar_type(accumulator, class_name, ivar, Type::Combinator.untyped)
1235
+ end
1236
+ body = ReceiverWriterCall.define_method_block(node)&.body
1237
+ return if body.nil?
1238
+
1239
+ body_scope = census_body_scope(default_scope, target, Type::Combinator.nominal_of(class_name))
1240
+ gather_ivar_writes(body, body_scope, class_name, accumulator, EMPTY_GUARDED_IVARS, mutated_ivars)
1241
+ define_method_written_ivars(body, (accumulator[DEFINE_METHOD_WRITES] ||= {})[class_name] ||= Set.new)
1242
+ end
1243
+
1149
1244
  # The ivars a `define_method` block body writes, in any write form, outside a nested `def` / `class` / `module`.
1150
1245
  def define_method_written_ivars(node, acc)
1151
1246
  return unless node.is_a?(Prism::Node)
@@ -1191,7 +1286,7 @@ module Rigor
1191
1286
 
1192
1287
  seen_writes = Set.new
1193
1288
  read_first = Set.new
1194
- detect_read_before_write(def_node.body, seen_writes, read_first)
1289
+ detect_read_before_write(def_node.body, seen_writes, read_first, class_name)
1195
1290
 
1196
1291
  # ADR-38 — `initialize` is the built-in initializer gate; a plugin may declare additional `def`-form initializer
1197
1292
  # methods (minitest `setup`, Rails `after_initialize`, DI setters) on a constrained class. Both fold their
@@ -1306,7 +1401,7 @@ module Rigor
1306
1401
  )
1307
1402
  end
1308
1403
 
1309
- def detect_read_before_write(node, seen_writes, read_first)
1404
+ def detect_read_before_write(node, seen_writes, read_first, class_name = nil)
1310
1405
  return unless node.is_a?(Prism::Node)
1311
1406
  return if IVAR_BARRIER_NODES.any? { |klass| node.is_a?(klass) }
1312
1407
 
@@ -1316,20 +1411,21 @@ module Rigor
1316
1411
  # not `InstanceVariableWriteNode`s, so the generic descent below never records them as writes. Left unhandled,
1317
1412
  # an ivar written only via massign in `initialize` stays absent from `init_writes`, and
1318
1413
  # `contribute_read_before_write_nil!` then unions a spurious `nil` into its class-ivar seed — masking a
1319
- # genuinely-typed `@m` (e.g. `Tuple[]`) as `T | nil` at every sibling read. The RHS runs before any target is
1320
- # committed, so descend into `value` FIRST (an ivar read there is read-before-write), then mark every ivar
1321
- # target as written.
1414
+ # genuinely-typed `@m` (e.g. `Tuple[]`) as `T | nil` at every sibling read. The RHS runs before any
1415
+ # target is committed, so descend into `value` FIRST (an ivar read there is read-before-write), then mark every
1416
+ # ivar target as written.
1322
1417
  if node.is_a?(Prism::MultiWriteNode)
1323
- detect_read_before_write(node.value, seen_writes, read_first) if node.value
1324
- detect_multi_write_target_writes(node, seen_writes, read_first)
1418
+ detect_read_before_write(node.value, seen_writes, read_first, class_name) if node.value
1419
+ detect_multi_write_target_writes(node, seen_writes, read_first, class_name)
1325
1420
  return
1326
1421
  end
1327
1422
 
1328
1423
  # Descend BEFORE recording a write — `@x = @x + 1`'s RHS is an `InstanceVariableReadNode` that runs before the
1329
1424
  # write is committed; the read is therefore read-before-write semantically. `each_child` yields the value
1330
1425
  # child before the lvalue target (`compact_child_nodes` field order), matching this order.
1331
- node.rigor_each_child do |c|
1332
- detect_read_before_write(c, seen_writes, read_first)
1426
+ # #2490 — what a block whose `self` is another object reads or writes is that object's.
1427
+ each_census_child(node, class_name) do |c|
1428
+ detect_read_before_write(c, seen_writes, read_first, class_name)
1333
1429
  end
1334
1430
 
1335
1431
  seen_writes << node.name if IVAR_WRITE_NODES.any? { |klass| node.is_a?(klass) }
@@ -1341,19 +1437,19 @@ module Rigor
1341
1437
  # Records each ivar target of a `MultiWriteNode` / nested `MultiTargetNode` into `seen_writes`, and descends into
1342
1438
  # any non-ivar target (a `CallTargetNode` / `IndexTargetNode` receiver such as `@obj.x, @y = …`) so an ivar read
1343
1439
  # inside a target receiver still counts as read-before-write.
1344
- def detect_multi_write_target_writes(node, seen_writes, read_first)
1440
+ def detect_multi_write_target_writes(node, seen_writes, read_first, class_name = nil)
1345
1441
  targets = (node.lefts || []) + [node.rest].compact + (node.rights || [])
1346
1442
  targets.each do |target|
1347
1443
  case target
1348
1444
  when Prism::InstanceVariableTargetNode
1349
1445
  seen_writes << target.name
1350
1446
  when Prism::MultiTargetNode
1351
- detect_multi_write_target_writes(target, seen_writes, read_first)
1447
+ detect_multi_write_target_writes(target, seen_writes, read_first, class_name)
1352
1448
  when Prism::SplatNode
1353
1449
  inner = target.expression
1354
1450
  seen_writes << inner.name if inner.is_a?(Prism::InstanceVariableTargetNode)
1355
1451
  else
1356
- detect_read_before_write(target, seen_writes, read_first)
1452
+ detect_read_before_write(target, seen_writes, read_first, class_name)
1357
1453
  end
1358
1454
  end
1359
1455
  end
@@ -1361,6 +1457,21 @@ module Rigor
1361
1457
  IVAR_BARRIER_NODES = [Prism::DefNode, Prism::ClassNode, Prism::ModuleNode].freeze
1362
1458
  private_constant :IVAR_BARRIER_NODES
1363
1459
 
1460
+ # Issue #2490 — yields the children of `node` a census walk for `class_name` descends into: all of them, or only
1461
+ # those {CensusForeignBlock.census_children} keeps when `node` runs code whose `self` is another object. A nil
1462
+ # `class_name` keeps them all. A reentered `def` (#2941) is yielded as itself, so a walk that stops at a `def`
1463
+ # skips it; `def_bodies:` yields its body instead, for a walk that counts its writes. `reentry: false` (a
1464
+ # singleton-side `def`) keeps none of the reentered code.
1465
+ def each_census_child(node, class_name, def_bodies: false, reentry: true, &)
1466
+ outside = class_name && CensusForeignBlock.census_children(node, class_name, reentry: reentry)
1467
+ return node.rigor_each_child(&) unless outside
1468
+
1469
+ outside.each do |child|
1470
+ child = child.body if def_bodies && child.is_a?(Prism::DefNode)
1471
+ yield child if child
1472
+ end
1473
+ end
1474
+
1364
1475
  EMPTY_GUARDED_IVARS = Set.new.freeze
1365
1476
 
1366
1477
  # Shared empty result for a def that returns no ivar — the overwhelmingly common case.
@@ -1394,6 +1505,7 @@ module Rigor
1394
1505
  # array/tuple-shaped, else the unanalyzable floor (the same `Dynamic[top]` a single write to an unknown RHS
1395
1506
  # records — an unanalyzable multi-write means unknown, not nil).
1396
1507
  record_multi_write_ivars(node, scope, class_name, accumulator)
1508
+ record_binding_target_ivars(node, scope, class_name, accumulator)
1397
1509
 
1398
1510
  record_ivar_mutator_call(node, class_name, mutated_ivars) if mutated_ivars
1399
1511
 
@@ -1406,7 +1518,10 @@ module Rigor
1406
1518
  return
1407
1519
  end
1408
1520
 
1409
- node.rigor_each_child do |c|
1521
+ # #2490 — a block or `class <<` body whose `self` is another object writes that object's ivars. #2941 — a `def`
1522
+ # in it that runs on a `class_name` instance again counts its writes.
1523
+ # The reentry is for code that runs on an instance again, so a singleton-side `def` takes none.
1524
+ each_census_child(node, class_name, def_bodies: true, reentry: !scope.self_type.is_a?(Type::Singleton)) do |c|
1410
1525
  gather_ivar_writes(c, scope, class_name, accumulator, guarded_ivars, mutated_ivars, dead_writes, declared)
1411
1526
  end
1412
1527
  end
@@ -1445,13 +1560,21 @@ module Rigor
1445
1560
  method_name, receiver = mutation_target(node)
1446
1561
  return if method_name.nil?
1447
1562
  return unless receiver.is_a?(Prism::InstanceVariableReadNode)
1448
- return unless MutationWidening::SHAPE_MUTATORS.include?(method_name)
1563
+ return unless ivar_census_mutator?(method_name)
1449
1564
 
1450
1565
  per_class = (mutated_ivars[class_name] ||= {})
1451
1566
  per_ivar = (per_class[receiver.name] ||= Set.new)
1452
1567
  per_ivar << method_name
1453
1568
  end
1454
1569
 
1570
+ # The Set-only names (`add`, `merge`, … — issue #2294) are recorded here rather than in `SHAPE_MUTATORS`, which
1571
+ # every scan reads by name alone: there `merge` would make `Hash#merge`, which returns a new Hash, a mutation of
1572
+ # its receiver. The census is safe to over-collect, since {#widen_member_for_observed_mutators} reads each name
1573
+ # against the table of the carrier it widens.
1574
+ def ivar_census_mutator?(method_name)
1575
+ MutationWidening::SHAPE_MUTATORS.include?(method_name) || SetMutation::MUTATORS.include?(method_name)
1576
+ end
1577
+
1455
1578
  # `[mutator name, receiver node]` for a node that mutates its receiver, `nil` otherwise.
1456
1579
  #
1457
1580
  # `@h[k] ||= v` and its `&&=` / `+=` siblings store through `[]=` but are not `[]=` CallNodes; missing them left
@@ -1586,10 +1709,9 @@ module Rigor
1586
1709
  SAME_CLASS_CALL_DEPTH_CAP = 3
1587
1710
  private_constant :SAME_CLASS_CALL_DEPTH_CAP
1588
1711
 
1589
- # WD3 — builds the per-class definite-assignment summary `{class_name => {method_name => Set<ivar names assigned
1590
- # non-nil on every completing path>}}`. Used so a ctor's `dead_transient_nil_writes` can credit an indirect
1591
- # overwrite through an unconditionally-called same-class method (ipaddr's `initialize` reassigns `@mask_addr` via
1592
- # `mask!`).
1712
+ # WD3 — builds the per-class definite-assignment summary: per class, then per method, the ivars assigned non-nil
1713
+ # on every completing path. Used so a ctor's `dead_transient_nil_writes` can credit an indirect overwrite through
1714
+ # an unconditionally-called same-class method (ipaddr's `initialize` reassigns `@mask_addr` via `mask!`).
1593
1715
  #
1594
1716
  # Each method's set is computed by the same suffix definite-assignment analysis used for the ctor seed, run from
1595
1717
  # the method body's first statement for every ivar the method writes anywhere. Same-class calls inside a method
@@ -1976,10 +2098,11 @@ module Rigor
1976
2098
  end
1977
2099
 
1978
2100
  def record_ivar_write(node, scope, class_name, accumulator, guarded: false, declared: nil)
1979
- rvalue_type = declared || scope.type_of(node.value)
2101
+ rvalue_type = declared || accumulator[CONSTANT_IVAR_VALUES]&.type_of(node.value, scope) ||
2102
+ scope.type_of(node.value)
1980
2103
 
1981
2104
  # `@x = nil unless @x` / `@y = false unless @y` — follow-up to the polarity-aware defensive-init guard fix
1982
- # (ROADMAP § Future cycles — "Defensive ivar-init with nil / false rvalue"). When the rvalue is itself a falsey
2105
+ # (defensive ivar-init with a nil / false rvalue). When the rvalue is itself a falsey
1983
2106
  # Constant, `union(rvalue, Constant[nil])` collapses (for `nil`) or doesn't widen the type's truthiness profile
1984
2107
  # (for `false`) — the predicate `unless @x` then folds to a single `Constant[nil]` / `Constant[false]` and the
1985
2108
  # `flow.always-truthy-condition` / `-always-falsey-` rule false-fires on the no-op-but-documented-default idiom.
@@ -1989,6 +2112,8 @@ module Rigor
1989
2112
  # `lib/tdiary/configuration.rb:157` for the worked site.
1990
2113
  return if guarded && falsey_constant?(rvalue_type)
1991
2114
 
2115
+ rvalue_type = EmptySeed.erase(node.value, rvalue_type)
2116
+
1992
2117
  rvalue_type = Type::Combinator.union(rvalue_type, Type::Combinator.constant_of(nil)) if guarded
1993
2118
  accumulate_ivar_type(accumulator, class_name, node.name, rvalue_type)
1994
2119
  end
@@ -2018,7 +2143,7 @@ module Rigor
2018
2143
  # `nil`, the guard bound its own ivar, and `ExpressionTyper#compound_write_value` read
2019
2144
  # `truthy(nil) | bot` — an instance method that sig-gen declared `-> bot`.
2020
2145
  def record_ivar_or_write(node, scope, class_name, accumulator)
2021
- rvalue_type = scope.type_of(node.value)
2146
+ rvalue_type = EmptySeed.erase(node.value, scope.type_of(node.value))
2022
2147
  return if Narrowing.narrow_truthy(rvalue_type).is_a?(Type::Bot)
2023
2148
 
2024
2149
  accumulate_ivar_type(accumulator, class_name, node.name,
@@ -2182,7 +2307,13 @@ module Rigor
2182
2307
  CLASS_BODY_WRITES = :class_body_writes
2183
2308
  # The classes a computed-name `instance_variable_set` may write any ivar of ({#record_reflective_ivar_write}).
2184
2309
  REFLECTIVE_OPAQUE_CLASSES = :reflective_opaque_classes
2185
- private_constant :DEFINE_METHOD_WRITES, :CLASS_BODY_WRITES, :REFLECTIVE_OPAQUE_CLASSES
2310
+ # The census's {ConstantIvarValue}, which {#record_ivar_write} reads.
2311
+ CONSTANT_IVAR_VALUES = :constant_ivar_values
2312
+ # Issue #2931 — per class, the ivars a singleton-side `def` (`def self.x`) of it writes, which the census files
2313
+ # under the class too (#1958).
2314
+ SINGLETON_IVAR_WRITES = :singleton_ivar_writes
2315
+ private_constant :DEFINE_METHOD_WRITES, :CLASS_BODY_WRITES, :REFLECTIVE_OPAQUE_CLASSES, :CONSTANT_IVAR_VALUES,
2316
+ :SINGLETON_IVAR_WRITES
2186
2317
 
2187
2318
  # Unions `type` into the class-ivar accumulator for `(class_name, ivar_name)`. Shared by the single-write and
2188
2319
  # multi-write (parallel-assignment) collectors.
@@ -2215,9 +2346,65 @@ module Rigor
2215
2346
 
2216
2347
  rhs_type = scope.type_of(node.value)
2217
2348
  ivars = MultiTargetBinder.bind_marked(node, rhs_type, scope: scope, soften_slots: false).ivars
2218
- ivars.each { |name, type| accumulate_ivar_type(accumulator, class_name, name, type) }
2349
+ # #2499 — `@a, @b = {}, []` erases each empty seed as a single write does.
2350
+ values = EmptySeed.multi_write_values(node)
2351
+ ivars.each do |name, type|
2352
+ accumulate_ivar_type(accumulator, class_name, name, EmptySeed.erase(values[name], type))
2353
+ end
2354
+ end
2355
+
2356
+ # Issue #1217 — `rescue => @err` and `for @x in xs` (with `for *@x in`, `for @x, y in`) write the ivar through a
2357
+ # target node, not an `InstanceVariableWriteNode`, so the census missed them: a sibling method read the
2358
+ # constructor's `@err = nil` alone and folded `if @err` always-falsey. A rescue reference contributes the
2359
+ # exception instance type the clause names; a `for` index the `Dynamic[top]` floor, since the census does not
2360
+ # type the element, and an unknown value is honest where a missing one is not.
2361
+ def record_binding_target_ivars(node, scope, class_name, accumulator)
2362
+ names = BindingTargets.names(node, Prism::InstanceVariableTargetNode)
2363
+ return if names.empty?
2364
+
2365
+ type = BindingTargets.value_type(node, scope)
2366
+ names.each { |name| accumulate_ivar_type(accumulator, class_name, name, type) }
2219
2367
  end
2220
2368
 
2369
+ # The names a `rescue =>` reference or a `for` index binds through a target of `target_class`, looking through
2370
+ # a splat and a multi-target index.
2371
+ module BindingTargets
2372
+ EMPTY = [].freeze
2373
+ private_constant :EMPTY
2374
+
2375
+ module_function
2376
+
2377
+ def names(node, target_class)
2378
+ root =
2379
+ case node
2380
+ when Prism::RescueNode then node.reference
2381
+ when Prism::ForNode then node.index
2382
+ end
2383
+ return EMPTY if root.nil?
2384
+
2385
+ collect(root, target_class, [])
2386
+ end
2387
+
2388
+ # What an ivar target is written with: a rescue reference's exception instance, as the in-method binding
2389
+ # types it ({ErrorInfo.rescue_exception_type}), a `for` index's `Dynamic[top]` floor.
2390
+ def value_type(node, scope)
2391
+ return Type::Combinator.untyped unless node.is_a?(Prism::RescueNode)
2392
+
2393
+ ErrorInfo.rescue_exception_type(node) { |exception_node| scope.type_of(exception_node) }
2394
+ end
2395
+
2396
+ def collect(target, target_class, acc)
2397
+ case target
2398
+ when target_class then acc << target.name
2399
+ when Prism::SplatNode then collect(target.expression, target_class, acc) if target.expression
2400
+ when Prism::MultiTargetNode
2401
+ [*target.lefts, target.rest, *target.rights].compact.each { |t| collect(t, target_class, acc) }
2402
+ end
2403
+ acc
2404
+ end
2405
+ end
2406
+ private_constant :BindingTargets
2407
+
2221
2408
  def falsey_constant?(type)
2222
2409
  type.is_a?(Type::Constant) && (type.value.nil? || type.value == false)
2223
2410
  end
@@ -2242,7 +2429,7 @@ module Rigor
2242
2429
  def legacy_class_cvar_index(root, default_scope)
2243
2430
  accumulator = {}
2244
2431
  walk_class_cvars(root, [], default_scope, accumulator)
2245
- accumulator.transform_values(&:freeze).freeze
2432
+ finished_cvar_census(accumulator)
2246
2433
  end
2247
2434
 
2248
2435
  # `def_owner` names the class a `def` leaf's cvar writes belong to when `self` is
@@ -2320,6 +2507,8 @@ module Rigor
2320
2507
  _self_decl, child_prefix, child_cref = ctx
2321
2508
  return true unless node.body
2322
2509
 
2510
+ collect_body_cvar_writes(node.body, child_cref ? [] : child_prefix,
2511
+ scope_entering_declaration(default_scope, node.constant_path), accumulator)
2323
2512
  walk_class_cvars(node.body, child_cref ? [] : child_prefix,
2324
2513
  scope_entering_declaration(default_scope, node.constant_path), accumulator,
2325
2514
  singleton_cref: child_cref)
@@ -2369,24 +2558,89 @@ module Rigor
2369
2558
  class_name = qualified_prefix.join("::")
2370
2559
  body_scope = census_body_scope(default_scope, qualified_prefix,
2371
2560
  Type::Combinator.nominal_of(class_name))
2372
- gather_cvar_writes(def_node.body, body_scope, class_name, accumulator)
2561
+ gather_cvar_writes(def_node.body, body_scope, class_name, accumulator, join_only: false)
2562
+ end
2563
+
2564
+ # Issue #3204 — the writes a `class` / `module` body makes outside its `def`s (`@@x = nil` at the top of
2565
+ # the body, in a block or a `class << self` there): Ruby resolves each through the body's lexical cref,
2566
+ # so it is the same cvar a sibling `def` reads. Such a write only joins an entry a `def` body's write of
2567
+ # the same class creates: the census misses most of the other ways a cvar is written
2568
+ # (`||=`, `+=`, an including class, a subclass, another file, `class_variable_set` from outside), so a
2569
+ # body-only seed would be precise where Ruby's value is not, and stays unbound. The rvalue is typed with
2570
+ # the class itself as `self`. The gather stops at `def`, `class` and `module`, which their own events enter.
2571
+ def collect_body_cvar_writes(body, qualified_prefix, default_scope, accumulator)
2572
+ return if body.nil? || qualified_prefix.empty?
2573
+
2574
+ class_name = qualified_prefix.join("::")
2575
+ body_scope = census_body_scope(default_scope, qualified_prefix,
2576
+ Type::Combinator.singleton_of(class_name))
2577
+ gather_cvar_writes(body, body_scope, class_name, accumulator, join_only: true)
2373
2578
  end
2374
2579
 
2375
- def gather_cvar_writes(node, scope, class_name, accumulator)
2580
+ def gather_cvar_writes(node, scope, class_name, accumulator, join_only:)
2376
2581
  return unless node.is_a?(Prism::Node)
2377
2582
 
2378
- record_cvar_write(node, scope, class_name, accumulator) if node.is_a?(Prism::ClassVariableWriteNode)
2583
+ if node.is_a?(Prism::ClassVariableWriteNode)
2584
+ record_cvar_write(accumulator, class_name, node.name, EmptySeed.erase(node.value, scope.type_of(node.value)),
2585
+ join_only: join_only)
2586
+ end
2587
+ record_reflective_cvar_write(node, scope, class_name, accumulator) if node.is_a?(Prism::CallNode)
2588
+ # Issue #1217 — `rescue => @@c` / `for @@c in xs` write through a target node. Each enters as the
2589
+ # `Dynamic[top]` floor, the rescue reference too: the census keys each class alone, so a superclass's
2590
+ # `@@c = nil` is not in a subclass's entry, and a typed exception entry would read as non-nil there.
2591
+ BindingTargets.names(node, Prism::ClassVariableTargetNode).each do |name|
2592
+ record_cvar_write(accumulator, class_name, name, Type::Combinator.untyped, join_only: join_only)
2593
+ end
2379
2594
  return if IVAR_BARRIER_NODES.any? { |klass| node.is_a?(klass) }
2380
2595
 
2381
- node.rigor_each_child { |c| gather_cvar_writes(c, scope, class_name, accumulator) }
2596
+ node.rigor_each_child { |c| gather_cvar_writes(c, scope, class_name, accumulator, join_only: join_only) }
2382
2597
  end
2383
2598
 
2384
- def record_cvar_write(node, scope, class_name, accumulator)
2385
- rvalue_type = scope.type_of(node.value)
2386
- accumulator[class_name] ||= {}
2387
- existing = accumulator[class_name][node.name]
2388
- accumulator[class_name][node.name] =
2389
- existing ? Type::Combinator.union(existing, rvalue_type) : rvalue_type
2599
+ # Issue #3204 — `class_variable_set(:@@x, v)` on an implicit or `self` receiver writes the cvar a sibling
2600
+ # `def` reads, as `@@x = v` does. It only joins, like a class-body write: a dynamic name, a `send`, an
2601
+ # explicit receiver or another file may write the same cvar unseen.
2602
+ def record_reflective_cvar_write(node, scope, class_name, accumulator)
2603
+ return unless node.name == :class_variable_set && (node.receiver.nil? || node.receiver.is_a?(Prism::SelfNode))
2604
+
2605
+ args = node.arguments&.arguments
2606
+ return unless args&.size == 2
2607
+
2608
+ name_node, value = args
2609
+ # A name that is not a cvar's never meets a `def` write's entry, so the join drops it.
2610
+ name = name_node.unescaped if name_node.is_a?(Prism::SymbolNode) || name_node.is_a?(Prism::StringNode)
2611
+ return unless name
2612
+
2613
+ record_cvar_write(accumulator, class_name, name.to_sym, EmptySeed.erase(value, scope.type_of(value)),
2614
+ join_only: true)
2615
+ end
2616
+
2617
+ # A class-body or `class_variable_set` write no `def` body's write has met yet (issue #3204): it joins an
2618
+ # entry a creating write makes later, and {#finished_cvar_census} drops it when none does.
2619
+ JoinOnlyCvarWrite = Data.define(:type)
2620
+ private_constant :JoinOnlyCvarWrite
2621
+
2622
+ # Every census write unions with what the entry already holds, so the seed does not depend on the order of
2623
+ # the writes. A `join_only` write (a class or module body's, or `class_variable_set`) never creates an entry
2624
+ # on its own: it is held as a {JoinOnlyCvarWrite} until a `def` body's write of the same cvar arrives.
2625
+ def record_cvar_write(accumulator, class_name, name, type, join_only:)
2626
+ entries = (accumulator[class_name] ||= {})
2627
+ existing = entries[name]
2628
+ entries[name] =
2629
+ if existing.nil? then join_only ? JoinOnlyCvarWrite.new(type) : type
2630
+ elsif existing.is_a?(JoinOnlyCvarWrite)
2631
+ joined = Type::Combinator.union(existing.type, type)
2632
+ join_only ? JoinOnlyCvarWrite.new(joined) : joined
2633
+ else Type::Combinator.union(existing, type)
2634
+ end
2635
+ end
2636
+
2637
+ # The census as the discovery index holds it: each cvar only joining writes reached dropped, each class's
2638
+ # entry frozen, then the whole.
2639
+ def finished_cvar_census(accumulator)
2640
+ accumulator.filter_map do |class_name, entries|
2641
+ kept = entries.reject { |_, type| type.is_a?(JoinOnlyCvarWrite) }
2642
+ [class_name, kept.freeze] unless kept.empty?
2643
+ end.to_h.freeze
2390
2644
  end
2391
2645
 
2392
2646
  # The program-global pre-pass's tables on the seeded scope's discovery index, and each global materialised into
@@ -2504,7 +2758,7 @@ module Rigor
2504
2758
  end
2505
2759
 
2506
2760
  def record_global_write(node, scope, accumulator)
2507
- rvalue_type = scope.type_of(node.value)
2761
+ rvalue_type = EmptySeed.erase(node.value, scope.type_of(node.value))
2508
2762
  existing = accumulator[node.name]
2509
2763
  accumulator[node.name] =
2510
2764
  existing ? Type::Combinator.union(existing, rvalue_type) : rvalue_type
@@ -3269,7 +3523,7 @@ module Rigor
3269
3523
  unless owner.empty?
3270
3524
  body_scope = census_body_scope(default_scope, enclosing_prefix, Type::Combinator.singleton_of(owner))
3271
3525
  end
3272
- rvalue_type = meta_new_constant_type(node, full) || body_scope.type_of(node.value)
3526
+ rvalue_type = meta_new_constant_type(node, full) || EmptySeed.erase(node.value, body_scope.type_of(node.value))
3273
3527
  existing = accumulator[full]
3274
3528
  accumulator[full] = existing ? Type::Combinator.union(existing, rvalue_type) : rvalue_type
3275
3529
  end
@@ -3443,6 +3697,31 @@ module Rigor
3443
3697
  tables.siblings]
3444
3698
  end
3445
3699
 
3700
+ # Issue #3015 — the bodies of a concern's `included` / `prepended` / `class_methods` blocks, each with the prefix
3701
+ # of the module whose body writes it ({ScopeIndexer#concern_hook_blocks}). Ruby runs such a block on the includer
3702
+ # (or on `Owner::ClassMethods`), so a `def self.x` or `class << self` def in it is not the module's own singleton
3703
+ # method, and the walks that file one ask {#hook_block?} first. Mixed into an accumulator with a
3704
+ # `hook_block_spans` slot.
3705
+ module HookBlockSpans
3706
+ def mark_hook_blocks(blocks, owner)
3707
+ blocks.each do |block|
3708
+ (self.hook_block_spans ||= []) << [block.location.start_offset, block.location.end_offset, owner]
3709
+ end
3710
+ end
3711
+
3712
+ # Whether `node` sits in a block marked for the module `owner` (a prefix).
3713
+ def hook_block?(node, owner)
3714
+ offset = node.location.start_offset
3715
+ !hook_block_spans.nil? &&
3716
+ hook_block_spans.any? { |start, stop, marked| offset >= start && offset < stop && marked == owner }
3717
+ end
3718
+
3719
+ # Whether `def_node` is a `def self.x` in a block marked for the module `owner`.
3720
+ def hook_block_self_def?(def_node, owner)
3721
+ def_node.receiver.is_a?(Prism::SelfNode) && hook_block?(def_node, owner)
3722
+ end
3723
+ end
3724
+
3446
3725
  # The accumulator {#walk_methods_and_def_nodes} threads: the existence table and its issue #992
3447
3726
  # envelope twin, which only {#record_method} and {#record_surface_mark} write, and the issue #1120
3448
3727
  # refinement table, which only {#record_refinement_defs} writes. That last one stays nil until a
@@ -3465,7 +3744,9 @@ module Rigor
3465
3744
  MethodTables = Struct.new(:existence, :envelopes, :refinements, :certainty, :certain, :possible,
3466
3745
  :contested_envelopes, :contested_def_nodes, :root, :in_effect, :census,
3467
3746
  :refine_text, :literal_seen, :source_bytes, :loose_constants, :block_spans,
3468
- :definer_history) do
3747
+ :definer_history, :forwardable_extends, :hook_block_spans) do
3748
+ include HookBlockSpans
3749
+
3469
3750
  # Issue #2006 — records, in walk order, that a definer other than an alias wrote the instance method `name`
3470
3751
  # of `class_name` at `node`: `def_node` for a `def`, nil for a `define_method(:x)` or an `attr_*` reader or
3471
3752
  # writer, and whether the write is {#certain}. {ScopeIndexer#apply_alias_def_nodes} reads the history to copy
@@ -3567,7 +3848,8 @@ module Rigor
3567
3848
  REFINEMENT_WILDCARD = Scope::DiscoveryIndex::REFINEMENT_WILDCARD
3568
3849
  REFINE_WORD = /\brefine\b/
3569
3850
  HEREDOC_OPENER = /<<[~-]?["'`A-Za-z_]/
3570
- private_constant :EMPTY_REFINEMENTS, :REFINEMENT_WILDCARD, :REFINE_WORD, :HEREDOC_OPENER
3851
+ COMPARISON_OPERATORS = Set[:==, :!=, :===].freeze
3852
+ private_constant :EMPTY_REFINEMENTS, :REFINEMENT_WILDCARD, :REFINE_WORD, :HEREDOC_OPENER, :COMPARISON_OPERATORS
3571
3853
 
3572
3854
  NO_OFFSETS = [].freeze
3573
3855
  private_constant :NO_OFFSETS
@@ -3660,7 +3942,8 @@ module Rigor
3660
3942
  # `self`, charged to its module or the wildcard), `:literal` (A3's `:refine` / eval-string literal), `:nested` (a
3661
3943
  # `refine` in a refine body, or in a singleton method of one), `:dsl` (where `self` is a class, #1689) and
3662
3944
  # `:raises` (no block, a Proc as the block, another receiver, or a computed target where `self` is `main`), and
3663
- # `:data` (a `:refine` literal where `self` is a plain class or one of its instances).
3945
+ # `:data` (a `:refine` literal where `self` is a plain class or one of its instances), and `:inert` (issue #2203:
3946
+ # a `:refine` Symbol that is a hash key or a comparison operand, which no call reads as a method name).
3664
3947
  def refine_census(root)
3665
3948
  census = []
3666
3949
  refinements = build_methods_and_def_nodes(root, census: census)[3]
@@ -3839,6 +4122,8 @@ module Rigor
3839
4122
  end
3840
4123
 
3841
4124
  census_call(node, qualified_prefix, tables, refine_body)
4125
+ when Prism::AssocNode
4126
+ census_assoc_key(node, tables)
3842
4127
  when Prism::SymbolNode
3843
4128
  census_symbol(node, tables)
3844
4129
  end
@@ -3894,6 +4179,8 @@ module Rigor
3894
4179
  arguments = node.arguments&.arguments
3895
4180
  return if arguments.nil?
3896
4181
 
4182
+ return census_compared_literal(arguments.first, tables) if COMPARISON_OPERATORS.include?(node.name)
4183
+
3897
4184
  strings = RefineCensus.string_literal_call?(node)
3898
4185
  arguments.each do |argument|
3899
4186
  next unless RefineCensus.refine_symbol?(argument) || (strings && RefineCensus.refine_string?(argument))
@@ -3928,6 +4215,26 @@ module Rigor
3928
4215
 
3929
4216
  def constant_shaped?(node) = node.is_a?(Prism::ConstantReadNode) || node.is_a?(Prism::ConstantPathNode)
3930
4217
 
4218
+ # Issue #2203 — a `:refine` Symbol a comparison reads (`node.name == :refine`, `!=`, `===`) is a value compared,
4219
+ # never a method name a call runs: it is inert (`:inert`), and records no row.
4220
+ def census_compared_literal(argument, tables)
4221
+ return unless RefineCensus.refine_symbol?(argument) && !tables.literal_seen?(argument)
4222
+
4223
+ tables.see_literal(argument)
4224
+ tables.account(argument, :inert)
4225
+ end
4226
+
4227
+ # Issue #2203 — a `:refine` Symbol that is a hash key (`h(x, refine: true)`, `{ refine: 1 }`, `{ :refine => 1 }`)
4228
+ # names an option, not `Module#refine`: it is inert (`:inert`), and records no row. A `:refine` value stays a
4229
+ # literal.
4230
+ def census_assoc_key(node, tables)
4231
+ key = node.key
4232
+ return unless RefineCensus.refine_symbol?(key) && !tables.literal_seen?(key)
4233
+
4234
+ tables.see_literal(key)
4235
+ tables.account(key, :inert)
4236
+ end
4237
+
3931
4238
  # A3 — a `:refine` Symbol anywhere else (`alias my_refine refine`, `%i[refine]`, a hash) names `Module#refine`
3932
4239
  # for code the walk cannot follow, run on the module the in-effect walk says `self` is there; where `self` is a
3933
4240
  # plain class or one of its instances it is data.
@@ -4101,6 +4408,11 @@ module Rigor
4101
4408
  census_scan(node.superclass, qualified_prefix, methods_acc)
4102
4409
  end
4103
4410
  record_declaration_facts(node, child_prefix, methods_acc.at(node)) unless child_cref
4411
+ unless child_cref
4412
+ methods_acc.mark_hook_blocks(concern_hook_blocks(node, child_prefix), child_prefix)
4413
+ record_concern_class_methods(node, child_prefix, methods_acc, def_nodes_acc)
4414
+ record_hook_definer_calls(node, child_prefix, methods_acc)
4415
+ end
4104
4416
  body_prefix = child_cref ? [] : child_prefix
4105
4417
  if node.body
4106
4418
  walk_methods_and_def_nodes(node.body, body_prefix, false, methods_acc, def_nodes_acc,
@@ -4120,11 +4432,12 @@ module Rigor
4120
4432
  if !node.expression.is_a?(Prism::SelfNode) || tables.in_block?(node)
4121
4433
  record_singleton_body_prepends(node, qualified_prefix, tables)
4122
4434
  end
4435
+ record_constant_singleton_prepends(node, qualified_prefix, tables)
4123
4436
  walk_methods_and_def_nodes(node.expression, qualified_prefix, in_singleton_class,
4124
4437
  methods_acc, def_nodes_acc, source_path, def_owner_prefix,
4125
4438
  singleton_cref: singleton_cref,
4126
4439
  defs_singleton: defs_singleton)
4127
- if node.body
4440
+ if node.body && !hook_block_singleton?(node, in_singleton_class, owner_prefix, methods_acc)
4128
4441
  # The body's lexical cref is unnameable AND its qualified prefix stays the
4129
4442
  # ENCLOSING lexical one — `class C3::CD` below `class <<` resolves `C3`
4130
4443
  # lexically, never against the singleton's name. `singleton_prefix` rides
@@ -4138,6 +4451,7 @@ module Rigor
4138
4451
  when Prism::ConstantWriteNode, Prism::ConstantPathWriteNode, Prism::ConstantOrWriteNode,
4139
4452
  Prism::ConstantPathOrWriteNode
4140
4453
  record_meta_superclass(node, qualified_prefix, def_owner_prefix, methods_acc.at(node))
4454
+ record_struct_same_class(node, qualified_prefix, def_owner_prefix, methods_acc.at(node))
4141
4455
  if (split = meta_new_block_split(node, qualified_prefix, def_owner_prefix, singleton_cref))
4142
4456
  enclosing, body, body_self = split
4143
4457
  # A meta-new block rebinds only `self` — `Module.nesting` stays lexical — so
@@ -4179,11 +4493,13 @@ module Rigor
4179
4493
  node.rigor_each_child { |child| census_scan(child, qualified_prefix, methods_acc) }
4180
4494
  end
4181
4495
  record_foreign_singleton_def(node, qualified_prefix, owner_prefix, methods_acc.at(node))
4496
+ return if methods_acc.hook_block_self_def?(node, owner_prefix)
4497
+
4182
4498
  unless def_owner_prefix&.empty? || defs_singleton == :unnameable ||
4183
4499
  (singleton_cref && owner_prefix.empty?)
4184
- singleton_def = in_singleton_class || defs_singleton
4500
+ singleton_def = def_context(in_singleton_class, defs_singleton)
4185
4501
  record_def_method(node, owner_prefix, singleton_def, methods_acc.at(node))
4186
- record_def_body_evidence(node, owner_prefix, methods_acc)
4502
+ record_def_body_evidence(node, owner_prefix, methods_acc, singleton_def ? :singleton : :instance)
4187
4503
  record_def_node(node, owner_prefix, singleton_def, def_nodes_acc, methods_acc)
4188
4504
  end
4189
4505
  return
@@ -4199,11 +4515,15 @@ module Rigor
4199
4515
  end
4200
4516
  end
4201
4517
  return
4518
+ when Prism::AssocNode
4519
+ census_assoc_key(node, methods_acc) if methods_acc.mentions_refine?
4202
4520
  when Prism::SymbolNode
4203
4521
  census_symbol(node, methods_acc) if methods_acc.mentions_refine?
4204
4522
  when Prism::CallNode
4205
4523
  census_literal_arguments(node, qualified_prefix, methods_acc) if methods_acc.mentions_refine?
4206
4524
  if receiver_eval_call?(node)
4525
+ record_singleton_eval_definers(node, owner_prefix, in_singleton_class, methods_acc.at(node))
4526
+ record_constant_singleton_eval_defs(node, qualified_prefix, methods_acc.at(node))
4207
4527
  return walk_eval_methods_and_defs(node, qualified_prefix, in_singleton_class, methods_acc,
4208
4528
  def_nodes_acc, source_path, def_owner_prefix,
4209
4529
  singleton_cref: singleton_cref,
@@ -4212,13 +4532,15 @@ module Rigor
4212
4532
  anonymous = record_call_node_methods(node, owner_prefix, in_singleton_class, methods_acc.at(node),
4213
4533
  source_path)
4214
4534
  unless in_singleton_class
4215
- superseded = call_superseded_names(node)
4535
+ superseded = call_superseded_names(node, owner_prefix, methods_acc)
4216
4536
  supersede_def_nodes(superseded, owner_prefix, def_nodes_acc, methods_acc.at(node))
4217
4537
  note_call_definers(node, superseded, owner_prefix, methods_acc.at(node))
4218
4538
  end
4219
4539
  tables = methods_acc.at(node)
4220
4540
  record_singleton_prepend(node, qualified_prefix, tables,
4221
4541
  self_ok: node.name == :prepend && tables.in_block?(node))
4542
+ own_prefix = def_owner_prefix&.empty? || defs_singleton == :unnameable ? EMPTY_PREFIX : owner_prefix
4543
+ record_own_singleton_prepend(node, [own_prefix, qualified_prefix], in_singleton_class, tables)
4222
4544
  # Issue #1120 — a refine body's defs go to the refinement table and nowhere else.
4223
4545
  if (target = methods_acc.refine_target_of(node))
4224
4546
  return record_literal_target_refine(node, target, qualified_prefix, owner_prefix, methods_acc)
@@ -4332,6 +4654,27 @@ module Rigor
4332
4654
  record_surface_mark(tables, child_prefix.join("::"), Scope::DiscoveryIndex::ENVELOPE_META_SUPERCLASS_MARK)
4333
4655
  end
4334
4656
 
4657
+ # Issue #2979 — `K = Struct.new("Named", :a)`, with or without a block, binds `K` and `Struct::Named` to one class
4658
+ # object, so each name's bucket takes a `[ENVELOPE_SAME_CLASS_KIND, other]` entry and the method chain of either
4659
+ # reads the other's tables (`Scope::ResolutionChain`). Only the bare factory call qualifies: under a wrapping
4660
+ # `Class.new(Struct.new("Named", :a))` the constant names a SUBCLASS of `Struct::Named`, which already reaches it
4661
+ # as an ancestor only one way.
4662
+ def record_struct_same_class(node, qualified_prefix, self_base, tables)
4663
+ rvalue = meta_new_rvalue(node)
4664
+ return unless struct_new_call?(rvalue)
4665
+
4666
+ named = AnonymousMetaClass.struct_class_name(rvalue)
4667
+ return if named.nil?
4668
+
4669
+ child_prefix = meta_new_child_prefix(node, qualified_prefix, self_base)
4670
+ return if child_prefix.nil? || child_prefix.empty?
4671
+
4672
+ class_name = child_prefix.join("::")
4673
+ kind = Scope::DiscoveryIndex::ENVELOPE_SAME_CLASS_KIND
4674
+ record_envelope(tables.envelopes, class_name, [kind, named], Source::ParameterEnvelope::OPAQUE)
4675
+ record_envelope(tables.envelopes, named, [kind, class_name], Source::ParameterEnvelope::OPAQUE)
4676
+ end
4677
+
4335
4678
  def record_meta_new_facts(rvalue, child_prefix, tables)
4336
4679
  record_meta_members(rvalue, child_prefix, tables)
4337
4680
  # `Module.new(&BODY)`: the body runs on the new module but cannot be read, so its surface is unknown (#1816).
@@ -4359,11 +4702,111 @@ module Rigor
4359
4702
  record_define_method(node, qualified_prefix, in_singleton_class, methods_acc) if node.name == :define_method
4360
4703
  record_attr_methods(node, qualified_prefix, in_singleton_class, methods_acc) if attr_macro(node)
4361
4704
  record_module_attr_methods(node, qualified_prefix, methods_acc) if MODULE_ATTR_MACROS.key?(node.name)
4705
+ record_forwardable_methods(node, qualified_prefix, in_singleton_class, methods_acc)
4362
4706
  record_alias_method_call(node, qualified_prefix, in_singleton_class, methods_acc)
4363
4707
  record_surface_evidence(node, qualified_prefix, methods_acc)
4708
+ record_self_definer_call(node, qualified_prefix, in_singleton_class, methods_acc)
4364
4709
  AnonymousMetaClass.name_for(node, source_path)
4365
4710
  end
4366
4711
 
4712
+ # Issue #2901 — the class-body calls on `self` that open the class to `call.undefined-method` without a name
4713
+ # this walk can read: `send(:define_method, m)` (the instance side, or the singleton side in a `class << self`
4714
+ # body), `define_singleton_method(m)` (the singleton side, outside `class << self`, where it would define on the
4715
+ # singleton's own singleton), and a call of any other name, filed as a
4716
+ # {Scope::DiscoveryIndex::ENVELOPE_BODY_CALL_KIND} entry for the reader to resolve against the class's macros.
4717
+ def record_self_definer_call(node, qualified_prefix, in_singleton_class, accumulator)
4718
+ return if qualified_prefix.empty? || !(node.receiver.nil? || node.receiver.is_a?(Prism::SelfNode))
4719
+
4720
+ class_name = qualified_prefix.join("::")
4721
+ if node.name == :define_singleton_method
4722
+ record_open_singleton_definer(node, class_name, in_singleton_class, accumulator)
4723
+ record_literal_self_definer(node, class_name, in_singleton_class, accumulator)
4724
+ elsif SURFACE_SEND_CALLS.include?(node.name)
4725
+ record_sent_define_method(node, class_name, in_singleton_class, accumulator)
4726
+ record_literal_self_definer(node, class_name, in_singleton_class, accumulator)
4727
+ elsif MODULE_NEW_MIXIN_CALLS.include?(node.name)
4728
+ record_module_new_mixin(node, class_name, in_singleton_class, accumulator)
4729
+ elsif !in_singleton_class && !BODY_CALL_EXCLUDED.include?(node.name)
4730
+ key = [Scope::DiscoveryIndex::ENVELOPE_BODY_CALL_KIND, node.name]
4731
+ record_envelope(accumulator.envelopes, class_name, key, Source::ParameterEnvelope::OPAQUE)
4732
+ end
4733
+ end
4734
+
4735
+ # The class-body calls this walk reads as definers of their own, or that core's `Module` answers: a project
4736
+ # macro of one of these names is not looked for.
4737
+ BODY_CALL_EXCLUDED = %i[
4738
+ define_method define_singleton_method alias_method attr_reader attr_writer attr_accessor include extend prepend
4739
+ private public protected module_function private_class_method public_class_method private_constant
4740
+ public_constant require require_relative autoload autoload_relative
4741
+ ].to_set.freeze
4742
+ private_constant :BODY_CALL_EXCLUDED
4743
+
4744
+ def record_open_singleton_definer(node, class_name, in_singleton_class, accumulator)
4745
+ return if in_singleton_class || !computed_first_name?(node.arguments&.arguments)
4746
+
4747
+ record_surface_mark(accumulator, class_name, Scope::DiscoveryIndex::ENVELOPE_OPEN_SINGLETON_NAMES_MARK)
4748
+ end
4749
+
4750
+ def record_sent_define_method(node, class_name, in_singleton_class, accumulator)
4751
+ arguments = node.arguments&.arguments
4752
+ return unless arguments && literal_method_name(arguments.first) == :define_method
4753
+ return unless computed_first_name?(arguments.drop(1))
4754
+
4755
+ record_surface_mark(accumulator, class_name, open_names_mark(in_singleton_class))
4756
+ end
4757
+
4758
+ # Issue #2975 — `include(Module.new { %i[a b].each { |x| define_method(x) {} } })` mixes in a module whose body
4759
+ # runs a computed `define_method` on its own `self`, so the side the mixin lands on (the instance side, or the
4760
+ # singleton side in a `class << self` body) is open as a direct `define_method(m)` there would leave it.
4761
+ def record_module_new_mixin(node, class_name, in_singleton_class, accumulator)
4762
+ return unless module_new_definer_argument?(node)
4763
+
4764
+ record_surface_mark(accumulator, class_name, open_names_mark(in_singleton_class))
4765
+ end
4766
+
4767
+ MODULE_NEW_MIXIN_CALLS = %i[include prepend].to_set.freeze
4768
+ private_constant :MODULE_NEW_MIXIN_CALLS
4769
+
4770
+ # Whether a mixin call's argument is a `Module.new { … }` whose body runs a computed `define_method` on the
4771
+ # module ({#runs_computed_self_definer?}).
4772
+ def module_new_definer_argument?(node)
4773
+ (node.arguments&.arguments || []).any? do |argument|
4774
+ module_new_call?(argument) && argument.block.is_a?(Prism::BlockNode) &&
4775
+ runs_computed_self_definer?(argument.block.body)
4776
+ end
4777
+ end
4778
+
4779
+ # Issue #2975 — `singleton_class.class_eval { def my_attr(*n) = n.each { |x| define_method(x) {} } }` in a class
4780
+ # body, or the same on `Const.singleton_class`, defines a class macro the eval-body walk files under no owner: the
4781
+ # method takes the singleton-side definer entry {#record_computed_definer} gives a `def self.my_attr`.
4782
+ def record_singleton_eval_definers(node, qualified_prefix, in_singleton_class, tables)
4783
+ return if in_singleton_class || !CLASS_EVAL_BLOCK_CALLS.include?(node.name)
4784
+
4785
+ receiver = node.receiver
4786
+ return unless singleton_class_of_receiver?(receiver)
4787
+
4788
+ current = qualified_prefix.empty? ? nil : qualified_prefix.join("::")
4789
+ owner = singleton_class_owner(node, receiver.receiver, qualified_prefix, current)
4790
+ return if owner.nil?
4791
+
4792
+ body = node.block.body
4793
+ return unless body.is_a?(Prism::StatementsNode)
4794
+
4795
+ body.body.each do |statement|
4796
+ next unless statement.is_a?(Prism::DefNode) && statement.receiver.nil?
4797
+
4798
+ record_computed_definer(statement, owner.split("::"), tables, :singleton)
4799
+ end
4800
+ end
4801
+
4802
+ CLASS_EVAL_BLOCK_CALLS = %i[class_eval module_eval class_exec module_exec].to_set.freeze
4803
+ private_constant :CLASS_EVAL_BLOCK_CALLS
4804
+
4805
+ # Whether a definer's argument list starts with a name that is not a Symbol or String literal.
4806
+ def computed_first_name?(arguments)
4807
+ !arguments.nil? && !arguments.empty? && literal_method_name(arguments.first).nil?
4808
+ end
4809
+
4367
4810
  # The `alias_method :new, :old` CallNode twin of {#record_alias_method} (#533): the NEW name joins
4368
4811
  # the discovered-methods table so calls to it stop reading undefined, and
4369
4812
  # {#apply_alias_def_nodes}'s map walk gives it the ORIGINAL def node for return inference.
@@ -4664,17 +5107,27 @@ module Rigor
4664
5107
  # (#590). Their bodies are entered as class bodies, so a member read inside one dispatches on the struct's
4665
5108
  # own class and must find the reader there: a member that shadows a `Kernel` private (`lambda`) otherwise
4666
5109
  # falls through to `Kernel#lambda`, and `lambda.upcase` reports an undefined method on `Proc` — a wrong-type
4667
- # diagnostic on correct code. No-op for `Class.new` / `Module.new` and for a factory whose members are not
4668
- # literal Symbols (nothing to register).
5110
+ # diagnostic on correct code. No-op for `Class.new` / `Module.new`. A splatted member list registers its literal
5111
+ # members and opens the instance side ({#record_meta_member_names}).
4669
5112
  def record_meta_members(factory_call, qualified_prefix, accumulator, allow_outer_block: true)
4670
5113
  factory_call = resolve_meta_factory_call(factory_call, allow_outer_block: allow_outer_block)
4671
5114
  return unless factory_call
4672
5115
 
4673
- members = meta_member_names(factory_call)
4674
- return if members.empty?
5116
+ # Issue #2968 — `K = Struct.new("Named", :a)` makes one class under two names, `K` and `Struct::Named`; the
5117
+ # members are credited on both.
5118
+ named = AnonymousMetaClass.struct_class_name(factory_call) if struct_new_call?(factory_call)
5119
+ [qualified_prefix.join("::"), named].compact.uniq.each do |class_name|
5120
+ record_meta_member_names(factory_call, class_name, accumulator)
5121
+ end
5122
+ end
4675
5123
 
4676
- class_name = qualified_prefix.join("::")
4677
- members.each { |member| record_method(accumulator, class_name, member, :instance) }
5124
+ # Issue #2968 — a splatted member list (`Struct.new(*FIELDS)`) names members this walk cannot read, so the
5125
+ # instance side opens: no name is provably missing there.
5126
+ def record_meta_member_names(factory_call, class_name, accumulator)
5127
+ if meta_members_open?(factory_call)
5128
+ record_surface_mark(accumulator, class_name, Scope::DiscoveryIndex::ENVELOPE_OPEN_INSTANCE_NAMES_MARK)
5129
+ end
5130
+ meta_member_names(factory_call).each { |member| record_method(accumulator, class_name, member, :instance) }
4678
5131
  end
4679
5132
 
4680
5133
  # Unwinds nested single-parent `Class.new(...)` calls to a root `Struct.new(...)` / `Data.define(...)`.
@@ -4705,22 +5158,33 @@ module Rigor
4705
5158
  data_define_call?(call_node) || struct_new_call?(call_node)
4706
5159
  end
4707
5160
 
4708
- # The Symbol member names of a `Data.define(*Symbol)` / `Struct.new(*Symbol [, keyword_init:])` call. For
4709
- # `Struct.new` the trailing `keyword_init:` hash is stripped by {#struct_new_positionals}; a leading
4710
- # String class name is NOT stripped, so `Struct.new("Name", :a) do` registers no members and the body
4711
- # is entered under the anonymous name (consistent between passes). `Data.define` args are all Symbols.
5161
+ # The literal Symbol member names of a `Data.define` / `Struct.new` call ({#meta_member_positionals}). A splatted
5162
+ # argument contributes none; {#meta_members_open?} says the list is then incomplete.
4712
5163
  def meta_member_names(call_node)
5164
+ meta_member_positionals(call_node).filter_map { |arg| arg.unescaped.to_sym if arg.is_a?(Prism::SymbolNode) }
5165
+ end
5166
+
5167
+ # Issue #2968 — whether a `Data.define` / `Struct.new` call's member list holds a splat, so its members are not
5168
+ # all literal and the reader set is unknown. A layout table or a closed reader set must not be built from it.
5169
+ def meta_members_open?(call_node)
5170
+ meta_member_positionals(call_node).any?(Prism::SplatNode)
5171
+ end
5172
+
5173
+ # The member arguments of a `Data.define` / `Struct.new` call. For `Struct.new` the trailing `keyword_init:` hash
5174
+ # is stripped ({#struct_new_positionals}). A leading class-name String (`Struct.new("Named", :a)`) is neither a
5175
+ # Symbol nor a splat, so it is no member and does not open the list.
5176
+ def meta_member_positionals(call_node)
4713
5177
  raw = call_node.arguments&.arguments || []
4714
- symbols = struct_new_call?(call_node) ? (struct_new_positionals(raw) || []) : raw
4715
- symbols.filter_map { |arg| arg.unescaped.to_sym if arg.is_a?(Prism::SymbolNode) }
5178
+ struct_new_call?(call_node) ? struct_new_positionals(raw) : raw
4716
5179
  end
4717
5180
 
4718
5181
  def record_def_method(def_node, qualified_prefix, in_singleton_class, accumulator)
4719
5182
  return if qualified_prefix.empty?
4720
5183
 
4721
5184
  class_name = qualified_prefix.join("::")
4722
- singleton = def_singleton?(def_node, qualified_prefix, in_singleton_class)
4723
- kind = singleton ? :singleton : :instance
5185
+ kind = def_side(def_node, qualified_prefix, in_singleton_class)
5186
+ return if kind.nil?
5187
+
4724
5188
  record_method(accumulator, class_name, def_node.name, kind, Source::ParameterEnvelope.of(def_node))
4725
5189
  end
4726
5190
 
@@ -4729,12 +5193,20 @@ module Rigor
4729
5193
  # `def decorate(obj) = obj.extend(Decorator)`. A class-body macro records the same evidence in
4730
5194
  # {#record_surface_evidence}; `send` is left out here because inside a body it is an ordinary call far
4731
5195
  # more often than a definition.
4732
- def record_def_body_evidence(def_node, qualified_prefix, tables)
5196
+ # The side a `def`'s body evidence is filed on: a `def self.x`'s singleton side, unless the caller files it as an
5197
+ # instance method elsewhere (`receiver_side: false`, #3005's `Owner::<included>`).
5198
+ def evidence_side(def_node, side, receiver_side) = receiver_side && def_node.receiver ? :singleton : side
5199
+
5200
+ def record_def_body_evidence(def_node, qualified_prefix, tables, side, receiver_side: true)
4733
5201
  return if def_node.body.nil?
4734
5202
 
5203
+ side = evidence_side(def_node, side, receiver_side)
5204
+
5205
+ definer = false
4735
5206
  Source::NodeWalker.each(def_node.body) do |node|
4736
5207
  if node.is_a?(Prism::SingletonClassNode)
4737
5208
  record_singleton_body_prepends(node, qualified_prefix, tables)
5209
+ record_constant_singleton_prepends(node, qualified_prefix, tables, owner: true)
4738
5210
  next
4739
5211
  end
4740
5212
  next unless node.is_a?(Prism::CallNode)
@@ -4742,13 +5214,629 @@ module Rigor
4742
5214
  if node.name == :extend
4743
5215
  record_object_extension(node, qualified_prefix, tables)
4744
5216
  elsif SURFACE_EVAL_CALLS.include?(node.name) || SURFACE_NAMING_CALLS.include?(node.name)
5217
+ definer ||= computed_self_define_method?(node)
4745
5218
  record_body_rewrite(node, qualified_prefix, tables)
4746
5219
  else
4747
5220
  record_singleton_prepend(node, qualified_prefix, tables, self_ok: true)
4748
5221
  end
4749
5222
  end
5223
+ record_computed_definer(def_node, qualified_prefix, tables, side) if definer
5224
+ record_hook_extends(def_node, qualified_prefix, tables, side)
5225
+ end
5226
+
5227
+ # Issue #2975 — an ActiveSupport::Concern-style hook, `def self.included(base) = base.extend(ClassMethods)`, puts
5228
+ # `ClassMethods` on every includer's singleton chain, which no `extend` edge of the includer records. The module
5229
+ # takes a `[ENVELOPE_HOOK_EXTEND_KIND, "ClassMethods"]` entry per constant extended on the hook's parameter, as
5230
+ # written, so `call.undefined-method` can resolve an includer's class-body macro calls against it.
5231
+ #
5232
+ # Issue #1592 — the entry also names the hook (`[ENVELOPE_HOOK_EXTEND_KIND, "ClassMethods", :included]`), so the
5233
+ # includer's singleton resolution chain draws the module only for the edge whose hook runs. Only a direct
5234
+ # statement of the hook body with constant arguments and no `return` / `next` / `break` in an earlier
5235
+ # statement is a fact the chain can place. Any other extend of the parameter anywhere in the body (nested,
5236
+ # conditional, after an early exit, or with an argument that is not a constant), a `base.send(:extend, …)`, a
5237
+ # `base.singleton_class.include` / `prepend` and a `class_eval`-family block on the parameter take a
5238
+ # `[ENVELOPE_HOOK_EXTEND_OPEN_KIND, hook]` entry instead of, or beside, the named one. Each definition also takes
5239
+ # a `[ENVELOPE_HOOK_DEF_KIND, hook, source]` entry, so a module that defines the hook twice (in two files, or
5240
+ # twice in one) is read as open: which definition Ruby keeps is load order.
5241
+ def record_hook_extends(def_node, qualified_prefix, tables, side)
5242
+ return unless side == :singleton && HOOK_EXTEND_DEFS.include?(def_node.name) && !qualified_prefix.empty?
5243
+
5244
+ owner = qualified_prefix.join("::")
5245
+ hook = def_node.name
5246
+ def_key = [Scope::DiscoveryIndex::ENVELOPE_HOOK_DEF_KIND, hook, def_node.slice.split.join(" ")]
5247
+ record_envelope(tables.envelopes, owner, def_key, Source::ParameterEnvelope::OPAQUE)
5248
+ param = def_node.parameters&.requireds&.first
5249
+ return unless param.is_a?(Prism::RequiredParameterNode)
5250
+
5251
+ target = ->(call) { hook_param_receiver?(call.receiver, param.name) }
5252
+ scan_hook_body(def_node.body, target, owner, tables, hook) do |node|
5253
+ hook_singleton_reshape?(node, param.name)
5254
+ end
5255
+ end
5256
+
5257
+ # The shared walk of a hook body or a concern's hook block: `target` says whether a call's receiver is the
5258
+ # includer, the block says whether a call reshapes the includer's singleton some other way.
5259
+ def scan_hook_body(body, target, owner, tables, hook)
5260
+ placeable = placeable_hook_statements(body)
5261
+ Source::NodeWalker.each(body) do |node|
5262
+ if node.is_a?(Prism::CallNode) && node.name == :extend && target.call(node)
5263
+ complete = record_hook_extend_arguments(node, owner, tables, hook)
5264
+ record_hook_extend_open(owner, tables, hook) unless complete && placeable.include?(node)
5265
+ elsif (node.is_a?(Prism::CallNode) && yield(node)) || singleton_mixin_body?(node)
5266
+ record_hook_extend_open(owner, tables, hook)
5267
+ end
5268
+ end
5269
+ end
5270
+
5271
+ # The direct statements of a body that run unless an earlier statement raised: those before the first
5272
+ # statement holding a `return`, `next` or `break` anywhere in it. An endless `def` has its one expression.
5273
+ def placeable_hook_statements(body)
5274
+ statements = body.is_a?(Prism::StatementsNode) ? body.body : [body].compact
5275
+ placeable = Set.new.compare_by_identity
5276
+ statements.each do |statement|
5277
+ break if exits_early?(statement)
5278
+
5279
+ placeable << statement
5280
+ end
5281
+ placeable
5282
+ end
5283
+
5284
+ # A `next` or `break` inside a block or lambda leaves only that block, and a lambda's `return` only the lambda;
5285
+ # a `return` inside a block leaves the hook.
5286
+ def exits_early?(node, in_block: false)
5287
+ case node
5288
+ when Prism::ReturnNode then true
5289
+ when Prism::NextNode, Prism::BreakNode then !in_block
5290
+ when Prism::LambdaNode then false
5291
+ else
5292
+ inner = in_block || node.is_a?(Prism::BlockNode)
5293
+ node.rigor_each_child { |child| return true if exits_early?(child, in_block: inner) }
5294
+ false
5295
+ end
5296
+ end
5297
+
5298
+ # A `class << self` body that mixes in, anywhere under a hook body or block.
5299
+ def singleton_mixin_body?(node)
5300
+ return false unless node.is_a?(Prism::SingletonClassNode) && node.expression.is_a?(Prism::SelfNode)
5301
+
5302
+ found = false
5303
+ Source::NodeWalker.each(node.body) do |child|
5304
+ found ||= child.is_a?(Prism::CallNode) && MIXIN_SELF_CALLS.include?(child.name) && child.receiver.nil?
5305
+ end
5306
+ found
5307
+ end
5308
+
5309
+ MIXIN_SELF_CALLS = %i[include prepend extend].freeze
5310
+ private_constant :MIXIN_SELF_CALLS
5311
+
5312
+ def hook_param_receiver?(receiver, name)
5313
+ receiver.is_a?(Prism::LocalVariableReadNode) && receiver.name == name
5314
+ end
5315
+
5316
+ # A hook-body call that may mix into the parameter's singleton class without a plain `base.extend(…)`:
5317
+ # `base.send(:extend, M)` (or `__send__` / `public_send`), `base.singleton_class.include(M)` / `prepend(M)`, and a
5318
+ # `class_eval` / `instance_eval` / `module_eval`-family call on the parameter, whose block runs with it as `self`.
5319
+ def hook_singleton_reshape?(node, name)
5320
+ receiver = node.receiver
5321
+ if HOOK_SEND_CALLS.include?(node.name)
5322
+ send_extend?(node) && hook_param_receiver?(receiver, name)
5323
+ elsif HOOK_EVAL_CALLS.include?(node.name)
5324
+ hook_param_receiver?(receiver, name)
5325
+ else
5326
+ %i[include prepend].include?(node.name) && receiver.is_a?(Prism::CallNode) &&
5327
+ receiver.name == :singleton_class && hook_param_receiver?(receiver.receiver, name)
5328
+ end
5329
+ end
5330
+
5331
+ HOOK_EVAL_CALLS = %i[class_eval class_exec module_eval module_exec instance_eval instance_exec].freeze
5332
+ private_constant :HOOK_EVAL_CALLS
5333
+
5334
+ # `send(:extend, …)`, `__send__` or `public_send`, on whatever receiver.
5335
+ def send_extend?(node)
5336
+ first = node.arguments&.arguments&.first
5337
+ HOOK_SEND_CALLS.include?(node.name) && first.is_a?(Prism::SymbolNode) && first.unescaped == "extend"
5338
+ end
5339
+
5340
+ HOOK_SEND_CALLS = %i[send __send__ public_send].freeze
5341
+ private_constant :HOOK_SEND_CALLS
5342
+
5343
+ # Records each constant argument as written and answers whether every argument was one.
5344
+ def record_hook_extend_arguments(node, owner, tables, hook)
5345
+ arguments = node.arguments&.arguments || []
5346
+ complete = !arguments.empty?
5347
+ arguments.each do |argument|
5348
+ written = Source::ConstantPath.qualified_name_or_nil(argument)
5349
+ next complete = false if written.nil?
5350
+
5351
+ key = [Scope::DiscoveryIndex::ENVELOPE_HOOK_EXTEND_KIND, written, hook]
5352
+ record_envelope(tables.envelopes, owner, key, Source::ParameterEnvelope::OPAQUE)
5353
+ end
5354
+ complete
5355
+ end
5356
+
5357
+ def record_hook_extend_open(owner, tables, hook)
5358
+ key = [Scope::DiscoveryIndex::ENVELOPE_HOOK_EXTEND_OPEN_KIND, hook]
5359
+ record_envelope(tables.envelopes, owner, key, Source::ParameterEnvelope::OPAQUE)
5360
+ end
5361
+
5362
+ HOOK_EXTEND_DEFS = %i[included prepended].to_set.freeze
5363
+ private_constant :HOOK_EXTEND_DEFS
5364
+
5365
+ # Issue #2997 — a module's `included` / `prepended` hook defined by a call rather than `def self.included`:
5366
+ # `define_singleton_method(:included) { |base| base.extend(X) }` written as a statement of the module body is
5367
+ # read as {#record_hook_extends} reads the `def` (a `[ENVELOPE_HOOK_DEF_KIND, hook, source]` entry, and the
5368
+ # block body scanned with its first parameter as the includer). (`class << self; def included(base)` already
5369
+ # reaches {#record_hook_extends} as a singleton `def`.) Any other call that may define a hook takes a
5370
+ # `[ENVELOPE_HOOK_EXTEND_OPEN_KIND, hook]` entry, for each hook a non-literal name may spell: one nested in
5371
+ # a block, method or condition, one without a block of one plain parameter, `send(:define_singleton_method,
5372
+ # …)`, `singleton_class.define_method(…)`, and `define_method` in the body of `class << self`.
5373
+ def record_hook_definer_calls(node, child_prefix, methods_acc)
5374
+ return unless node.is_a?(Prism::ModuleNode) && node.body.is_a?(Prism::StatementsNode)
5375
+
5376
+ owner = child_prefix.join("::")
5377
+ direct = node.body.body
5378
+ each_hook_definer(node.body, false) do |call, hooks|
5379
+ tables = methods_acc.at(call)
5380
+ param = readable_hook_parameter(call) if direct.any? { |statement| statement.equal?(call) }
5381
+ if param
5382
+ record_hook_block(call, param, owner, tables, hooks.first)
5383
+ else
5384
+ hooks.each { |hook| record_hook_extend_open(owner, tables, hook) }
5385
+ end
5386
+ end
5387
+ end
5388
+
5389
+ # The entries of one readable `define_singleton_method(:hook) { |base| … }`.
5390
+ def record_hook_block(call, param, owner, tables, hook)
5391
+ def_key = [Scope::DiscoveryIndex::ENVELOPE_HOOK_DEF_KIND, hook, call.slice.split.join(" ")]
5392
+ record_envelope(tables.envelopes, owner, def_key, Source::ParameterEnvelope::OPAQUE)
5393
+ body = call.block&.body
5394
+ return if body.nil?
5395
+
5396
+ target = ->(extend_call) { hook_param_receiver?(extend_call.receiver, param.name) }
5397
+ scan_hook_body(body, target, owner, tables, hook) { |inner| hook_singleton_reshape?(inner, param.name) }
5398
+ end
5399
+
5400
+ # Yields each call under a module body that may define the module's `included` / `prepended`, with the hooks
5401
+ # it may define. A nested class or module body has its own self, and so has an instance `def`'s body; in a
5402
+ # `class << self` body `define_method` defines on the module and `define_singleton_method` on its singleton
5403
+ # class, and in a singleton `def`'s body `self` is the module again.
5404
+ def each_hook_definer(node, in_singleton, &)
5405
+ case node
5406
+ when Prism::ClassNode, Prism::ModuleNode then nil
5407
+ when Prism::SingletonClassNode
5408
+ each_hook_definer(node.expression, in_singleton, &)
5409
+ each_hook_definer(node.body, !in_singleton && node.expression.is_a?(Prism::SelfNode), &)
5410
+ when Prism::DefNode
5411
+ each_hook_definer(node.body, false, &) if in_singleton || node.receiver.is_a?(Prism::SelfNode)
5412
+ else
5413
+ if node.is_a?(Prism::CallNode)
5414
+ hooks = hook_definer_names(node, in_singleton)
5415
+ yield node, hooks unless hooks.nil?
5416
+ end
5417
+ node&.rigor_each_child { |child| each_hook_definer(child, in_singleton, &) }
5418
+ end
5419
+ end
5420
+
5421
+ # The hooks a call may define on the module whose body runs it, or nil for a call that defines none.
5422
+ def hook_definer_names(node, in_singleton)
5423
+ name_node = hook_definer_name_node(node, in_singleton)
5424
+ return nil if name_node.nil?
5425
+
5426
+ name = literal_method_name(name_node)
5427
+ return HOOK_EXTEND_DEFS.include?(name) ? [name] : nil if name
5428
+
5429
+ hooks = HOOK_EXTEND_DEFS.select { |hook| hook.start_with?(literal_name_prefix(name_node)) }
5430
+ hooks.empty? ? nil : hooks
5431
+ end
5432
+
5433
+ # The literal text an interpolated name starts with (`"is_"` for `"is_#{n}"`), or `""` for a name computed any
5434
+ # other way: only a hook whose name starts with it may be the one defined.
5435
+ def literal_name_prefix(node)
5436
+ return "" unless node.is_a?(Prism::InterpolatedSymbolNode) || node.is_a?(Prism::InterpolatedStringNode)
5437
+
5438
+ first = node.parts.first
5439
+ first.is_a?(Prism::StringNode) ? first.unescaped : ""
5440
+ end
5441
+
5442
+ # The argument naming the method a call that may define a singleton method of the module defines, or nil.
5443
+ def hook_definer_name_node(node, in_singleton)
5444
+ arguments = node.arguments&.arguments || []
5445
+ if HOOK_SEND_CALLS.include?(node.name) && self_receiver?(node.receiver)
5446
+ definer = literal_method_name(arguments.first)
5447
+ arguments[1] if definer == :define_singleton_method || (in_singleton && definer == :define_method)
5448
+ elsif singleton_definer_call?(node, in_singleton)
5449
+ arguments.first
5450
+ end
5451
+ end
5452
+
5453
+ def singleton_definer_call?(node, in_singleton)
5454
+ receiver = node.receiver
5455
+ case node.name
5456
+ when :define_singleton_method then !in_singleton && self_receiver?(receiver)
5457
+ when :define_method
5458
+ (in_singleton && self_receiver?(receiver)) ||
5459
+ (receiver.is_a?(Prism::CallNode) && receiver.name == :singleton_class && self_receiver?(receiver.receiver))
5460
+ else false
5461
+ end
5462
+ end
5463
+
5464
+ def self_receiver?(receiver) = receiver.nil? || receiver.is_a?(Prism::SelfNode)
5465
+
5466
+ # The includer parameter of a `define_singleton_method(:hook) { |base| … }` the hook read can place: one literal
5467
+ # hook name, a literal block, and a plain first parameter. Nil for any other shape.
5468
+ def readable_hook_parameter(call)
5469
+ arguments = call.arguments&.arguments || []
5470
+ return nil unless call.name == :define_singleton_method && arguments.size == 1
5471
+ return nil unless literal_method_name(arguments.first) && call.block.is_a?(Prism::BlockNode)
5472
+
5473
+ params = call.block.parameters
5474
+ param = params.parameters&.requireds&.first if params.is_a?(Prism::BlockParametersNode)
5475
+ param.is_a?(Prism::RequiredParameterNode) ? param : nil
5476
+ end
5477
+
5478
+ # Issue #2986 — a module whose body runs `extend ActiveSupport::Concern` has `Concern#append_features` (or
5479
+ # `prepend_features`) extend each direct includer with `const_get(:ClassMethods)`, as #2975's hook does with its
5480
+ # constant, so the module takes a `[ENVELOPE_HOOK_EXTEND_KIND, "ClassMethods", :const_get]` entry. A
5481
+ # `class_methods do … end` block of that body is a `module_eval` of `Owner::ClassMethods`: each `def` in it is
5482
+ # also recorded on that module, with its body's evidence (a computed definer's entry among it), so the hook
5483
+ # reader resolves and reads it. The walk keeps recording the same `def` on the module itself as before. A call on
5484
+ # `self` in an `included do … end` or `prepended do … end` block takes a
5485
+ # `[ENVELOPE_HOOK_BODY_CALL_KIND, hook, name]` entry, since Ruby runs that block in the includer's class body.
5486
+ #
5487
+ # Issue #1592 — the `def` node is filed on `Owner::ClassMethods` too, so the includer's singleton resolution
5488
+ # chain, which holds that module, answers a class-side call with the `def`'s own return type.
5489
+ #
5490
+ # Issue #2997 — a `class_methods` block is read in every body of the module, not only one that runs `extend
5491
+ # ActiveSupport::Concern` itself: a reopening in another file runs it on a module that is already a concern, and
5492
+ # where it loads first Ruby raises `NoMethodError` at load, so no running program has the includer without the
5493
+ # method. The module is a concern, and its includers draw on `Owner::ClassMethods`, only when some body records
5494
+ # the concern entry.
5495
+ def record_concern_class_methods(node, child_prefix, methods_acc, def_nodes_acc)
5496
+ return unless node.is_a?(Prism::ModuleNode) && node.body.is_a?(Prism::StatementsNode)
5497
+
5498
+ concern = concern_body?(node.body, child_prefix)
5499
+ owner = child_prefix.join("::")
5500
+ if concern
5501
+ key = [Scope::DiscoveryIndex::ENVELOPE_HOOK_EXTEND_KIND, "ClassMethods", :const_get]
5502
+ record_envelope(methods_acc.at(node).envelopes, owner, key, Source::ParameterEnvelope::OPAQUE)
5503
+ record_concern_dependencies(node.body.body, child_prefix, owner, methods_acc)
5504
+ end
5505
+ node.body.body.each do |statement|
5506
+ next unless statement.is_a?(Prism::CallNode) && block_statement?(statement)
5507
+
5508
+ if statement.name == :class_methods
5509
+ record_class_methods_block(statement, [*child_prefix, "ClassMethods"], methods_acc, def_nodes_acc)
5510
+ elsif CONCERN_HOOK_BLOCKS.include?(statement.name)
5511
+ hook = concern ? statement.name : Scope::DiscoveryIndex::CONCERN_BLOCK_HOOKS.fetch(statement.name)
5512
+ record_concern_hook_block(statement, hook, owner, methods_acc)
5513
+ record_hook_block_singleton_defs(statement, hook, child_prefix, methods_acc, def_nodes_acc)
5514
+ end
5515
+ end
5516
+ end
5517
+
5518
+ # Issue #3005 — a `def self.x` in an `included do … end` (or `prepended do … end`) block defines `x` on the
5519
+ # includer's singleton class, since Ruby runs the block in the includer's class body. Each such `def` is filed
5520
+ # as an instance method of the module's `Owner::<included>` (`<prepended>`), a name no program can spell, and
5521
+ # the module takes a `[ENVELOPE_HOOK_EXTEND_KIND, "<included>", hook]` entry, so the includer's singleton
5522
+ # resolution chain holds that module where the hook's own extends go: after the includer's singleton and any
5523
+ # module prepended to it, ahead of the concern's `ClassMethods`.
5524
+ #
5525
+ # Issue #3015 — a receiverless `def` written as a statement of a `class << self` body in the block (found with
5526
+ # {#each_class_methods_def}'s stops) is filed the same way. A `class << self` body with a statement-level
5527
+ # visibility call (`private`, `private :x`, `private def x`) may make a `def` non-public, which the module does
5528
+ # not record, so it files none of its `def`s and opens the hook instead ({#record_hook_extend_open}).
5529
+ def record_hook_block_singleton_defs(statement, hook, child_prefix, methods_acc, def_nodes_acc)
5530
+ segment = "<#{statement.name}>"
5531
+ prefix = [*child_prefix, segment]
5532
+ name = prefix.join("::")
5533
+ owner = child_prefix.join("::")
5534
+ defs = hook_block_singleton_defs(statement.block.body, owner, hook, methods_acc)
5535
+ return if defs.empty?
5536
+
5537
+ defs.each do |def_node|
5538
+ record_method(methods_acc.at(def_node), name, def_node.name, :instance, Source::ParameterEnvelope.of(def_node))
5539
+ (def_nodes_acc[name] ||= {})[def_node.name] = def_node
5540
+ record_def_body_evidence(def_node, prefix, methods_acc, :instance, receiver_side: false)
5541
+ end
5542
+ key = [Scope::DiscoveryIndex::ENVELOPE_HOOK_EXTEND_KIND, segment, hook]
5543
+ record_envelope(methods_acc.at(statement).envelopes, owner, key, Source::ParameterEnvelope::OPAQUE)
5544
+ end
5545
+
5546
+ # The `def`s {#record_hook_block_singleton_defs} files, opening the hook of `owner` for each `class << self` body
5547
+ # with a visibility call.
5548
+ def hook_block_singleton_defs(body, owner, hook, methods_acc)
5549
+ defs = []
5550
+ each_class_methods_def(body) { |def_node| defs << def_node if def_node.receiver.is_a?(Prism::SelfNode) }
5551
+ each_hook_block_singleton_class(body) do |singleton|
5552
+ statements = statements_of(singleton.body)
5553
+ if statements.any? { |child| singleton_body_visibility_call?(child) }
5554
+ record_hook_extend_open(owner, methods_acc.at(singleton), hook)
5555
+ else
5556
+ statements.each { |child| defs << child if child.is_a?(Prism::DefNode) && child.receiver.nil? }
5557
+ end
5558
+ end
5559
+ defs
5560
+ end
5561
+
5562
+ # Issue #3015 — the `class << self` bodies of an `included` / `prepended` block, with {#each_class_methods_def}'s
5563
+ # stops.
5564
+ def each_hook_block_singleton_class(node, &)
5565
+ case node
5566
+ when Prism::SingletonClassNode then yield node if node.expression.is_a?(Prism::SelfNode)
5567
+ when *CONCERN_BLOCK_STOPS then nil
5568
+ else node&.rigor_each_child { |child| each_hook_block_singleton_class(child, &) }
5569
+ end
5570
+ end
5571
+
5572
+ SINGLETON_BODY_VISIBILITY_CALLS = %i[private protected public private_class_method public_class_method].freeze
5573
+ private_constant :SINGLETON_BODY_VISIBILITY_CALLS
5574
+
5575
+ def singleton_body_visibility_call?(statement)
5576
+ statement.is_a?(Prism::CallNode) && statement.receiver.nil? &&
5577
+ SINGLETON_BODY_VISIBILITY_CALLS.include?(statement.name)
5578
+ end
5579
+
5580
+ def record_class_methods_block(statement, class_methods_prefix, methods_acc, def_nodes_acc)
5581
+ each_class_methods_def(statement.block&.body) do |def_node|
5582
+ record_def_method(def_node, class_methods_prefix, false, methods_acc.at(def_node))
5583
+ record_def_node(def_node, class_methods_prefix, false, def_nodes_acc)
5584
+ record_def_body_evidence(def_node, class_methods_prefix, methods_acc, :instance)
5585
+ end
5586
+ end
5587
+
5588
+ # Issue #2992 — `Concern#append_features` (or `prepend_features`) of a concern `include`d (or `prepend`ed) into a
5589
+ # module that has already run `extend ActiveSupport::Concern` records it as a dependency of that module and
5590
+ # includes nothing; the module's final includer includes it. Only a constant argument of an `include` /
5591
+ # `prepend` on the module's own `self` ({#concern_dependency_arguments}) written as a statement of the body
5592
+ # AFTER the body's own `extend` is provably such a dependency: one written ahead of it, or in a reopening
5593
+ # without it, may run first and is an ordinary include.
5594
+ # Each such constant takes a `[ENVELOPE_CONCERN_DEPENDENCY_KIND, written]` entry; whether it names a concern is
5595
+ # the reader's question. One statement's constants are recorded last first: `include A, B` runs
5596
+ # `B.append_features` first, so the dependencies list it ahead, and a reader that reverses the entries to put a
5597
+ # later statement's nearer keeps one statement's in argument order, as Ruby searches them.
5598
+ def record_concern_dependencies(statements, child_prefix, owner, methods_acc)
5599
+ extend_at = statements.index { |statement| concern_extend?(statement, child_prefix) }
5600
+ statements.drop(extend_at + 1).each do |statement|
5601
+ arguments = concern_dependency_arguments(statement)
5602
+ next if arguments.nil?
5603
+
5604
+ arguments.reverse_each do |argument|
5605
+ written = Source::ConstantPath.qualified_name_or_nil(argument)
5606
+ next if written.nil?
5607
+
5608
+ key = [Scope::DiscoveryIndex::ENVELOPE_CONCERN_DEPENDENCY_KIND, written]
5609
+ record_envelope(methods_acc.at(statement).envelopes, owner, key, Source::ParameterEnvelope::OPAQUE)
5610
+ end
5611
+ end
5612
+ end
5613
+
5614
+ # The arguments of a statement that `include`s or `prepend`s on the module's own `self`, or nil. Issue #2995 —
5615
+ # `self.include Dep` and a receiverless or `self.` `send(:include, Dep)` run the same `append_features` as a
5616
+ # bare `include Dep`, and the include walk records them as it does.
5617
+ def concern_dependency_arguments(statement)
5618
+ return nil unless statement.is_a?(Prism::CallNode) && statement.block.nil? &&
5619
+ lexical_self_receiver?(statement.receiver)
5620
+
5621
+ kind, arguments, = mixin_call_view(statement)
5622
+ arguments if MIXIN_CALL_NAMES.include?(kind)
5623
+ end
5624
+
5625
+ # Issue #1592 — an `extend` on `self` in the block extends the includer as `base.extend` in a hook does, so it
5626
+ # records the same entries, named by the block's hook ({#record_hook_extends}), and any `extend` nested in the
5627
+ # block, a `class << self` mixin or a `send(:extend, …)` there is open.
5628
+ #
5629
+ # Issue #3005 — read in every body of the module, as a `class_methods` block is: in a body that does not run
5630
+ # `extend ActiveSupport::Concern` itself the entries name the block's gated hook
5631
+ # ({Scope::DiscoveryIndex::CONCERN_BLOCK_HOOKS}), which a reader takes as the plain hook only for a concern.
5632
+ def record_concern_hook_block(statement, hook, owner, methods_acc)
5633
+ body = statement.block.body
5634
+ each_self_call(body) do |call|
5635
+ entry = [Scope::DiscoveryIndex::ENVELOPE_HOOK_BODY_CALL_KIND, hook, call.name]
5636
+ record_envelope(methods_acc.at(call).envelopes, owner, entry, Source::ParameterEnvelope::OPAQUE)
5637
+ end
5638
+ return if body.nil?
5639
+
5640
+ target = ->(call) { call.receiver.nil? || call.receiver.is_a?(Prism::SelfNode) }
5641
+ scan_hook_body(body, target, owner, methods_acc.at(statement), hook) { |call| send_extend?(call) }
5642
+ end
5643
+
5644
+ CONCERN_HOOK_BLOCKS = %i[included prepended].to_set.freeze
5645
+ HOOK_BLOCK_CALLS = (CONCERN_HOOK_BLOCKS | [:class_methods]).freeze
5646
+
5647
+ # Issue #3015 — the blocks of the `included` / `prepended` / `class_methods` statements of a module body that
5648
+ # runs `extend ActiveSupport::Concern` itself; none for any other body, whose `included` may be its own DSL. A
5649
+ # reopening without the `extend` is not read.
5650
+ def concern_hook_blocks(node, child_prefix)
5651
+ return [] unless node.is_a?(Prism::ModuleNode) && concern_body?(node.body, child_prefix)
5652
+
5653
+ node.body.body.filter_map do |statement|
5654
+ statement.block if block_statement?(statement) && HOOK_BLOCK_CALLS.include?(statement.name)
5655
+ end
5656
+ end
5657
+
5658
+ # Issue #3015 — whether `node` is a `class << self` in a block {HookBlockSpans#mark_hook_blocks} marked for the
5659
+ # module the walk is in: the defs of its body are not the module's own singleton methods, and the walks do not
5660
+ # file them there.
5661
+ def hook_block_singleton?(node, in_singleton_class, owner_prefix, accumulator)
5662
+ !in_singleton_class && node.expression.is_a?(Prism::SelfNode) && accumulator.hook_block?(node, owner_prefix)
5663
+ end
5664
+ private_constant :CONCERN_HOOK_BLOCKS, :HOOK_BLOCK_CALLS
5665
+
5666
+ # Whether a module body runs `extend ActiveSupport::Concern` as one of its own statements (`extend Concern`
5667
+ # in a module nested in `module ActiveSupport`).
5668
+ def concern_body?(body, child_prefix)
5669
+ return false unless body.is_a?(Prism::StatementsNode)
5670
+
5671
+ body.body.any? { |statement| concern_extend?(statement, child_prefix) }
5672
+ end
5673
+
5674
+ #
5675
+ # Issue #3005 — `self.extend ActiveSupport::Concern` and a receiverless or `self.` `send` / `public_send` /
5676
+ # `__send__(:extend, ActiveSupport::Concern)` run the same `extend` on the module ({#mixin_call_view}).
5677
+ def concern_extend?(statement, child_prefix)
5678
+ return false unless statement.is_a?(Prism::CallNode) && lexical_self_receiver?(statement.receiver)
5679
+
5680
+ kind, arguments, = mixin_call_view(statement)
5681
+ return false unless kind == :extend
5682
+
5683
+ arguments.any? do |argument|
5684
+ written = Source::ConstantPath.qualified_name_or_nil(argument)&.delete_prefix("::")
5685
+ written == "ActiveSupport::Concern" || (written == "Concern" && child_prefix.first == "ActiveSupport")
5686
+ end
5687
+ end
5688
+
5689
+ # A receiverless block call (`class_methods do … end`, `included do … end`). Concern's methods take no
5690
+ # positional argument here (`class_methods` raises on one), so arguments are not looked at.
5691
+ def block_statement?(node)
5692
+ node.is_a?(Prism::CallNode) && node.receiver.nil? && node.block.is_a?(Prism::BlockNode)
5693
+ end
5694
+
5695
+ # The `def`s a `class_methods` block body writes, directly or wrapped in a call (`private def x`), not those in
5696
+ # a nested `def`, class, module, singleton-class, block or lambda body, which define elsewhere or later.
5697
+ def each_class_methods_def(node, &)
5698
+ case node
5699
+ when Prism::DefNode then yield node
5700
+ when *CONCERN_BLOCK_STOPS then nil
5701
+ else node&.rigor_each_child { |child| each_class_methods_def(child, &) }
5702
+ end
5703
+ end
5704
+
5705
+ # The calls on `self` (no receiver, or `self.`) an `included` / `prepended` block body makes, not those in a
5706
+ # nested `def`, class, module, singleton-class or lambda body.
5707
+ #
5708
+ # Issue #2992 — a block that keeps `self` and runs as the block body runs (`[1].each { my_c :x }`) makes its
5709
+ # calls on the includer too, so the walk enters it ({#self_call_block?}). Any other block stays out; its call's
5710
+ # receiver and arguments are still read.
5711
+ def each_self_call(node, &)
5712
+ case node
5713
+ when *SELF_CALL_STOPS then nil
5714
+ when Prism::CallNode
5715
+ yield node if node.receiver.nil? || node.receiver.is_a?(Prism::SelfNode)
5716
+ if node.block.is_a?(Prism::BlockNode) && !self_call_block?(node)
5717
+ [node.receiver, node.arguments].each { |part| each_self_call(part, &) }
5718
+ else
5719
+ node.rigor_each_child { |child| each_self_call(child, &) }
5720
+ end
5721
+ else node&.rigor_each_child { |child| each_self_call(child, &) }
5722
+ end
5723
+ end
5724
+
5725
+ # Whether a call's block may run on the includer while the hook block runs. Every block may, since leaving one
5726
+ # out would report the macro's names, except one that rebinds `self` ({#rebinds_self_block?}) and the calls
5727
+ # known to keep a block for later: a receiverless `lambda` / `proc` and `Proc.new`, which run it only when
5728
+ # called, and `Kernel.lambda` / `Kernel.proc`; `on_load` on any receiver, which runs it when a framework
5729
+ # component loads; and a receiverless framework callback macro (`after_save { }`), which runs it on an instance.
5730
+ #
5731
+ # Issue #3000 — the callback stop is a list of the Rails callback macros, not a `before_` / `after_` /
5732
+ # `around_` prefix: a project macro so named may yield at once (`def self.after_foo = yield`), and entering a
5733
+ # block that runs later only loses a report.
5734
+ def self_call_block?(node)
5735
+ return false if rebinds_self_block?(node)
5736
+ return false if node.name == :on_load
5737
+
5738
+ receiver = node.receiver
5739
+ if receiver.nil?
5740
+ !DEFERRED_BLOCK_CALLS.include?(node.name) && !CALLBACK_MACROS.include?(node.name)
5741
+ elsif DEFERRED_BLOCK_CALLS.include?(node.name)
5742
+ !constant_named?(receiver, "Kernel")
5743
+ else
5744
+ !(node.name == :new && constant_named?(receiver, "Proc"))
5745
+ end
5746
+ end
5747
+
5748
+ def constant_named?(node, name)
5749
+ Source::ConstantPath.qualified_name_or_nil(node)&.delete_prefix("::") == name
5750
+ end
5751
+
5752
+ DEFERRED_BLOCK_CALLS = %i[lambda proc].freeze
5753
+ # The callback macros of ActiveModel (validations), ActiveRecord (`Callbacks::CALLBACKS`, transactions),
5754
+ # ActiveJob (callbacks, `after_discard`), AbstractController (with its `prepend_` / `append_` forms),
5755
+ # ActionMailer, ActionMailbox, ActionCable (channel, connection), ActiveSupport::CurrentAttributes and
5756
+ # ActiveSupport::Reloader, read from Rails 8.1.3.
5757
+ CALLBACK_MACROS = %i[
5758
+ before_validation after_validation
5759
+ after_initialize after_find after_touch
5760
+ before_save around_save after_save before_create around_create after_create
5761
+ before_update around_update after_update before_destroy around_destroy after_destroy
5762
+ before_commit after_commit after_rollback
5763
+ after_save_commit after_create_commit after_update_commit after_destroy_commit
5764
+ before_perform around_perform after_perform before_enqueue around_enqueue after_enqueue after_discard
5765
+ before_action around_action after_action
5766
+ prepend_before_action prepend_around_action prepend_after_action
5767
+ append_before_action append_around_action append_after_action
5768
+ before_processing around_processing after_processing
5769
+ before_reset after_reset before_class_unload after_class_unload
5770
+ before_deliver around_deliver after_deliver
5771
+ before_subscribe after_subscribe before_unsubscribe after_unsubscribe
5772
+ before_command around_command after_command
5773
+ ].to_set.freeze
5774
+ private_constant :DEFERRED_BLOCK_CALLS, :CALLBACK_MACROS
5775
+
5776
+ CONCERN_BLOCK_STOPS = [Prism::DefNode, Prism::ClassNode, Prism::ModuleNode, Prism::SingletonClassNode,
5777
+ Prism::BlockNode, Prism::LambdaNode].freeze
5778
+ SELF_CALL_STOPS = (CONCERN_BLOCK_STOPS - [Prism::BlockNode]).freeze
5779
+ private_constant :CONCERN_BLOCK_STOPS, :SELF_CALL_STOPS
5780
+
5781
+ # Issue #2901 — a method whose body runs a `define_method` on its own `self` with a computed name, directly or in
5782
+ # a block that keeps `self` (`def self.my_attr(*n) = n.each { |x| define_method(x) {} }`), is a class macro: a
5783
+ # class body that calls it gains members no table names. The method takes a
5784
+ # `[ENVELOPE_COMPUTED_DEFINER_KIND, side, name]` entry, which `call.undefined-method` resolves a class's body
5785
+ # calls against. A `def Other.x` (another object's method) records none, and neither does a `define_method` inside
5786
+ # a nested `def`, a class body or a block that rebinds `self` ({#rebinds_self_block?}).
5787
+ #
5788
+ # Issue #2975 — the entry carries the side the method is defined on (`:singleton` for `def self.x` or a
5789
+ # `class << self` body, `:instance` otherwise), so a body call resolves only against a macro on the side its
5790
+ # singleton chain reaches.
5791
+ def record_computed_definer(def_node, qualified_prefix, tables, side)
5792
+ return if qualified_prefix.empty?
5793
+ return unless def_node.receiver.nil? || def_node.receiver.is_a?(Prism::SelfNode)
5794
+ return unless runs_computed_self_definer?(def_node.body)
5795
+
5796
+ key = [Scope::DiscoveryIndex::ENVELOPE_COMPUTED_DEFINER_KIND, side, def_node.name]
5797
+ record_envelope(tables.envelopes, qualified_prefix.join("::"), key, Source::ParameterEnvelope::OPAQUE)
5798
+ end
5799
+
5800
+ def computed_self_define_method?(node)
5801
+ node.name == :define_method && (node.receiver.nil? || node.receiver.is_a?(Prism::SelfNode)) &&
5802
+ computed_first_name?(node.arguments&.arguments)
4750
5803
  end
4751
5804
 
5805
+ def runs_computed_self_definer?(node)
5806
+ case node
5807
+ when Prism::DefNode, Prism::ClassNode, Prism::ModuleNode, Prism::SingletonClassNode
5808
+ return false
5809
+ when Prism::CallNode
5810
+ return true if computed_self_define_method?(node)
5811
+ # Issue #2975 — `include(Module.new { define_method(x) {} })` on `self` puts the computed method on `self`'s
5812
+ # instance side, as a direct `define_method(x)` would.
5813
+ return true if MODULE_NEW_MIXIN_CALLS.include?(node.name) &&
5814
+ (node.receiver.nil? || node.receiver.is_a?(Prism::SelfNode)) &&
5815
+ module_new_definer_argument?(node)
5816
+ if rebinds_self_block?(node)
5817
+ return [node.receiver, node.arguments].any? do |part|
5818
+ runs_computed_self_definer?(part)
5819
+ end
5820
+ end
5821
+ end
5822
+ return false unless node.is_a?(Prism::Node)
5823
+
5824
+ node.rigor_each_child { |child| return true if runs_computed_self_definer?(child) }
5825
+ false
5826
+ end
5827
+
5828
+ # A call whose block runs with another `self`: a class factory (`Class.new { }`), a definer's own body
5829
+ # (`define_method(:x) { }` runs on an instance), or an eval on a receiver other than `self`.
5830
+ def rebinds_self_block?(node)
5831
+ return false unless node.block.is_a?(Prism::BlockNode)
5832
+ return true if meta_new_constant_rvalue?(node) || SELF_DEFINER_BLOCK_CALLS.include?(node.name)
5833
+
5834
+ RECEIVER_EVAL_CALLS.include?(node.name) && !(node.receiver.nil? || node.receiver.is_a?(Prism::SelfNode))
5835
+ end
5836
+
5837
+ SELF_DEFINER_BLOCK_CALLS = %i[define_method define_singleton_method].freeze
5838
+ private_constant :SELF_DEFINER_BLOCK_CALLS
5839
+
4752
5840
  # Issue #1820 — `obj.singleton_class.prepend(M)`, or `prepend M` in a `class << obj` body, puts `M` ahead of
4753
5841
  # `obj`'s class even when the class already includes it (`prepend` re-inserts it; `extend` would not), so
4754
5842
  # `M`'s refinement answers first for that object. `M` takes {Scope::DiscoveryIndex::ENVELOPE_OBJECT_EXTENDED_MARK},
@@ -4766,6 +5854,56 @@ module Rigor
4766
5854
  record_object_extension(node, qualified_prefix, tables) if singleton
4767
5855
  end
4768
5856
 
5857
+ # Issue #2998 — a prepend to the class object's own singleton class, `prepend P` in a `class << self` body or
5858
+ # `singleton_class.prepend P` / `singleton_class.send(:prepend, P)` in the class body, files a
5859
+ # `[ENVELOPE_SINGLETON_PREPEND_KIND, "P"]` entry per constant argument, as the extends walk spells it, under the
5860
+ # owner the walk passes in. The extends walk records the same edge in `discovered_extends`, and the singleton
5861
+ # resolution chain reads the entries that match one of those edges as prepends.
5862
+ #
5863
+ # A constant receiver's `Const.singleton_class.prepend P` (or its `send` spelling) files the entry on every name
5864
+ # the constant can denote from the call site, as {#record_body_rewrite} marks it; the extends walk records that
5865
+ # edge unpositioned, so the chain's reading of it is master's and the typing read declines where they differ.
5866
+ # `prefixes` is `[owner_prefix, qualified_prefix]`: the class `self` names (empty where it names none) and the
5867
+ # lexical prefix a constant resolves against.
5868
+ #
5869
+ # Issue #3020 — an argument that names no module (`prepend(*MODS)`, `prepend helper`; {#opaque_mixin_argument?})
5870
+ # files `[ENVELOPE_SINGLETON_PREPEND_KIND, "?"]`: the extends walk lists that edge as {MixinAccumulator::UNNAMED},
5871
+ # which cannot say a prepend from an `extend`, and the chain reads the entry as a prepend region it cannot name.
5872
+ def record_own_singleton_prepend(node, prefixes, in_singleton_class, tables)
5873
+ kind, arguments, = mixin_call_view(node)
5874
+ return unless kind == :prepend
5875
+
5876
+ owners = singleton_prepend_owners(node.receiver, *prefixes, in_singleton_class)
5877
+ arguments.each do |argument|
5878
+ name = Source::ConstantPath.qualified_name(argument)
5879
+ name ||= MixinAccumulator::UNNAMED if opaque_mixin_argument?(argument)
5880
+ next unless name
5881
+
5882
+ key = [Scope::DiscoveryIndex::ENVELOPE_SINGLETON_PREPEND_KIND, name]
5883
+ owners.each { |owner| record_envelope(tables.envelopes, owner, key, Source::ParameterEnvelope::OPAQUE) }
5884
+ end
5885
+ end
5886
+
5887
+ # The classes whose singleton a prepend call on `receiver` reaches: the enclosing class for `prepend` in its
5888
+ # `class << self` body or `singleton_class.prepend` on a bare or `self.` receiver, and every name a constant
5889
+ # receiver's `Const.singleton_class` can denote.
5890
+ def singleton_prepend_owners(receiver, owner_prefix, qualified_prefix, in_singleton_class)
5891
+ own = owner_prefix.empty? ? EMPTY_PREFIX : [owner_prefix.join("::")]
5892
+ return own if in_singleton_class && lexical_self_receiver?(receiver)
5893
+ return EMPTY_PREFIX unless singleton_class_of_receiver?(receiver)
5894
+
5895
+ object = receiver.receiver
5896
+ return in_singleton_class ? EMPTY_PREFIX : own if lexical_self_receiver?(object)
5897
+ return EMPTY_PREFIX unless object.is_a?(Prism::ConstantReadNode) || object.is_a?(Prism::ConstantPathNode)
5898
+
5899
+ constant_receiver_candidates(object, qualified_prefix)
5900
+ end
5901
+
5902
+ # A bare or `self.` receiver's `singleton_class` call: `singleton_class` or `self.singleton_class`.
5903
+ def self_singleton_class?(receiver)
5904
+ singleton_class_of_receiver?(receiver) && lexical_self_receiver?(receiver.receiver)
5905
+ end
5906
+
4769
5907
  def singleton_class_call?(receiver, self_ok)
4770
5908
  return false unless receiver.is_a?(Prism::CallNode) && receiver.name == :singleton_class
4771
5909
  return false unless receiver.arguments.nil?
@@ -4774,6 +5912,28 @@ module Rigor
4774
5912
  self_ok || !(object.nil? || object.is_a?(Prism::SelfNode))
4775
5913
  end
4776
5914
 
5915
+ # Issue #3010 — the singleton-prepend entries of a `class << Const` body's prepends on the other names the
5916
+ # constant can denote ({#constant_singleton_owners}); the body's own walk files them under the owner. The
5917
+ # extends walk records the matching edges, unpositioned ({#record_constant_singleton_candidates}). In a method
5918
+ # body, which the methods walk does not enter, `owner:` files them under the owner too.
5919
+ def record_constant_singleton_prepends(node, qualified_prefix, tables, owner: false)
5920
+ return unless constant_singleton_class?(node)
5921
+
5922
+ resolved, others = constant_singleton_owners(node, qualified_prefix)
5923
+ others = [resolved, *others] if owner
5924
+ return if others.empty? || node.body.nil?
5925
+
5926
+ each_singleton_body_prepend(node.body) do |call|
5927
+ mixin_call_view(call)[1].each do |argument|
5928
+ name = Source::ConstantPath.qualified_name(argument)
5929
+ next unless name
5930
+
5931
+ key = [Scope::DiscoveryIndex::ENVELOPE_SINGLETON_PREPEND_KIND, name]
5932
+ others.each { |owner| record_envelope(tables.envelopes, owner, key, Source::ParameterEnvelope::OPAQUE) }
5933
+ end
5934
+ end
5935
+ end
5936
+
4777
5937
  def record_singleton_body_prepends(node, qualified_prefix, tables)
4778
5938
  return if node.body.nil?
4779
5939
 
@@ -4805,17 +5965,44 @@ module Rigor
4805
5965
  end
4806
5966
  end
4807
5967
 
4808
- # `def Foo.bar` inside `module Foo` (or `def Meta.init` inside `module Meta`) is semantically equivalent to `def
4809
- # self.bar`: at the def-site, the runtime value of the constant `Foo` is the module itself (== `self`). Recognise
4810
- # the form so the method registers as singleton on the enclosing class.
5968
+ # The side of the enclosing class `qualified_prefix` a `def` defines its method on: `:instance`, `:singleton`, or
5969
+ # nil when the def defines no method of that class.
4811
5970
  #
4812
- # The cross-class form `def Bar.baz` inside `module Foo` — where the receiver names a constant other than the
4813
- # enclosing class — is not supported at this slice; falls through to `:instance` (current behaviour) rather than
4814
- # silently re-routing the registration.
5971
+ # `in_singleton_class` is true in a `class << self` body and `:eval` in an `instance_eval` block on the class
5972
+ # ({#def_context}), whose `self` is the class itself.
5973
+ #
5974
+ # - A receiverless def lands on the instance side, or on the singleton side in either body.
5975
+ # - `def self.x` is a singleton method. `def Foo.x` inside `module Foo` (or `def Meta.init` inside `module Meta`)
5976
+ # is the same: at the def-site the constant `Foo` is the module itself (== `self`).
5977
+ # - Issue #3155 — any other receiver (`def String.q`, `def A.z` in `A::B`, `def obj.q` on a local or a constant
5978
+ # holding an object) is a singleton method of that object, not the enclosing class, and `def self.x` in a
5979
+ # `class << self` body is a method of the singleton class's own singleton: nil. The def's body is still
5980
+ # walked; only the method is not filed on the enclosing class. A constant receiver's own singleton is
5981
+ # {#record_foreign_singleton_def}'s.
5982
+ def def_side(def_node, qualified_prefix, in_singleton_class)
5983
+ receiver = def_node.receiver
5984
+ if receiver.nil?
5985
+ in_singleton_class ? :singleton : :instance
5986
+ elsif receiver.is_a?(Prism::SelfNode)
5987
+ in_singleton_class == true ? nil : :singleton
5988
+ elsif def_receiver_targets_lexical_self?(receiver, qualified_prefix)
5989
+ :singleton
5990
+ end
5991
+ end
5992
+
5993
+ # The `in_singleton_class` {#def_side} reads for a def under a walk's two flags: an `instance_eval` block's
5994
+ # `defs_singleton` (`self` is the receiver, so `def self.x` is its singleton method) unless the def is also in
5995
+ # a `class << self` body.
5996
+ def def_context(in_singleton_class, defs_singleton)
5997
+ in_singleton_class || (defs_singleton ? :eval : false)
5998
+ end
5999
+
4815
6000
  def def_singleton?(def_node, qualified_prefix, in_singleton_class)
4816
- return true if def_node.receiver.is_a?(Prism::SelfNode) || in_singleton_class
6001
+ def_side(def_node, qualified_prefix, in_singleton_class) == :singleton
6002
+ end
4817
6003
 
4818
- def_receiver_targets_lexical_self?(def_node.receiver, qualified_prefix)
6004
+ def foreign_def?(def_node, qualified_prefix, in_singleton_class)
6005
+ def_side(def_node, qualified_prefix, in_singleton_class).nil?
4819
6006
  end
4820
6007
 
4821
6008
  # Only `Prism::ConstantReadNode` is observed in real Ruby — Prism mis-parses `def C::P.method` as `def C.P` (Ruby
@@ -4848,7 +6035,7 @@ module Rigor
4848
6035
  # ADR-119 WD3 — the slot follows its writer (later-wins): a certain write clears the key from
4849
6036
  # `contested_discovered_def_nodes` and a possible one adds it ({MethodTables#certain}, set for `def_node`).
4850
6037
  def record_def_node(def_node, qualified_prefix, in_singleton_class, accumulator, tables = nil)
4851
- return if def_singleton?(def_node, qualified_prefix, in_singleton_class)
6038
+ return unless def_side(def_node, qualified_prefix, in_singleton_class) == :instance
4852
6039
 
4853
6040
  class_name = qualified_prefix.empty? ? TOP_LEVEL_DEF_KEY : qualified_prefix.join("::")
4854
6041
  accumulator[class_name] ||= {}
@@ -4873,8 +6060,9 @@ module Rigor
4873
6060
 
4874
6061
  # Issue #1989 — the instance method names a non-`def` definer written on the class body's self replaces:
4875
6062
  # each reader (and `x=` writer) an `attr_*` call declares, a literal `define_method(:x)`, and the new name of
4876
- # an `alias_method :x, :y` call. A non-literal name replaces nothing Rigor can name.
4877
- def call_superseded_names(call_node)
6063
+ # an `alias_method :x, :y` call, and each instance-side name of a Forwardable delegator (#2793). A non-literal
6064
+ # name replaces nothing Rigor can name.
6065
+ def call_superseded_names(call_node, qualified_prefix, accumulator)
4878
6066
  if (macro = attr_macro(call_node))
4879
6067
  names = []
4880
6068
  each_attr_macro_name(call_node) do |base|
@@ -4884,6 +6072,8 @@ module Rigor
4884
6072
  names
4885
6073
  elsif call_node.name == :define_method
4886
6074
  define_method_superseded_name(call_node)
6075
+ elsif (forwarded = forwardable_superseded_names(call_node, qualified_prefix, accumulator)).any?
6076
+ forwarded
4887
6077
  else
4888
6078
  Source::AliasNames.alias_method_call_names(call_node)&.first(1) || []
4889
6079
  end
@@ -4969,6 +6159,10 @@ module Rigor
4969
6159
  # The singleton walk's accumulator: the `{class => {name => DefNode}}` table, plus the contested slots, which
4970
6160
  # follow each slot's writer ({#follow_slot_writer}'s rule).
4971
6161
  class SingletonDefTable < Hash
6162
+ include HookBlockSpans
6163
+
6164
+ attr_accessor :hook_block_spans
6165
+
4972
6166
  # Issue #1936 — contests the slots of `names` (every slot of `class_name` for `:all`) that an earlier write
4973
6167
  # filled: a later non-`def` definer replaced the `def` they hold. The slot keeps the `def`, as
4974
6168
  # {ScopeIndexer#supersede_def_nodes} keeps the instance one, and a later write clears the contest.
@@ -5160,7 +6354,7 @@ module Rigor
5160
6354
  mf_offsets, ranges, def_owner_prefix, singleton_cref,
5161
6355
  defs_singleton)
5162
6356
  owner = defs_singleton == :unnameable ? EMPTY_PREFIX : (def_owner_prefix || qualified_prefix)
5163
- record_deferred_def(node, owner, in_singleton_class || defs_singleton, inside_deferred,
6357
+ record_deferred_def(node, owner, def_context(in_singleton_class, defs_singleton), inside_deferred,
5164
6358
  mf_offsets, ranges)
5165
6359
  walk_deferred_children(node, qualified_prefix, in_singleton_class, true, mf_offsets,
5166
6360
  ranges, def_owner_prefix, singleton_cref: singleton_cref,
@@ -5428,18 +6622,35 @@ module Rigor
5428
6622
  # proves `S::A` exists). Memoized on the `Prism::Source` every node's
5429
6623
  # location shares — threading the set through the dozen eval-consuming
5430
6624
  # walks would dwarf the resolution it feeds, so the one re-parse this
5431
- # needs happens once per file. The memo lives in an ivar ON the source
5432
- # (rather than a WeakMap keyed on it): the entry's lifetime is the
5433
- # parse's own, nothing mutable sits in a constant, and a worker parsing
5434
- # the same text gets its own source and its own set.
6625
+ # needs happens once per file. The memo is a {TypingState} Memo, a
6626
+ # WeakKeyMap keyed on the source: the entry's lifetime is the parse's own
6627
+ # (the map holds the key weakly), the set is a pure function of the
6628
+ # source's text, and a worker parsing the same text gets its own source
6629
+ # and its own set. Not run-scoped: an entry holds no run's tables.
6630
+ DECLARED_NAMES = TypingState.memo(
6631
+ :scope_index_declared_names,
6632
+ audit: "scope index declared names",
6633
+ reason: "a WeakKeyMap keyed on the Prism::Source; the frozen set is a pure function of the source's text"
6634
+ )
6635
+ private_constant :DECLARED_NAMES
6636
+
5435
6637
  def eval_file_declared_names(node)
5436
6638
  source = node.location.send(:source)
5437
- source.instance_variable_get(:@rigor_declared_names) ||
5438
- source.instance_variable_set(:@rigor_declared_names, begin
5439
- names = Set.new
5440
- collect_declared_constant_names(Prism.parse(source.source).value, [], names)
5441
- names.freeze
5442
- end)
6639
+ return scan_declared_names(source) if MemoAudit.enabled? && MemoAudit.suspended?
6640
+
6641
+ table = TypingState.current.weak_key_map(DECLARED_NAMES)
6642
+ stored = table[source]
6643
+ if stored.nil?
6644
+ return table[source] = MemoAudit.key_audit("scope index declared names") { scan_declared_names(source) }
6645
+ end
6646
+
6647
+ MemoAudit.check("scope index declared names", stored, source.source.bytesize) { scan_declared_names(source) }
6648
+ end
6649
+
6650
+ def scan_declared_names(source)
6651
+ names = Set.new
6652
+ collect_declared_constant_names(Prism.parse(source.source).value, [], names)
6653
+ names.freeze
5443
6654
  end
5444
6655
 
5445
6656
  # The walk behind {eval_file_declared_names}: every qualified constant
@@ -5669,7 +6880,11 @@ module Rigor
5669
6880
  return
5670
6881
  end
5671
6882
 
5672
- kind = if def_singleton?(def_node, qualified_prefix, in_singleton_class)
6883
+ side = def_side(def_node, qualified_prefix, in_singleton_class)
6884
+ # Issue #3155 — a def on another object's singleton claims no owner or side, as a deferred one.
6885
+ return ranges << [start, def_node.location.end_offset, def_node.name, nil, nil] if side.nil?
6886
+
6887
+ kind = if side == :singleton
5673
6888
  :singleton
5674
6889
  elsif ModuleFunctionState.deferred_module_function?(mf_offsets, start)
5675
6890
  :both
@@ -5684,7 +6899,7 @@ module Rigor
5684
6899
  # `module_function` toggle threads correctly across the body's *sibling* statements (a child-by-child recursion
5685
6900
  # would reset it). At the top level / inside an arbitrary node there is no `module_function` state to carry, so
5686
6901
  # descent is a plain per-child walk.
5687
- def walk_singleton_def_nodes(node, qualified_prefix, in_singleton_class, accumulator, # rubocop:disable Metrics/CyclomaticComplexity, Metrics/MethodLength, Metrics/PerceivedComplexity
6902
+ def walk_singleton_def_nodes(node, qualified_prefix, in_singleton_class, accumulator, # rubocop:disable Metrics/AbcSize, Metrics/CyclomaticComplexity, Metrics/MethodLength, Metrics/PerceivedComplexity
5688
6903
  def_owner_prefix = nil, singleton_cref: false,
5689
6904
  defs_singleton: false)
5690
6905
  return unless node.is_a?(Prism::Node)
@@ -5708,6 +6923,7 @@ module Rigor
5708
6923
  # ownerless like `class << <non-constant>`; nameable headers re-anchor.
5709
6924
  child_cref = unnameable_decl?(node, self_decl, singleton_cref)
5710
6925
  body_prefix = child_cref ? [] : child_prefix
6926
+ accumulator.mark_hook_blocks(concern_hook_blocks(node, child_prefix), child_prefix) unless child_cref
5711
6927
  if node.body
5712
6928
  walk_singleton_body(node.body, body_prefix, false, accumulator, nil,
5713
6929
  singleton_cref: child_cref)
@@ -5723,7 +6939,8 @@ module Rigor
5723
6939
  walk_singleton_def_nodes(node.expression, qualified_prefix, in_singleton_class,
5724
6940
  accumulator, def_owner_prefix, singleton_cref: singleton_cref,
5725
6941
  defs_singleton: defs_singleton)
5726
- if node.body
6942
+ if node.body && !hook_block_singleton?(node, in_singleton_class, def_owner_prefix || qualified_prefix,
6943
+ accumulator)
5727
6944
  walk_singleton_body(node.body, qualified_prefix, true, accumulator, singleton_prefix,
5728
6945
  singleton_cref: true)
5729
6946
  end
@@ -5735,9 +6952,11 @@ module Rigor
5735
6952
  when Prism::DefNode
5736
6953
  # `:unnameable` is the `class <<` + `instance_eval` definee — the singleton's own
5737
6954
  # singleton — which this table cannot name either.
6955
+ owner = def_owner_prefix || qualified_prefix
6956
+ return if accumulator.hook_block_self_def?(node, owner)
6957
+
5738
6958
  unless defs_singleton == :unnameable
5739
- record_singleton_def_node(node, def_owner_prefix || qualified_prefix,
5740
- in_singleton_class || defs_singleton, false, accumulator)
6959
+ record_singleton_def_node(node, owner, def_context(in_singleton_class, defs_singleton), false, accumulator)
5741
6960
  end
5742
6961
  return
5743
6962
  end
@@ -5995,7 +7214,7 @@ module Rigor
5995
7214
  # `:unnameable` is the `class <<` + `instance_eval` definee — the singleton's own
5996
7215
  # singleton — which this table cannot name either.
5997
7216
  unless defs_singleton == :unnameable
5998
- record_singleton_def_node(stmt, owner_prefix, in_singleton_class || defs_singleton,
7217
+ record_singleton_def_node(stmt, owner_prefix, def_context(in_singleton_class, defs_singleton),
5999
7218
  module_function_on, accumulator)
6000
7219
  end
6001
7220
  next
@@ -6037,8 +7256,9 @@ module Rigor
6037
7256
  # function's copy as possible, so the slot holds the later def, contested, rather than the def the
6038
7257
  # slot held before (#1968).
6039
7258
  def record_singleton_def_node(def_node, qualified_prefix, in_singleton_class, module_function_on, accumulator)
6040
- declared = def_singleton?(def_node, qualified_prefix, in_singleton_class)
6041
- return unless declared || module_function_on
7259
+ side = def_side(def_node, qualified_prefix, in_singleton_class)
7260
+ declared = side == :singleton
7261
+ return unless declared || (module_function_on && side == :instance)
6042
7262
  return if qualified_prefix.empty?
6043
7263
 
6044
7264
  class_name = qualified_prefix.join("::")
@@ -6634,6 +7854,7 @@ module Rigor
6634
7854
  def record_data_member_layout(accumulator, qualified_parts, expr, allow_outer_block: true)
6635
7855
  expr = resolve_meta_factory_call(expr, allow_outer_block: allow_outer_block)
6636
7856
  return unless expr && data_define_call?(expr)
7857
+ return if meta_members_open?(expr)
6637
7858
 
6638
7859
  members = meta_member_names(expr)
6639
7860
  return if members.empty?
@@ -6756,6 +7977,7 @@ module Rigor
6756
7977
  def record_struct_member_layout(accumulator, qualified_parts, expr, allow_outer_block: true)
6757
7978
  expr = resolve_meta_factory_call(expr, allow_outer_block: allow_outer_block)
6758
7979
  return unless expr && struct_new_call?(expr)
7980
+ return if meta_members_open?(expr)
6759
7981
 
6760
7982
  members = meta_member_names(expr)
6761
7983
  return if members.empty?
@@ -6924,6 +8146,7 @@ module Rigor
6924
8146
 
6925
8147
  _self_decl, child_prefix, child_cref = ctx
6926
8148
  accumulator.direct_body(node.body) if accumulator.direct?(node)
8149
+ accumulator.class_body(node.body)
6927
8150
  walk_class_includes(node.body, child_cref ? [] : child_prefix,
6928
8151
  child_cref ? nil : child_prefix.join("::"), accumulator,
6929
8152
  singleton_cref: child_cref)
@@ -7020,7 +8243,8 @@ module Rigor
7020
8243
  def record_mixin_call(node, qualified_prefix, current_class, accumulator)
7021
8244
  taint_opaque_eval(node, current_class, accumulator)
7022
8245
  view = mixin_call_view(node)
7023
- return if view.nil? || view.first.nil?
8246
+ return taint_computed_mixin_send(node, current_class, accumulator, INSTANCE_MIXIN_NAMES, :include) if view.nil?
8247
+ return if view.first.nil?
7024
8248
 
7025
8249
  kind = view.first
7026
8250
  current_class ||= toplevel_include_owner(node, qualified_prefix, current_class, accumulator)
@@ -7030,20 +8254,68 @@ module Rigor
7030
8254
  return if instance_mixin_recorded?(node, view, qualified_prefix, current_class, accumulator)
7031
8255
 
7032
8256
  # A mixin call the tables cannot hold still reshapes the ancestry: taint the whole owner side.
7033
- effects.each { |owner, side| accumulator.taint(owner, side) }
8257
+ taint_unrecorded_mixin(node, kind, view[1], effects, accumulator)
8258
+ end
8259
+
8260
+ # Issue #2991 — an unrecorded `include` / `prepend` that reaches the instance side (its receiver is not a
8261
+ # `singleton_class` call) leaves that side's module SET unknown when an argument is not a constant
8262
+ # (`include(*MODS)`, `include helper`) or the receiver is a hook parameter (`base.include M`), which lists
8263
+ # {MixinAccumulator::UNNAMED}. A `Module.new { … }` argument keeps its #2975 reading. A hook's `base.extend M` or
8264
+ # `base.singleton_class.include M` taints the instance side's order alone: it mixes nothing into it.
8265
+ #
8266
+ # Issue #2995 — an instance-side call whose arguments are all constants is recorded wherever the walk names its
8267
+ # owner (`self.include M` and a receiverless `send(:include, M)` included), so none reaches here with a module
8268
+ # the tables do not hold. The extends walk's taint of `Const.include M` inside `class << self` is the one
8269
+ # constant-argument call that does, and the include walk records that edge under `Const`.
8270
+ #
8271
+ # Issue #2994 — the singleton side lists {MixinAccumulator::UNNAMED} too when an unrecorded call reaching it has
8272
+ # an argument that is not a constant: `extend(*EMODS)`, `extend helper`, `include(*MODS)` inside `class << self`,
8273
+ # or a `self` argument. A hook parameter's `base.extend(*X)` extends the includer, not the module the hook is
8274
+ # written in, so it leaves that module's singleton side unnamed-free.
8275
+ def taint_unrecorded_mixin(node, kind, arguments, effects, accumulator)
8276
+ opaque = arguments.any? { |arg| opaque_mixin_argument?(arg) }
8277
+ hook = hook_parameter_receiver?(node.receiver, accumulator)
8278
+ unnamed = INSTANCE_MIXIN_NAMES.include?(kind) && !singleton_class_eval_receiver?(node.receiver) &&
8279
+ (hook || opaque)
8280
+ effects.each do |owner, side|
8281
+ accumulator.taint(owner, side, unnamed: side == :include ? unnamed : opaque && !hook)
8282
+ end
7034
8283
  end
7035
8284
 
7036
- # Records the instance-side edges of a call {#mixin_call_recorded?} accepts. False when it recorded
7037
- # nothing — the caller then taints the owner instead. `view` is {#mixin_call_view}'s, so
7038
- # `Integer.send(:include, Ext)` records `Ext` as `Integer.include(Ext)` does (issue #1722).
8285
+ # A `self` argument counts as opaque, as the instance side reads it: a `Target.send(:extend, self)` written in a
8286
+ # module body reaches here with the module no table records under `Target`.
8287
+ def opaque_mixin_argument?(arg)
8288
+ Source::ConstantPath.qualified_name(arg).nil? && !module_new_call?(arg)
8289
+ end
8290
+
8291
+ # A hook parameter itself (`base` in `def self.included(base)`): its class is the includer, which the walk cannot
8292
+ # name, so a mixin on it is read as unnamed rather than by its constant (#2991).
8293
+ def hook_parameter_receiver?(receiver, accumulator)
8294
+ receiver.is_a?(Prism::LocalVariableReadNode) && accumulator.hook_local?(receiver.name)
8295
+ end
8296
+
8297
+ # Records the instance-side edges of an `include` / `prepend` call. False when it recorded nothing — the caller
8298
+ # then taints the owner instead. `view` is {#mixin_call_view}'s, so `Integer.send(:include, Ext)` records `Ext` as
8299
+ # `Integer.include(Ext)` does (issue #1722).
8300
+ #
8301
+ # A receiverless `include` / `prepend` needs an enclosing declaration to file the edge under, and the RECEIVER
8302
+ # form (`Base.prepend(Loud)`, issue #1123; `Integer.include(Ext)` and `Integer.send(:include, Ext)`, issue
8303
+ # #1722) files it under the class the receiver names. A receiver-form edge is unpositioned wherever it is written
8304
+ # ({MixinAccumulator#note}), so a reader that depends on ORDER declines on it, while the set-shaped readers
8305
+ # (undefined-method suppression, arity, visibility) see the module as they see an in-body `include`.
8306
+ #
8307
+ # Issue #2995 — `self.include M`, `self.prepend M` and a receiverless or `self.` `send` / `public_send` /
8308
+ # `__send__(:include | :prepend, M)` are the same call on the same `self` as a bare `include M`, so they file
8309
+ # the edge under the enclosing class as it does, positioned where it would be ({MixinAccumulator#note}). The
8310
+ # walk names that `self` exactly where it names a bare `include`'s: `current_class` is nil wherever it cannot.
7039
8311
  def instance_mixin_recorded?(node, view, qualified_prefix, current_class, accumulator)
7040
- kind, arguments, via_send = view
7041
- return false unless mixin_call_recorded?(node, kind, via_send, current_class)
8312
+ kind, arguments, = view
8313
+ return false unless mixin_call_recorded?(kind)
7042
8314
 
7043
8315
  targets = arguments.filter_map { |arg| Source::ConstantPath.qualified_name(arg) }
7044
8316
  return false if targets.empty?
7045
8317
 
7046
- owner = node.receiver.nil? ? current_class : prepend_call_receiver(node, qualified_prefix)
8318
+ owner = lexical_self_receiver?(node.receiver) ? current_class : prepend_call_receiver(node, qualified_prefix)
7047
8319
  return false if owner.nil?
7048
8320
 
7049
8321
  write_mixin_targets(accumulator, owner, targets, prepend: kind == :prepend)
@@ -7053,7 +8325,234 @@ module Rigor
7053
8325
 
7054
8326
  MIXIN_KINDS = %i[include prepend extend].freeze
7055
8327
  SEND_CALL_NAMES = %i[send public_send __send__].freeze
7056
- private_constant :MIXIN_KINDS, :SEND_CALL_NAMES
8328
+ EXTEND_MIXIN_NAMES = %i[extend].freeze
8329
+ private_constant :MIXIN_KINDS, :SEND_CALL_NAMES, :EXTEND_MIXIN_NAMES
8330
+
8331
+ # Issue #3006 — a receiverless or `self.` `send` / `public_send` / `__send__` written in a class body
8332
+ # ({MixinAccumulator#class_body_scope}) whose name is computed and which passes at least one further argument
8333
+ # may be an `include`, `prepend` or `extend` the walk cannot record: it lists {MixinAccumulator::UNNAMED} on
8334
+ # `side` of the enclosing class when the name may be one of `kinds`. A name that is a local of the call's scope
8335
+ # bound only to name literals is read as those names ({#local_send_names}), so `m = :attr_reader; send(m, :x)`
8336
+ # marks nothing. A hook parameter's `base.send(meth, M)` is read wherever the hook is, as
8337
+ # {#taint_unrecorded_mixin} reads `base.send(:include, M)`.
8338
+ def taint_computed_mixin_send(node, current_class, accumulator, kinds, side, in_singleton: false)
8339
+ return unless current_class
8340
+
8341
+ name = computed_send_name(node, accumulator)
8342
+ return if name.nil?
8343
+
8344
+ scope = accumulator.class_body_scope(node)
8345
+ hook = hook_parameter_receiver?(node.receiver, accumulator)
8346
+ return if scope.nil? && !hook
8347
+
8348
+ names = scope && local_send_names(name, scope)
8349
+ # Inside `class << self` the instance walk is ownerless, so this walk keeps the hook's instance-side taint too,
8350
+ # as {#record_extend_call} does for `base.send(:include, M)`.
8351
+ sides = in_singleton ? [side, :include] : [side]
8352
+ return taint_computed_hook_send(current_class, accumulator, names, sides) if hook
8353
+ return if names&.none? { |literal| kinds.any? { |kind| kind.name == literal } }
8354
+
8355
+ accumulator.taint(current_class, side, unnamed: true)
8356
+ end
8357
+
8358
+ # A hook parameter's computed `send`, read as {#taint_unrecorded_mixin} reads the literal one: it taints the
8359
+ # enclosing module's `sides` when the name may be any mixin's, and lists the unnamed marker on the instance side
8360
+ # alone, when the name may be `include` or `prepend`.
8361
+ def taint_computed_hook_send(current_class, accumulator, names, sides)
8362
+ return if names&.none? { |literal| MIXIN_KINDS.any? { |kind| kind.name == literal } }
8363
+
8364
+ instance = names.nil? || names.any? { |literal| INSTANCE_MIXIN_NAMES.any? { |kind| kind.name == literal } }
8365
+ sides.each { |side| accumulator.taint(current_class, side, unnamed: side == :include && instance) }
8366
+ end
8367
+
8368
+ # The name argument of a receiverless, `self.` or hook-parameter `send`-family call that passes at least one
8369
+ # further argument, when no literal spells it; else nil.
8370
+ def computed_send_name(node, accumulator)
8371
+ return unless SEND_CALL_NAMES.include?(node.name)
8372
+ return unless lexical_self_receiver?(node.receiver) || hook_parameter_receiver?(node.receiver, accumulator)
8373
+
8374
+ arguments = node.arguments&.arguments
8375
+ return if arguments.nil? || arguments.size < 2
8376
+
8377
+ first = arguments.first
8378
+ first unless literal_send_name(first)
8379
+ end
8380
+
8381
+ LOCAL_SCOPE_BOUNDARIES = [Prism::DefNode, Prism::ClassNode, Prism::ModuleNode, Prism::SingletonClassNode].freeze
8382
+ private_constant :LOCAL_SCOPE_BOUNDARIES
8383
+
8384
+ # The names a local `send` argument can hold: every literal a `local = :name` / `local = "name"` in `scope`
8385
+ # writes, or nil when the argument is not a local, or when the local has no such write or any other binding in
8386
+ # `scope` — a parameter (the `def`'s or a block's, which may shadow it), a multiple or operator write, a pattern
8387
+ # or a regexp capture. Blocks share their scope's locals, so they are searched; a nested `def` or declaration
8388
+ # opens a scope of its own, so it is not.
8389
+ #
8390
+ # Issue #3024 — one block parameter is not opaque: as the FIRST parameter of `each` / `each_with_index` /
8391
+ # `map` over a literal array (or a constant the same scope assigns a literal array), it holds each element in
8392
+ # turn, so the elements are read as the candidate names ({#iterated_element_source}). An element that is not
8393
+ # a name literal still means "may be anything".
8394
+ def local_send_names(argument, scope)
8395
+ return nil unless argument.is_a?(Prism::LocalVariableReadNode)
8396
+
8397
+ names = []
8398
+ root = scope.is_a?(Prism::DefNode) ? scope.compact_child_nodes : [scope]
8399
+ return nil unless collect_local_send_names(root, argument.name, names, root)
8400
+
8401
+ names.empty? ? nil : names
8402
+ end
8403
+
8404
+ def collect_local_send_names(nodes, local, names, scope_nodes)
8405
+ nodes.all? do |node|
8406
+ next true if LOCAL_SCOPE_BOUNDARIES.any? { |kind| node.is_a?(kind) }
8407
+
8408
+ iterated = iterated_element_names(node, local, scope_nodes)
8409
+ next false if iterated == false
8410
+
8411
+ if iterated
8412
+ names.concat(iterated)
8413
+ next collect_local_send_names(iterated_element_children(node), local, names, scope_nodes)
8414
+ end
8415
+
8416
+ literal = local_send_name_literal(node, local)
8417
+ next false if literal == :clash
8418
+
8419
+ names << literal if literal
8420
+
8421
+ collect_local_send_names(node.compact_child_nodes, local, names, scope_nodes)
8422
+ end
8423
+ end
8424
+
8425
+ # Issue #3024 — the `each` / `each_with_index` / `map` arm of {#collect_local_send_names}: when `node` is
8426
+ # such an iterator whose first block parameter is `local`, the iterated array's elements are `local`'s
8427
+ # candidate names ({#iterated_element_source}), each read as {#literal_send_name}. False when an element
8428
+ # may spell any name; nil when `node` is not that iterator, so the caller's plain-write and binding checks
8429
+ # apply.
8430
+ def iterated_element_names(node, local, scope_nodes)
8431
+ elements = iterated_element_source(node, local, scope_nodes)
8432
+ return if elements.nil?
8433
+ return false if elements == :unknown
8434
+
8435
+ names = elements.map { |element| literal_send_name(element) }
8436
+ return false if names.any?(&:nil?)
8437
+
8438
+ names
8439
+ end
8440
+
8441
+ # The name literal a `local = :name` / `local = "name"` write binds `local` to for
8442
+ # {#collect_local_send_names}, `:clash` when `node` binds `local` to anything else — a non-literal write,
8443
+ # a target, an operator write, a parameter — or nil when `node` does not bind `local` at all.
8444
+ def local_send_name_literal(node, local)
8445
+ if node.is_a?(Prism::LocalVariableWriteNode) && node.name == local
8446
+ name = literal_send_name(node.value)
8447
+ return :clash if name.nil?
8448
+
8449
+ return name
8450
+ end
8451
+ :clash if local_binding?(node, local)
8452
+ end
8453
+
8454
+ # Issue #3024 — the array elements `node` iterates when it is an `each` / `each_with_index` / `map` call
8455
+ # whose FIRST block parameter is `local`: a literal `ArrayNode` receiver's elements, or a bare constant the
8456
+ # same scope assigns a literal array exactly once ({#constant_array_elements}). `:unknown` when the call is
8457
+ # such an iterator on `local` but its elements cannot be read — a non-literal receiver, or a constant that
8458
+ # is unwritten, multiply written or not array-valued — so the parameter may hold anything; nil when `node`
8459
+ # is not the iterator or `local` is not its first parameter.
8460
+ def iterated_element_source(node, local, scope_nodes)
8461
+ return unless node.is_a?(Prism::CallNode) && ARRAY_ITERATORS.include?(node.name)
8462
+
8463
+ block = node.block
8464
+ return unless block.is_a?(Prism::BlockNode)
8465
+ return unless iterated_first_parameter?(block, local)
8466
+
8467
+ iterated_receiver_elements(node.receiver, scope_nodes) || :unknown
8468
+ end
8469
+
8470
+ # Whether `local` is the first parameter of `block` and no other parameter binds it too (`|m, m = x|` — the
8471
+ # later binding wins, so the first is not the element).
8472
+ def iterated_first_parameter?(block, local)
8473
+ declared = block.parameters
8474
+ return false unless declared.is_a?(Prism::BlockParametersNode)
8475
+
8476
+ parameters = declared.parameters
8477
+ first = parameters&.requireds&.first
8478
+ return false unless first.is_a?(Prism::RequiredParameterNode) && first.name == local
8479
+
8480
+ Source::NodeWalker.each(parameters).none? do |param|
8481
+ !param.equal?(first) && local_binding?(param, local)
8482
+ end
8483
+ end
8484
+
8485
+ # The elements of the array `receiver` denotes for {#iterated_element_source}: a literal array's own, or a
8486
+ # bare constant's when the same scope assigns it a literal array exactly once. Nil when it denotes anything
8487
+ # else — a qualified or rooted constant, a local, a call — so the iterated value may be anything.
8488
+ def iterated_receiver_elements(receiver, scope_nodes)
8489
+ return receiver.elements if receiver.is_a?(Prism::ArrayNode)
8490
+ return unless receiver.is_a?(Prism::ConstantReadNode)
8491
+
8492
+ constant_array_elements(receiver.name, scope_nodes)
8493
+ end
8494
+
8495
+ # The elements of the literal array `name` is assigned in `nodes`' scope, or nil when it is written any
8496
+ # other number of times or its value is not an array literal — the constant may then hold anything. Only
8497
+ # a single plain `NAME = […]` write closes; in-place mutation (`KINDS << :include`) is unmodeled.
8498
+ def constant_array_elements(name, nodes)
8499
+ writes = constant_write_nodes(nodes, name)
8500
+ return unless writes.one? && writes.first.is_a?(Prism::ConstantWriteNode)
8501
+
8502
+ value = writes.first.value
8503
+ value.elements if value.is_a?(Prism::ArrayNode)
8504
+ end
8505
+
8506
+ # Every write binding the bare constant `name` in `nodes`' scope — a plain `NAME = …`, a multi-assign
8507
+ # target or an operator / `&&=` / `||=` write, mirroring {#local_binding?} — stopping where
8508
+ # {#collect_local_send_names} stops, since a nested `def` or declaration binds its own constants — so
8509
+ # {#constant_array_elements} can tell one literal-array assignment from none or several.
8510
+ def constant_write_nodes(nodes, name)
8511
+ nodes.flat_map do |node|
8512
+ next [] if LOCAL_SCOPE_BOUNDARIES.any? { |kind| node.is_a?(kind) }
8513
+
8514
+ own = constant_binding?(node, name) ? [node] : []
8515
+ own + constant_write_nodes(node.compact_child_nodes, name)
8516
+ end
8517
+ end
8518
+
8519
+ # A node that binds the bare constant `name`: every `Prism::Constant*` binding class — write, target,
8520
+ # operator, `&&=` and `||=` — except the `ConstantReadNode` and `ConstantPath*` spellings, which read
8521
+ # `name` or bind a qualified `A::NAME` rather than `name` itself.
8522
+ def constant_binding?(node, name)
8523
+ return false unless node.respond_to?(:name) && node.name == name
8524
+
8525
+ klass = node.class.name
8526
+ klass.start_with?("Prism::Constant") && !klass.start_with?("Prism::ConstantPath") &&
8527
+ !node.is_a?(Prism::ConstantReadNode)
8528
+ end
8529
+
8530
+ # The children of an iterator call {#iterated_element_source} matched, minus its block's parameters — the
8531
+ # first is `local` bound to the elements and the rest bind other names — so the usual scan still sees every
8532
+ # other binding of `local` in the receiver, arguments and body.
8533
+ def iterated_element_children(node)
8534
+ [node.receiver, node.arguments, node.block.body].compact
8535
+ end
8536
+
8537
+ # The method name a `send` argument spells: a name literal's, or `""` — no mixin's — for an interpolation with a
8538
+ # literal part no mixin name contains (`"#{name}_from_params"`). Nil when it may spell any name.
8539
+ def literal_send_name(value)
8540
+ return value.unescaped if value.is_a?(Prism::SymbolNode) || value.is_a?(Prism::StringNode)
8541
+ return nil unless value.is_a?(Prism::InterpolatedStringNode) || value.is_a?(Prism::InterpolatedSymbolNode)
8542
+
8543
+ excluded = value.parts.any? do |part|
8544
+ part.is_a?(Prism::StringNode) && MIXIN_KINDS.none? { |kind| kind.name.include?(part.unescaped) }
8545
+ end
8546
+ excluded ? "" : nil
8547
+ end
8548
+
8549
+ # A node other than a plain write that binds `local`: a target, an operator write or a parameter.
8550
+ def local_binding?(node, local)
8551
+ return false if node.is_a?(Prism::LocalVariableReadNode) || !node.respond_to?(:name) || node.name != local
8552
+
8553
+ node.class.name.start_with?("Prism::LocalVariable") || node.class.name.end_with?("ParameterNode") ||
8554
+ node.is_a?(Prism::BlockLocalVariableNode)
8555
+ end
7057
8556
 
7058
8557
  # `[kind, arguments, via_send]` for a call that mixes modules in — `include` / `prepend` / `extend`, or
7059
8558
  # `send(:include, M)` and its `public_send` / `__send__` twins with a literal first argument — else nil.
@@ -7112,8 +8611,9 @@ module Rigor
7112
8611
  end
7113
8612
 
7114
8613
  HOOK_DEFS = %i[included extended prepended inherited].freeze
8614
+ INSTANCE_MIXIN_NAMES = %i[include prepend].freeze
7115
8615
  MIXIN_EVAL_CALLS = %i[class_eval module_eval class_exec module_exec instance_eval instance_exec].freeze
7116
- private_constant :HOOK_DEFS, :MIXIN_EVAL_CALLS
8616
+ private_constant :HOOK_DEFS, :MIXIN_EVAL_CALLS, :INSTANCE_MIXIN_NAMES
7117
8617
 
7118
8618
  # The parameter names of a `def included(base)`-style hook (a hook is written `def self.included`, or
7119
8619
  # `def included` inside `class << self`), else nil.
@@ -7148,9 +8648,14 @@ module Rigor
7148
8648
  receiver = node.receiver
7149
8649
  return if receiver.nil? || receiver.is_a?(Prism::SelfNode)
7150
8650
  return if Source::ConstantPath.qualified_name_or_nil(receiver)
7151
- return unless hook_receiver?(receiver, accumulator) || block_mixes_in?(node.block)
7152
8651
 
7153
- accumulator.taint(current_class, :include)
8652
+ hook = hook_receiver?(receiver, accumulator)
8653
+ return unless hook || block_mixes_in?(node.block)
8654
+
8655
+ # Issue #2991 — the set is unknown only where the block may mix into the class itself: a hook's `base`, or a
8656
+ # receiverless `include` / `prepend` in the block of a receiver other than a `singleton_class`.
8657
+ unnamed = hook || (!singleton_class_eval_receiver?(receiver) && block_mixes_into_self?(node.block))
8658
+ accumulator.taint(current_class, :include, unnamed: unnamed)
7154
8659
  accumulator.taint(current_class, :extend)
7155
8660
  end
7156
8661
 
@@ -7162,6 +8667,22 @@ module Rigor
7162
8667
  found
7163
8668
  end
7164
8669
 
8670
+ # A receiverless or `self.` `include` / `prepend` (or its `send` spelling) anywhere in `node`.
8671
+ def block_mixes_into_self?(node)
8672
+ if node.is_a?(Prism::CallNode) && (node.receiver.nil? || node.receiver.is_a?(Prism::SelfNode))
8673
+ view = mixin_call_view(node)
8674
+ return true if view && INSTANCE_MIXIN_NAMES.include?(view.first)
8675
+ end
8676
+
8677
+ found = false
8678
+ node.rigor_each_child { |child| found ||= block_mixes_into_self?(child) }
8679
+ found
8680
+ end
8681
+
8682
+ def singleton_class_eval_receiver?(receiver)
8683
+ receiver.is_a?(Prism::CallNode) && receiver.name == :singleton_class
8684
+ end
8685
+
7165
8686
  def singleton_class_receiver?(receiver)
7166
8687
  receiver.is_a?(Prism::CallNode) && receiver.name == :singleton_class && receiver.arguments.nil? &&
7167
8688
  (receiver.receiver.nil? || receiver.receiver.is_a?(Prism::SelfNode))
@@ -7193,19 +8714,8 @@ module Rigor
7193
8714
  list.unshift(*(prepend ? targets : targets.reject { |target| list.include?(target) }))
7194
8715
  end
7195
8716
 
7196
- # Whether a mixin call contributes to the tables at all: a receiverless `include` / `prepend` needs an
7197
- # enclosing declaration to file the edge under, and the RECEIVER form (`Base.prepend(Loud)`, issue #1123;
7198
- # `Integer.include(Ext)` and `Integer.send(:include, Ext)`, issue #1722) files it under the class the
7199
- # receiver names. A receiver-form edge is unpositioned wherever it is written ({MixinAccumulator#note}), so
7200
- # a reader that depends on ORDER declines on it, while the set-shaped readers (undefined-method
7201
- # suppression, arity, visibility) see the module as they see an in-body `include`. A receiverless `send`
7202
- # stays a taint: the walk does not resolve its `self`.
7203
- def mixin_call_recorded?(node, kind, via_send, current_class)
7204
- return false unless MIXIN_CALL_NAMES.include?(kind)
7205
- return !via_send && !current_class.nil? if node.receiver.nil?
7206
-
7207
- true
7208
- end
8717
+ # Whether a mixin call's kind reaches the instance-side tables: `include` and `prepend`.
8718
+ def mixin_call_recorded?(kind) = MIXIN_CALL_NAMES.include?(kind)
7209
8719
 
7210
8720
  # Issue #1697 — a receiverless `include M` written as a statement of the file's own top level is
7211
8721
  # `main.include`, which is `Object.include`: it mixes M into `Object`. It is recorded under its own
@@ -7214,14 +8724,22 @@ module Rigor
7214
8724
  # walk that types a call reaches it: what Ruby reaches first for a name such an include supplies
7215
8725
  # depends on more than the chain shows (a block that rebinds `self`, an `extend` or a singleton `def`
7216
8726
  # on `main`), so only the diagnostic silences read it (`Inference::ObjectMixins`), and typing is
7217
- # issue #1715's. Only a direct program statement counts.
7218
- # The same call inside a top-level block, a method body or a conditional stays unrecorded, as before:
7219
- # its `self` may not be `main` (a `describe do include M end` or `class_exec` block rebinds it, while
7220
- # `[1].each do include M end` does not, and the walk cannot tell them apart), and it may not run. `main`
7221
- # has no `prepend`, and a top-level `extend M` reaches `main`'s singleton alone, so neither is recorded.
8727
+ # issue #1715's. `main` has no `prepend`, and a top-level `extend M` reaches `main`'s singleton alone, so
8728
+ # neither is recorded.
8729
+ #
8730
+ # Issue #3007 — the call counts wherever {MixinAccumulator::MainSelfCalls} takes its `self` to be `main`: a
8731
+ # program statement, or a call under top-level control flow (`if`, `begin`, `case`, …) or in a top-level
8732
+ # `def` body. Such a call may not run, so its edge is in the set with no position ({MixinAccumulator#note}),
8733
+ # as ADR-119 WD7 records an `include` in a method body. The same call inside a top-level block stays
8734
+ # unrecorded: its `self` may not be `main` (a `describe do include M end` or `class_exec` block rebinds it,
8735
+ # while `[1].each do include M end` does not, and the walk cannot tell them apart).
8736
+ #
8737
+ # Issue #2995 — `self.include M`, a receiverless or `self.` `send(:include, M)` and its `__send__` twin are the
8738
+ # same `main.include`. `public_send(:include, M)` is not: `main`'s `include` is private, so it raises.
7222
8739
  def toplevel_include_owner(node, qualified_prefix, current_class, accumulator)
7223
8740
  return nil unless current_class.nil? && qualified_prefix.empty?
7224
- return nil unless node.receiver.nil? && node.name == :include
8741
+ return nil unless lexical_self_receiver?(node.receiver) && node.name != :public_send
8742
+ return nil unless mixin_call_view(node)&.first == :include
7225
8743
 
7226
8744
  accumulator.program_statement?(node) ? ObjectMixins::TOPLEVEL_INCLUDE_KEY : nil
7227
8745
  end
@@ -7283,8 +8801,8 @@ module Rigor
7283
8801
  singleton_cref)
7284
8802
  when Prism::SingletonClassNode
7285
8803
  # Issue #915 — `class << self` opens the enclosing declaration's OWN singleton, so an `include`
7286
- # written in it is the same singleton ancestor an `extend` in the class body would add. Only the
7287
- # `self` form is followed: `class << obj` names something this walk cannot resolve to a class.
8804
+ # written in it is the same singleton ancestor an `extend` in the class body would add. Issue #3010 —
8805
+ # `class << Const` is followed too; `class << obj` names something this walk cannot resolve to a class.
7288
8806
  # Nested declarations inside the body take the `ClassNode` branch above and reset the flag with
7289
8807
  # their own owner, so the marker cannot leak past the singleton body it belongs to.
7290
8808
  return walk_extends_singleton_class(node, qualified_prefix, current_class, accumulator,
@@ -7337,7 +8855,8 @@ module Rigor
7337
8855
 
7338
8856
  # The `class <<` arm of {#walk_class_extends}: the expression evaluates in the enclosing
7339
8857
  # context while the body is the singleton's — `class << self` keeps the enclosing owner
7340
- # (its `include` is the singleton ancestor #915 folds), other operands walk ownerless.
8858
+ # (its `include` is the singleton ancestor #915 folds), `class << Const` takes the class the
8859
+ # constant names (#3010), other operands walk ownerless.
7341
8860
  # `self` inside is the singleton — `self::` receivers decline — and the lexical cref
7342
8861
  # below is unnameable in every case.
7343
8862
  def walk_extends_singleton_class(node, qualified_prefix, current_class, accumulator,
@@ -7346,17 +8865,114 @@ module Rigor
7346
8865
  in_singleton: in_singleton, singleton_self: singleton_self,
7347
8866
  singleton_cref: singleton_cref)
7348
8867
  return unless node.body
8868
+ return walk_extends_constant_singleton(node, qualified_prefix, accumulator) if constant_singleton_class?(node)
7349
8869
 
7350
8870
  # `class << self` keeps the owner only at the FIRST level — nested inside a
7351
8871
  # singleton body, `self` IS the singleton and `class << self` opens the
7352
8872
  # singleton's own singleton (`#<Class:#<Class:C>>`), which nothing names.
7353
8873
  opens_self = node.expression.is_a?(Prism::SelfNode) && !in_singleton
7354
8874
  accumulator.direct_body(node.body) if opens_self && accumulator.direct?(node)
8875
+ accumulator.class_body(node.body) if opens_self
7355
8876
  walk_class_extends(node.body, qualified_prefix, opens_self ? current_class : nil,
7356
8877
  accumulator, in_singleton: opens_self,
7357
8878
  singleton_self: true, singleton_cref: true)
7358
8879
  end
7359
8880
 
8881
+ # Issue #3010 — `class << Const` opens the class object's own singleton class, so an `include` / `prepend` in its
8882
+ # body is a singleton mixin on `Const`, as one in `class << self` inside `Const`'s body is. The body walks with
8883
+ # the class `Const` resolves to from the lexical scope ({#constant_singleton_owners}, the class its `def`s record
8884
+ # under) as owner. Its edges are positioned only where the order is a fact: the `class <<` is a direct statement
8885
+ # of a file that declares that class, and no more-nested name the constant could denote is left. Anywhere else (a
8886
+ # method body, a conditional, another file) they are unpositioned, and a prepend also records, unpositioned, on
8887
+ # every other name the constant can denote ({#record_constant_singleton_candidates}), as #2998's
8888
+ # `Const.singleton_class.prepend P` does.
8889
+ def walk_extends_constant_singleton(node, qualified_prefix, accumulator)
8890
+ owner, others = constant_singleton_owners(node, qualified_prefix)
8891
+ if others.empty? && accumulator.direct?(node) && eval_file_declared_names(node).include?(owner)
8892
+ accumulator.direct_body(node.body)
8893
+ end
8894
+ walk_class_extends(node.body, qualified_prefix, owner, accumulator,
8895
+ in_singleton: true, singleton_self: true, singleton_cref: true)
8896
+ record_constant_singleton_candidates(node.body, others, accumulator)
8897
+ end
8898
+
8899
+ # Each candidate class past the owner takes the body's prepends unpositioned; an argument that names no module
8900
+ # (`prepend(*MODS)`) leaves its singleton set unknown, as {#taint_unrecorded_mixin} reads it on the owner.
8901
+ def record_constant_singleton_candidates(body, candidates, accumulator)
8902
+ return if candidates.empty?
8903
+
8904
+ each_singleton_body_prepend(body) do |call|
8905
+ candidates.each do |candidate|
8906
+ next unless record_extend_targets(call, candidate, accumulator).empty?
8907
+
8908
+ accumulator.taint(candidate, :extend, unnamed: true)
8909
+ end
8910
+ end
8911
+ end
8912
+
8913
+ # A `class << Const` whose operand is a constant read (`class << Foo = expr` opens an object's singleton).
8914
+ def constant_singleton_class?(node)
8915
+ node.expression.is_a?(Prism::ConstantReadNode) || node.expression.is_a?(Prism::ConstantPathNode)
8916
+ end
8917
+
8918
+ # `[owner, others]` for a `class << Const`: the class the constant resolves to from the lexical scope
8919
+ # ({#singleton_class_prefix}, where the body's `def`s record), and the other names it can denote from there
8920
+ # ({#constant_receiver_candidates}). `others` is empty when the constant can denote one name only, when it names
8921
+ # the enclosing class itself (`class << Foo` inside `class Foo`, which {#singleton_class_prefix} reads as
8922
+ # `class << self`), or when the owner is the innermost candidate and the file declares it ahead of the `class <<`
8923
+ # ({#declared_before?}): Ruby resolves the constant when the statement runs, so a nested class the file declares
8924
+ # later does not exist yet there.
8925
+ def constant_singleton_owners(node, qualified_prefix)
8926
+ owner = singleton_class_prefix(node, nil, qualified_prefix).join("::")
8927
+ candidates = constant_receiver_candidates(node.expression, qualified_prefix)
8928
+ # One candidate needs no re-parse: whichever statement declares it, the constant can name nothing else.
8929
+ return [owner, EMPTY_PREFIX] if candidates.size == 1 || qualified_prefix.join("::") == owner
8930
+ return [owner, EMPTY_PREFIX] if candidates.first == owner && declared_before?(node, owner)
8931
+
8932
+ [owner, candidates - [owner]]
8933
+ end
8934
+
8935
+ # Issue #3010 — whether `node`'s file declares `name` (a `class` / `module` declaration or a bare constant
8936
+ # write) at a statement that starts ahead of `node`. Asked only where a `class << Const` has a nested candidate
8937
+ # the file declares, so it re-parses the file rather than keep a table every file would pay for.
8938
+ def declared_before?(node, name)
8939
+ source = node.location.send(:source).source
8940
+ declaration_before?(Prism.parse(source).value, EMPTY_PREFIX, name, node.location.start_offset)
8941
+ end
8942
+
8943
+ def declaration_before?(node, prefix, name, limit)
8944
+ return false unless node.is_a?(Prism::Node) && node.location.start_offset < limit
8945
+
8946
+ case node
8947
+ when Prism::ClassNode, Prism::ModuleNode
8948
+ child = Source::ConstantPath.declaration_prefix(prefix, node.constant_path)
8949
+ return false if child.nil?
8950
+ return true if child.join("::") == name
8951
+
8952
+ return declaration_before?(node.body, child, name, limit)
8953
+ when Prism::ConstantWriteNode
8954
+ return true if (prefix + [node.name.to_s]).join("::") == name
8955
+ when Prism::DefNode, Prism::SingletonClassNode
8956
+ # A `def` body declares no constant, and a `class <<` body's are the singleton class's.
8957
+ return false
8958
+ end
8959
+ node.rigor_each_child { |child| return true if declaration_before?(child, prefix, name, limit) }
8960
+ false
8961
+ end
8962
+
8963
+ # The receiverless or `self.` `prepend` calls (direct or `send` spelling) of a `class <<` body, outside any
8964
+ # nested `def` or declaration body.
8965
+ def each_singleton_body_prepend(node, &)
8966
+ return unless node.is_a?(Prism::Node)
8967
+ return if CODE_BODY_NODES.any? { |klass| node.is_a?(klass) }
8968
+
8969
+ if node.is_a?(Prism::CallNode) && lexical_self_receiver?(node.receiver) &&
8970
+ mixin_call_view(node)&.first == :prepend
8971
+ yield node
8972
+ end
8973
+ node.rigor_each_child { |child| each_singleton_body_prepend(child, &) }
8974
+ end
8975
+
7360
8976
  # The declaration arm of {#walk_class_extends}: under an unnameable cref a bare/`self::`
7361
8977
  # header opens `#<singleton>::Name` — ownerless; nameable headers re-anchor at a
7362
8978
  # real cref.
@@ -7370,6 +8986,7 @@ module Rigor
7370
8986
  child_cref = unnameable_decl?(node, self_decl, singleton_cref)
7371
8987
  accumulator.direct_body(node.body) if accumulator.direct?(node)
7372
8988
  accumulator.body_statements(node.body)
8989
+ accumulator.class_body(node.body)
7373
8990
  walk_class_extends(node.body, child_cref ? [] : child_prefix,
7374
8991
  child_cref ? nil : child_prefix.join("::"), accumulator,
7375
8992
  singleton_cref: child_cref)
@@ -7646,16 +9263,24 @@ module Rigor
7646
9263
  return if singleton_class_rewrite_tainted?(node, qualified_prefix, current_class, accumulator,
7647
9264
  in_singleton: in_singleton)
7648
9265
 
7649
- kind, _arguments, via_send = mixin_call_view(node)
7650
- return if kind.nil?
9266
+ kind, arguments, via_send = mixin_call_view(node)
9267
+ if kind.nil?
9268
+ # Inside `class << self` a computed `include` / `prepend` reaches the singleton side; an `extend` reaches the
9269
+ # singleton's own singleton, which nothing names.
9270
+ kinds = in_singleton ? INSTANCE_MIXIN_NAMES : EXTEND_MIXIN_NAMES
9271
+ return taint_computed_mixin_send(node, current_class, accumulator, kinds, :extend, in_singleton: in_singleton)
9272
+ end
7651
9273
 
7652
9274
  effects = mixin_effects(node, kind, qualified_prefix, current_class, accumulator, in_singleton: in_singleton)
7653
9275
  .select { |_owner, side| side == :extend || in_singleton }
7654
9276
  return if effects.empty?
7655
- return if !via_send && singleton_mixin_recorded?(node, kind, qualified_prefix, current_class, accumulator,
7656
- in_singleton)
9277
+ # A `send` spelling is recorded only on the lexical `self` (#2995); `Const.send(:extend, M)` stays a taint.
9278
+ return if (!via_send || lexical_self_receiver?(node.receiver)) &&
9279
+ singleton_mixin_recorded?(node, kind, qualified_prefix, current_class, accumulator, in_singleton)
7657
9280
 
7658
- effects.each { |owner, side| accumulator.taint(owner, side) }
9281
+ # Issue #2991 — as in {#record_mixin_call}: an unrecorded instance-side call (a hook's `base.include M` written
9282
+ # in `class << self`) lists its names, or the unnamed marker.
9283
+ taint_unrecorded_mixin(node, kind, arguments, effects, accumulator)
7659
9284
  end
7660
9285
 
7661
9286
  # Issue #1752 — a call that rewrites a singleton class reached through `singleton_class` (`singleton_class.
@@ -7679,23 +9304,53 @@ module Rigor
7679
9304
  # Inside `class << self` a bare `singleton_class` is the singleton's own singleton, one level up from the
7680
9305
  # owner's extend side, so that call stays a taint.
7681
9306
  up_level = in_singleton && lexical_self_receiver?(receiver.receiver)
7682
- return true if !up_level && singleton_class_include_recorded?(node, owner, accumulator)
9307
+ if !up_level && singleton_class_include_recorded?(node, owner, accumulator)
9308
+ record_prepend_candidates(node, receiver.receiver, qualified_prefix, owner, accumulator)
9309
+ return true
9310
+ end
7683
9311
 
7684
- accumulator.taint(owner, :extend)
9312
+ # Issue #2994 — an `include` / `prepend` whose argument names no module (`singleton_class.include(*MODS)`)
9313
+ # leaves the owner's singleton set unknown, as {#taint_unrecorded_mixin} reads an `extend(*EMODS)`.
9314
+ accumulator.taint(owner, :extend, unnamed: unnamed_singleton_include?(node))
7685
9315
  true
7686
9316
  end
7687
9317
 
9318
+ def unnamed_singleton_include?(node)
9319
+ INSTANCE_MIXIN_NAMES.include?(node.name) &&
9320
+ (node.arguments&.arguments || []).any? { |arg| opaque_mixin_argument?(arg) }
9321
+ end
9322
+
7688
9323
  # Issue #2094 — `Float.singleton_class.include(Ext)` (or `prepend`, which the extend table cannot order, so the
7689
9324
  # edge is unpositioned as every receiver-form edge is) puts `Ext` on the singleton side as `Float.extend(Ext)`
7690
9325
  # does, the fold #915 gives `class << self; include M`. A `send` spelling or an argument that is not a constant
7691
9326
  # stays a taint.
9327
+ #
9328
+ # Issue #2998 — a prepend, spelt directly or as `send(:prepend, P)`, is recorded, and the methods walk files it as
9329
+ # a singleton prepend ({#record_own_singleton_prepend}). One on the enclosing class's own `singleton_class` (a
9330
+ # bare or `self.` receiver) runs where it is written, as `prepend P` in `class << self` does, so a direct
9331
+ # statement of the body is positioned; a constant receiver's stays unpositioned.
7692
9332
  def singleton_class_include_recorded?(node, owner, accumulator)
7693
- return false unless %i[include prepend].include?(node.name)
7694
-
7695
- arguments = node.arguments&.arguments || []
9333
+ kind, sent, = mixin_call_view(node)
9334
+ prepend = kind == :prepend
9335
+ arguments = prepend ? sent : node.arguments&.arguments || []
9336
+ return false unless prepend || node.name == :include
7696
9337
  return false if arguments.empty? || arguments.any? { |arg| Source::ConstantPath.qualified_name(arg).nil? }
7697
9338
 
7698
- !record_extend_targets(node, owner, accumulator).empty?
9339
+ positioned = prepend && lexical_self_receiver?(node.receiver.receiver)
9340
+ !record_extend_targets(node, owner, accumulator, positioned: positioned).empty?
9341
+ end
9342
+
9343
+ # Issue #2998 — a constant receiver's `Const.singleton_class.prepend P` also records the edge, unpositioned, under
9344
+ # every other name the constant can denote from the call site, the names the methods walk files its
9345
+ # singleton-prepend entry under ({#singleton_prepend_owners}): which class Ruby reaches is not a fact here, so
9346
+ # each candidate's typing read declines where the prepend could answer.
9347
+ def record_prepend_candidates(node, object, qualified_prefix, owner, accumulator)
9348
+ return unless mixin_call_view(node)&.first == :prepend
9349
+ return unless object.is_a?(Prism::ConstantReadNode) || object.is_a?(Prism::ConstantPathNode)
9350
+
9351
+ (constant_receiver_candidates(object, qualified_prefix) - [owner]).each do |candidate|
9352
+ record_extend_targets(node, candidate, accumulator)
9353
+ end
7699
9354
  end
7700
9355
 
7701
9356
  # A bare or `self.` receiver: the lexical `self`.
@@ -7759,10 +9414,12 @@ module Rigor
7759
9414
  accumulator.note(node, current_class, :extend, [current_class], complete: true)
7760
9415
  end
7761
9416
 
7762
- # Records the singleton-side edges of a receiverless `extend` (or, inside `class << self`, `include` /
7763
- # `prepend`). False when it recorded nothing.
9417
+ # Records the singleton-side edges of a receiverless or `self.` `extend` (or, inside `class << self`,
9418
+ # `include` / `prepend`), and of their `send` spellings (#2995). False when it recorded nothing.
7764
9419
  def singleton_mixin_recorded?(node, kind, qualified_prefix, current_class, accumulator, in_singleton)
7765
- return receiver_extend_recorded?(node, kind, qualified_prefix, accumulator) if node.receiver
9420
+ unless lexical_self_receiver?(node.receiver)
9421
+ return receiver_extend_recorded?(node, kind, qualified_prefix, accumulator)
9422
+ end
7766
9423
  return false unless current_class
7767
9424
  return false unless in_singleton ? MIXIN_CALL_NAMES.include?(kind) : kind == :extend
7768
9425
 
@@ -7788,9 +9445,12 @@ module Rigor
7788
9445
  # (`extend A; extend B` → singleton ancestors `[B, A]`) while the arguments of a single
7789
9446
  # `extend A, B` keep call order (`[A, B]`). Prepending each call's argument list preserves both,
7790
9447
  # and consumers iterating the list forward read the same order the singleton ancestry searches.
7791
- def record_extend_targets(node, current_class, accumulator)
9448
+ #
9449
+ # The arguments are {#mixin_call_view}'s, so a `send(:extend, M)` (#2995) reads `M` and not its name literal.
9450
+ # `positioned` says a receiver-form call still runs where it is written ({#singleton_class_include_recorded?}).
9451
+ def record_extend_targets(node, current_class, accumulator, positioned: false)
7792
9452
  targets = []
7793
- arguments = node.arguments&.arguments || []
9453
+ arguments = mixin_call_view(node)[1]
7794
9454
  arguments.each do |arg|
7795
9455
  target = arg.is_a?(Prism::SelfNode) ? current_class : Source::ConstantPath.qualified_name(arg)
7796
9456
  targets << target if target
@@ -7804,7 +9464,8 @@ module Rigor
7804
9464
  # unpositioned and the chain settles to the readers' own answer.
7805
9465
  accumulator.taint(current_class, :extend) if targets.any? { |target| list.include?(target) }
7806
9466
  list.unshift(*targets)
7807
- accumulator.note(node, current_class, :extend, targets, complete: targets.size == arguments.size)
9467
+ accumulator.note(node, current_class, :extend, targets, complete: targets.size == arguments.size,
9468
+ positioned: positioned)
7808
9469
  targets
7809
9470
  end
7810
9471
 
@@ -7990,6 +9651,10 @@ module Rigor
7990
9651
  # written directly in the body settles the default again. A named target does not read the running default,
7991
9652
  # so a toggle does not contest it. A certain write clears the slot's contest.
7992
9653
  class VisibilityTable < Hash
9654
+ include HookBlockSpans
9655
+
9656
+ attr_accessor :hook_block_spans
9657
+
7993
9658
  # The bare toggles the walk has met so far; a body's statement loop compares it around each child.
7994
9659
  attr_accessor :toggles
7995
9660
  # Whether the running default of the body being walked rests on an uncertain toggle.
@@ -8112,6 +9777,22 @@ module Rigor
8112
9777
  VISIBILITY_TOGGLES.include?(name) ? [name, arguments] : nil
8113
9778
  end
8114
9779
 
9780
+ # Issue #1592 — a concern's `class_methods do … end` block is a `module_eval` of `Owner::ClassMethods`, whose
9781
+ # visibility starts public: its `def`s' visibilities are recorded on that module too, as its `def`s are.
9782
+ # Issue #2997 — in every body of the module, as {#record_concern_class_methods} reads its `def`s.
9783
+ def walk_class_methods_visibilities(node, child_prefix, accumulator)
9784
+ return unless node.is_a?(Prism::ModuleNode) && node.body.is_a?(Prism::StatementsNode)
9785
+
9786
+ node.body.body.each do |statement|
9787
+ next unless statement.is_a?(Prism::CallNode) && block_statement?(statement) &&
9788
+ statement.name == :class_methods && statement.block.body
9789
+
9790
+ accumulator.body do
9791
+ walk_method_visibilities(statement.block.body, [*child_prefix, "ClassMethods"], false, :public, accumulator)
9792
+ end
9793
+ end
9794
+ end
9795
+
8115
9796
  # rubocop:disable-next Metrics/CyclomaticComplexity, Metrics/MethodLength, Metrics/PerceivedComplexity, Metrics/AbcSize
8116
9797
  def walk_method_visibilities(node, qualified_prefix, in_singleton_class, current_visibility, accumulator, # rubocop:disable Metrics/ParameterLists
8117
9798
  def_owner_prefix = nil, singleton_cref: false,
@@ -8130,11 +9811,13 @@ module Rigor
8130
9811
  # ownerless; nameable headers re-anchor at a real cref.
8131
9812
  child_cref = unnameable_decl?(node, self_decl, singleton_cref)
8132
9813
  body_prefix = child_cref ? [] : child_prefix
9814
+ accumulator.mark_hook_blocks(concern_hook_blocks(node, child_prefix), child_prefix) unless child_cref
8133
9815
  if node.body
8134
9816
  accumulator.body do
8135
9817
  walk_method_visibilities(node.body, body_prefix, false, :public,
8136
9818
  accumulator, nil, singleton_cref: child_cref)
8137
9819
  end
9820
+ walk_class_methods_visibilities(node, child_prefix, accumulator) unless child_cref
8138
9821
  end
8139
9822
  return current_visibility
8140
9823
  end
@@ -8163,6 +9846,8 @@ module Rigor
8163
9846
  return current_visibility
8164
9847
  end
8165
9848
  when Prism::DefNode
9849
+ return current_visibility if accumulator.hook_block_self_def?(node, owner_prefix)
9850
+
8166
9851
  contest_foreign_singleton_visibility(node, qualified_prefix, owner_prefix, in_singleton_class, accumulator)
8167
9852
  record_def_visibility(node, owner_prefix, visibility_side(in_singleton_class, defs_singleton),
8168
9853
  current_visibility, accumulator)
@@ -8273,6 +9958,7 @@ module Rigor
8273
9958
  singleton_cref: singleton_cref,
8274
9959
  defs_singleton: defs_singleton)
8275
9960
  return unless node.body
9961
+ return if hook_block_singleton?(node, in_singleton_class, def_owner_prefix || qualified_prefix, accumulator)
8276
9962
 
8277
9963
  accumulator.body do
8278
9964
  walk_method_visibilities(node.body, qualified_prefix, true, :public, accumulator,
@@ -8775,6 +10461,7 @@ module Rigor
8775
10461
  constant_receiver_candidates(receiver, qualified_prefix).each do |name|
8776
10462
  record_surface_mark(tables, name, Scope::DiscoveryIndex::ENVELOPE_DYNAMIC_MARK)
8777
10463
  end
10464
+ record_constant_singleton_definer(node, receiver, qualified_prefix, tables)
8778
10465
  elsif singleton_class_of_receiver?(receiver) && singleton_class_named_call?(node)
8779
10466
  record_singleton_class_names(node, receiver.receiver, qualified_prefix, tables)
8780
10467
  end
@@ -8809,8 +10496,64 @@ module Rigor
8809
10496
  return if def_receiver_targets_lexical_self?(receiver, owner_prefix)
8810
10497
 
8811
10498
  constant_receiver_candidates(receiver, qualified_prefix).each do |name|
8812
- record_envelope(tables.envelopes, name, [:singleton, node.name], Source::ParameterEnvelope::OPAQUE)
8813
- contest_envelope(tables, name, [:singleton, node.name]) unless tables.certain
10499
+ record_object_singleton(tables, name, node.name)
10500
+ end
10501
+ end
10502
+
10503
+ # Issue #3016 — `Const.define_singleton_method(:x) { … }` with a literal name defines `x` on `Const`'s object, and
10504
+ # is filed as {#record_foreign_singleton_def} files `def Const.x`: an opaque singleton envelope on every name the
10505
+ # constant can denote. A computed name may define any name, so it marks each candidate with the singleton side's
10506
+ # open-names mark instead, as a computed `define_method` on a constant marks the instance side.
10507
+ def record_constant_singleton_definer(node, receiver, qualified_prefix, tables)
10508
+ names = define_singleton_method_names(node)
10509
+ return if names.nil?
10510
+
10511
+ candidates = constant_receiver_candidates(receiver, qualified_prefix)
10512
+ if names == :all
10513
+ candidates.each do |owner|
10514
+ record_surface_mark(tables, owner, Scope::DiscoveryIndex::ENVELOPE_OPEN_SINGLETON_NAMES_MARK)
10515
+ end
10516
+ return
10517
+ end
10518
+
10519
+ candidates.product(names) do |owner, name|
10520
+ record_envelope(tables.envelopes, owner, [:singleton, name], Source::ParameterEnvelope::OPAQUE)
10521
+ record_envelope(tables.envelopes, owner, [Scope::DiscoveryIndex::ENVELOPE_OBJECT_SINGLETON_KIND, name],
10522
+ Source::ParameterEnvelope::OPAQUE)
10523
+ end
10524
+ end
10525
+
10526
+ # A `def Const.x` written outside `Const`'s body, or one in a `Const.singleton_class.class_eval` block: the opaque
10527
+ # `[:singleton, x]` envelope, contested unless certain, and the
10528
+ # {Scope::DiscoveryIndex::ENVELOPE_OBJECT_SINGLETON_KIND} entry the class-object path reads (#3018).
10529
+ def record_object_singleton(tables, owner, method_name)
10530
+ record_envelope(tables.envelopes, owner, [:singleton, method_name], Source::ParameterEnvelope::OPAQUE)
10531
+ contest_envelope(tables, owner, [:singleton, method_name]) unless tables.certain
10532
+ record_envelope(tables.envelopes, owner, [Scope::DiscoveryIndex::ENVELOPE_OBJECT_SINGLETON_KIND, method_name],
10533
+ Source::ParameterEnvelope::OPAQUE)
10534
+ end
10535
+
10536
+ # Issue #3018 — `Const.singleton_class.class_eval { def x … }` (or `module_eval` / `class_exec` / `module_exec`)
10537
+ # defines `x` on `Const`'s object, as `def Const.x` does, and is filed as {#record_foreign_singleton_def} files
10538
+ # that def ({#record_object_singleton}) on every name the constant can denote. The eval-body walk files these
10539
+ # defs under no owner, since the receiver is a call. Every receiverless `def` in the block counts, one under a
10540
+ # conditional included, as {#each_refinement_def} yields them: not one under a `class` / `module` / `class <<`
10541
+ # body, which opens another definee, nor one inside another `def`, which installs only when that runs.
10542
+ # `instance_eval` on the singleton class is not filed: its defs land on the singleton class's own singleton (Ruby
10543
+ # 4.0.5 raises NoMethodError on `Const.x` there).
10544
+ def record_constant_singleton_eval_defs(node, qualified_prefix, tables)
10545
+ return unless CLASS_EVAL_BLOCK_CALLS.include?(node.name)
10546
+
10547
+ receiver = node.receiver
10548
+ return unless singleton_class_of_receiver?(receiver)
10549
+
10550
+ body = node.block.body
10551
+ return if body.nil?
10552
+
10553
+ # A bare, `self` or local receiver of `singleton_class` renders no constant, so it yields no candidate.
10554
+ candidates = constant_receiver_candidates(receiver.receiver, qualified_prefix)
10555
+ each_refinement_def(body) do |def_node|
10556
+ candidates.each { |owner| record_object_singleton(tables, owner, def_node.name) }
8814
10557
  end
8815
10558
  end
8816
10559
 
@@ -8859,22 +10602,33 @@ module Rigor
8859
10602
  SURFACE_MIXIN_CALLS.include?(node.name) || SURFACE_MIXIN_HELPER.match?(node.name.to_s)
8860
10603
  end
8861
10604
 
10605
+ # Issue #2995 — a `send` / `public_send` / `__send__` of `:include`, `:prepend` or `:extend` is that mixin call,
10606
+ # which the mixin walk records as the bare call, so it is judged by its own arguments as the bare call is.
8862
10607
  def dynamic_surface_call?(node)
8863
- return true if SURFACE_EVAL_CALLS.include?(node.name) || SURFACE_SEND_CALLS.include?(node.name)
10608
+ return true if SURFACE_EVAL_CALLS.include?(node.name)
10609
+
10610
+ if SURFACE_SEND_CALLS.include?(node.name)
10611
+ kind, arguments, = mixin_call_view(node)
10612
+ return kind.nil? || dynamic_mixin_arguments?(arguments)
10613
+ end
8864
10614
 
8865
10615
  arguments = node.arguments&.arguments || []
8866
10616
  if SURFACE_NAMING_CALLS.include?(node.name)
8867
10617
  arguments.empty? || literal_method_name(arguments.first).nil?
8868
10618
  elsif surface_mixin_call?(node)
8869
- arguments.any? do |argument|
8870
- !(argument.is_a?(Prism::ConstantReadNode) || argument.is_a?(Prism::ConstantPathNode) ||
8871
- argument.is_a?(Prism::SelfNode))
8872
- end
10619
+ dynamic_mixin_arguments?(arguments)
8873
10620
  else
8874
10621
  false
8875
10622
  end
8876
10623
  end
8877
10624
 
10625
+ def dynamic_mixin_arguments?(arguments)
10626
+ arguments.any? do |argument|
10627
+ !(argument.is_a?(Prism::ConstantReadNode) || argument.is_a?(Prism::ConstantPathNode) ||
10628
+ argument.is_a?(Prism::SelfNode))
10629
+ end
10630
+ end
10631
+
8878
10632
  # Every qualified name a constant receiver written inside `qualified_prefix` can denote, innermost
8879
10633
  # first: the walk runs before any scope exists, so it cannot resolve the constant, and over-marking a
8880
10634
  # same-named class elsewhere only withholds a check.
@@ -9155,15 +10909,76 @@ module Rigor
9155
10909
  end
9156
10910
 
9157
10911
  def record_define_method(call_node, qualified_prefix, in_singleton_class, accumulator)
9158
- return if qualified_prefix.empty?
9159
10912
  return if call_node.arguments.nil? || call_node.arguments.arguments.empty?
9160
10913
 
9161
- first_arg = call_node.arguments.arguments.first
9162
- method_name = literal_method_name(first_arg)
9163
- return if method_name.nil?
10914
+ method_name = literal_method_name(call_node.arguments.arguments.first)
10915
+ receiver = call_node.receiver
10916
+ if method_name && (receiver.is_a?(Prism::ConstantReadNode) || receiver.is_a?(Prism::ConstantPathNode))
10917
+ return record_constant_define_method(call_node, receiver, method_name, qualified_prefix, accumulator)
10918
+ end
10919
+ return if qualified_prefix.empty?
10920
+ if method_name.nil?
10921
+ return record_open_define_method(call_node, qualified_prefix, in_singleton_class,
10922
+ accumulator)
10923
+ end
9164
10924
 
9165
- class_name = qualified_prefix.join("::")
9166
- record_method(accumulator, class_name, method_name, in_singleton_class ? :singleton : :instance)
10925
+ # A receiver other than a constant may still be `self` (`[self, k].each { |c| c.define_method(:x) {} }`), so
10926
+ # the name stays on the class the walk places the call in.
10927
+ record_method(accumulator, qualified_prefix.join("::"), method_name,
10928
+ in_singleton_class ? :singleton : :instance)
10929
+ end
10930
+
10931
+ # Issue #2975 — `Other.define_method(:x)` defines `Other#x`, whichever class body or side it is written in: the
10932
+ # constant resolves as a `class << Other` operand does ({#singleton_class_owner}), and the enclosing class
10933
+ # records nothing.
10934
+ def record_constant_define_method(call_node, receiver, method_name, qualified_prefix, accumulator)
10935
+ owner = singleton_class_owner(call_node, receiver, qualified_prefix, nil)
10936
+ record_method(accumulator, owner, method_name, :instance) if owner
10937
+ end
10938
+
10939
+ # Issue #2975 — the literal twins of {#record_self_definer_call}'s computed definers on `self`:
10940
+ # `send(:define_method, :x)` records `x` where a direct `define_method(:x)` there would, and
10941
+ # `define_singleton_method(:x)` records the class object's `x`. In a `class << self` body the latter defines on
10942
+ # the singleton's own singleton, which nothing names, so it records nothing.
10943
+ def record_literal_self_definer(node, class_name, in_singleton_class, accumulator)
10944
+ arguments = node.arguments&.arguments
10945
+ return if arguments.nil? || arguments.empty?
10946
+
10947
+ if node.name == :define_singleton_method
10948
+ name = literal_method_name(arguments.first)
10949
+ record_method(accumulator, class_name, name, :singleton) if name && !in_singleton_class
10950
+ elsif literal_method_name(arguments.first) == :define_method && arguments.length > 1
10951
+ name = literal_method_name(arguments[1])
10952
+ record_method(accumulator, class_name, name, in_singleton_class ? :singleton : :instance) if name
10953
+ end
10954
+ end
10955
+
10956
+ # Issue #2793 — a `define_method` on `self` whose name is not a literal defines a method this walk cannot name,
10957
+ # on the side and owner a literal one would record under (the walk's placement: a `Class.new { }` or
10958
+ # `Struct.new { }` body is its own class's, an eval body its receiver's, a `class << self` body the singleton
10959
+ # side), so that side reads as open to `call.undefined-method`.
10960
+ #
10961
+ # Issue #2901 — on a constant receiver (`Other.define_method(m)`) it defines instance methods of that constant's
10962
+ # class, whichever side the call sits on: each class the constant can denote from here opens its instance side
10963
+ # ({#constant_receiver_candidates}), and the enclosing class does not. Any other receiver opens nothing.
10964
+ def record_open_define_method(call_node, qualified_prefix, in_singleton_class, accumulator)
10965
+ receiver = call_node.receiver
10966
+ if receiver.is_a?(Prism::ConstantReadNode) || receiver.is_a?(Prism::ConstantPathNode)
10967
+ constant_receiver_candidates(receiver, qualified_prefix).each do |name|
10968
+ record_surface_mark(accumulator, name, Scope::DiscoveryIndex::ENVELOPE_OPEN_INSTANCE_NAMES_MARK)
10969
+ end
10970
+ elsif receiver.nil? || receiver.is_a?(Prism::SelfNode)
10971
+ record_surface_mark(accumulator, qualified_prefix.join("::"), open_names_mark(in_singleton_class))
10972
+ end
10973
+ end
10974
+
10975
+ # The side's open-names mark a computed definer on `self` records: the singleton side in a `class << self` body.
10976
+ def open_names_mark(in_singleton_class)
10977
+ if in_singleton_class
10978
+ Scope::DiscoveryIndex::ENVELOPE_OPEN_SINGLETON_NAMES_MARK
10979
+ else
10980
+ Scope::DiscoveryIndex::ENVELOPE_OPEN_INSTANCE_NAMES_MARK
10981
+ end
9167
10982
  end
9168
10983
 
9169
10984
  # The `attr_*` accessor macros that introduce methods Rigor must treat as source-declared. Without this, a class
@@ -9187,6 +11002,19 @@ module Rigor
9187
11002
  record_method(accumulator, class_name, base, kind) if reader
9188
11003
  record_method(accumulator, class_name, :"#{base}=", kind) if writer
9189
11004
  end
11005
+ return unless attr_macro_computed?(call_node)
11006
+
11007
+ # Issue #2901 — `attr_accessor(*NAMES)` defines members this walk cannot name, so its side opens.
11008
+ record_surface_mark(accumulator, class_name, open_names_mark(in_singleton_class))
11009
+ end
11010
+
11011
+ # Whether an `attr_*` call ({#attr_macro}) passes a name argument that is not a Symbol or String literal.
11012
+ def attr_macro_computed?(call_node)
11013
+ arguments = call_node.arguments&.arguments
11014
+ return false if arguments.nil?
11015
+
11016
+ arguments = arguments.drop(1) if SURFACE_SEND_CALLS.include?(call_node.name)
11017
+ arguments.any? { |arg| literal_method_name(arg).nil? }
9190
11018
  end
9191
11019
 
9192
11020
  # Yields each name an `attr_*` call declares ({#attr_macro}): a Symbol or String literal argument. A
@@ -9231,13 +11059,14 @@ module Rigor
9231
11059
  # because they are not monkey-patches), and no plugin surface reaches it — the ADR-16 synthetic-method
9232
11060
  # tier feeds the DISPATCHER, which `Analysis::CheckRules` does not consult. A name table is the smallest
9233
11061
  # thing that works; a plugin-supplied one can replace it whenever that surface exists.
9234
- MODULE_ATTR_MACROS = {
9235
- mattr_reader: [true, false, false], mattr_writer: [false, true, false],
9236
- mattr_accessor: [true, true, false], cattr_reader: [true, false, false],
9237
- cattr_writer: [false, true, false], cattr_accessor: [true, true, false],
9238
- class_attribute: [true, true, true]
9239
- }.freeze
9240
- Ractor.make_shareable(MODULE_ATTR_MACROS)
11062
+ MODULE_ATTR_MACROS = Ractor.make_shareable(
11063
+ {
11064
+ mattr_reader: [true, false, false], mattr_writer: [false, true, false],
11065
+ mattr_accessor: [true, true, false], cattr_reader: [true, false, false],
11066
+ cattr_writer: [false, true, false], cattr_accessor: [true, true, false],
11067
+ class_attribute: [true, true, true]
11068
+ }
11069
+ )
9241
11070
 
9242
11071
  # Both kinds, unconditionally. `instance_accessor: false` (and its `instance_reader:` /
9243
11072
  # `instance_writer:` / `instance_predicate:` siblings) narrows the real surface, and honouring them here
@@ -9260,6 +11089,70 @@ module Rigor
9260
11089
  end
9261
11090
  end
9262
11091
 
11092
+ # Issue #2793 — the `extend` that decides the side of an {Source::ForwardableDelegators::AMBIGUOUS} macro
11093
+ # (`def_delegators`, `def_delegator`, `delegate`): `Forwardable` makes them instance methods,
11094
+ # `SingleForwardable` singleton methods.
11095
+ FORWARDABLE_MODULES = { Forwardable: :instance, SingleForwardable: :singleton }.freeze
11096
+
11097
+ # Issue #2793 — a Forwardable delegator macro defines its literal names as methods, which join the existence
11098
+ # table so `call.undefined-method` stops firing on them (their return types stay unknown: the envelope is
11099
+ # opaque and no `def` node backs them). The side is the macro's own, or, for an ambiguous macro, the side
11100
+ # of the `extend` this walk met earlier in the body: singleton under a lone `extend SingleForwardable`,
11101
+ # instance otherwise (a class extended in another file or by a superclass is far likelier `Forwardable`).
11102
+ # Inside `class << self` the macro defines on the singleton class. A name the syntax hides
11103
+ # (`def_delegators :@x, *NAMES`) marks the class {Scope::DiscoveryIndex::ENVELOPE_OPEN_NAMES_MARK}.
11104
+ def record_forwardable_methods(call_node, qualified_prefix, in_singleton_class, accumulator)
11105
+ return if qualified_prefix.empty?
11106
+
11107
+ class_name = qualified_prefix.join("::")
11108
+ note_forwardable_extend(call_node, class_name, accumulator) unless in_singleton_class
11109
+ delegation = Source::ForwardableDelegators.of(call_node)
11110
+ return if delegation.nil?
11111
+
11112
+ kind = forwardable_side(delegation, class_name, in_singleton_class, accumulator)
11113
+ delegation.names.each { |name| record_method(accumulator, class_name, name, kind) }
11114
+ return unless delegation.open?
11115
+
11116
+ record_surface_mark(accumulator, class_name, Scope::DiscoveryIndex::ENVELOPE_OPEN_NAMES_MARK)
11117
+ end
11118
+
11119
+ def note_forwardable_extend(call_node, class_name, accumulator)
11120
+ return unless call_node.name == :extend && call_node.receiver.nil?
11121
+
11122
+ (call_node.arguments&.arguments || []).each do |argument|
11123
+ side = FORWARDABLE_MODULES[forwardable_module_name(argument)]
11124
+ next if side.nil?
11125
+
11126
+ ((accumulator.forwardable_extends ||= {})[class_name] ||= Set.new) << side
11127
+ end
11128
+ end
11129
+
11130
+ def forwardable_module_name(node)
11131
+ case node
11132
+ when Prism::ConstantReadNode then node.name
11133
+ when Prism::ConstantPathNode then node.parent.nil? ? node.name : nil
11134
+ end
11135
+ end
11136
+
11137
+ def forwardable_side(delegation, class_name, in_singleton_class, accumulator)
11138
+ return :singleton if in_singleton_class
11139
+ return delegation.side unless delegation.side == Source::ForwardableDelegators::AMBIGUOUS
11140
+
11141
+ extended = accumulator.forwardable_extends&.[](class_name)
11142
+ extended == Set[:singleton] ? :singleton : :instance
11143
+ end
11144
+
11145
+ # The instance names a Forwardable macro defines, for {#call_superseded_names}: a delegator written after a
11146
+ # `def` of the name replaces it. Asked only outside `class << self`, and a macro whose side settles to the
11147
+ # singleton (`extend SingleForwardable; def_delegators :@x, :size`) leaves an instance `def size` standing.
11148
+ def forwardable_superseded_names(call_node, qualified_prefix, accumulator)
11149
+ delegation = Source::ForwardableDelegators.of(call_node)
11150
+ return [] if delegation.nil? || qualified_prefix.empty?
11151
+
11152
+ side = forwardable_side(delegation, qualified_prefix.join("::"), false, accumulator)
11153
+ side == :instance ? delegation.names : []
11154
+ end
11155
+
9263
11156
  def record_both_kinds(accumulator, class_name, method_name)
9264
11157
  record_method(accumulator, class_name, method_name, :instance)
9265
11158
  record_method(accumulator, class_name, method_name, :singleton)
@@ -9290,8 +11183,12 @@ module Rigor
9290
11183
  physical = buffer ? buffer.resolve(path) : path
9291
11184
  source = File.read(physical)
9292
11185
  root = Prism.parse(source, filepath: path).value
9293
- collect_class_decls(root, [], accumulator)
9294
- rescue StandardError
11186
+ file_classes = {}
11187
+ collect_class_decls(root, [], file_classes)
11188
+ accumulator.merge!(file_classes) # all or nothing: an overflow above contributes none of this file (#2455)
11189
+ rescue StandardError, SystemStackError => e
11190
+ raise if uncontainable_overflow?(e)
11191
+
9295
11192
  # Skip files that fail to parse or read; the per-file analyzer surfaces the parse error separately.
9296
11193
  next
9297
11194
  end
@@ -9318,10 +11215,12 @@ module Rigor
9318
11215
  paths.each do |path|
9319
11216
  physical = buffer ? buffer.resolve(path) : path
9320
11217
  root = Prism.parse(File.read(physical), filepath: path).value
9321
- accumulate_project_index(acc, path, root)
11218
+ fold_file_index(acc, build_file_index(path, root)) # all or nothing per file (#2455)
9322
11219
  rescue DeclarationWalk::ContractError
9323
11220
  raise # a broken walk contract, not an unreadable file: skipping it would hide the failure
9324
- rescue StandardError
11221
+ rescue StandardError, SystemStackError => e
11222
+ raise if uncontainable_overflow?(e)
11223
+
9325
11224
  # Skip files that fail to parse or read; the per-file analyzer surfaces the parse error separately.
9326
11225
  next
9327
11226
  end
@@ -9358,7 +11257,9 @@ module Rigor
9358
11257
  file_summary(file_index).each { |key, table| per_file[key][path] = table unless table.nil? }
9359
11258
  rescue DeclarationWalk::ContractError
9360
11259
  raise # a broken walk contract, not an unreadable file: skipping it would hide the failure
9361
- rescue StandardError
11260
+ rescue StandardError, SystemStackError => e
11261
+ raise if uncontainable_overflow?(e)
11262
+
9362
11263
  next
9363
11264
  end
9364
11265
  { def_index: finalize_def_index(acc), code_fingerprints: code_fingerprints,
@@ -9642,21 +11543,35 @@ module Rigor
9642
11543
  paths.each do |path|
9643
11544
  physical = buffer ? buffer.resolve(path) : path
9644
11545
  root = Prism.parse(File.read(physical), filepath: path).value
9645
- collect_class_decls(root, [], classes)
9646
- if uncertain.include?(path)
9647
- fold_file_index(acc, without_written_slots(build_file_index(path, root, uncertain: true), acc))
9648
- else
9649
- accumulate_project_index(acc, path, root)
9650
- end
11546
+ # All or nothing per file, as the incremental loop below: both tables are built in isolation and folded only
11547
+ # once the whole file walked, so a file that overflows the stack contributes none of either (#2455).
11548
+ file_classes = {}
11549
+ collect_class_decls(root, [], file_classes)
11550
+ file_index = if uncertain.include?(path)
11551
+ without_written_slots(build_file_index(path, root, uncertain: true), acc)
11552
+ else
11553
+ build_file_index(path, root)
11554
+ end
11555
+ fold_file_index(acc, file_index)
11556
+ classes.merge!(file_classes)
9651
11557
  rescue DeclarationWalk::ContractError
9652
11558
  raise # a broken walk contract, not an unreadable file: skipping it would hide the failure
9653
- rescue StandardError
11559
+ rescue StandardError, SystemStackError => e
11560
+ raise if uncontainable_overflow?(e)
11561
+
9654
11562
  # Skip files that fail to parse or read; the per-file analyzer surfaces the parse error separately.
9655
11563
  next
9656
11564
  end
9657
11565
  finalize_project_index(classes, acc)
9658
11566
  end
9659
11567
 
11568
+ # #2455 — a `SystemStackError` raised from C (`Prism.parse` of a source nested near its own cap) has no
11569
+ # backtrace and leaves the process unable to survive a second one, so it is not contained: it keeps the
11570
+ # run-level abort.
11571
+ def uncontainable_overflow?(error)
11572
+ error.is_a?(SystemStackError) && error.backtrace.nil?
11573
+ end
11574
+
9660
11575
  # The shared tail of the two whole-project discovery walks: settles the def-index and re-anchors the
9661
11576
  # `classes` table it accumulated beside it.
9662
11577
  def finalize_project_index(classes, acc)
@@ -9697,7 +11612,7 @@ module Rigor
9697
11612
  # @return `{ classes:, def_index:, bundles: }`.
9698
11613
  # @param uncertain — issue #2021: as {#discovered_project_index_for_paths}. Such a file is always re-walked and
9699
11614
  # keeps no bundle, since a bundle does not record that its contributions were all possible.
9700
- def discovered_project_index_incremental(paths, seed_bundles:, buffer: nil, uncertain: EMPTY_PATH_SET) # rubocop:disable Metrics/AbcSize
11615
+ def discovered_project_index_incremental(paths, seed_bundles:, buffer: nil, uncertain: EMPTY_PATH_SET) # rubocop:disable Metrics/AbcSize, Metrics/MethodLength
9701
11616
  classes = {}
9702
11617
  acc = new_def_index_accumulator
9703
11618
  bundles = {}
@@ -9726,7 +11641,9 @@ module Rigor
9726
11641
  end
9727
11642
  rescue DeclarationWalk::ContractError
9728
11643
  raise # a broken walk contract, not an unreadable file: skipping it would hide the failure
9729
- rescue StandardError
11644
+ rescue StandardError, SystemStackError => e
11645
+ raise if uncontainable_overflow?(e)
11646
+
9730
11647
  # Skip files that fail to parse / read; the per-file analyzer surfaces the parse error separately.
9731
11648
  next
9732
11649
  end
@@ -10423,10 +12340,11 @@ module Rigor
10423
12340
 
10424
12341
  # #1154 — tags each `[:own_write, class, ivar]` entry with the file that records it, so a reader of the merged
10425
12342
  # census can tell a second file writing the same ivar of the same class (`UnseenIvarWriters`).
12343
+ # Issue #2079 — and each `[:own_write_type, class, ivar, value]` entry, which {IvarContributions} reads.
10426
12344
  def attribute_own_writes(census, path)
10427
- return census unless census.any? { |entry| entry.first == :own_write }
12345
+ return census unless census.any? { |entry| PATH_TAGGED_KINDS.include?(entry.first) }
10428
12346
 
10429
- census.to_set { |entry| entry.first == :own_write ? [*entry, path].freeze : entry }
12347
+ census.to_set { |entry| PATH_TAGGED_KINDS.include?(entry.first) ? [*entry, path].freeze : entry }
10430
12348
  end
10431
12349
 
10432
12350
  # Folds one file's Data + Struct member-layout tables into the cross-file accumulator (kept out of
@@ -10562,7 +12480,8 @@ module Rigor
10562
12480
  singleton_cref)
10563
12481
  else
10564
12482
  census_constant_write(node, qualified_prefix, tables, self_owner, singleton_cref: singleton_cref)
10565
- tables.write_census.visit(node, top_level: qualified_prefix.empty?, owner: ivar_owner)
12483
+ tables.write_census.visit(node, top_level: qualified_prefix.empty?, owner: ivar_owner,
12484
+ receiver_owner: census_receiver_owner(node, qualified_prefix, self_owner))
10566
12485
  end
10567
12486
 
10568
12487
  rebound = rebound_block_self(node, qualified_prefix, nil, meta_owner, self_owner)
@@ -10604,6 +12523,16 @@ module Rigor
10604
12523
  qualified_prefix
10605
12524
  end
10606
12525
 
12526
+ # Issue #2488 — the class an explicit-receiver writer call (`E.attr_writer :w`, `E.define_method(:m) { … }`)
12527
+ # names, resolved as a `class_eval` receiver is in this census (keyed as written), or nil.
12528
+ def census_receiver_owner(node, qualified_prefix, self_owner)
12529
+ receiver = ReceiverWriterCall.receiver(node)
12530
+ return nil if receiver.nil?
12531
+
12532
+ name = eval_receiver_self(receiver, qualified_prefix, nil, rebound_self_base(self_owner))
12533
+ name if name.is_a?(String)
12534
+ end
12535
+
10607
12536
  # Under an unnameable cref a bare/`self::` header pushes `#<singleton>::Name` — still
10608
12537
  # unnameable — so the body keeps the OPAQUE self and the cref flag; nameable
10609
12538
  # headers re-anchor at a real cref.
@@ -10859,7 +12788,7 @@ module Rigor
10859
12788
  # A {DYNAMIC_TARGET_PREFIX} key is not a name and never publishes; it is expanded by
10860
12789
  # {#apply_dynamic_target_writes} into a retraction over the names it could have written.
10861
12790
  #
10862
- # @return `[{name => Type}, {name => Set[path]}]`.
12791
+ # @return the value table and the table of source paths per name.
10863
12792
  def finalize_constant_writes(writes)
10864
12793
  values = {}
10865
12794
  sources = {}
@@ -11607,20 +13536,25 @@ module Rigor
11607
13536
 
11608
13537
  # Recognises `Data.define(*Symbol)` and `Data.define(*Symbol) do ... end` at constant-write rvalue position. The
11609
13538
  # receiver MUST be the bare `Data` constant (or `::Data`); other receivers (a local variable, a method call
11610
- # return) are rejected because their identity is not statically known.
13539
+ # return) are rejected because their identity is not statically known. Issue #2968 — a splatted member list
13540
+ # (`Data.define(*FIELDS)`) still creates the class; {#meta_members_open?} marks its members unknown.
11611
13541
  def data_define_call?(node)
11612
13542
  return false unless node.is_a?(Prism::CallNode)
11613
13543
  return false unless node.name == :define
11614
13544
  return false unless meta_constant_receiver?(node.receiver, :Data)
11615
13545
 
11616
13546
  args = node.arguments&.arguments || []
11617
- args.all?(Prism::SymbolNode)
13547
+ args.all? { |arg| meta_member_argument?(arg) }
11618
13548
  end
11619
13549
 
11620
13550
  # Recognises `Struct.new(*Symbol)` and `Struct.new(*Symbol, keyword_init: <expr>)` at constant-write rvalue
11621
13551
  # position. A trailing `KeywordHashNode` (the `keyword_init: ...` form) is accepted but does not contribute to
11622
13552
  # member discovery; every other argument MUST be a `Prism::SymbolNode`. At least one Symbol member is required —
11623
13553
  # `Struct.new()` is a degenerate form callers don't typically use.
13554
+ #
13555
+ # Issue #2968 — a leading String (`Struct.new("Named", :a)`, the class `Struct::Named`) and a splatted member list
13556
+ # (`Struct.new(*FIELDS)`) create the class just the same. {#meta_member_positionals} drops the String;
13557
+ # {#meta_members_open?} marks a splatted list's members unknown.
11624
13558
  def struct_new_call?(node)
11625
13559
  return false unless meta_call_with_name?(node, :Struct, :new)
11626
13560
 
@@ -11628,7 +13562,12 @@ module Rigor
11628
13562
  positional = struct_new_positionals(args)
11629
13563
  return false if positional.nil? || positional.empty?
11630
13564
 
11631
- positional.all?(Prism::SymbolNode)
13565
+ positional = positional.drop(1) if positional.first.is_a?(Prism::StringNode)
13566
+ positional.all? { |arg| meta_member_argument?(arg) }
13567
+ end
13568
+
13569
+ def meta_member_argument?(arg)
13570
+ arg.is_a?(Prism::SymbolNode) || arg.is_a?(Prism::SplatNode)
11632
13571
  end
11633
13572
 
11634
13573
  # Recognises `Module.new` and `Module.new(&block)` / `Module.new do ... end` at constant-write rvalue position.
@@ -11734,8 +13673,18 @@ module Rigor
11734
13673
  }.freeze
11735
13674
  private_constant :UNENTERED_FLOW_ENTRY
11736
13675
 
11737
- UNENTERED_EVALUATOR_KEY = :rigor_scope_index_unentered_evaluator
11738
- private_constant :UNENTERED_EVALUATOR_KEY
13676
+ # ADR-129 WD1 — a Frame: {#with_unentered_evaluator} installs the walk's evaluation for the walk and restores the
13677
+ # outer one, and whether one is installed changes answers (an unentered construct is evaluated only inside a
13678
+ # walk).
13679
+ UNENTERED_EVALUATOR = TypingState.frame(
13680
+ :scope_index_unentered_evaluator,
13681
+ reason: "installed around the index walk that evaluates its unentered constructs; the lambda writes the " \
13682
+ "walk's own table (the walk's state, not typing state) and reads its converged-loop recording and " \
13683
+ "clause widening, and is frozen while installed",
13684
+ # G1′: the evaluation types under the Frame, but only the index walk, outside `type_of`, reads it.
13685
+ typing_never_reads: true
13686
+ )
13687
+ private_constant :UNENTERED_EVALUATOR
11739
13688
 
11740
13689
  # Issues #2196 and #2056 — an `&&` / `||` / `and` / `or`, an `if` / `unless` / ternary or a `while` / `until`
11741
13690
  # in a value position (an array element, a hash value, a call argument, an interpolation, a multi-value
@@ -11746,6 +13695,8 @@ module Rigor
11746
13695
  # the scope this walk reached it with, under the index's own recorder, so each part is recorded with the scope
11747
13696
  # the statement form records it with. What it does not visit (a dead arm) is walked as before.
11748
13697
  def evaluate_unentered_flow(node, table, current_scope)
13698
+ return evaluate_unentered_block(node, table, current_scope) if node.is_a?(Prism::CallNode)
13699
+
11749
13700
  entry = UNENTERED_FLOW_ENTRY[node.class]
11750
13701
  return if entry.nil?
11751
13702
 
@@ -11754,10 +13705,27 @@ module Rigor
11754
13705
  part = node.public_send(entry) || (node.conditions.first&.conditions&.first if node.is_a?(Prism::CaseNode))
11755
13706
  return if part.nil? || table.key?(part)
11756
13707
 
11757
- evaluate = Thread.current[UNENTERED_EVALUATOR_KEY]
13708
+ evaluate = TypingState.current[UNENTERED_EVALUATOR]
11758
13709
  evaluate&.call(node, current_scope, code_body_nodes(node, current_scope.source_path))
11759
13710
  end
11760
13711
 
13712
+ # Issue #1246 — the block of a call in a value position (`show([1].map { x = "s"; x.upcase })`, a receiver
13713
+ # chain, an array element, a `->`'s body) is never entered: {StatementEvaluator} types the call through
13714
+ # `ExpressionTyper`, and this walk could only floor a local the block rebinds ({#closure_scope}). Such a block
13715
+ # is entered here instead, alone, from the scope this walk reached its call with, so its body is recorded with
13716
+ # the scopes the statement form records: a capture read before the block rebinds it reads its binding, and the
13717
+ # block's parameters are bound. The call's receiver and arguments stay with the walk. A class-creating or
13718
+ # `refine` block keeps the walk ({#unentered_block_entry}), and a code body inside the block is left out of the
13719
+ # recording ({#code_body_nodes}). A `super` block is a `SuperNode`'s, which the evaluator never enters either.
13720
+ def evaluate_unentered_block(node, table, current_scope)
13721
+ block = node.block
13722
+ return unless block.is_a?(Prism::BlockNode)
13723
+ return if table.key?(block) || class_body_block_call?(node, current_scope.source_path)
13724
+
13725
+ evaluate = TypingState.current[UNENTERED_EVALUATOR]
13726
+ evaluate&.call(node, current_scope, code_body_nodes(block, current_scope.source_path), block_only: true)
13727
+ end
13728
+
11761
13729
  # The evaluation of an unentered construct starts with no class frame and no lexical nesting, which only a code
11762
13730
  # body observes: a `def`, a `class` / `module` / `class << …` body, and the block of a class-creating call
11763
13731
  # (`Class.new { … }`, `Struct.new`, `Data.define`, `Module.new`) or of a `refine`, whose `def`s and constants
@@ -11791,12 +13759,8 @@ module Rigor
11791
13759
  # Index walks nest (a rule may re-index a file while another file's walk is suspended), so the outer one is
11792
13760
  # restored on exit. A `propagate` called directly while an outer index walk is running would record into the
11793
13761
  # OUTER index's table through it; nothing outside {#index} calls `propagate`.
11794
- def with_unentered_evaluator(evaluate)
11795
- outer = Thread.current[UNENTERED_EVALUATOR_KEY]
11796
- Thread.current[UNENTERED_EVALUATOR_KEY] = evaluate
11797
- yield
11798
- ensure
11799
- Thread.current[UNENTERED_EVALUATOR_KEY] = outer
13762
+ def with_unentered_evaluator(evaluate, &)
13763
+ TypingState.current.with(UNENTERED_EVALUATOR, evaluate.freeze, &)
11800
13764
  end
11801
13765
 
11802
13766
  SELF_OPENING_BODIES = [Prism::DefNode, Prism::ClassNode, Prism::ModuleNode, Prism::SingletonClassNode].freeze
@@ -11804,6 +13768,12 @@ module Rigor
11804
13768
 
11805
13769
  # Issue #1717 — a `def` / `class` / `module` body opens its own `self`, so an enclosing unentered block's
11806
13770
  # unknown-`self` mark ({Scope#block_self_unknown?}) stops at it, as the evaluator's fresh body scope does.
13771
+ # Issue #1246 — it opens its own local table too, so a name in it (a `def`'s parameter, a body local) is a new
13772
+ # variable, never the enclosing statement's local of that name: `show(Class.new { def call(o) = o + 1 })` after
13773
+ # `o = { x: 1 }` read the parameter as the Hash. Such a name reads `Dynamic[top]` ({#shadow_local}), as in an
13774
+ # unentered closure ({#closure_scope}); a body the evaluator entered is recorded and keeps its own scopes. Only
13775
+ # the parameters and the body take that table: a `def`'s receiver, a `class`'s name and superclass and a
13776
+ # `class << …`'s expression run in the enclosing scope (`class K < base.nope; base = 2; end`).
11807
13777
  # A `case` narrows its clauses ({#propagate_subjectless_case}, {#propagate_subject_case}). Every other node
11808
13778
  # hands its children the scope as is.
11809
13779
  def propagate_children(node, table, current_scope)
@@ -11812,10 +13782,19 @@ module Rigor
11812
13782
 
11813
13783
  return propagate_subject_case(node, table, current_scope)
11814
13784
  end
13785
+ return propagate_code_body(node, table, current_scope) if SELF_OPENING_BODIES.include?(node.class)
11815
13786
 
11816
- child_scope =
11817
- SELF_OPENING_BODIES.include?(node.class) ? current_scope.with_block_self_unknown(false) : current_scope
11818
- node.rigor_each_child { |child| propagate(child, table, child_scope) }
13787
+ node.rigor_each_child { |child| propagate(child, table, current_scope) }
13788
+ end
13789
+
13790
+ def propagate_code_body(node, table, current_scope)
13791
+ outer_scope = current_scope.with_block_self_unknown(false)
13792
+ inner = [node.body]
13793
+ inner << node.parameters if node.is_a?(Prism::DefNode)
13794
+ inner_scope = node.locals.reduce(outer_scope) { |acc, name| shadow_local(acc, name) }
13795
+ node.rigor_each_child do |child|
13796
+ propagate(child, table, inner.any? { |part| part.equal?(child) } ? inner_scope : outer_scope)
13797
+ end
11819
13798
  end
11820
13799
 
11821
13800
  # Issue #1361 — the block of `Thread.new`, `Fiber.new`, `define_method` and the other calls
@@ -11833,6 +13812,10 @@ module Rigor
11833
13812
  # argument one scope and threads none, so arguments that write keep the call's: an earlier one may rebind the
11834
13813
  # receiver a later one reads (`return y&.concat((y = nil).to_s, y.upcase)`, where the evaluator threads no
11835
13814
  # operand).
13815
+ #
13816
+ # Issue #1246 — the block of a value-position call is evaluated before this walk reaches it
13817
+ # ({#evaluate_unentered_block}), so it arrives recorded and keeps the evaluator's entry. The block entries
13818
+ # above therefore reach only a block that evaluation declines: a class-creating or `refine` block.
11836
13819
  def propagate_call(node, table, current_scope, recorded: false)
11837
13820
  block = node.block
11838
13821
  operands =
@@ -11868,6 +13851,7 @@ module Rigor
11868
13851
  current_scope = Narrowing.safe_navigation_block_scope(node, current_scope)
11869
13852
  # Issue #1429 — nor a guard's narrowing of a global or constant the body may run after code rebinds.
11870
13853
  current_scope = GuardRebinding.block_entry(current_scope, block, node)
13854
+ current_scope = current_scope.entering_block_without_gradual_marks(block)
11871
13855
  entry =
11872
13856
  if FreshFrameBlocks.fresh_entry?(node, current_scope)
11873
13857
  FreshFrameBlocks.entry(current_scope, node)
@@ -11997,14 +13981,17 @@ module Rigor
11997
13981
  # `it` as the outer block's). An enclosing binding of such a name is replaced with `Dynamic[top]`. Not the
11998
13982
  # signature's parameter type: this walk evaluates nothing, and a body write to the name is never threaded,
11999
13983
  # so any narrower claim could be stale. A name the enclosing scope does not bind is left unbound, which
12000
- # reads the same `Dynamic[top]`. Captured names — outer locals the body reads or rebinds without
12001
- # redeclaring them — keep the enclosing binding. Issue #1360 — a `->` body and its parameter defaults run
12002
- # whenever the lambda is called, so they read `$!`, `$@` and `$?` unbound ({FreshFrameBlocks.closure_entry}).
13984
+ # reads the same `Dynamic[top]`. Issue #1246 — for the same reason a captured name the body (or a closure
13985
+ # nested in it) writes reads `Dynamic[top]` too ({#widen_condition_writes}): `x = 1; show([1].map { x = "s";
13986
+ # x.upcase })` read `x` as `1` after the write. A captured name the body only reads keeps the enclosing
13987
+ # binding. Issue #1360 — a `->` body and its parameter defaults run whenever the lambda is called, so they read
13988
+ # `$!`, `$@` and `$?` unbound ({FreshFrameBlocks.closure_entry}).
12003
13989
  def closure_scope(closure, scope)
12004
13990
  scope = GuardRebinding.block_entry(scope, closure, nil)
12005
13991
  scope = FreshFrameBlocks.closure_entry(scope, closure, nil)
12006
13992
  scope = shadow_local(scope, :it) if closure.parameters.is_a?(Prism::ItParametersNode)
12007
- closure.locals.reduce(scope) { |acc, name| shadow_local(acc, name) }
13993
+ scope = closure.locals.reduce(scope) { |acc, name| shadow_local(acc, name) }
13994
+ widen_condition_writes(closure, scope)
12008
13995
  end
12009
13996
 
12010
13997
  def shadow_local(scope, name)