rigortype 0.4.7 → 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 (268) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +3 -1
  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 +29 -0
  6. data/data/gem_overlay/addressable/addressable.rbs +40 -0
  7. data/docs/handbook/01-getting-started.md +1 -1
  8. data/docs/handbook/11-sig-gen.md +35 -0
  9. data/docs/install.md +20 -0
  10. data/docs/manual/01-installation.md +67 -26
  11. data/docs/manual/02-cli-reference.md +307 -91
  12. data/docs/manual/03-configuration.md +41 -3
  13. data/docs/manual/04-diagnostics.md +5 -4
  14. data/docs/manual/08-skills.md +11 -0
  15. data/docs/manual/09-editor-integration.md +8 -0
  16. data/docs/manual/14-rails-quickstart.md +1 -1
  17. data/docs/manual/19-effect-labels.md +3 -4
  18. data/docs/manual/plugins/rigor-actionpack.md +15 -6
  19. data/docs/manual/plugins/rigor-activesupport-core-ext.md +3 -0
  20. data/docs/manual/plugins/rigor-sorbet.md +17 -3
  21. data/lib/rigor/analysis/check_rules/always_raises_shield.rb +147 -0
  22. data/lib/rigor/analysis/check_rules/call_facts.rb +58 -0
  23. data/lib/rigor/analysis/check_rules/declaration_sourced_guard.rb +8 -0
  24. data/lib/rigor/analysis/check_rules/ivar_write_collector.rb +13 -2
  25. data/lib/rigor/analysis/check_rules/lexical_method_sites.rb +6 -0
  26. data/lib/rigor/analysis/check_rules/plugin_api_surface.rb +195 -0
  27. data/lib/rigor/analysis/check_rules/published_constant_guard.rb +40 -11
  28. data/lib/rigor/analysis/check_rules/rbs_receiver_surface.rb +27 -1
  29. data/lib/rigor/analysis/check_rules/replaced_singleton_defs.rb +96 -0
  30. data/lib/rigor/analysis/check_rules/rule_ids.rb +8 -1
  31. data/lib/rigor/analysis/check_rules/void_value_use_collector.rb +1 -1
  32. data/lib/rigor/analysis/check_rules.rb +598 -99
  33. data/lib/rigor/analysis/crash_signature.rb +43 -1
  34. data/lib/rigor/analysis/dependency_recorder.rb +233 -76
  35. data/lib/rigor/analysis/incremental.rb +11 -1
  36. data/lib/rigor/analysis/incremental_run_slot/write_guard.rb +2 -1
  37. data/lib/rigor/analysis/incremental_session.rb +279 -37
  38. data/lib/rigor/analysis/reachability/plugin_roots.rb +6 -3
  39. data/lib/rigor/analysis/rule_catalog.rb +37 -1
  40. data/lib/rigor/analysis/run_cache_key.rb +27 -3
  41. data/lib/rigor/analysis/run_disclosure_credit.rb +26 -6
  42. data/lib/rigor/analysis/runner/buffer_pool_dispatcher.rb +4 -0
  43. data/lib/rigor/analysis/runner/declaration_position.rb +7 -7
  44. data/lib/rigor/analysis/runner/diagnostic_aggregator.rb +72 -20
  45. data/lib/rigor/analysis/runner/effect_envelope_pass.rb +3 -8
  46. data/lib/rigor/analysis/runner/pool_coordinator.rb +26 -5
  47. data/lib/rigor/analysis/runner/project_pre_passes.rb +4 -37
  48. data/lib/rigor/analysis/runner.rb +222 -39
  49. data/lib/rigor/analysis/self_call_resolution_recorder.rb +61 -22
  50. data/lib/rigor/analysis/worker_session.rb +42 -41
  51. data/lib/rigor/bleeding_edge.rb +19 -4
  52. data/lib/rigor/cache/file_digest.rb +39 -31
  53. data/lib/rigor/cache/incremental_snapshot.rb +66 -7
  54. data/lib/rigor/cache/rbs_descriptor.rb +37 -2
  55. data/lib/rigor/cache/rbs_environment.rb +5 -1
  56. data/lib/rigor/cli/baseline_command.rb +15 -2
  57. data/lib/rigor/cli/check_command.rb +4 -2
  58. data/lib/rigor/cli/effects_snapshot_command.rb +8 -1
  59. data/lib/rigor/cli/lens_command.rb +17 -2
  60. data/lib/rigor/cli/sig_gen_command.rb +8 -0
  61. data/lib/rigor/cli/skill_describe.rb +1 -0
  62. data/lib/rigor/cli.rb +5 -2
  63. data/lib/rigor/config_audit.rb +32 -18
  64. data/lib/rigor/configuration/severity_profile.rb +9 -0
  65. data/lib/rigor/configuration.rb +32 -5
  66. data/lib/rigor/effects/collector.rb +53 -21
  67. data/lib/rigor/environment/bundle_sig_discovery.rb +63 -28
  68. data/lib/rigor/environment/constant_type_cache_holder.rb +5 -0
  69. data/lib/rigor/environment/default_libraries.rb +17 -0
  70. data/lib/rigor/environment/deferred_signature_plan.rb +104 -6
  71. data/lib/rigor/environment/hkt_registry_holder.rb +6 -0
  72. data/lib/rigor/environment/rbs_collection_discovery.rb +123 -8
  73. data/lib/rigor/environment/rbs_coverage_report.rb +12 -11
  74. data/lib/rigor/environment/rbs_hierarchy.rb +6 -0
  75. data/lib/rigor/environment/rbs_loader.rb +312 -63
  76. data/lib/rigor/environment.rb +131 -35
  77. data/lib/rigor/inference/acceptance.rb +50 -15
  78. data/lib/rigor/inference/alias_mutation_floor/foreign_locals.rb +117 -0
  79. data/lib/rigor/inference/alias_mutation_floor/memos.rb +85 -0
  80. data/lib/rigor/inference/alias_mutation_floor/reach.rb +119 -0
  81. data/lib/rigor/inference/alias_mutation_floor/site_collector.rb +252 -0
  82. data/lib/rigor/inference/alias_mutation_floor.rb +339 -0
  83. data/lib/rigor/inference/anonymous_meta_class.rb +18 -0
  84. data/lib/rigor/inference/block_auto_splat.rb +1 -1
  85. data/lib/rigor/inference/block_run_count.rb +9 -2
  86. data/lib/rigor/inference/builtins/array_catalog.rb +2 -2
  87. data/lib/rigor/inference/callee_mutation_chain.rb +1 -1
  88. data/lib/rigor/inference/captured_locals.rb +2 -0
  89. data/lib/rigor/inference/census_foreign_block.rb +354 -0
  90. data/lib/rigor/inference/clause_widening.rb +1 -1
  91. data/lib/rigor/inference/closure_escape_analyzer.rb +11 -4
  92. data/lib/rigor/inference/construction_escape.rb +56 -15
  93. data/lib/rigor/inference/declaration_walk.rb +4 -0
  94. data/lib/rigor/inference/def_node_resolver.rb +32 -14
  95. data/lib/rigor/inference/def_return_typer.rb +1 -1
  96. data/lib/rigor/inference/definer_resolution.rb +15 -7
  97. data/lib/rigor/inference/definers_index.rb +72 -16
  98. data/lib/rigor/inference/error_info.rb +16 -0
  99. data/lib/rigor/inference/expression_typer.rb +1459 -287
  100. data/lib/rigor/inference/flow_tracer.rb +31 -14
  101. data/lib/rigor/inference/fork_map.rb +3 -0
  102. data/lib/rigor/inference/fresh_frame_blocks.rb +52 -1
  103. data/lib/rigor/inference/global_write_census.rb +8 -6
  104. data/lib/rigor/inference/in_effect_refinements.rb +7 -0
  105. data/lib/rigor/inference/indexed_narrowing.rb +57 -8
  106. data/lib/rigor/inference/ivar_writer_census.rb +106 -11
  107. data/lib/rigor/inference/key_presence_guard.rb +51 -25
  108. data/lib/rigor/inference/last_line/self_evidence.rb +30 -0
  109. data/lib/rigor/inference/last_line.rb +5 -2
  110. data/lib/rigor/inference/loop_value_record.rb +63 -15
  111. data/lib/rigor/inference/match_rebinding/frame.rb +11 -0
  112. data/lib/rigor/inference/match_rebinding/yielding_match.rb +74 -18
  113. data/lib/rigor/inference/match_rebinding.rb +21 -8
  114. data/lib/rigor/inference/method_dispatcher/block_folding.rb +43 -29
  115. data/lib/rigor/inference/method_dispatcher/class_keyed_block_params.rb +65 -0
  116. data/lib/rigor/inference/method_dispatcher/core_enumerable_receiver.rb +44 -0
  117. data/lib/rigor/inference/method_dispatcher/declared_constructor.rb +151 -0
  118. data/lib/rigor/inference/method_dispatcher/facet_distribution.rb +1 -1
  119. data/lib/rigor/inference/method_dispatcher/imprecise_argument.rb +2 -2
  120. data/lib/rigor/inference/method_dispatcher/interface_shape.rb +181 -0
  121. data/lib/rigor/inference/method_dispatcher/overload_selector.rb +12 -0
  122. data/lib/rigor/inference/method_dispatcher/rbs_dispatch.rb +409 -67
  123. data/lib/rigor/inference/method_dispatcher/self_substitute.rb +1 -1
  124. data/lib/rigor/inference/method_dispatcher/shape_dispatch.rb +384 -64
  125. data/lib/rigor/inference/method_dispatcher/singleton_type_params.rb +104 -0
  126. data/lib/rigor/inference/method_dispatcher.rb +66 -18
  127. data/lib/rigor/inference/method_parameter_binder.rb +136 -56
  128. data/lib/rigor/inference/module_function_state/spellings.rb +2 -2
  129. data/lib/rigor/inference/module_function_state.rb +13 -2
  130. data/lib/rigor/inference/multi_target_binder.rb +1 -1
  131. data/lib/rigor/inference/mutation_widening.rb +44 -4
  132. data/lib/rigor/inference/narrowing.rb +201 -27
  133. data/lib/rigor/inference/operand_effects.rb +43 -2
  134. data/lib/rigor/inference/optimistic_origin.rb +133 -8
  135. data/lib/rigor/inference/origin_lookup.rb +1 -1
  136. data/lib/rigor/inference/precision_scanner.rb +1 -1
  137. data/lib/rigor/inference/project_method_ownership.rb +28 -10
  138. data/lib/rigor/inference/project_patched_methods.rb +65 -13
  139. data/lib/rigor/inference/project_patched_scanner.rb +120 -25
  140. data/lib/rigor/inference/project_patched_visibility.rb +39 -3
  141. data/lib/rigor/inference/rbs_type_translator.rb +68 -15
  142. data/lib/rigor/inference/receiver_writer_call.rb +67 -0
  143. data/lib/rigor/inference/recursion_rebinding.rb +157 -0
  144. data/lib/rigor/inference/refined_dispatch/constructed_class.rb +56 -18
  145. data/lib/rigor/inference/refined_dispatch/own_method.rb +51 -17
  146. data/lib/rigor/inference/refined_dispatch.rb +13 -0
  147. data/lib/rigor/inference/reflective_ivar_write.rb +19 -11
  148. data/lib/rigor/inference/remover_holder_scan.rb +76 -22
  149. data/lib/rigor/inference/scope_indexer/class_cvars_collector.rb +13 -6
  150. data/lib/rigor/inference/scope_indexer/declared_param_writes.rb +12 -11
  151. data/lib/rigor/inference/scope_indexer/ivar_contributions.rb +188 -28
  152. data/lib/rigor/inference/scope_indexer/mixin_accumulator.rb +136 -9
  153. data/lib/rigor/inference/scope_indexer.rb +2079 -183
  154. data/lib/rigor/inference/self_call_ivar_writes.rb +26 -7
  155. data/lib/rigor/inference/self_preserving_block.rb +87 -0
  156. data/lib/rigor/inference/singleton_hook_decline.rb +41 -6
  157. data/lib/rigor/inference/singleton_object_constant.rb +32 -2
  158. data/lib/rigor/inference/statement_evaluator.rb +970 -250
  159. data/lib/rigor/inference/string_mutation.rb +14 -0
  160. data/lib/rigor/inference/synthetic_method_scanner.rb +35 -11
  161. data/lib/rigor/inference/toplevel_def_census.rb +14 -6
  162. data/lib/rigor/inference/unknown_store_widening.rb +1 -1
  163. data/lib/rigor/inference/untyped_rejoin.rb +1 -1
  164. data/lib/rigor/inference/void_tail_summary.rb +29 -10
  165. data/lib/rigor/lens/claims/comparison.rb +78 -0
  166. data/lib/rigor/lens/claims/request.rb +131 -0
  167. data/lib/rigor/lens/claims/type_reader.rb +216 -0
  168. data/lib/rigor/lens/claims.rb +208 -0
  169. data/lib/rigor/lens/contract_reader.rb +5 -0
  170. data/lib/rigor/lens/document.rb +1 -1
  171. data/lib/rigor/lens/file_reader.rb +21 -8
  172. data/lib/rigor/lens/run.rb +8 -0
  173. data/lib/rigor/lens.rb +1 -1
  174. data/lib/rigor/library_probe.rb +68 -0
  175. data/lib/rigor/memo_audit.rb +282 -0
  176. data/lib/rigor/plugin/base.rb +15 -2
  177. data/lib/rigor/plugin/io_boundary.rb +25 -3
  178. data/lib/rigor/plugin/manifest.rb +7 -3
  179. data/lib/rigor/plugin/registry.rb +13 -2
  180. data/lib/rigor/plugin/trust_policy.rb +46 -0
  181. data/lib/rigor/protection/mutation_cache.rb +4 -1
  182. data/lib/rigor/reflection/constant_ancestors.rb +79 -22
  183. data/lib/rigor/reflection/declaration_collectors.rb +55 -0
  184. data/lib/rigor/reflection/declarations.rb +58 -0
  185. data/lib/rigor/reflection/project_method.rb +177 -0
  186. data/lib/rigor/reflection.rb +2 -0
  187. data/lib/rigor/rooted_glob.rb +18 -1
  188. data/lib/rigor/run_scoped_slots.rb +20 -20
  189. data/lib/rigor/scope/discovery_index.rb +149 -2
  190. data/lib/rigor/scope/resolution_chain/relevance.rb +14 -1
  191. data/lib/rigor/scope/resolution_chain.rb +900 -55
  192. data/lib/rigor/scope.rb +307 -34
  193. data/lib/rigor/sig_gen/bot_proposal_guard.rb +306 -0
  194. data/lib/rigor/sig_gen/classification.rb +14 -1
  195. data/lib/rigor/sig_gen/generator.rb +222 -13
  196. data/lib/rigor/sig_gen/method_candidate.rb +14 -0
  197. data/lib/rigor/sig_gen/observation_collector.rb +11 -2
  198. data/lib/rigor/sig_gen/platform_guarded_files.rb +184 -0
  199. data/lib/rigor/sig_gen/skip_reason_catalog.rb +47 -0
  200. data/lib/rigor/sig_gen/writer.rb +21 -0
  201. data/lib/rigor/sig_gen.rb +1 -0
  202. data/lib/rigor/source/forwardable_delegators.rb +122 -0
  203. data/lib/rigor/source/node_children.rb +13 -0
  204. data/lib/rigor/type/combinator.rb +164 -8
  205. data/lib/rigor/type/interface.rb +60 -0
  206. data/lib/rigor/type.rb +1 -0
  207. data/lib/rigor/type_of_memo/serving.rb +99 -0
  208. data/lib/rigor/type_of_memo/stats.rb +43 -0
  209. data/lib/rigor/type_of_memo.rb +337 -0
  210. data/lib/rigor/typing_state.rb +1094 -0
  211. data/lib/rigor/version.rb +1 -1
  212. data/plugins/rigor-actionpack/lib/rigor/plugin/actionpack/view_assigns.rb +2 -2
  213. data/plugins/rigor-actionpack/lib/rigor/plugin/actionpack.rb +57 -25
  214. data/plugins/rigor-active-model-serializers/lib/rigor/plugin/active_model_serializers.rb +3 -6
  215. data/plugins/rigor-activejob/lib/rigor/plugin/activejob/analyzer.rb +25 -1
  216. data/plugins/rigor-activejob/lib/rigor/plugin/activejob/job_discoverer.rb +16 -6
  217. data/plugins/rigor-activejob/lib/rigor/plugin/activejob/job_index.rb +4 -2
  218. data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord/analyzer.rb +2 -1
  219. data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord/schema_parser.rb +130 -30
  220. data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord/schema_table.rb +20 -5
  221. data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord/structure_sql_parser.rb +7 -4
  222. data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord.rb +49 -13
  223. data/plugins/rigor-activerecord/sig/active_record/relation.rbs +19 -5
  224. data/plugins/rigor-activestorage/lib/rigor/plugin/activestorage.rb +4 -2
  225. data/plugins/rigor-activesupport-core-ext/lib/rigor/plugin/activesupport_core_ext.rb +13 -5
  226. data/plugins/rigor-activesupport-core-ext/sig/active_support/core_ext.rbs +29 -0
  227. data/plugins/rigor-alba/lib/rigor/plugin/alba/resource_collector.rb +15 -21
  228. data/plugins/rigor-alba/lib/rigor/plugin/alba/resource_discoverer.rb +1 -6
  229. data/plugins/rigor-ffi/lib/rigor/plugin/ffi.rb +4 -2
  230. data/plugins/rigor-graphql/lib/rigor/plugin/graphql.rb +12 -2
  231. data/plugins/rigor-mangrove/lib/rigor/plugin/mangrove.rb +6 -0
  232. data/plugins/rigor-rails-routes/lib/rigor/plugin/rails_routes/analyzer.rb +199 -1
  233. data/plugins/rigor-rails-routes/lib/rigor/plugin/rails_routes.rb +3 -2
  234. data/plugins/rigor-rspec/lib/rigor/plugin/rspec/matcher_analyzer.rb +4 -4
  235. data/plugins/rigor-sorbet/lib/rigor/plugin/sorbet/catalog.rb +209 -2
  236. data/plugins/rigor-sorbet/lib/rigor/plugin/sorbet/catalog_walker.rb +146 -7
  237. data/plugins/rigor-sorbet/lib/rigor/plugin/sorbet/type_translator.rb +52 -34
  238. data/plugins/rigor-sorbet/lib/rigor/plugin/sorbet.rb +80 -3
  239. data/plugins/rigor-typelizer/lib/rigor/plugin/typelizer/serializer_collector.rb +16 -16
  240. data/plugins/rigor-typelizer/lib/rigor/plugin/typelizer/serializer_discoverer.rb +1 -6
  241. data/sig/rigor/analysis/check_rules/rule_walk.rbs +6 -0
  242. data/sig/rigor/analysis/fact_store.rbs +2 -3
  243. data/sig/rigor/cache/descriptor.rbs +14 -0
  244. data/sig/rigor/environment.rbs +4 -1
  245. data/sig/rigor/plugin/base.rbs +53 -19
  246. data/sig/rigor/plugin/interfaces.rbs +51 -0
  247. data/sig/rigor/plugin/io_boundary.rbs +2 -3
  248. data/sig/rigor/plugin/node_context.rbs +21 -0
  249. data/sig/rigor/reflection.rbs +37 -0
  250. data/sig/rigor/scope.rbs +22 -3
  251. data/sig/rigor/trinary.rbs +1 -0
  252. data/sig/rigor/type.rbs +9 -3
  253. data/sig/rigor.rbs +3 -2
  254. data/skills/rigor-ask/SKILL.md +1 -0
  255. data/skills/rigor-project-init/references/01-detect.md +1 -0
  256. data/skills/rigor-project-init/references/03-baseline-and-bugs.md +5 -1
  257. data/skills/rigor-project-init/references/06-agent-contract.md +4 -4
  258. data/skills/rigor-rbs-contract/SKILL.md +147 -0
  259. data/skills/rigor-rbs-contract/references/01-finding-candidates.md +212 -0
  260. data/skills/rigor-rbs-contract/references/02-contract-rules.md +157 -0
  261. data/skills/rigor-rbs-contract/references/03-target-own-code.md +90 -0
  262. data/skills/rigor-rbs-contract/references/04-target-gem-rbs-collection.md +210 -0
  263. data/skills/rigor-rbs-contract/references/05-declaration-mutants.md +182 -0
  264. data/skills/rigor-rbs-setup/SKILL.md +3 -1
  265. data/skills/rigor-type-oracle/SKILL.md +26 -1
  266. data/skills/rigor-type-oracle/references/02-agents-md-fragment.md +4 -3
  267. metadata +48 -2
  268. data/lib/rigor/inference/memo_audit.rb +0 -55
@@ -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,8 +13,11 @@ 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"
18
22
  require_relative "definers_index"
19
23
  require_relative "fresh_frame_blocks"
@@ -26,6 +30,7 @@ require_relative "hash_lookup_mutation"
26
30
  require_relative "in_effect_refinements"
27
31
  require_relative "refine_census"
28
32
  require_relative "refine_self"
33
+ require_relative "receiver_writer_call"
29
34
  require_relative "reflective_ivar_write"
30
35
  require_relative "index_write_widening"
31
36
  require_relative "module_function_state"
@@ -228,13 +233,14 @@ module Rigor
228
233
  # nodes in `excluded`: the file's own pass, and each construct {#evaluate_unentered_flow} evaluates in the walk
229
234
  # after it.
230
235
  def recording_evaluator(table, converged_loop_recording, widen_clauses)
231
- lambda do |node, scope, excluded = nil|
236
+ lambda do |node, scope, excluded = nil, block_only: false|
232
237
  on_enter =
233
238
  if excluded.nil? || excluded.empty? then ->(visited, at) { table[visited] = at }
234
239
  else ->(visited, at) { table[visited] = at unless excluded.include?(visited) }
235
240
  end
236
- StatementEvaluator.new(scope: scope, on_enter: on_enter, converged_loop_recording: converged_loop_recording,
237
- 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)
238
244
  end
239
245
  end
240
246
 
@@ -393,8 +399,9 @@ module Rigor
393
399
  default_scope.discovered_includes.merge(file[:includes]) do |_class, cross_file, per_file|
394
400
  (per_file + cross_file).uniq
395
401
  end,
402
+ # ADR-129 G4 — each merged list is frozen as the per-file walk's lists are ({#freeze_mixin_lists}).
396
403
  default_scope.discovered_prepends.merge(file[:prepends]) do |_class, cross_file, per_file|
397
- (per_file + cross_file).uniq
404
+ (per_file + cross_file).uniq.freeze
398
405
  end,
399
406
  file[:unpositioned]
400
407
  ]
@@ -569,12 +576,13 @@ module Rigor
569
576
  read_before_write, init_writes, method_assign_effects)
570
577
  accumulator.delete(CONSTANT_IVAR_VALUES)
571
578
  reflective_opaque = accumulator.delete(REFLECTIVE_OPAQUE_CLASSES)
579
+ singleton_writes = accumulator.delete(SINGLETON_IVAR_WRITES)
572
580
  exempt_class_body_writes!(init_writes, accumulator.delete(DEFINE_METHOD_WRITES) || {})
573
581
  merge_held_ivar_writes!(accumulator)
574
582
  record_aliased_ivar_mutations!(root, mutated_ivars)
575
583
  widen_mutated_ivar_entries!(accumulator, mutated_ivars)
576
584
  # #2170 — an ancestor's writes as this file's census types them, before its read-before-write `nil`.
577
- writes = accumulator.transform_values(&:dup)
585
+ writes = instance_side_writes(accumulator, singleton_writes)
578
586
  contribute_read_before_write_nil!(accumulator, read_before_write, init_writes)
579
587
  make_reflective_classes_gradual!(accumulator, reflective_opaque)
580
588
  # #2079 / #2170 — another file's writes, and the chain's modules' and superclasses', join existing entries.
@@ -586,6 +594,17 @@ module Rigor
586
594
  accumulator.transform_values(&:freeze).freeze
587
595
  end
588
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
+
589
608
  # Folds the class-body writes {#collect_class_body_ivar_writes} held under {CLASS_BODY_WRITES} into the
590
609
  # `init_writes` exemption, except for an ivar a `define_method` block, or a `block_as_methods:` block bound to the
591
610
  # receiver's instance ({#method_body_block_call?}), of the class writes (#1768). Such an ivar had no census entry
@@ -949,6 +968,7 @@ module Rigor
949
968
  collect_initializer_block_ivars(node, owner, default_scope, accumulator, mutated_ivars, init_writes)
950
969
  record_attr_writer_ivars(node, owner, accumulator) if ATTR_WRITER_MACROS.include?(attr_macro(node))
951
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)
952
972
  false
953
973
  else
954
974
  walk_ivars_meta_new?(node, qualified_prefix, default_scope, accumulator,
@@ -1077,6 +1097,8 @@ module Rigor
1077
1097
  read_before_write = nil, init_writes = nil, method_assign_effects = nil,
1078
1098
  singleton_self: false)
1079
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)
1080
1102
 
1081
1103
  class_name = qualified_prefix.join("::")
1082
1104
  singleton = singleton_self || def_node.receiver.is_a?(Prism::SelfNode) ||
@@ -1103,6 +1125,10 @@ module Rigor
1103
1125
  declared = DeclaredParamWrites.collect(def_node, class_name, body_scope) unless singleton
1104
1126
  gather_ivar_writes(def_node.body, body_scope, class_name, accumulator,
1105
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
1106
1132
 
1107
1133
  # B2.3 — collect per-method evidence for the read-before- write nil contribution. The accumulator-level decision
1108
1134
  # ("is this ivar truly read-before-write across the class lifetime?") is finalised at
@@ -1129,7 +1155,7 @@ module Rigor
1129
1155
 
1130
1156
  seen_writes = Set.new
1131
1157
  read_first = Set.new
1132
- 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)
1133
1159
  init_set = (init_writes[class_name] ||= Set.new)
1134
1160
  seen_writes.each { |name| init_set << name }
1135
1161
  end
@@ -1192,6 +1218,29 @@ module Rigor
1192
1218
  define_method_written_ivars(block.body, written)
1193
1219
  end
1194
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
+
1195
1244
  # The ivars a `define_method` block body writes, in any write form, outside a nested `def` / `class` / `module`.
1196
1245
  def define_method_written_ivars(node, acc)
1197
1246
  return unless node.is_a?(Prism::Node)
@@ -1237,7 +1286,7 @@ module Rigor
1237
1286
 
1238
1287
  seen_writes = Set.new
1239
1288
  read_first = Set.new
1240
- 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)
1241
1290
 
1242
1291
  # ADR-38 — `initialize` is the built-in initializer gate; a plugin may declare additional `def`-form initializer
1243
1292
  # methods (minitest `setup`, Rails `after_initialize`, DI setters) on a constrained class. Both fold their
@@ -1352,7 +1401,7 @@ module Rigor
1352
1401
  )
1353
1402
  end
1354
1403
 
1355
- def detect_read_before_write(node, seen_writes, read_first)
1404
+ def detect_read_before_write(node, seen_writes, read_first, class_name = nil)
1356
1405
  return unless node.is_a?(Prism::Node)
1357
1406
  return if IVAR_BARRIER_NODES.any? { |klass| node.is_a?(klass) }
1358
1407
 
@@ -1366,16 +1415,17 @@ module Rigor
1366
1415
  # target is committed, so descend into `value` FIRST (an ivar read there is read-before-write), then mark every
1367
1416
  # ivar target as written.
1368
1417
  if node.is_a?(Prism::MultiWriteNode)
1369
- detect_read_before_write(node.value, seen_writes, read_first) if node.value
1370
- 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)
1371
1420
  return
1372
1421
  end
1373
1422
 
1374
1423
  # Descend BEFORE recording a write — `@x = @x + 1`'s RHS is an `InstanceVariableReadNode` that runs before the
1375
1424
  # write is committed; the read is therefore read-before-write semantically. `each_child` yields the value
1376
1425
  # child before the lvalue target (`compact_child_nodes` field order), matching this order.
1377
- node.rigor_each_child do |c|
1378
- 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)
1379
1429
  end
1380
1430
 
1381
1431
  seen_writes << node.name if IVAR_WRITE_NODES.any? { |klass| node.is_a?(klass) }
@@ -1387,19 +1437,19 @@ module Rigor
1387
1437
  # Records each ivar target of a `MultiWriteNode` / nested `MultiTargetNode` into `seen_writes`, and descends into
1388
1438
  # any non-ivar target (a `CallTargetNode` / `IndexTargetNode` receiver such as `@obj.x, @y = …`) so an ivar read
1389
1439
  # inside a target receiver still counts as read-before-write.
1390
- 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)
1391
1441
  targets = (node.lefts || []) + [node.rest].compact + (node.rights || [])
1392
1442
  targets.each do |target|
1393
1443
  case target
1394
1444
  when Prism::InstanceVariableTargetNode
1395
1445
  seen_writes << target.name
1396
1446
  when Prism::MultiTargetNode
1397
- detect_multi_write_target_writes(target, seen_writes, read_first)
1447
+ detect_multi_write_target_writes(target, seen_writes, read_first, class_name)
1398
1448
  when Prism::SplatNode
1399
1449
  inner = target.expression
1400
1450
  seen_writes << inner.name if inner.is_a?(Prism::InstanceVariableTargetNode)
1401
1451
  else
1402
- detect_read_before_write(target, seen_writes, read_first)
1452
+ detect_read_before_write(target, seen_writes, read_first, class_name)
1403
1453
  end
1404
1454
  end
1405
1455
  end
@@ -1407,6 +1457,21 @@ module Rigor
1407
1457
  IVAR_BARRIER_NODES = [Prism::DefNode, Prism::ClassNode, Prism::ModuleNode].freeze
1408
1458
  private_constant :IVAR_BARRIER_NODES
1409
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
+
1410
1475
  EMPTY_GUARDED_IVARS = Set.new.freeze
1411
1476
 
1412
1477
  # Shared empty result for a def that returns no ivar — the overwhelmingly common case.
@@ -1440,6 +1505,7 @@ module Rigor
1440
1505
  # array/tuple-shaped, else the unanalyzable floor (the same `Dynamic[top]` a single write to an unknown RHS
1441
1506
  # records — an unanalyzable multi-write means unknown, not nil).
1442
1507
  record_multi_write_ivars(node, scope, class_name, accumulator)
1508
+ record_binding_target_ivars(node, scope, class_name, accumulator)
1443
1509
 
1444
1510
  record_ivar_mutator_call(node, class_name, mutated_ivars) if mutated_ivars
1445
1511
 
@@ -1452,7 +1518,10 @@ module Rigor
1452
1518
  return
1453
1519
  end
1454
1520
 
1455
- 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|
1456
1525
  gather_ivar_writes(c, scope, class_name, accumulator, guarded_ivars, mutated_ivars, dead_writes, declared)
1457
1526
  end
1458
1527
  end
@@ -2240,7 +2309,11 @@ module Rigor
2240
2309
  REFLECTIVE_OPAQUE_CLASSES = :reflective_opaque_classes
2241
2310
  # The census's {ConstantIvarValue}, which {#record_ivar_write} reads.
2242
2311
  CONSTANT_IVAR_VALUES = :constant_ivar_values
2243
- private_constant :DEFINE_METHOD_WRITES, :CLASS_BODY_WRITES, :REFLECTIVE_OPAQUE_CLASSES, :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
2244
2317
 
2245
2318
  # Unions `type` into the class-ivar accumulator for `(class_name, ivar_name)`. Shared by the single-write and
2246
2319
  # multi-write (parallel-assignment) collectors.
@@ -2280,6 +2353,58 @@ module Rigor
2280
2353
  end
2281
2354
  end
2282
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) }
2367
+ end
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
+
2283
2408
  def falsey_constant?(type)
2284
2409
  type.is_a?(Type::Constant) && (type.value.nil? || type.value == false)
2285
2410
  end
@@ -2304,7 +2429,7 @@ module Rigor
2304
2429
  def legacy_class_cvar_index(root, default_scope)
2305
2430
  accumulator = {}
2306
2431
  walk_class_cvars(root, [], default_scope, accumulator)
2307
- accumulator.transform_values(&:freeze).freeze
2432
+ finished_cvar_census(accumulator)
2308
2433
  end
2309
2434
 
2310
2435
  # `def_owner` names the class a `def` leaf's cvar writes belong to when `self` is
@@ -2382,6 +2507,8 @@ module Rigor
2382
2507
  _self_decl, child_prefix, child_cref = ctx
2383
2508
  return true unless node.body
2384
2509
 
2510
+ collect_body_cvar_writes(node.body, child_cref ? [] : child_prefix,
2511
+ scope_entering_declaration(default_scope, node.constant_path), accumulator)
2385
2512
  walk_class_cvars(node.body, child_cref ? [] : child_prefix,
2386
2513
  scope_entering_declaration(default_scope, node.constant_path), accumulator,
2387
2514
  singleton_cref: child_cref)
@@ -2431,24 +2558,89 @@ module Rigor
2431
2558
  class_name = qualified_prefix.join("::")
2432
2559
  body_scope = census_body_scope(default_scope, qualified_prefix,
2433
2560
  Type::Combinator.nominal_of(class_name))
2434
- 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)
2435
2562
  end
2436
2563
 
2437
- def gather_cvar_writes(node, scope, class_name, accumulator)
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)
2578
+ end
2579
+
2580
+ def gather_cvar_writes(node, scope, class_name, accumulator, join_only:)
2438
2581
  return unless node.is_a?(Prism::Node)
2439
2582
 
2440
- 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
2441
2594
  return if IVAR_BARRIER_NODES.any? { |klass| node.is_a?(klass) }
2442
2595
 
2443
- 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) }
2444
2597
  end
2445
2598
 
2446
- def record_cvar_write(node, scope, class_name, accumulator)
2447
- rvalue_type = EmptySeed.erase(node.value, scope.type_of(node.value))
2448
- accumulator[class_name] ||= {}
2449
- existing = accumulator[class_name][node.name]
2450
- accumulator[class_name][node.name] =
2451
- 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
2452
2644
  end
2453
2645
 
2454
2646
  # The program-global pre-pass's tables on the seeded scope's discovery index, and each global materialised into
@@ -3505,6 +3697,31 @@ module Rigor
3505
3697
  tables.siblings]
3506
3698
  end
3507
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
+
3508
3725
  # The accumulator {#walk_methods_and_def_nodes} threads: the existence table and its issue #992
3509
3726
  # envelope twin, which only {#record_method} and {#record_surface_mark} write, and the issue #1120
3510
3727
  # refinement table, which only {#record_refinement_defs} writes. That last one stays nil until a
@@ -3527,7 +3744,9 @@ module Rigor
3527
3744
  MethodTables = Struct.new(:existence, :envelopes, :refinements, :certainty, :certain, :possible,
3528
3745
  :contested_envelopes, :contested_def_nodes, :root, :in_effect, :census,
3529
3746
  :refine_text, :literal_seen, :source_bytes, :loose_constants, :block_spans,
3530
- :definer_history) do
3747
+ :definer_history, :forwardable_extends, :hook_block_spans) do
3748
+ include HookBlockSpans
3749
+
3531
3750
  # Issue #2006 — records, in walk order, that a definer other than an alias wrote the instance method `name`
3532
3751
  # of `class_name` at `node`: `def_node` for a `def`, nil for a `define_method(:x)` or an `attr_*` reader or
3533
3752
  # writer, and whether the write is {#certain}. {ScopeIndexer#apply_alias_def_nodes} reads the history to copy
@@ -4189,6 +4408,11 @@ module Rigor
4189
4408
  census_scan(node.superclass, qualified_prefix, methods_acc)
4190
4409
  end
4191
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
4192
4416
  body_prefix = child_cref ? [] : child_prefix
4193
4417
  if node.body
4194
4418
  walk_methods_and_def_nodes(node.body, body_prefix, false, methods_acc, def_nodes_acc,
@@ -4208,11 +4432,12 @@ module Rigor
4208
4432
  if !node.expression.is_a?(Prism::SelfNode) || tables.in_block?(node)
4209
4433
  record_singleton_body_prepends(node, qualified_prefix, tables)
4210
4434
  end
4435
+ record_constant_singleton_prepends(node, qualified_prefix, tables)
4211
4436
  walk_methods_and_def_nodes(node.expression, qualified_prefix, in_singleton_class,
4212
4437
  methods_acc, def_nodes_acc, source_path, def_owner_prefix,
4213
4438
  singleton_cref: singleton_cref,
4214
4439
  defs_singleton: defs_singleton)
4215
- if node.body
4440
+ if node.body && !hook_block_singleton?(node, in_singleton_class, owner_prefix, methods_acc)
4216
4441
  # The body's lexical cref is unnameable AND its qualified prefix stays the
4217
4442
  # ENCLOSING lexical one — `class C3::CD` below `class <<` resolves `C3`
4218
4443
  # lexically, never against the singleton's name. `singleton_prefix` rides
@@ -4226,6 +4451,7 @@ module Rigor
4226
4451
  when Prism::ConstantWriteNode, Prism::ConstantPathWriteNode, Prism::ConstantOrWriteNode,
4227
4452
  Prism::ConstantPathOrWriteNode
4228
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))
4229
4455
  if (split = meta_new_block_split(node, qualified_prefix, def_owner_prefix, singleton_cref))
4230
4456
  enclosing, body, body_self = split
4231
4457
  # A meta-new block rebinds only `self` — `Module.nesting` stays lexical — so
@@ -4267,11 +4493,13 @@ module Rigor
4267
4493
  node.rigor_each_child { |child| census_scan(child, qualified_prefix, methods_acc) }
4268
4494
  end
4269
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
+
4270
4498
  unless def_owner_prefix&.empty? || defs_singleton == :unnameable ||
4271
4499
  (singleton_cref && owner_prefix.empty?)
4272
- singleton_def = in_singleton_class || defs_singleton
4500
+ singleton_def = def_context(in_singleton_class, defs_singleton)
4273
4501
  record_def_method(node, owner_prefix, singleton_def, methods_acc.at(node))
4274
- record_def_body_evidence(node, owner_prefix, methods_acc)
4502
+ record_def_body_evidence(node, owner_prefix, methods_acc, singleton_def ? :singleton : :instance)
4275
4503
  record_def_node(node, owner_prefix, singleton_def, def_nodes_acc, methods_acc)
4276
4504
  end
4277
4505
  return
@@ -4294,6 +4522,8 @@ module Rigor
4294
4522
  when Prism::CallNode
4295
4523
  census_literal_arguments(node, qualified_prefix, methods_acc) if methods_acc.mentions_refine?
4296
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))
4297
4527
  return walk_eval_methods_and_defs(node, qualified_prefix, in_singleton_class, methods_acc,
4298
4528
  def_nodes_acc, source_path, def_owner_prefix,
4299
4529
  singleton_cref: singleton_cref,
@@ -4302,13 +4532,15 @@ module Rigor
4302
4532
  anonymous = record_call_node_methods(node, owner_prefix, in_singleton_class, methods_acc.at(node),
4303
4533
  source_path)
4304
4534
  unless in_singleton_class
4305
- superseded = call_superseded_names(node)
4535
+ superseded = call_superseded_names(node, owner_prefix, methods_acc)
4306
4536
  supersede_def_nodes(superseded, owner_prefix, def_nodes_acc, methods_acc.at(node))
4307
4537
  note_call_definers(node, superseded, owner_prefix, methods_acc.at(node))
4308
4538
  end
4309
4539
  tables = methods_acc.at(node)
4310
4540
  record_singleton_prepend(node, qualified_prefix, tables,
4311
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)
4312
4544
  # Issue #1120 — a refine body's defs go to the refinement table and nowhere else.
4313
4545
  if (target = methods_acc.refine_target_of(node))
4314
4546
  return record_literal_target_refine(node, target, qualified_prefix, owner_prefix, methods_acc)
@@ -4422,6 +4654,27 @@ module Rigor
4422
4654
  record_surface_mark(tables, child_prefix.join("::"), Scope::DiscoveryIndex::ENVELOPE_META_SUPERCLASS_MARK)
4423
4655
  end
4424
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
+
4425
4678
  def record_meta_new_facts(rvalue, child_prefix, tables)
4426
4679
  record_meta_members(rvalue, child_prefix, tables)
4427
4680
  # `Module.new(&BODY)`: the body runs on the new module but cannot be read, so its surface is unknown (#1816).
@@ -4449,11 +4702,111 @@ module Rigor
4449
4702
  record_define_method(node, qualified_prefix, in_singleton_class, methods_acc) if node.name == :define_method
4450
4703
  record_attr_methods(node, qualified_prefix, in_singleton_class, methods_acc) if attr_macro(node)
4451
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)
4452
4706
  record_alias_method_call(node, qualified_prefix, in_singleton_class, methods_acc)
4453
4707
  record_surface_evidence(node, qualified_prefix, methods_acc)
4708
+ record_self_definer_call(node, qualified_prefix, in_singleton_class, methods_acc)
4454
4709
  AnonymousMetaClass.name_for(node, source_path)
4455
4710
  end
4456
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
+
4457
4810
  # The `alias_method :new, :old` CallNode twin of {#record_alias_method} (#533): the NEW name joins
4458
4811
  # the discovered-methods table so calls to it stop reading undefined, and
4459
4812
  # {#apply_alias_def_nodes}'s map walk gives it the ORIGINAL def node for return inference.
@@ -4754,17 +5107,27 @@ module Rigor
4754
5107
  # (#590). Their bodies are entered as class bodies, so a member read inside one dispatches on the struct's
4755
5108
  # own class and must find the reader there: a member that shadows a `Kernel` private (`lambda`) otherwise
4756
5109
  # falls through to `Kernel#lambda`, and `lambda.upcase` reports an undefined method on `Proc` — a wrong-type
4757
- # diagnostic on correct code. No-op for `Class.new` / `Module.new` and for a factory whose members are not
4758
- # 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}).
4759
5112
  def record_meta_members(factory_call, qualified_prefix, accumulator, allow_outer_block: true)
4760
5113
  factory_call = resolve_meta_factory_call(factory_call, allow_outer_block: allow_outer_block)
4761
5114
  return unless factory_call
4762
5115
 
4763
- members = meta_member_names(factory_call)
4764
- 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
4765
5123
 
4766
- class_name = qualified_prefix.join("::")
4767
- 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) }
4768
5131
  end
4769
5132
 
4770
5133
  # Unwinds nested single-parent `Class.new(...)` calls to a root `Struct.new(...)` / `Data.define(...)`.
@@ -4795,22 +5158,33 @@ module Rigor
4795
5158
  data_define_call?(call_node) || struct_new_call?(call_node)
4796
5159
  end
4797
5160
 
4798
- # The Symbol member names of a `Data.define(*Symbol)` / `Struct.new(*Symbol [, keyword_init:])` call. For
4799
- # `Struct.new` the trailing `keyword_init:` hash is stripped by {#struct_new_positionals}; a leading
4800
- # String class name is NOT stripped, so `Struct.new("Name", :a) do` registers no members and the body
4801
- # 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.
4802
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)
4803
5177
  raw = call_node.arguments&.arguments || []
4804
- symbols = struct_new_call?(call_node) ? (struct_new_positionals(raw) || []) : raw
4805
- 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
4806
5179
  end
4807
5180
 
4808
5181
  def record_def_method(def_node, qualified_prefix, in_singleton_class, accumulator)
4809
5182
  return if qualified_prefix.empty?
4810
5183
 
4811
5184
  class_name = qualified_prefix.join("::")
4812
- singleton = def_singleton?(def_node, qualified_prefix, in_singleton_class)
4813
- kind = singleton ? :singleton : :instance
5185
+ kind = def_side(def_node, qualified_prefix, in_singleton_class)
5186
+ return if kind.nil?
5187
+
4814
5188
  record_method(accumulator, class_name, def_node.name, kind, Source::ParameterEnvelope.of(def_node))
4815
5189
  end
4816
5190
 
@@ -4819,12 +5193,20 @@ module Rigor
4819
5193
  # `def decorate(obj) = obj.extend(Decorator)`. A class-body macro records the same evidence in
4820
5194
  # {#record_surface_evidence}; `send` is left out here because inside a body it is an ordinary call far
4821
5195
  # more often than a definition.
4822
- 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)
4823
5201
  return if def_node.body.nil?
4824
5202
 
5203
+ side = evidence_side(def_node, side, receiver_side)
5204
+
5205
+ definer = false
4825
5206
  Source::NodeWalker.each(def_node.body) do |node|
4826
5207
  if node.is_a?(Prism::SingletonClassNode)
4827
5208
  record_singleton_body_prepends(node, qualified_prefix, tables)
5209
+ record_constant_singleton_prepends(node, qualified_prefix, tables, owner: true)
4828
5210
  next
4829
5211
  end
4830
5212
  next unless node.is_a?(Prism::CallNode)
@@ -4832,13 +5214,629 @@ module Rigor
4832
5214
  if node.name == :extend
4833
5215
  record_object_extension(node, qualified_prefix, tables)
4834
5216
  elsif SURFACE_EVAL_CALLS.include?(node.name) || SURFACE_NAMING_CALLS.include?(node.name)
5217
+ definer ||= computed_self_define_method?(node)
4835
5218
  record_body_rewrite(node, qualified_prefix, tables)
4836
5219
  else
4837
5220
  record_singleton_prepend(node, qualified_prefix, tables, self_ok: true)
4838
5221
  end
4839
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
4840
5387
  end
4841
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)
5803
+ end
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
+
4842
5840
  # Issue #1820 — `obj.singleton_class.prepend(M)`, or `prepend M` in a `class << obj` body, puts `M` ahead of
4843
5841
  # `obj`'s class even when the class already includes it (`prepend` re-inserts it; `extend` would not), so
4844
5842
  # `M`'s refinement answers first for that object. `M` takes {Scope::DiscoveryIndex::ENVELOPE_OBJECT_EXTENDED_MARK},
@@ -4856,6 +5854,56 @@ module Rigor
4856
5854
  record_object_extension(node, qualified_prefix, tables) if singleton
4857
5855
  end
4858
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
+
4859
5907
  def singleton_class_call?(receiver, self_ok)
4860
5908
  return false unless receiver.is_a?(Prism::CallNode) && receiver.name == :singleton_class
4861
5909
  return false unless receiver.arguments.nil?
@@ -4864,6 +5912,28 @@ module Rigor
4864
5912
  self_ok || !(object.nil? || object.is_a?(Prism::SelfNode))
4865
5913
  end
4866
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
+
4867
5937
  def record_singleton_body_prepends(node, qualified_prefix, tables)
4868
5938
  return if node.body.nil?
4869
5939
 
@@ -4895,17 +5965,44 @@ module Rigor
4895
5965
  end
4896
5966
  end
4897
5967
 
4898
- # `def Foo.bar` inside `module Foo` (or `def Meta.init` inside `module Meta`) is semantically equivalent to `def
4899
- # self.bar`: at the def-site, the runtime value of the constant `Foo` is the module itself (== `self`). Recognise
4900
- # 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.
5970
+ #
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.
4901
5973
  #
4902
- # The cross-class form `def Bar.baz` inside `module Foo` — where the receiver names a constant other than the
4903
- # enclosing class — is not supported at this slice; falls through to `:instance` (current behaviour) rather than
4904
- # silently re-routing the registration.
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
+
4905
6000
  def def_singleton?(def_node, qualified_prefix, in_singleton_class)
4906
- 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
4907
6003
 
4908
- 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?
4909
6006
  end
4910
6007
 
4911
6008
  # Only `Prism::ConstantReadNode` is observed in real Ruby — Prism mis-parses `def C::P.method` as `def C.P` (Ruby
@@ -4938,7 +6035,7 @@ module Rigor
4938
6035
  # ADR-119 WD3 — the slot follows its writer (later-wins): a certain write clears the key from
4939
6036
  # `contested_discovered_def_nodes` and a possible one adds it ({MethodTables#certain}, set for `def_node`).
4940
6037
  def record_def_node(def_node, qualified_prefix, in_singleton_class, accumulator, tables = nil)
4941
- return if def_singleton?(def_node, qualified_prefix, in_singleton_class)
6038
+ return unless def_side(def_node, qualified_prefix, in_singleton_class) == :instance
4942
6039
 
4943
6040
  class_name = qualified_prefix.empty? ? TOP_LEVEL_DEF_KEY : qualified_prefix.join("::")
4944
6041
  accumulator[class_name] ||= {}
@@ -4963,8 +6060,9 @@ module Rigor
4963
6060
 
4964
6061
  # Issue #1989 — the instance method names a non-`def` definer written on the class body's self replaces:
4965
6062
  # each reader (and `x=` writer) an `attr_*` call declares, a literal `define_method(:x)`, and the new name of
4966
- # an `alias_method :x, :y` call. A non-literal name replaces nothing Rigor can name.
4967
- 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)
4968
6066
  if (macro = attr_macro(call_node))
4969
6067
  names = []
4970
6068
  each_attr_macro_name(call_node) do |base|
@@ -4974,6 +6072,8 @@ module Rigor
4974
6072
  names
4975
6073
  elsif call_node.name == :define_method
4976
6074
  define_method_superseded_name(call_node)
6075
+ elsif (forwarded = forwardable_superseded_names(call_node, qualified_prefix, accumulator)).any?
6076
+ forwarded
4977
6077
  else
4978
6078
  Source::AliasNames.alias_method_call_names(call_node)&.first(1) || []
4979
6079
  end
@@ -5059,6 +6159,10 @@ module Rigor
5059
6159
  # The singleton walk's accumulator: the `{class => {name => DefNode}}` table, plus the contested slots, which
5060
6160
  # follow each slot's writer ({#follow_slot_writer}'s rule).
5061
6161
  class SingletonDefTable < Hash
6162
+ include HookBlockSpans
6163
+
6164
+ attr_accessor :hook_block_spans
6165
+
5062
6166
  # Issue #1936 — contests the slots of `names` (every slot of `class_name` for `:all`) that an earlier write
5063
6167
  # filled: a later non-`def` definer replaced the `def` they hold. The slot keeps the `def`, as
5064
6168
  # {ScopeIndexer#supersede_def_nodes} keeps the instance one, and a later write clears the contest.
@@ -5250,7 +6354,7 @@ module Rigor
5250
6354
  mf_offsets, ranges, def_owner_prefix, singleton_cref,
5251
6355
  defs_singleton)
5252
6356
  owner = defs_singleton == :unnameable ? EMPTY_PREFIX : (def_owner_prefix || qualified_prefix)
5253
- 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,
5254
6358
  mf_offsets, ranges)
5255
6359
  walk_deferred_children(node, qualified_prefix, in_singleton_class, true, mf_offsets,
5256
6360
  ranges, def_owner_prefix, singleton_cref: singleton_cref,
@@ -5518,18 +6622,35 @@ module Rigor
5518
6622
  # proves `S::A` exists). Memoized on the `Prism::Source` every node's
5519
6623
  # location shares — threading the set through the dozen eval-consuming
5520
6624
  # walks would dwarf the resolution it feeds, so the one re-parse this
5521
- # needs happens once per file. The memo lives in an ivar ON the source
5522
- # (rather than a WeakMap keyed on it): the entry's lifetime is the
5523
- # parse's own, nothing mutable sits in a constant, and a worker parsing
5524
- # 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
+
5525
6637
  def eval_file_declared_names(node)
5526
6638
  source = node.location.send(:source)
5527
- source.instance_variable_get(:@rigor_declared_names) ||
5528
- source.instance_variable_set(:@rigor_declared_names, begin
5529
- names = Set.new
5530
- collect_declared_constant_names(Prism.parse(source.source).value, [], names)
5531
- names.freeze
5532
- 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
5533
6654
  end
5534
6655
 
5535
6656
  # The walk behind {eval_file_declared_names}: every qualified constant
@@ -5759,7 +6880,11 @@ module Rigor
5759
6880
  return
5760
6881
  end
5761
6882
 
5762
- 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
5763
6888
  :singleton
5764
6889
  elsif ModuleFunctionState.deferred_module_function?(mf_offsets, start)
5765
6890
  :both
@@ -5774,7 +6899,7 @@ module Rigor
5774
6899
  # `module_function` toggle threads correctly across the body's *sibling* statements (a child-by-child recursion
5775
6900
  # would reset it). At the top level / inside an arbitrary node there is no `module_function` state to carry, so
5776
6901
  # descent is a plain per-child walk.
5777
- 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
5778
6903
  def_owner_prefix = nil, singleton_cref: false,
5779
6904
  defs_singleton: false)
5780
6905
  return unless node.is_a?(Prism::Node)
@@ -5798,6 +6923,7 @@ module Rigor
5798
6923
  # ownerless like `class << <non-constant>`; nameable headers re-anchor.
5799
6924
  child_cref = unnameable_decl?(node, self_decl, singleton_cref)
5800
6925
  body_prefix = child_cref ? [] : child_prefix
6926
+ accumulator.mark_hook_blocks(concern_hook_blocks(node, child_prefix), child_prefix) unless child_cref
5801
6927
  if node.body
5802
6928
  walk_singleton_body(node.body, body_prefix, false, accumulator, nil,
5803
6929
  singleton_cref: child_cref)
@@ -5813,7 +6939,8 @@ module Rigor
5813
6939
  walk_singleton_def_nodes(node.expression, qualified_prefix, in_singleton_class,
5814
6940
  accumulator, def_owner_prefix, singleton_cref: singleton_cref,
5815
6941
  defs_singleton: defs_singleton)
5816
- if node.body
6942
+ if node.body && !hook_block_singleton?(node, in_singleton_class, def_owner_prefix || qualified_prefix,
6943
+ accumulator)
5817
6944
  walk_singleton_body(node.body, qualified_prefix, true, accumulator, singleton_prefix,
5818
6945
  singleton_cref: true)
5819
6946
  end
@@ -5825,9 +6952,11 @@ module Rigor
5825
6952
  when Prism::DefNode
5826
6953
  # `:unnameable` is the `class <<` + `instance_eval` definee — the singleton's own
5827
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
+
5828
6958
  unless defs_singleton == :unnameable
5829
- record_singleton_def_node(node, def_owner_prefix || qualified_prefix,
5830
- in_singleton_class || defs_singleton, false, accumulator)
6959
+ record_singleton_def_node(node, owner, def_context(in_singleton_class, defs_singleton), false, accumulator)
5831
6960
  end
5832
6961
  return
5833
6962
  end
@@ -6085,7 +7214,7 @@ module Rigor
6085
7214
  # `:unnameable` is the `class <<` + `instance_eval` definee — the singleton's own
6086
7215
  # singleton — which this table cannot name either.
6087
7216
  unless defs_singleton == :unnameable
6088
- 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),
6089
7218
  module_function_on, accumulator)
6090
7219
  end
6091
7220
  next
@@ -6127,8 +7256,9 @@ module Rigor
6127
7256
  # function's copy as possible, so the slot holds the later def, contested, rather than the def the
6128
7257
  # slot held before (#1968).
6129
7258
  def record_singleton_def_node(def_node, qualified_prefix, in_singleton_class, module_function_on, accumulator)
6130
- declared = def_singleton?(def_node, qualified_prefix, in_singleton_class)
6131
- 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)
6132
7262
  return if qualified_prefix.empty?
6133
7263
 
6134
7264
  class_name = qualified_prefix.join("::")
@@ -6724,6 +7854,7 @@ module Rigor
6724
7854
  def record_data_member_layout(accumulator, qualified_parts, expr, allow_outer_block: true)
6725
7855
  expr = resolve_meta_factory_call(expr, allow_outer_block: allow_outer_block)
6726
7856
  return unless expr && data_define_call?(expr)
7857
+ return if meta_members_open?(expr)
6727
7858
 
6728
7859
  members = meta_member_names(expr)
6729
7860
  return if members.empty?
@@ -6846,6 +7977,7 @@ module Rigor
6846
7977
  def record_struct_member_layout(accumulator, qualified_parts, expr, allow_outer_block: true)
6847
7978
  expr = resolve_meta_factory_call(expr, allow_outer_block: allow_outer_block)
6848
7979
  return unless expr && struct_new_call?(expr)
7980
+ return if meta_members_open?(expr)
6849
7981
 
6850
7982
  members = meta_member_names(expr)
6851
7983
  return if members.empty?
@@ -7014,6 +8146,7 @@ module Rigor
7014
8146
 
7015
8147
  _self_decl, child_prefix, child_cref = ctx
7016
8148
  accumulator.direct_body(node.body) if accumulator.direct?(node)
8149
+ accumulator.class_body(node.body)
7017
8150
  walk_class_includes(node.body, child_cref ? [] : child_prefix,
7018
8151
  child_cref ? nil : child_prefix.join("::"), accumulator,
7019
8152
  singleton_cref: child_cref)
@@ -7110,7 +8243,8 @@ module Rigor
7110
8243
  def record_mixin_call(node, qualified_prefix, current_class, accumulator)
7111
8244
  taint_opaque_eval(node, current_class, accumulator)
7112
8245
  view = mixin_call_view(node)
7113
- 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?
7114
8248
 
7115
8249
  kind = view.first
7116
8250
  current_class ||= toplevel_include_owner(node, qualified_prefix, current_class, accumulator)
@@ -7120,20 +8254,68 @@ module Rigor
7120
8254
  return if instance_mixin_recorded?(node, view, qualified_prefix, current_class, accumulator)
7121
8255
 
7122
8256
  # A mixin call the tables cannot hold still reshapes the ancestry: taint the whole owner side.
7123
- effects.each { |owner, side| accumulator.taint(owner, side) }
8257
+ taint_unrecorded_mixin(node, kind, view[1], effects, accumulator)
7124
8258
  end
7125
8259
 
7126
- # Records the instance-side edges of a call {#mixin_call_recorded?} accepts. False when it recorded
7127
- # nothing — the caller then taints the owner instead. `view` is {#mixin_call_view}'s, so
7128
- # `Integer.send(:include, Ext)` records `Ext` as `Integer.include(Ext)` does (issue #1722).
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
8283
+ end
8284
+
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.
7129
8311
  def instance_mixin_recorded?(node, view, qualified_prefix, current_class, accumulator)
7130
- kind, arguments, via_send = view
7131
- return false unless mixin_call_recorded?(node, kind, via_send, current_class)
8312
+ kind, arguments, = view
8313
+ return false unless mixin_call_recorded?(kind)
7132
8314
 
7133
8315
  targets = arguments.filter_map { |arg| Source::ConstantPath.qualified_name(arg) }
7134
8316
  return false if targets.empty?
7135
8317
 
7136
- 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)
7137
8319
  return false if owner.nil?
7138
8320
 
7139
8321
  write_mixin_targets(accumulator, owner, targets, prepend: kind == :prepend)
@@ -7143,7 +8325,234 @@ module Rigor
7143
8325
 
7144
8326
  MIXIN_KINDS = %i[include prepend extend].freeze
7145
8327
  SEND_CALL_NAMES = %i[send public_send __send__].freeze
7146
- 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
7147
8556
 
7148
8557
  # `[kind, arguments, via_send]` for a call that mixes modules in — `include` / `prepend` / `extend`, or
7149
8558
  # `send(:include, M)` and its `public_send` / `__send__` twins with a literal first argument — else nil.
@@ -7202,8 +8611,9 @@ module Rigor
7202
8611
  end
7203
8612
 
7204
8613
  HOOK_DEFS = %i[included extended prepended inherited].freeze
8614
+ INSTANCE_MIXIN_NAMES = %i[include prepend].freeze
7205
8615
  MIXIN_EVAL_CALLS = %i[class_eval module_eval class_exec module_exec instance_eval instance_exec].freeze
7206
- private_constant :HOOK_DEFS, :MIXIN_EVAL_CALLS
8616
+ private_constant :HOOK_DEFS, :MIXIN_EVAL_CALLS, :INSTANCE_MIXIN_NAMES
7207
8617
 
7208
8618
  # The parameter names of a `def included(base)`-style hook (a hook is written `def self.included`, or
7209
8619
  # `def included` inside `class << self`), else nil.
@@ -7238,9 +8648,14 @@ module Rigor
7238
8648
  receiver = node.receiver
7239
8649
  return if receiver.nil? || receiver.is_a?(Prism::SelfNode)
7240
8650
  return if Source::ConstantPath.qualified_name_or_nil(receiver)
7241
- return unless hook_receiver?(receiver, accumulator) || block_mixes_in?(node.block)
7242
8651
 
7243
- 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)
7244
8659
  accumulator.taint(current_class, :extend)
7245
8660
  end
7246
8661
 
@@ -7252,6 +8667,22 @@ module Rigor
7252
8667
  found
7253
8668
  end
7254
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
+
7255
8686
  def singleton_class_receiver?(receiver)
7256
8687
  receiver.is_a?(Prism::CallNode) && receiver.name == :singleton_class && receiver.arguments.nil? &&
7257
8688
  (receiver.receiver.nil? || receiver.receiver.is_a?(Prism::SelfNode))
@@ -7283,19 +8714,8 @@ module Rigor
7283
8714
  list.unshift(*(prepend ? targets : targets.reject { |target| list.include?(target) }))
7284
8715
  end
7285
8716
 
7286
- # Whether a mixin call contributes to the tables at all: a receiverless `include` / `prepend` needs an
7287
- # enclosing declaration to file the edge under, and the RECEIVER form (`Base.prepend(Loud)`, issue #1123;
7288
- # `Integer.include(Ext)` and `Integer.send(:include, Ext)`, issue #1722) files it under the class the
7289
- # receiver names. A receiver-form edge is unpositioned wherever it is written ({MixinAccumulator#note}), so
7290
- # a reader that depends on ORDER declines on it, while the set-shaped readers (undefined-method
7291
- # suppression, arity, visibility) see the module as they see an in-body `include`. A receiverless `send`
7292
- # stays a taint: the walk does not resolve its `self`.
7293
- def mixin_call_recorded?(node, kind, via_send, current_class)
7294
- return false unless MIXIN_CALL_NAMES.include?(kind)
7295
- return !via_send && !current_class.nil? if node.receiver.nil?
7296
-
7297
- true
7298
- 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)
7299
8719
 
7300
8720
  # Issue #1697 — a receiverless `include M` written as a statement of the file's own top level is
7301
8721
  # `main.include`, which is `Object.include`: it mixes M into `Object`. It is recorded under its own
@@ -7304,14 +8724,22 @@ module Rigor
7304
8724
  # walk that types a call reaches it: what Ruby reaches first for a name such an include supplies
7305
8725
  # depends on more than the chain shows (a block that rebinds `self`, an `extend` or a singleton `def`
7306
8726
  # on `main`), so only the diagnostic silences read it (`Inference::ObjectMixins`), and typing is
7307
- # issue #1715's. Only a direct program statement counts.
7308
- # The same call inside a top-level block, a method body or a conditional stays unrecorded, as before:
7309
- # its `self` may not be `main` (a `describe do include M end` or `class_exec` block rebinds it, while
7310
- # `[1].each do include M end` does not, and the walk cannot tell them apart), and it may not run. `main`
7311
- # 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.
7312
8739
  def toplevel_include_owner(node, qualified_prefix, current_class, accumulator)
7313
8740
  return nil unless current_class.nil? && qualified_prefix.empty?
7314
- 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
7315
8743
 
7316
8744
  accumulator.program_statement?(node) ? ObjectMixins::TOPLEVEL_INCLUDE_KEY : nil
7317
8745
  end
@@ -7373,8 +8801,8 @@ module Rigor
7373
8801
  singleton_cref)
7374
8802
  when Prism::SingletonClassNode
7375
8803
  # Issue #915 — `class << self` opens the enclosing declaration's OWN singleton, so an `include`
7376
- # written in it is the same singleton ancestor an `extend` in the class body would add. Only the
7377
- # `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.
7378
8806
  # Nested declarations inside the body take the `ClassNode` branch above and reset the flag with
7379
8807
  # their own owner, so the marker cannot leak past the singleton body it belongs to.
7380
8808
  return walk_extends_singleton_class(node, qualified_prefix, current_class, accumulator,
@@ -7427,7 +8855,8 @@ module Rigor
7427
8855
 
7428
8856
  # The `class <<` arm of {#walk_class_extends}: the expression evaluates in the enclosing
7429
8857
  # context while the body is the singleton's — `class << self` keeps the enclosing owner
7430
- # (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.
7431
8860
  # `self` inside is the singleton — `self::` receivers decline — and the lexical cref
7432
8861
  # below is unnameable in every case.
7433
8862
  def walk_extends_singleton_class(node, qualified_prefix, current_class, accumulator,
@@ -7436,17 +8865,114 @@ module Rigor
7436
8865
  in_singleton: in_singleton, singleton_self: singleton_self,
7437
8866
  singleton_cref: singleton_cref)
7438
8867
  return unless node.body
8868
+ return walk_extends_constant_singleton(node, qualified_prefix, accumulator) if constant_singleton_class?(node)
7439
8869
 
7440
8870
  # `class << self` keeps the owner only at the FIRST level — nested inside a
7441
8871
  # singleton body, `self` IS the singleton and `class << self` opens the
7442
8872
  # singleton's own singleton (`#<Class:#<Class:C>>`), which nothing names.
7443
8873
  opens_self = node.expression.is_a?(Prism::SelfNode) && !in_singleton
7444
8874
  accumulator.direct_body(node.body) if opens_self && accumulator.direct?(node)
8875
+ accumulator.class_body(node.body) if opens_self
7445
8876
  walk_class_extends(node.body, qualified_prefix, opens_self ? current_class : nil,
7446
8877
  accumulator, in_singleton: opens_self,
7447
8878
  singleton_self: true, singleton_cref: true)
7448
8879
  end
7449
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
+
7450
8976
  # The declaration arm of {#walk_class_extends}: under an unnameable cref a bare/`self::`
7451
8977
  # header opens `#<singleton>::Name` — ownerless; nameable headers re-anchor at a
7452
8978
  # real cref.
@@ -7460,6 +8986,7 @@ module Rigor
7460
8986
  child_cref = unnameable_decl?(node, self_decl, singleton_cref)
7461
8987
  accumulator.direct_body(node.body) if accumulator.direct?(node)
7462
8988
  accumulator.body_statements(node.body)
8989
+ accumulator.class_body(node.body)
7463
8990
  walk_class_extends(node.body, child_cref ? [] : child_prefix,
7464
8991
  child_cref ? nil : child_prefix.join("::"), accumulator,
7465
8992
  singleton_cref: child_cref)
@@ -7736,16 +9263,24 @@ module Rigor
7736
9263
  return if singleton_class_rewrite_tainted?(node, qualified_prefix, current_class, accumulator,
7737
9264
  in_singleton: in_singleton)
7738
9265
 
7739
- kind, _arguments, via_send = mixin_call_view(node)
7740
- 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
7741
9273
 
7742
9274
  effects = mixin_effects(node, kind, qualified_prefix, current_class, accumulator, in_singleton: in_singleton)
7743
9275
  .select { |_owner, side| side == :extend || in_singleton }
7744
9276
  return if effects.empty?
7745
- return if !via_send && singleton_mixin_recorded?(node, kind, qualified_prefix, current_class, accumulator,
7746
- 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)
7747
9280
 
7748
- 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)
7749
9284
  end
7750
9285
 
7751
9286
  # Issue #1752 — a call that rewrites a singleton class reached through `singleton_class` (`singleton_class.
@@ -7769,23 +9304,53 @@ module Rigor
7769
9304
  # Inside `class << self` a bare `singleton_class` is the singleton's own singleton, one level up from the
7770
9305
  # owner's extend side, so that call stays a taint.
7771
9306
  up_level = in_singleton && lexical_self_receiver?(receiver.receiver)
7772
- 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
7773
9311
 
7774
- 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))
7775
9315
  true
7776
9316
  end
7777
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
+
7778
9323
  # Issue #2094 — `Float.singleton_class.include(Ext)` (or `prepend`, which the extend table cannot order, so the
7779
9324
  # edge is unpositioned as every receiver-form edge is) puts `Ext` on the singleton side as `Float.extend(Ext)`
7780
9325
  # does, the fold #915 gives `class << self; include M`. A `send` spelling or an argument that is not a constant
7781
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.
7782
9332
  def singleton_class_include_recorded?(node, owner, accumulator)
7783
- return false unless %i[include prepend].include?(node.name)
7784
-
7785
- 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
7786
9337
  return false if arguments.empty? || arguments.any? { |arg| Source::ConstantPath.qualified_name(arg).nil? }
7787
9338
 
7788
- !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
7789
9354
  end
7790
9355
 
7791
9356
  # A bare or `self.` receiver: the lexical `self`.
@@ -7849,10 +9414,12 @@ module Rigor
7849
9414
  accumulator.note(node, current_class, :extend, [current_class], complete: true)
7850
9415
  end
7851
9416
 
7852
- # Records the singleton-side edges of a receiverless `extend` (or, inside `class << self`, `include` /
7853
- # `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.
7854
9419
  def singleton_mixin_recorded?(node, kind, qualified_prefix, current_class, accumulator, in_singleton)
7855
- 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
7856
9423
  return false unless current_class
7857
9424
  return false unless in_singleton ? MIXIN_CALL_NAMES.include?(kind) : kind == :extend
7858
9425
 
@@ -7878,9 +9445,12 @@ module Rigor
7878
9445
  # (`extend A; extend B` → singleton ancestors `[B, A]`) while the arguments of a single
7879
9446
  # `extend A, B` keep call order (`[A, B]`). Prepending each call's argument list preserves both,
7880
9447
  # and consumers iterating the list forward read the same order the singleton ancestry searches.
7881
- 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)
7882
9452
  targets = []
7883
- arguments = node.arguments&.arguments || []
9453
+ arguments = mixin_call_view(node)[1]
7884
9454
  arguments.each do |arg|
7885
9455
  target = arg.is_a?(Prism::SelfNode) ? current_class : Source::ConstantPath.qualified_name(arg)
7886
9456
  targets << target if target
@@ -7894,7 +9464,8 @@ module Rigor
7894
9464
  # unpositioned and the chain settles to the readers' own answer.
7895
9465
  accumulator.taint(current_class, :extend) if targets.any? { |target| list.include?(target) }
7896
9466
  list.unshift(*targets)
7897
- 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)
7898
9469
  targets
7899
9470
  end
7900
9471
 
@@ -8080,6 +9651,10 @@ module Rigor
8080
9651
  # written directly in the body settles the default again. A named target does not read the running default,
8081
9652
  # so a toggle does not contest it. A certain write clears the slot's contest.
8082
9653
  class VisibilityTable < Hash
9654
+ include HookBlockSpans
9655
+
9656
+ attr_accessor :hook_block_spans
9657
+
8083
9658
  # The bare toggles the walk has met so far; a body's statement loop compares it around each child.
8084
9659
  attr_accessor :toggles
8085
9660
  # Whether the running default of the body being walked rests on an uncertain toggle.
@@ -8202,6 +9777,22 @@ module Rigor
8202
9777
  VISIBILITY_TOGGLES.include?(name) ? [name, arguments] : nil
8203
9778
  end
8204
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
+
8205
9796
  # rubocop:disable-next Metrics/CyclomaticComplexity, Metrics/MethodLength, Metrics/PerceivedComplexity, Metrics/AbcSize
8206
9797
  def walk_method_visibilities(node, qualified_prefix, in_singleton_class, current_visibility, accumulator, # rubocop:disable Metrics/ParameterLists
8207
9798
  def_owner_prefix = nil, singleton_cref: false,
@@ -8220,11 +9811,13 @@ module Rigor
8220
9811
  # ownerless; nameable headers re-anchor at a real cref.
8221
9812
  child_cref = unnameable_decl?(node, self_decl, singleton_cref)
8222
9813
  body_prefix = child_cref ? [] : child_prefix
9814
+ accumulator.mark_hook_blocks(concern_hook_blocks(node, child_prefix), child_prefix) unless child_cref
8223
9815
  if node.body
8224
9816
  accumulator.body do
8225
9817
  walk_method_visibilities(node.body, body_prefix, false, :public,
8226
9818
  accumulator, nil, singleton_cref: child_cref)
8227
9819
  end
9820
+ walk_class_methods_visibilities(node, child_prefix, accumulator) unless child_cref
8228
9821
  end
8229
9822
  return current_visibility
8230
9823
  end
@@ -8253,6 +9846,8 @@ module Rigor
8253
9846
  return current_visibility
8254
9847
  end
8255
9848
  when Prism::DefNode
9849
+ return current_visibility if accumulator.hook_block_self_def?(node, owner_prefix)
9850
+
8256
9851
  contest_foreign_singleton_visibility(node, qualified_prefix, owner_prefix, in_singleton_class, accumulator)
8257
9852
  record_def_visibility(node, owner_prefix, visibility_side(in_singleton_class, defs_singleton),
8258
9853
  current_visibility, accumulator)
@@ -8363,6 +9958,7 @@ module Rigor
8363
9958
  singleton_cref: singleton_cref,
8364
9959
  defs_singleton: defs_singleton)
8365
9960
  return unless node.body
9961
+ return if hook_block_singleton?(node, in_singleton_class, def_owner_prefix || qualified_prefix, accumulator)
8366
9962
 
8367
9963
  accumulator.body do
8368
9964
  walk_method_visibilities(node.body, qualified_prefix, true, :public, accumulator,
@@ -8865,6 +10461,7 @@ module Rigor
8865
10461
  constant_receiver_candidates(receiver, qualified_prefix).each do |name|
8866
10462
  record_surface_mark(tables, name, Scope::DiscoveryIndex::ENVELOPE_DYNAMIC_MARK)
8867
10463
  end
10464
+ record_constant_singleton_definer(node, receiver, qualified_prefix, tables)
8868
10465
  elsif singleton_class_of_receiver?(receiver) && singleton_class_named_call?(node)
8869
10466
  record_singleton_class_names(node, receiver.receiver, qualified_prefix, tables)
8870
10467
  end
@@ -8899,8 +10496,64 @@ module Rigor
8899
10496
  return if def_receiver_targets_lexical_self?(receiver, owner_prefix)
8900
10497
 
8901
10498
  constant_receiver_candidates(receiver, qualified_prefix).each do |name|
8902
- record_envelope(tables.envelopes, name, [:singleton, node.name], Source::ParameterEnvelope::OPAQUE)
8903
- 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) }
8904
10557
  end
8905
10558
  end
8906
10559
 
@@ -8949,22 +10602,33 @@ module Rigor
8949
10602
  SURFACE_MIXIN_CALLS.include?(node.name) || SURFACE_MIXIN_HELPER.match?(node.name.to_s)
8950
10603
  end
8951
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.
8952
10607
  def dynamic_surface_call?(node)
8953
- 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
8954
10614
 
8955
10615
  arguments = node.arguments&.arguments || []
8956
10616
  if SURFACE_NAMING_CALLS.include?(node.name)
8957
10617
  arguments.empty? || literal_method_name(arguments.first).nil?
8958
10618
  elsif surface_mixin_call?(node)
8959
- arguments.any? do |argument|
8960
- !(argument.is_a?(Prism::ConstantReadNode) || argument.is_a?(Prism::ConstantPathNode) ||
8961
- argument.is_a?(Prism::SelfNode))
8962
- end
10619
+ dynamic_mixin_arguments?(arguments)
8963
10620
  else
8964
10621
  false
8965
10622
  end
8966
10623
  end
8967
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
+
8968
10632
  # Every qualified name a constant receiver written inside `qualified_prefix` can denote, innermost
8969
10633
  # first: the walk runs before any scope exists, so it cannot resolve the constant, and over-marking a
8970
10634
  # same-named class elsewhere only withholds a check.
@@ -9245,15 +10909,76 @@ module Rigor
9245
10909
  end
9246
10910
 
9247
10911
  def record_define_method(call_node, qualified_prefix, in_singleton_class, accumulator)
9248
- return if qualified_prefix.empty?
9249
10912
  return if call_node.arguments.nil? || call_node.arguments.arguments.empty?
9250
10913
 
9251
- first_arg = call_node.arguments.arguments.first
9252
- method_name = literal_method_name(first_arg)
9253
- 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
9254
10924
 
9255
- class_name = qualified_prefix.join("::")
9256
- 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
9257
10982
  end
9258
10983
 
9259
10984
  # The `attr_*` accessor macros that introduce methods Rigor must treat as source-declared. Without this, a class
@@ -9277,6 +11002,19 @@ module Rigor
9277
11002
  record_method(accumulator, class_name, base, kind) if reader
9278
11003
  record_method(accumulator, class_name, :"#{base}=", kind) if writer
9279
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? }
9280
11018
  end
9281
11019
 
9282
11020
  # Yields each name an `attr_*` call declares ({#attr_macro}): a Symbol or String literal argument. A
@@ -9321,13 +11059,14 @@ module Rigor
9321
11059
  # because they are not monkey-patches), and no plugin surface reaches it — the ADR-16 synthetic-method
9322
11060
  # tier feeds the DISPATCHER, which `Analysis::CheckRules` does not consult. A name table is the smallest
9323
11061
  # thing that works; a plugin-supplied one can replace it whenever that surface exists.
9324
- MODULE_ATTR_MACROS = {
9325
- mattr_reader: [true, false, false], mattr_writer: [false, true, false],
9326
- mattr_accessor: [true, true, false], cattr_reader: [true, false, false],
9327
- cattr_writer: [false, true, false], cattr_accessor: [true, true, false],
9328
- class_attribute: [true, true, true]
9329
- }.freeze
9330
- 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
+ )
9331
11070
 
9332
11071
  # Both kinds, unconditionally. `instance_accessor: false` (and its `instance_reader:` /
9333
11072
  # `instance_writer:` / `instance_predicate:` siblings) narrows the real surface, and honouring them here
@@ -9350,6 +11089,70 @@ module Rigor
9350
11089
  end
9351
11090
  end
9352
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
+
9353
11156
  def record_both_kinds(accumulator, class_name, method_name)
9354
11157
  record_method(accumulator, class_name, method_name, :instance)
9355
11158
  record_method(accumulator, class_name, method_name, :singleton)
@@ -9380,8 +11183,12 @@ module Rigor
9380
11183
  physical = buffer ? buffer.resolve(path) : path
9381
11184
  source = File.read(physical)
9382
11185
  root = Prism.parse(source, filepath: path).value
9383
- collect_class_decls(root, [], accumulator)
9384
- 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
+
9385
11192
  # Skip files that fail to parse or read; the per-file analyzer surfaces the parse error separately.
9386
11193
  next
9387
11194
  end
@@ -9408,10 +11215,12 @@ module Rigor
9408
11215
  paths.each do |path|
9409
11216
  physical = buffer ? buffer.resolve(path) : path
9410
11217
  root = Prism.parse(File.read(physical), filepath: path).value
9411
- accumulate_project_index(acc, path, root)
11218
+ fold_file_index(acc, build_file_index(path, root)) # all or nothing per file (#2455)
9412
11219
  rescue DeclarationWalk::ContractError
9413
11220
  raise # a broken walk contract, not an unreadable file: skipping it would hide the failure
9414
- rescue StandardError
11221
+ rescue StandardError, SystemStackError => e
11222
+ raise if uncontainable_overflow?(e)
11223
+
9415
11224
  # Skip files that fail to parse or read; the per-file analyzer surfaces the parse error separately.
9416
11225
  next
9417
11226
  end
@@ -9448,7 +11257,9 @@ module Rigor
9448
11257
  file_summary(file_index).each { |key, table| per_file[key][path] = table unless table.nil? }
9449
11258
  rescue DeclarationWalk::ContractError
9450
11259
  raise # a broken walk contract, not an unreadable file: skipping it would hide the failure
9451
- rescue StandardError
11260
+ rescue StandardError, SystemStackError => e
11261
+ raise if uncontainable_overflow?(e)
11262
+
9452
11263
  next
9453
11264
  end
9454
11265
  { def_index: finalize_def_index(acc), code_fingerprints: code_fingerprints,
@@ -9732,21 +11543,35 @@ module Rigor
9732
11543
  paths.each do |path|
9733
11544
  physical = buffer ? buffer.resolve(path) : path
9734
11545
  root = Prism.parse(File.read(physical), filepath: path).value
9735
- collect_class_decls(root, [], classes)
9736
- if uncertain.include?(path)
9737
- fold_file_index(acc, without_written_slots(build_file_index(path, root, uncertain: true), acc))
9738
- else
9739
- accumulate_project_index(acc, path, root)
9740
- 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)
9741
11557
  rescue DeclarationWalk::ContractError
9742
11558
  raise # a broken walk contract, not an unreadable file: skipping it would hide the failure
9743
- rescue StandardError
11559
+ rescue StandardError, SystemStackError => e
11560
+ raise if uncontainable_overflow?(e)
11561
+
9744
11562
  # Skip files that fail to parse or read; the per-file analyzer surfaces the parse error separately.
9745
11563
  next
9746
11564
  end
9747
11565
  finalize_project_index(classes, acc)
9748
11566
  end
9749
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
+
9750
11575
  # The shared tail of the two whole-project discovery walks: settles the def-index and re-anchors the
9751
11576
  # `classes` table it accumulated beside it.
9752
11577
  def finalize_project_index(classes, acc)
@@ -9787,7 +11612,7 @@ module Rigor
9787
11612
  # @return `{ classes:, def_index:, bundles: }`.
9788
11613
  # @param uncertain — issue #2021: as {#discovered_project_index_for_paths}. Such a file is always re-walked and
9789
11614
  # keeps no bundle, since a bundle does not record that its contributions were all possible.
9790
- 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
9791
11616
  classes = {}
9792
11617
  acc = new_def_index_accumulator
9793
11618
  bundles = {}
@@ -9816,7 +11641,9 @@ module Rigor
9816
11641
  end
9817
11642
  rescue DeclarationWalk::ContractError
9818
11643
  raise # a broken walk contract, not an unreadable file: skipping it would hide the failure
9819
- rescue StandardError
11644
+ rescue StandardError, SystemStackError => e
11645
+ raise if uncontainable_overflow?(e)
11646
+
9820
11647
  # Skip files that fail to parse / read; the per-file analyzer surfaces the parse error separately.
9821
11648
  next
9822
11649
  end
@@ -10653,7 +12480,8 @@ module Rigor
10653
12480
  singleton_cref)
10654
12481
  else
10655
12482
  census_constant_write(node, qualified_prefix, tables, self_owner, singleton_cref: singleton_cref)
10656
- 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))
10657
12485
  end
10658
12486
 
10659
12487
  rebound = rebound_block_self(node, qualified_prefix, nil, meta_owner, self_owner)
@@ -10695,6 +12523,16 @@ module Rigor
10695
12523
  qualified_prefix
10696
12524
  end
10697
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
+
10698
12536
  # Under an unnameable cref a bare/`self::` header pushes `#<singleton>::Name` — still
10699
12537
  # unnameable — so the body keeps the OPAQUE self and the cref flag; nameable
10700
12538
  # headers re-anchor at a real cref.
@@ -11698,20 +13536,25 @@ module Rigor
11698
13536
 
11699
13537
  # Recognises `Data.define(*Symbol)` and `Data.define(*Symbol) do ... end` at constant-write rvalue position. The
11700
13538
  # receiver MUST be the bare `Data` constant (or `::Data`); other receivers (a local variable, a method call
11701
- # 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.
11702
13541
  def data_define_call?(node)
11703
13542
  return false unless node.is_a?(Prism::CallNode)
11704
13543
  return false unless node.name == :define
11705
13544
  return false unless meta_constant_receiver?(node.receiver, :Data)
11706
13545
 
11707
13546
  args = node.arguments&.arguments || []
11708
- args.all?(Prism::SymbolNode)
13547
+ args.all? { |arg| meta_member_argument?(arg) }
11709
13548
  end
11710
13549
 
11711
13550
  # Recognises `Struct.new(*Symbol)` and `Struct.new(*Symbol, keyword_init: <expr>)` at constant-write rvalue
11712
13551
  # position. A trailing `KeywordHashNode` (the `keyword_init: ...` form) is accepted but does not contribute to
11713
13552
  # member discovery; every other argument MUST be a `Prism::SymbolNode`. At least one Symbol member is required —
11714
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.
11715
13558
  def struct_new_call?(node)
11716
13559
  return false unless meta_call_with_name?(node, :Struct, :new)
11717
13560
 
@@ -11719,7 +13562,12 @@ module Rigor
11719
13562
  positional = struct_new_positionals(args)
11720
13563
  return false if positional.nil? || positional.empty?
11721
13564
 
11722
- 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)
11723
13571
  end
11724
13572
 
11725
13573
  # Recognises `Module.new` and `Module.new(&block)` / `Module.new do ... end` at constant-write rvalue position.
@@ -11825,8 +13673,18 @@ module Rigor
11825
13673
  }.freeze
11826
13674
  private_constant :UNENTERED_FLOW_ENTRY
11827
13675
 
11828
- UNENTERED_EVALUATOR_KEY = :rigor_scope_index_unentered_evaluator
11829
- 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
11830
13688
 
11831
13689
  # Issues #2196 and #2056 — an `&&` / `||` / `and` / `or`, an `if` / `unless` / ternary or a `while` / `until`
11832
13690
  # in a value position (an array element, a hash value, a call argument, an interpolation, a multi-value
@@ -11837,6 +13695,8 @@ module Rigor
11837
13695
  # the scope this walk reached it with, under the index's own recorder, so each part is recorded with the scope
11838
13696
  # the statement form records it with. What it does not visit (a dead arm) is walked as before.
11839
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
+
11840
13700
  entry = UNENTERED_FLOW_ENTRY[node.class]
11841
13701
  return if entry.nil?
11842
13702
 
@@ -11845,10 +13705,27 @@ module Rigor
11845
13705
  part = node.public_send(entry) || (node.conditions.first&.conditions&.first if node.is_a?(Prism::CaseNode))
11846
13706
  return if part.nil? || table.key?(part)
11847
13707
 
11848
- evaluate = Thread.current[UNENTERED_EVALUATOR_KEY]
13708
+ evaluate = TypingState.current[UNENTERED_EVALUATOR]
11849
13709
  evaluate&.call(node, current_scope, code_body_nodes(node, current_scope.source_path))
11850
13710
  end
11851
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
+
11852
13729
  # The evaluation of an unentered construct starts with no class frame and no lexical nesting, which only a code
11853
13730
  # body observes: a `def`, a `class` / `module` / `class << …` body, and the block of a class-creating call
11854
13731
  # (`Class.new { … }`, `Struct.new`, `Data.define`, `Module.new`) or of a `refine`, whose `def`s and constants
@@ -11882,12 +13759,8 @@ module Rigor
11882
13759
  # Index walks nest (a rule may re-index a file while another file's walk is suspended), so the outer one is
11883
13760
  # restored on exit. A `propagate` called directly while an outer index walk is running would record into the
11884
13761
  # OUTER index's table through it; nothing outside {#index} calls `propagate`.
11885
- def with_unentered_evaluator(evaluate)
11886
- outer = Thread.current[UNENTERED_EVALUATOR_KEY]
11887
- Thread.current[UNENTERED_EVALUATOR_KEY] = evaluate
11888
- yield
11889
- ensure
11890
- Thread.current[UNENTERED_EVALUATOR_KEY] = outer
13762
+ def with_unentered_evaluator(evaluate, &)
13763
+ TypingState.current.with(UNENTERED_EVALUATOR, evaluate.freeze, &)
11891
13764
  end
11892
13765
 
11893
13766
  SELF_OPENING_BODIES = [Prism::DefNode, Prism::ClassNode, Prism::ModuleNode, Prism::SingletonClassNode].freeze
@@ -11895,6 +13768,12 @@ module Rigor
11895
13768
 
11896
13769
  # Issue #1717 — a `def` / `class` / `module` body opens its own `self`, so an enclosing unentered block's
11897
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`).
11898
13777
  # A `case` narrows its clauses ({#propagate_subjectless_case}, {#propagate_subject_case}). Every other node
11899
13778
  # hands its children the scope as is.
11900
13779
  def propagate_children(node, table, current_scope)
@@ -11903,10 +13782,19 @@ module Rigor
11903
13782
 
11904
13783
  return propagate_subject_case(node, table, current_scope)
11905
13784
  end
13785
+ return propagate_code_body(node, table, current_scope) if SELF_OPENING_BODIES.include?(node.class)
11906
13786
 
11907
- child_scope =
11908
- SELF_OPENING_BODIES.include?(node.class) ? current_scope.with_block_self_unknown(false) : current_scope
11909
- 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
11910
13798
  end
11911
13799
 
11912
13800
  # Issue #1361 — the block of `Thread.new`, `Fiber.new`, `define_method` and the other calls
@@ -11924,6 +13812,10 @@ module Rigor
11924
13812
  # argument one scope and threads none, so arguments that write keep the call's: an earlier one may rebind the
11925
13813
  # receiver a later one reads (`return y&.concat((y = nil).to_s, y.upcase)`, where the evaluator threads no
11926
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.
11927
13819
  def propagate_call(node, table, current_scope, recorded: false)
11928
13820
  block = node.block
11929
13821
  operands =
@@ -11959,6 +13851,7 @@ module Rigor
11959
13851
  current_scope = Narrowing.safe_navigation_block_scope(node, current_scope)
11960
13852
  # Issue #1429 — nor a guard's narrowing of a global or constant the body may run after code rebinds.
11961
13853
  current_scope = GuardRebinding.block_entry(current_scope, block, node)
13854
+ current_scope = current_scope.entering_block_without_gradual_marks(block)
11962
13855
  entry =
11963
13856
  if FreshFrameBlocks.fresh_entry?(node, current_scope)
11964
13857
  FreshFrameBlocks.entry(current_scope, node)
@@ -12088,14 +13981,17 @@ module Rigor
12088
13981
  # `it` as the outer block's). An enclosing binding of such a name is replaced with `Dynamic[top]`. Not the
12089
13982
  # signature's parameter type: this walk evaluates nothing, and a body write to the name is never threaded,
12090
13983
  # so any narrower claim could be stale. A name the enclosing scope does not bind is left unbound, which
12091
- # reads the same `Dynamic[top]`. Captured names — outer locals the body reads or rebinds without
12092
- # redeclaring them — keep the enclosing binding. Issue #1360 — a `->` body and its parameter defaults run
12093
- # 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}).
12094
13989
  def closure_scope(closure, scope)
12095
13990
  scope = GuardRebinding.block_entry(scope, closure, nil)
12096
13991
  scope = FreshFrameBlocks.closure_entry(scope, closure, nil)
12097
13992
  scope = shadow_local(scope, :it) if closure.parameters.is_a?(Prism::ItParametersNode)
12098
- 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)
12099
13995
  end
12100
13996
 
12101
13997
  def shadow_local(scope, name)