rigortype 0.4.1 → 0.4.2

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 (182) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +1 -1
  3. data/data/builtins/ruby_core/io.yml +1 -0
  4. data/data/builtins/ruby_core/proc.yml +4 -0
  5. data/data/builtins/ruby_core/random.yml +4 -0
  6. data/data/builtins/ruby_core/struct.yml +3 -0
  7. data/data/core_overlay/env.rbs +34 -0
  8. data/data/core_overlay/gc.rbs +50 -0
  9. data/data/core_overlay/hash.rbs +45 -0
  10. data/data/core_overlay/integer.rbs +31 -0
  11. data/data/core_overlay/io_buffer.rbs +27 -0
  12. data/data/core_overlay/kernel.rbs +40 -0
  13. data/data/core_overlay/match_data.rbs +28 -0
  14. data/data/core_overlay/module.rbs +51 -0
  15. data/data/core_overlay/object_space.rbs +29 -0
  16. data/data/core_overlay/proc.rbs +45 -0
  17. data/data/core_overlay/range.rbs +40 -0
  18. data/data/core_overlay/source_range.rbs +78 -0
  19. data/data/core_overlay/string.rbs +86 -0
  20. data/data/vendored_gem_sigs/prime/integer-extension.rbs +41 -0
  21. data/data/vendored_gem_sigs/prime/prime.rbs +372 -0
  22. data/docs/handbook/01-getting-started.md +3 -3
  23. data/docs/handbook/06-classes.md +3 -1
  24. data/docs/handbook/11-sig-gen.md +6 -0
  25. data/docs/handbook/appendix-liskov.md +5 -0
  26. data/docs/manual/03-configuration.md +1 -1
  27. data/docs/manual/04-diagnostics.md +2 -1
  28. data/docs/manual/06-baseline.md +2 -0
  29. data/docs/manual/07-plugins.md +1 -1
  30. data/docs/manual/18-removing-dead-code.md +32 -2
  31. data/docs/manual/plugins/README.md +6 -1
  32. data/docs/manual/plugins/rigor-activerecord.md +21 -0
  33. data/docs/manual/plugins/rigor-alba.md +60 -0
  34. data/docs/manual/plugins/rigor-minitest.md +22 -4
  35. data/docs/manual/plugins/rigor-typelizer.md +71 -0
  36. data/lib/rigor/analysis/check_rules/ivar_write_collector.rb +27 -3
  37. data/lib/rigor/analysis/check_rules/lexical_method_sites.rb +21 -92
  38. data/lib/rigor/analysis/check_rules/ruby_deprecations.rb +275 -0
  39. data/lib/rigor/analysis/check_rules/rule_ids.rb +4 -0
  40. data/lib/rigor/analysis/check_rules/source_arity.rb +21 -2
  41. data/lib/rigor/analysis/check_rules.rb +289 -57
  42. data/lib/rigor/analysis/dependency_recorder.rb +15 -0
  43. data/lib/rigor/analysis/dependency_source_inference/builder.rb +8 -1
  44. data/lib/rigor/analysis/dependency_source_inference/gem_resolver.rb +21 -0
  45. data/lib/rigor/analysis/dependency_source_inference/index.rb +36 -11
  46. data/lib/rigor/analysis/dependency_source_inference/walker.rb +196 -27
  47. data/lib/rigor/analysis/erb_template_detector.rb +2 -1
  48. data/lib/rigor/analysis/incremental.rb +29 -9
  49. data/lib/rigor/analysis/incremental_run_slot/write_guard.rb +30 -5
  50. data/lib/rigor/analysis/incremental_run_slot.rb +3 -1
  51. data/lib/rigor/analysis/incremental_session.rb +14 -1
  52. data/lib/rigor/analysis/reachability/graph.rb +123 -23
  53. data/lib/rigor/analysis/reachability/scan.rb +290 -36
  54. data/lib/rigor/analysis/rule_catalog.rb +43 -0
  55. data/lib/rigor/analysis/run_cache_key.rb +16 -1
  56. data/lib/rigor/analysis/runner/diagnostic_aggregator.rb +87 -5
  57. data/lib/rigor/analysis/runner/pool_coordinator.rb +50 -11
  58. data/lib/rigor/analysis/runner/run_snapshots.rb +6 -1
  59. data/lib/rigor/analysis/runner.rb +34 -3
  60. data/lib/rigor/analysis/worker_session.rb +7 -4
  61. data/lib/rigor/cache/incremental_snapshot.rb +22 -0
  62. data/lib/rigor/cli/annotate_command.rb +5 -0
  63. data/lib/rigor/cli/baseline_command.rb +7 -18
  64. data/lib/rigor/cli/check_command.rb +2 -1
  65. data/lib/rigor/cli/trace_command.rb +5 -0
  66. data/lib/rigor/cli/type_of_command.rb +5 -3
  67. data/lib/rigor/cli/unused_command.rb +13 -7
  68. data/lib/rigor/cli.rb +10 -2
  69. data/lib/rigor/configuration/severity_profile.rb +6 -0
  70. data/lib/rigor/configuration.rb +46 -2
  71. data/lib/rigor/environment/gated_signature_guard.rb +43 -0
  72. data/lib/rigor/environment/gated_signature_plan.rb +339 -0
  73. data/lib/rigor/environment/rbs_coverage_report.rb +1 -1
  74. data/lib/rigor/environment/rbs_loader.rb +283 -14
  75. data/lib/rigor/environment/required_features.rb +206 -0
  76. data/lib/rigor/environment.rb +21 -3
  77. data/lib/rigor/inference/acceptance.rb +28 -3
  78. data/lib/rigor/inference/builtins/cruby_definers.rb +61 -0
  79. data/lib/rigor/inference/builtins/method_catalog.rb +3 -0
  80. data/lib/rigor/inference/def_node_resolver.rb +23 -1
  81. data/lib/rigor/inference/expression_typer.rb +215 -16
  82. data/lib/rigor/inference/global_write_census.rb +7 -4
  83. data/lib/rigor/inference/guard_rebinding.rb +1 -1
  84. data/lib/rigor/inference/in_effect_refinements/proc_literals.rb +83 -0
  85. data/lib/rigor/inference/in_effect_refinements/refine_defs.rb +54 -0
  86. data/lib/rigor/inference/in_effect_refinements/scope_reads.rb +282 -0
  87. data/lib/rigor/inference/in_effect_refinements/walk.rb +276 -0
  88. data/lib/rigor/inference/in_effect_refinements.rb +256 -0
  89. data/lib/rigor/inference/key_presence_guard/scan.rb +281 -0
  90. data/lib/rigor/inference/key_presence_guard.rb +303 -0
  91. data/lib/rigor/inference/macro_block_self_type.rb +59 -12
  92. data/lib/rigor/inference/method_dispatcher/alias_strict_nominals.rb +36 -0
  93. data/lib/rigor/inference/method_dispatcher/arithmetic_sequence_elements.rb +73 -0
  94. data/lib/rigor/inference/method_dispatcher/imprecise_argument.rb +68 -0
  95. data/lib/rigor/inference/method_dispatcher/integer_step_block_params.rb +81 -0
  96. data/lib/rigor/inference/method_dispatcher/iterator_dispatch.rb +6 -0
  97. data/lib/rigor/inference/method_dispatcher/keyword_arguments.rb +267 -0
  98. data/lib/rigor/inference/method_dispatcher/method_folding.rb +2 -1
  99. data/lib/rigor/inference/method_dispatcher/overload_selector.rb +122 -105
  100. data/lib/rigor/inference/method_dispatcher/rbs_dispatch.rb +259 -28
  101. data/lib/rigor/inference/method_dispatcher/splat_arity.rb +71 -0
  102. data/lib/rigor/inference/method_dispatcher.rb +44 -11
  103. data/lib/rigor/inference/module_function_state.rb +2 -1
  104. data/lib/rigor/inference/narrowing.rb +20 -6
  105. data/lib/rigor/inference/object_mixins.rb +180 -0
  106. data/lib/rigor/inference/project_patched_methods.rb +10 -5
  107. data/lib/rigor/inference/project_patched_scanner.rb +71 -3
  108. data/lib/rigor/inference/refine_census.rb +239 -0
  109. data/lib/rigor/inference/refine_self.rb +317 -0
  110. data/lib/rigor/inference/refined_dispatch/own_method.rb +403 -0
  111. data/lib/rigor/inference/refined_dispatch.rb +272 -0
  112. data/lib/rigor/inference/scope_indexer/mixin_accumulator.rb +13 -0
  113. data/lib/rigor/inference/scope_indexer.rb +687 -60
  114. data/lib/rigor/inference/statement_evaluator.rb +71 -20
  115. data/lib/rigor/inference/string_mutation.rb +9 -3
  116. data/lib/rigor/inference/synthetic_method_scanner.rb +2 -1
  117. data/lib/rigor/inference/version_guard.rb +19 -10
  118. data/lib/rigor/language_server/project_context.rb +31 -0
  119. data/lib/rigor/language_server/server.rb +18 -0
  120. data/lib/rigor/plugin/inflector.rb +1 -1
  121. data/lib/rigor/plugin/io_boundary.rb +42 -3
  122. data/lib/rigor/plugin/macro/block_as_method.rb +51 -8
  123. data/lib/rigor/project_environment.rb +29 -6
  124. data/lib/rigor/protection/mutation_cache.rb +1 -1
  125. data/lib/rigor/protection/mutator.rb +1 -1
  126. data/lib/rigor/reflection/constant_ancestors.rb +127 -11
  127. data/lib/rigor/reflection/constant_path.rb +13 -2
  128. data/lib/rigor/reflection.rb +40 -8
  129. data/lib/rigor/scope/discovery_index.rb +40 -1
  130. data/lib/rigor/scope/resolution_chain.rb +159 -11
  131. data/lib/rigor/scope.rb +189 -10
  132. data/lib/rigor/sig_gen/classification.rb +4 -1
  133. data/lib/rigor/sig_gen/generator.rb +55 -5
  134. data/lib/rigor/sig_gen/method_candidate.rb +2 -2
  135. data/lib/rigor/sig_gen/skip_reason_catalog.rb +12 -0
  136. data/lib/rigor/source/alias_names.rb +52 -0
  137. data/lib/rigor/source/literals.rb +3 -2
  138. data/lib/rigor/source/symbol_name.rb +20 -0
  139. data/lib/rigor/type/combinator.rb +34 -2
  140. data/lib/rigor/type/float_range.rb +4 -1
  141. data/lib/rigor/type/nominal.rb +6 -1
  142. data/lib/rigor/version.rb +1 -1
  143. data/plugins/rigor-ac-library-rb/lib/rigor/plugin/ac_library_rb.rb +28 -0
  144. data/plugins/rigor-ac-library-rb/lib/rigor-ac-library-rb.rb +3 -0
  145. data/plugins/rigor-ac-library-rb/sig/ac_library_rb/convolution.rbs +7 -0
  146. data/plugins/rigor-ac-library-rb/sig/ac_library_rb/data_structures.rbs +160 -0
  147. data/plugins/rigor-ac-library-rb/sig/ac_library_rb/graph.rbs +94 -0
  148. data/plugins/rigor-ac-library-rb/sig/ac_library_rb/math.rbs +42 -0
  149. data/plugins/rigor-ac-library-rb/sig/ac_library_rb/modint.rbs +71 -0
  150. data/plugins/rigor-ac-library-rb/sig/ac_library_rb/object.rbs +37 -0
  151. data/plugins/rigor-ac-library-rb/sig/ac_library_rb/segtree.rbs +83 -0
  152. data/plugins/rigor-actionpack/lib/rigor/plugin/actionpack/analyzer.rb +2 -1
  153. data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord/association_roots.rb +100 -0
  154. data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord/model_discoverer.rb +94 -10
  155. data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord.rb +43 -2
  156. data/plugins/rigor-activerecord/sig/active_record/relation.rbs +3 -1
  157. data/plugins/rigor-alba/lib/rigor/plugin/alba/resource_collector.rb +149 -0
  158. data/plugins/rigor-alba/lib/rigor/plugin/alba/resource_discoverer.rb +74 -0
  159. data/plugins/rigor-alba/lib/rigor/plugin/alba/resource_index.rb +104 -0
  160. data/plugins/rigor-alba/lib/rigor/plugin/alba.rb +85 -0
  161. data/plugins/rigor-alba/lib/rigor-alba.rb +3 -0
  162. data/plugins/rigor-alba/sig/alba.rbs +4 -0
  163. data/plugins/rigor-ffi/lib/rigor/plugin/ffi/analyzer.rb +2 -2
  164. data/plugins/rigor-ffi/lib/rigor/plugin/ffi/target_detector.rb +29 -12
  165. data/plugins/rigor-ffi/lib/rigor/plugin/ffi.rb +7 -6
  166. data/plugins/rigor-minitest/lib/rigor/plugin/minitest/assertion_analyzer.rb +130 -21
  167. data/plugins/rigor-minitest/lib/rigor/plugin/minitest.rb +6 -4
  168. data/plugins/rigor-pundit/lib/rigor/plugin/pundit/analyzer.rb +4 -4
  169. data/plugins/rigor-rails-i18n/lib/rigor/plugin/rails_i18n/analyzer.rb +1 -1
  170. data/plugins/rigor-rails-routes/lib/rigor/plugin/rails_routes/routes_parser.rb +1 -1
  171. data/plugins/rigor-rbs-inline/lib/rigor/plugin/rbs_inline.rb +31 -0
  172. data/plugins/rigor-typelizer/lib/rigor/plugin/typelizer/serializer_collector.rb +102 -0
  173. data/plugins/rigor-typelizer/lib/rigor/plugin/typelizer/serializer_discoverer.rb +87 -0
  174. data/plugins/rigor-typelizer/lib/rigor/plugin/typelizer/serializer_index.rb +76 -0
  175. data/plugins/rigor-typelizer/lib/rigor/plugin/typelizer.rb +73 -0
  176. data/plugins/rigor-typelizer/lib/rigor-typelizer.rb +3 -0
  177. data/sig/rigor/analysis/dependency_source_inference/index.rbs +1 -1
  178. data/sig/rigor/plugin/io_boundary.rbs +3 -0
  179. data/sig/rigor/scope.rbs +27 -4
  180. data/sig/rigor/source.rbs +4 -0
  181. data/sig/rigor.rbs +3 -1
  182. metadata +66 -1
@@ -4,7 +4,10 @@ require "prism"
4
4
 
5
5
  require_relative "../scope"
6
6
  require_relative "../type"
7
+ require_relative "../source/alias_names"
8
+ require_relative "../source/literals"
7
9
  require_relative "../source/constant_path"
10
+ require_relative "object_mixins"
8
11
  require_relative "../source/node_children"
9
12
  require_relative "../source/node_walker"
10
13
  require_relative "../source/parameter_envelope"
@@ -19,6 +22,9 @@ require_relative "last_line"
19
22
  require_relative "last_status"
20
23
  require_relative "error_info"
21
24
  require_relative "hash_lookup_mutation"
25
+ require_relative "in_effect_refinements"
26
+ require_relative "refine_census"
27
+ require_relative "refine_self"
22
28
  require_relative "index_write_widening"
23
29
  require_relative "module_function_state"
24
30
  require_relative "multi_target_binder"
@@ -2249,11 +2255,13 @@ module Rigor
2249
2255
  # @return the `program_globals` table and the census, keyed by the discovery index members it fills
2250
2256
  def build_program_global_index(root, default_scope)
2251
2257
  accumulator = {}
2258
+ in_effect = InEffectRefinements.new(root)
2252
2259
  census = { patched_line_readers: Set.new, clears_last_status: false, defines_case_equality: false,
2253
- write_census: GlobalWriteCensus::Collector.new }
2260
+ write_census: GlobalWriteCensus::Collector.new(in_effect) }
2254
2261
  gather_global_writes(root, default_scope, accumulator, census)
2255
2262
  census[:patched_line_readers] = census[:patched_line_readers].freeze
2256
2263
  census[:implicit_self_evidence] = LastLine::SelfEvidence.new(root)
2264
+ census[:in_effect_refinements] = in_effect
2257
2265
  [accumulator.freeze, census]
2258
2266
  end
2259
2267
 
@@ -3245,8 +3253,10 @@ module Rigor
3245
3253
  # `possible_discovered_methods`, `contested_discovered_def_nodes` and
3246
3254
  # `contested_discovered_parameter_envelopes`, from the {Certainty} classification `certainty` (computed here
3247
3255
  # when the caller has none).
3248
- def build_methods_and_def_nodes(root, source_path = nil, certainty: Certainty.possible_nodes(root))
3256
+ def build_methods_and_def_nodes(root, source_path = nil, certainty: Certainty.possible_nodes(root), census: nil)
3249
3257
  tables = MethodTables.new({}, {}, nil, certainty, true)
3258
+ tables.root = root
3259
+ tables.census = census
3250
3260
  def_nodes = {}
3251
3261
  walk_methods_and_def_nodes(root, [], false, tables, def_nodes, source_path)
3252
3262
  apply_alias_def_nodes(root, def_nodes, tables)
@@ -3263,8 +3273,79 @@ module Rigor
3263
3273
  # ADR-119 WD3 — `certainty` is the file's {Certainty} answer and `certain` whether the contribution being
3264
3274
  # recorded is certain ({#at} sets it before each recorder). `possible`, `contested_envelopes` and
3265
3275
  # `contested_def_nodes` are the siblings the recorders fill, each nil until a possible contribution writes it.
3276
+ #
3277
+ # Issue #1689 — `root` is the walked tree, and `in_effect` its {InEffectRefinements}, built the first time a
3278
+ # `refine`-shaped call asks {#refine_target_of}, so a file without one never builds it.
3279
+ #
3280
+ # ADR-121 WD7 — `census`, when an Array, collects `[start offset, outcome]` for every `refine`-shaped node the
3281
+ # walk accounts for ({ScopeIndexer.refine_census}); `refine_text` memoises the byte offsets of the `refine`
3282
+ # words in the file's text, so a file with none skips every census scan and a `def` with none is not scanned;
3283
+ # `literal_seen` holds the offsets of the refine literals a call's arguments already accounted for;
3284
+ # `source_bytes` memoises the byte offsets of the file's heredoc openers; `loose_constants` memoises
3285
+ # {ScopeIndexer.loose_constant_segments}. The module a `refine`-shaped node is charged to is what the in-effect
3286
+ # walk worked out for it ({#refine_context}).
3266
3287
  MethodTables = Struct.new(:existence, :envelopes, :refinements, :certainty, :certain, :possible,
3267
- :contested_envelopes, :contested_def_nodes) do
3288
+ :contested_envelopes, :contested_def_nodes, :root, :in_effect, :census,
3289
+ :refine_text, :literal_seen, :source_bytes, :loose_constants, :block_spans) do
3290
+ # {ScopeIndexer.module_refine_target} against this tree's {InEffectRefinements}.
3291
+ def refine_target_of(node)
3292
+ return nil if ScopeIndexer.refine_target(node).nil?
3293
+
3294
+ ScopeIndexer.module_refine_target(node, in_effect_query)
3295
+ end
3296
+
3297
+ # {InEffectRefinements#class_body_refine?} for a `refine` call of this tree.
3298
+ def class_body_refine?(node) = ScopeIndexer.refine_call?(node) && in_effect_query.class_body_refine?(node)
3299
+
3300
+ def in_effect_query = (self.in_effect ||= InEffectRefinements.new(root))
3301
+
3302
+ # Does the walked file's text name `refine` as a word? Every census scan asks first, so a file that only says
3303
+ # `refined` or `refinement` pays nothing for it.
3304
+ def mentions_refine? = !refine_offsets.empty?
3305
+
3306
+ # Does the text in `[start, stop)` name `refine` as a word, or open a heredoc whose body (which follows the
3307
+ # line, and so may end after `stop`) could? A `def` that does neither is not scanned.
3308
+ def refine_in?(start, stop)
3309
+ ScopeIndexer.offset_in?(refine_offsets, start, stop) ||
3310
+ ScopeIndexer.offset_in?(self.source_bytes ||= ScopeIndexer.heredoc_offsets(root), start, stop)
3311
+ end
3312
+
3313
+ # ADR-121 WD7 — the `self` the `refine`-shaped `node` runs on, as the in-effect walk worked it out
3314
+ # ({InEffectRefinements#refine_context}); a module the walk cannot name for a node it did not record.
3315
+ def refine_context(node) = in_effect_query.refine_context(node) || RefineSelf.unknown
3316
+
3317
+ # The byte offsets of each `refine` word in the file's text, ascending.
3318
+ def refine_offsets
3319
+ self.refine_text ||= ScopeIndexer.refine_word_offsets(root)
3320
+ end
3321
+
3322
+ # Records that the walk accounted for the `refine`-shaped `node` with `outcome` (and the module it charged),
3323
+ # when a census is taken.
3324
+ def account(node, outcome, charged = nil)
3325
+ census&.push([node.location.start_offset, outcome, charged])
3326
+ end
3327
+
3328
+ # Marks a refine literal accounted for by its call, so the walk's descent does not account for it again.
3329
+ def see_literal(node) = (self.literal_seen ||= Set.new) << node.location.start_offset
3330
+
3331
+ def literal_seen?(node) = !literal_seen.nil? && literal_seen.include?(node.location.start_offset)
3332
+
3333
+ # A6 — the last segments of the constants this file binds by `||=` or by a literal `const_set`, which the
3334
+ # project-wide `constant_writers` a reader consults leaves out.
3335
+ def loose_constant?(segment)
3336
+ self.loose_constants ||= ScopeIndexer.loose_constant_segments(root)
3337
+ loose_constants.include?(segment)
3338
+ end
3339
+
3340
+ # Issue #1820 — does `node` sit inside a block or lambda of this tree? A block's `self` may be an object even in
3341
+ # a class body or at the top level (`s.instance_eval { … }`, a `define_method` block). The spans are found once
3342
+ # per file, on the first ask.
3343
+ def in_block?(node)
3344
+ self.block_spans ||= ScopeIndexer.block_spans(root)
3345
+ offset = node.location.start_offset
3346
+ block_spans.any? { |start, stop| offset >= start && offset < stop }
3347
+ end
3348
+
3268
3349
  # Sets {#certain} for the contribution `node` and returns the tables.
3269
3350
  def at(node)
3270
3351
  self.certain = !Certainty.possible?(certainty, node)
@@ -3295,7 +3376,121 @@ module Rigor
3295
3376
  end
3296
3377
 
3297
3378
  EMPTY_REFINEMENTS = {}.freeze
3298
- private_constant :EMPTY_REFINEMENTS
3379
+ REFINEMENT_WILDCARD = Scope::DiscoveryIndex::REFINEMENT_WILDCARD
3380
+ REFINE_WORD = /\brefine\b/
3381
+ HEREDOC_OPENER = /<<[~-]?["'`A-Za-z_]/
3382
+ private_constant :EMPTY_REFINEMENTS, :REFINEMENT_WILDCARD, :REFINE_WORD, :HEREDOC_OPENER
3383
+
3384
+ NO_OFFSETS = [].freeze
3385
+ private_constant :NO_OFFSETS
3386
+
3387
+ # The byte offsets of the `refine` words in `root`'s text (none for no tree), which `MethodTables#refine_in?`
3388
+ # matches node offsets against.
3389
+ def refine_word_offsets(root) = pattern_offsets(root, REFINE_WORD)
3390
+
3391
+ # The byte offsets of the heredoc openers (`<<~ID`, `<<-ID`, `<<ID`, `<<"ID"`) in `root`'s text: a heredoc's body
3392
+ # follows its line, so it may end after the `def` that opens it.
3393
+ def heredoc_offsets(root) = pattern_offsets(root, HEREDOC_OPENER)
3394
+
3395
+ def pattern_offsets(root, pattern)
3396
+ return NO_OFFSETS if root.nil?
3397
+
3398
+ # Byte-wise: a file may hold bytes its encoding does not (binary `__END__` data, a stray comment byte), and a
3399
+ # regexp over such a String raises.
3400
+ binary = root.send(:source).source.b
3401
+ return NO_OFFSETS unless pattern.match?(binary)
3402
+
3403
+ offsets = []
3404
+ position = 0
3405
+ while (match = pattern.match(binary, position))
3406
+ offsets << match.begin(0)
3407
+ position = match.end(0)
3408
+ end
3409
+ offsets.freeze
3410
+ end
3411
+
3412
+ # Is one of the ascending `offsets` in `[start, stop)`?
3413
+ # The `[start, end)` byte span of every block and lambda in `root`.
3414
+ def block_spans(root)
3415
+ spans = []
3416
+ return spans if root.nil?
3417
+
3418
+ Source::NodeWalker.each(root) do |node|
3419
+ next unless node.is_a?(Prism::BlockNode) || node.is_a?(Prism::LambdaNode)
3420
+
3421
+ location = node.location
3422
+ spans << [location.start_offset, location.end_offset]
3423
+ end
3424
+ spans
3425
+ end
3426
+
3427
+ def offset_in?(offsets, start, stop)
3428
+ index = offsets.bsearch_index { |offset| offset >= start }
3429
+ !index.nil? && offsets[index] < stop
3430
+ end
3431
+
3432
+ # A6 — the last segments of the constants `root` binds by `||=` (`K ||= String`, `A::K ||= …`) or by a
3433
+ # `const_set` whose name is a literal (`Object.const_set(:K, String)`), and (issue #1749) by any write in a
3434
+ # `class << …` body: the project-wide constant census files a write there under no name a `refine` argument
3435
+ # spells, but a `def` in that body reads it lexically (`class << self; K = String; def setup = refine(K) {…}`).
3436
+ # Scanned only for a file with a literal-target `refine`.
3437
+ def loose_constant_segments(root)
3438
+ segments = Set.new
3439
+ Source::NodeWalker.each(root) do |node|
3440
+ case node
3441
+ when Prism::ConstantOrWriteNode then segments << node.name.to_s
3442
+ when Prism::ConstantPathOrWriteNode then segments << node.target.name.to_s
3443
+ when Prism::SingletonClassNode then singleton_constant_segments(node.body, segments)
3444
+ when Prism::CallNode
3445
+ name = node.name == :const_set && node.arguments&.arguments&.first
3446
+ segments << name.unescaped if name.is_a?(Prism::SymbolNode) || name.is_a?(Prism::StringNode)
3447
+ end
3448
+ end
3449
+ segments
3450
+ end
3451
+
3452
+ # The last segment of every constant a write, an operator write or a multiple assignment target in `body` binds.
3453
+ def singleton_constant_segments(body, segments)
3454
+ return if body.nil?
3455
+
3456
+ Source::NodeWalker.each(body) do |node|
3457
+ case node
3458
+ when Prism::ConstantWriteNode, Prism::ConstantTargetNode, Prism::ConstantOperatorWriteNode,
3459
+ Prism::ConstantAndWriteNode, Prism::ConstantPathTargetNode
3460
+ segments << node.name.to_s
3461
+ when Prism::ConstantPathWriteNode, Prism::ConstantPathOperatorWriteNode, Prism::ConstantPathAndWriteNode
3462
+ segments << node.target.name.to_s
3463
+ end
3464
+ end
3465
+ end
3466
+
3467
+ # ADR-121 WD7 — the census gate's view of one parsed file (`root`): `[refinement table, [[start offset,
3468
+ # outcome], …]]`, one pair for every `refine`-shaped node the method walk accounted for. The outcomes are
3469
+ # `:recorded` (a literal target and block, its names recorded; also one in a module's own singleton method,
3470
+ # #1749), `:class_unknown` (a block whose target the walk cannot name), `:targets_wildcard` (a `refine` that
3471
+ # runs once a method is called, other than a module's own singleton method's, or in a block that may rebind
3472
+ # `self`, charged to its module or the wildcard), `:literal` (A3's `:refine` / eval-string literal), `:nested` (a
3473
+ # `refine` in a refine body, or in a singleton method of one), `:dsl` (where `self` is a class, #1689) and
3474
+ # `:raises` (no block, a Proc as the block, another receiver, or a computed target where `self` is `main`), and
3475
+ # `:data` (a `:refine` literal where `self` is a plain class or one of its instances).
3476
+ def refine_census(root)
3477
+ census = []
3478
+ refinements = build_methods_and_def_nodes(root, census: census)[3]
3479
+ [refinements, census]
3480
+ end
3481
+
3482
+ # ADR-121 WD7 (issue #1814) — the refinement table one parsed file contributes: the census of
3483
+ # {#build_methods_and_def_nodes}, whose fourth table is the project walk's own (`seed_discovered_methods` and
3484
+ # `walk_def_contributions` take it from the same call), run for a caller that wants nothing else. The gem source
3485
+ # walker reads a gem file this way, so a gem module's rows carry the wildcards a project module's would
3486
+ # (`walker_walk_census_parity_spec.rb`, beside the walker's specs, holds the two to one table). A file whose text
3487
+ # never names `refine` as a word records no row, so it is not walked. `source_path` names an anonymous module's
3488
+ # row (`#<Module:<path>:L:C>`) as the project walk does, so two files' bare `Module.new` blocks never share one.
3489
+ def refinement_table(root, source_path = nil)
3490
+ return EMPTY_REFINEMENTS if refine_word_offsets(root).empty?
3491
+
3492
+ build_methods_and_def_nodes(root, source_path)[3]
3493
+ end
3299
3494
 
3300
3495
  # Issue #1120 — a refinement table, deep-frozen; the shared empty table when nothing was recorded.
3301
3496
  def freeze_refinements(table)
@@ -3319,8 +3514,7 @@ module Rigor
3319
3514
  # implicit- or `self`-receiver `refine` with one constant argument and a literal block. A computed target
3320
3515
  # (`refine(klass) { … }`) names no class this walk can key, so its body walks as it did before.
3321
3516
  def refine_target(node)
3322
- return nil unless node.name == :refine && node.block.is_a?(Prism::BlockNode)
3323
- return nil unless node.receiver.nil? || node.receiver.is_a?(Prism::SelfNode)
3517
+ return nil unless refine_call?(node)
3324
3518
 
3325
3519
  arguments = node.arguments&.arguments
3326
3520
  return nil unless arguments&.size == 1
@@ -3329,25 +3523,237 @@ module Rigor
3329
3523
  target if target.is_a?(Prism::ConstantReadNode) || target.is_a?(Prism::ConstantPathNode)
3330
3524
  end
3331
3525
 
3526
+ # An implicit- or `self`-receiver `refine` call with a literal block, whatever its argument.
3527
+ def refine_call?(node)
3528
+ node.name == :refine && node.block.is_a?(Prism::BlockNode) &&
3529
+ (node.receiver.nil? || node.receiver.is_a?(Prism::SelfNode))
3530
+ end
3531
+
3532
+ # Issue #1689 — {#refine_target}, for a call that `in_effect` (its file's {InEffectRefinements}) does not place
3533
+ # where `self` is a class. `Class` undefines `refine`, so `refine X do … end` directly in a `class` body is the
3534
+ # class's own method (a DSL), and its block walks as any other block. Every walk that treats a refine body
3535
+ # specially asks this, so the method tables, the in-effect refinements and the typer agree on which calls are
3536
+ # refine bodies.
3537
+ def module_refine_target(node, in_effect)
3538
+ target = refine_target(node)
3539
+ target unless target.nil? || in_effect.class_body_refine?(node)
3540
+ end
3541
+
3332
3542
  # Issue #1120 — records the instance `def`s of a `refine X do … end` body as refinement methods of X, keyed by
3333
- # the refining module (`owner_prefix`, the `self` the `refine` call runs on). X is resolved LEXICALLY
3543
+ # the refining module (`refining`, the `self` the `refine` call runs on). X is resolved LEXICALLY
3334
3544
  # (`qualified_prefix`), which is where Ruby resolves the argument even inside a `Module.new { … }` block
3335
3545
  # whose `self` is anonymous; every name it can denote is recorded, as {#constant_receiver_candidates}
3336
3546
  # explains. Nothing in the body reaches the class tables: `refine String do def shout` does not give
3337
3547
  # `String` or the refining module a `shout` outside a `using`.
3338
- def record_refinement_defs(node, target, qualified_prefix, owner_prefix, tables)
3548
+ #
3549
+ # ADR-121 WD7 (issue #1799) — the names are what {RefineCensus.read_body} reads (`def`, `alias`,
3550
+ # `alias_method`, `define_method`, `attr_*`, `undef`), and a body that may define a name it cannot spell adds a
3551
+ # names-wildcard row per target. `refining` is `""` at the top level (where Ruby raises). The body is not
3552
+ # walked any further, so the census scans it here.
3553
+ #
3554
+ # A6 — a target this file binds by `||=` or a literal `const_set` (`K ||= String; refine(K)`) names whatever value
3555
+ # the write holds, so its names go under the wildcard class, as a computed target's do.
3556
+ def record_refinement_defs(node, target, qualified_prefix, refining, tables)
3557
+ loose = tables.loose_constant?(target.name.to_s)
3558
+ tables.account(node, loose ? :class_unknown : :recorded, census_charge(refining))
3339
3559
  body = node.block.body
3340
3560
  return if body.nil?
3341
3561
 
3342
- refining = owner_prefix.join("::")
3343
- targets = constant_receiver_candidates(target, qualified_prefix)
3344
- each_refinement_def(body) do |def_node|
3345
- targets.each do |class_name|
3346
- methods = ((tables.refinements ||= {})[class_name] ||= {})
3347
- modules = (methods[def_node.name] ||= [])
3348
- modules << refining unless modules.include?(refining)
3562
+ targets = loose ? [REFINEMENT_WILDCARD] : constant_receiver_candidates(target, qualified_prefix)
3563
+ reading = RefineCensus.read_body(body)
3564
+ refining.each do |owner|
3565
+ tables.refinements = RefineCensus.record_rows(tables.refinements, targets, reading, owner)
3566
+ end
3567
+ census_scan(body, qualified_prefix, tables, refine_body: true) if tables.mentions_refine?
3568
+ end
3569
+
3570
+ # What the census records as charged for `owners`: the one module, or every name.
3571
+ def census_charge(owners) = owners.size == 1 ? owners.first : owners
3572
+
3573
+ # ADR-121 WD7 — a literal-target `refine X do … end` the module-body walk reaches, charged where the in-effect
3574
+ # walk says it runs ({MethodTables#refine_context}): rows for the module it names (the walk's own name for an
3575
+ # anonymous `Module.new` block, `""` at the top level), or a targets-wildcard for that module (the wildcard
3576
+ # where it cannot be named) when it runs only once a method is called or on a module the walk cannot name.
3577
+ def record_literal_target_refine(node, target, qualified_prefix, owner_prefix, tables)
3578
+ context = tables.refine_context(node)
3579
+ here = context.here
3580
+ return record_refinement_defs(node, target, qualified_prefix, [""], tables) if here == RefineSelf::MAIN
3581
+ if context.deferred || here == REFINEMENT_WILDCARD
3582
+ return record_deferred_refine(node, RefineSelf.charged_modules(context), qualified_prefix, tables)
3583
+ end
3584
+
3585
+ record_refinement_defs(node, target, qualified_prefix, census_owners(context, owner_prefix), tables)
3586
+ end
3587
+
3588
+ # The modules a `refine` charged to `context` refines for; an anonymous `Module.new` block's is named by its
3589
+ # position (`owner_prefix`).
3590
+ def census_owners(context, owner_prefix)
3591
+ context.here == RefineSelf::ANONYMOUS ? [owner_prefix.join("::")] : RefineSelf.charged_modules(context)
3592
+ end
3593
+
3594
+ # ADR-121 WD7 — a targets-wildcard for each of `owners`, as a `refine` that runs once a method is called records
3595
+ # (an instance `def`, a `define_method` or `define_singleton_method` block; a module's own singleton method
3596
+ # records rows instead, #1749). The body is not walked any further, so the census scans it here.
3597
+ def record_deferred_refine(node, owners, qualified_prefix, tables)
3598
+ tables.account(node, :targets_wildcard, census_charge(owners))
3599
+ owners.each { |owner| record_targets_wildcard(tables, owner) }
3600
+ body = node.block.body
3601
+ census_scan(body, qualified_prefix, tables, refine_body: true) if body && tables.mentions_refine?
3602
+ end
3603
+
3604
+ # ADR-121 WD7 — a `refine`-shaped call the canonical arm ({#refine_target}) does not take: on another receiver
3605
+ # (`Module#refine` is private, so it raises), where `self` is a class (#1689's DSL), with no literal block (no
3606
+ # block raises, and so does a Proc passed as one), or with a target the walk cannot name (`refine(k)`,
3607
+ # `refine(self)`, `refine(K.dup)`). The last records the body's names under the wildcard class, so they decline
3608
+ # on every receiver, and a names-wildcard row when they are incomplete, which makes the module opaque; one that
3609
+ # runs once a method is called, or on a module the walk cannot name, records a targets-wildcard. At the top
3610
+ # level `self` is `main`, which has no `refine`.
3611
+ def account_other_refine(node, owner_prefix, tables)
3612
+ context = tables.refine_context(node)
3613
+ outcome, charged =
3614
+ if tables.class_body_refine?(node) || context.here == RefineSelf::CLASS then :dsl
3615
+ elsif !RefineCensus.self_call?(node) || !node.block.is_a?(Prism::BlockNode) ||
3616
+ context.here == RefineSelf::MAIN
3617
+ :raises
3618
+ elsif context.deferred || context.here == REFINEMENT_WILDCARD
3619
+ owners = RefineSelf.charged_modules(context)
3620
+ owners.each { |owner| record_targets_wildcard(tables, owner) }
3621
+ [:targets_wildcard, census_charge(owners)]
3622
+ else
3623
+ owners = census_owners(context, owner_prefix)
3624
+ owners.each { |owner| record_unreadable_target(node, owner, tables) }
3625
+ [:class_unknown, census_charge(owners)]
3626
+ end
3627
+ tables.account(node, outcome, charged)
3628
+ end
3629
+
3630
+ def record_unreadable_target(node, owner, tables)
3631
+ tables.refinements = RefineCensus.record_rows(tables.refinements, [REFINEMENT_WILDCARD],
3632
+ RefineCensus.read_body(node.block.body), owner)
3633
+ end
3634
+
3635
+ # `{"*" => {"*" => [owner]}}`: some `refine` of `owner` (every module, for the wildcard) is unreadable.
3636
+ def record_targets_wildcard(tables, owner)
3637
+ tables.refinements = RefineCensus.add_row(tables.refinements || {}, REFINEMENT_WILDCARD, REFINEMENT_WILDCARD,
3638
+ owner)
3639
+ end
3640
+
3641
+ # ADR-121 WD7 — the census over a subtree the method walk does not descend (a `def` body, a refine body), each
3642
+ # node charged where the in-effect walk says it runs. `refine_body` says `self` is the refinement itself, where a
3643
+ # nested `refine` refines for the refinement and only makes the body's names incomplete (§2.2).
3644
+ def census_scan(node, qualified_prefix, tables, refine_body: false)
3645
+ case node
3646
+ when Prism::DefNode, Prism::ClassNode, Prism::ModuleNode, Prism::SingletonClassNode
3647
+ refine_body = false
3648
+ when Prism::CallNode
3649
+ if (target = own_method_refine_target(node, tables))
3650
+ return record_own_method_refine(node, target, qualified_prefix, tables)
3349
3651
  end
3652
+
3653
+ census_call(node, qualified_prefix, tables, refine_body)
3654
+ when Prism::SymbolNode
3655
+ census_symbol(node, tables)
3350
3656
  end
3657
+ node.rigor_each_child { |child| census_scan(child, qualified_prefix, tables, refine_body: refine_body) }
3658
+ end
3659
+
3660
+ # ADR-121 WD7 (issue #1749) — the target of a canonical `refine X do … end` ({#refine_target}) in a module's own
3661
+ # singleton method (`def self.x`, a `def` in the module's `class << self`, or a block there that keeps `self`),
3662
+ # else nil. `self` is the module itself there, so the call refines X for it once the method is called. Anything
3663
+ # else in a method body (an instance `def`, a `define_method` or `define_singleton_method` block, a computed
3664
+ # target, a block that may rebind `self`) charges a targets-wildcard in {#census_call}, and a `refine` in a refine
3665
+ # body is nested there, whichever method it sits in: its context ({RefineSelf.refinement_body}) is not an
3666
+ # own method's.
3667
+ def own_method_refine_target(node, tables)
3668
+ return nil if node.name != :refine
3669
+
3670
+ target = tables.refine_target_of(node)
3671
+ target if target && tables.refine_context(node).own_method
3672
+ end
3673
+
3674
+ # Records `node` as the refine of its module, as a `refine` in the module body is recorded, with the refined
3675
+ # class marked as that one is ({#record_refinement}). The call's block is scanned here, so {#census_scan} does
3676
+ # not descend into it.
3677
+ def record_own_method_refine(node, target, qualified_prefix, tables)
3678
+ record_refinement(node, qualified_prefix, tables)
3679
+ owners = RefineSelf.charged_modules(tables.refine_context(node))
3680
+ record_refinement_defs(node, target, qualified_prefix, owners, tables)
3681
+ end
3682
+
3683
+ def census_call(node, qualified_prefix, tables, refine_body)
3684
+ census_literal_arguments(node, qualified_prefix, tables)
3685
+ return unless node.name == :refine
3686
+
3687
+ context = tables.refine_context(node)
3688
+ nested = refine_body || context.here == RefineSelf::REFINEMENT
3689
+ outcome, charged =
3690
+ if tables.class_body_refine?(node) || context.here == RefineSelf::CLASS then :dsl
3691
+ elsif nested && RefineCensus.self_call?(node) then :nested
3692
+ elsif !RefineCensus.self_call?(node) || node.block.nil? || context.here == RefineSelf::MAIN then :raises
3693
+ else
3694
+ owners = RefineSelf.charged_modules(context)
3695
+ owners.each { |owner| record_targets_wildcard(tables, owner) }
3696
+ [:targets_wildcard, census_charge(owners)]
3697
+ end
3698
+ tables.account(node, outcome, charged)
3699
+ end
3700
+
3701
+ # A3 — a `:refine` Symbol argument of any call, or a String argument holding `refine` of an eval, `send` or
3702
+ # method-naming call, records a targets-wildcard for the module the call runs `refine` on: the receiver of a
3703
+ # `send` or `*_eval` (each name a constant one can denote), else the module the in-effect walk says `self` is
3704
+ # there. Where `self` is a plain class or one of its instances, a literal charged to `self` is data.
3705
+ def census_literal_arguments(node, qualified_prefix, tables)
3706
+ arguments = node.arguments&.arguments
3707
+ return if arguments.nil?
3708
+
3709
+ strings = RefineCensus.string_literal_call?(node)
3710
+ arguments.each do |argument|
3711
+ next unless RefineCensus.refine_symbol?(argument) || (strings && RefineCensus.refine_string?(argument))
3712
+ next if tables.literal_seen?(argument)
3713
+
3714
+ tables.see_literal(argument)
3715
+ census_literal_argument(node, argument, qualified_prefix, tables)
3716
+ end
3717
+ end
3718
+
3719
+ def census_literal_argument(call, literal, qualified_prefix, tables)
3720
+ target = RefineCensus.literal_target(call)
3721
+ context = tables.refine_context(literal)
3722
+ return tables.account(literal, :data) if target == :self && context.literal == RefineSelf::CLASS
3723
+ return tables.account(literal, :nested) if target == :self && context.literal == RefineSelf::REFINEMENT
3724
+
3725
+ owners = literal_owners(call, target, RefineSelf.charged_modules(context, literal: true), qualified_prefix)
3726
+ owners.each { |owner| record_targets_wildcard(tables, owner) }
3727
+ tables.account(literal, :literal, target == :self ? census_charge(owners) : [:receiver, owners])
3728
+ end
3729
+
3730
+ def literal_owners(call, target, self_modules, qualified_prefix)
3731
+ case target
3732
+ when :self then self_modules
3733
+ when :unknown then [REFINEMENT_WILDCARD]
3734
+ else
3735
+ receiver = call.receiver
3736
+ names = constant_shaped?(receiver) ? constant_receiver_candidates(receiver, qualified_prefix) : []
3737
+ names.empty? ? [REFINEMENT_WILDCARD] : names
3738
+ end
3739
+ end
3740
+
3741
+ def constant_shaped?(node) = node.is_a?(Prism::ConstantReadNode) || node.is_a?(Prism::ConstantPathNode)
3742
+
3743
+ # A3 — a `:refine` Symbol anywhere else (`alias my_refine refine`, `%i[refine]`, a hash) names `Module#refine`
3744
+ # for code the walk cannot follow, run on the module the in-effect walk says `self` is there; where `self` is a
3745
+ # plain class or one of its instances it is data.
3746
+ def census_symbol(node, tables)
3747
+ return unless RefineCensus.refine_symbol?(node) && !tables.literal_seen?(node)
3748
+
3749
+ tables.see_literal(node)
3750
+ context = tables.refine_context(node)
3751
+ return tables.account(node, :data) if context.literal == RefineSelf::CLASS
3752
+ return tables.account(node, :nested) if context.literal == RefineSelf::REFINEMENT
3753
+
3754
+ owners = RefineSelf.charged_modules(context, literal: true)
3755
+ owners.each { |owner| record_targets_wildcard(tables, owner) }
3756
+ tables.account(node, :literal, census_charge(owners))
3351
3757
  end
3352
3758
 
3353
3759
  # The instance `def`s a refine body defines on the refined class: nested declarations open their own
@@ -3503,6 +3909,9 @@ module Rigor
3503
3909
  # `self::` header under a REBOUND self (eval/meta-new body) names
3504
3910
  # `owner::Name` instead.
3505
3911
  child_cref = unnameable_decl?(node, self_decl, singleton_cref)
3912
+ if node.is_a?(Prism::ClassNode) && node.superclass && methods_acc.mentions_refine?
3913
+ census_scan(node.superclass, qualified_prefix, methods_acc)
3914
+ end
3506
3915
  record_declaration_facts(node, child_prefix, methods_acc.at(node)) unless child_cref
3507
3916
  body_prefix = child_cref ? [] : child_prefix
3508
3917
  if node.body
@@ -3519,6 +3928,10 @@ module Rigor
3519
3928
  # still decline. The lexical cref below is unnameable in every case.
3520
3929
  singleton_prefix = singleton_body_prefix(node, in_singleton_class, owner_prefix,
3521
3930
  qualified_prefix)
3931
+ tables = methods_acc.at(node)
3932
+ if !node.expression.is_a?(Prism::SelfNode) || tables.in_block?(node)
3933
+ record_singleton_body_prepends(node, qualified_prefix, tables)
3934
+ end
3522
3935
  walk_methods_and_def_nodes(node.expression, qualified_prefix, in_singleton_class,
3523
3936
  methods_acc, def_nodes_acc, source_path, def_owner_prefix,
3524
3937
  singleton_cref: singleton_cref,
@@ -3536,6 +3949,7 @@ module Rigor
3536
3949
  return
3537
3950
  when Prism::ConstantWriteNode, Prism::ConstantPathWriteNode, Prism::ConstantOrWriteNode,
3538
3951
  Prism::ConstantPathOrWriteNode
3952
+ record_meta_superclass(node, qualified_prefix, def_owner_prefix, methods_acc.at(node))
3539
3953
  if (split = meta_new_block_split(node, qualified_prefix, def_owner_prefix, singleton_cref))
3540
3954
  enclosing, body, body_self = split
3541
3955
  # A meta-new block rebinds only `self` — `Module.nesting` stays lexical — so
@@ -3572,6 +3986,10 @@ module Rigor
3572
3986
  # receiver, a singleton nothing names) or any ownerless prefix under an unnameable
3573
3987
  # cref — is NOT top level; `record_def_node` would file the def under `<toplevel>`
3574
3988
  # where an implicit-self call could find a method Ruby never installed there.
3989
+ location = node.location
3990
+ if methods_acc.mentions_refine? && methods_acc.refine_in?(location.start_offset, location.end_offset)
3991
+ node.rigor_each_child { |child| census_scan(child, qualified_prefix, methods_acc) }
3992
+ end
3575
3993
  unless def_owner_prefix&.empty? || defs_singleton == :unnameable ||
3576
3994
  (singleton_cref && owner_prefix.empty?)
3577
3995
  singleton_def = in_singleton_class || defs_singleton
@@ -3581,13 +3999,17 @@ module Rigor
3581
3999
  end
3582
4000
  return
3583
4001
  when Prism::AliasMethodNode, Prism::UndefNode
4002
+ census_scan(node, qualified_prefix, methods_acc) if methods_acc.mentions_refine?
3584
4003
  unless def_owner_prefix&.empty? || defs_singleton == :unnameable ||
3585
4004
  (singleton_cref && owner_prefix.empty?)
3586
4005
  record_alias_or_undef(node, owner_prefix, in_singleton_class || defs_singleton,
3587
4006
  methods_acc.at(node))
3588
4007
  end
3589
4008
  return
4009
+ when Prism::SymbolNode
4010
+ census_symbol(node, methods_acc) if methods_acc.mentions_refine?
3590
4011
  when Prism::CallNode
4012
+ census_literal_arguments(node, qualified_prefix, methods_acc) if methods_acc.mentions_refine?
3591
4013
  if receiver_eval_call?(node)
3592
4014
  return walk_eval_methods_and_defs(node, qualified_prefix, in_singleton_class, methods_acc,
3593
4015
  def_nodes_acc, source_path, def_owner_prefix,
@@ -3596,11 +4018,16 @@ module Rigor
3596
4018
  end
3597
4019
  anonymous = record_call_node_methods(node, owner_prefix, in_singleton_class, methods_acc.at(node),
3598
4020
  source_path)
4021
+ tables = methods_acc.at(node)
4022
+ record_singleton_prepend(node, qualified_prefix, tables,
4023
+ self_ok: node.name == :prepend && tables.in_block?(node))
3599
4024
  # Issue #1120 — a refine body's defs go to the refinement table and nowhere else.
3600
- if (target = refine_target(node))
3601
- return record_refinement_defs(node, target, qualified_prefix, owner_prefix, methods_acc)
4025
+ if (target = methods_acc.refine_target_of(node))
4026
+ return record_literal_target_refine(node, target, qualified_prefix, owner_prefix, methods_acc)
3602
4027
  end
3603
4028
 
4029
+ account_other_refine(node, owner_prefix, methods_acc) if node.name == :refine
4030
+
3604
4031
  if anonymous
3605
4032
  walk_anonymous_meta_block(node, anonymous, qualified_prefix, in_singleton_class, methods_acc,
3606
4033
  def_nodes_acc, source_path, def_owner_prefix,
@@ -3666,8 +4093,24 @@ module Rigor
3666
4093
  end
3667
4094
 
3668
4095
  # The block form of a meta-new constant write: its members, and the module mark `Module.new` earns.
4096
+ # Issue #1820 — `K = Class.new(Parent)`, with or without a block, names a class whose superclass the superclass
4097
+ # table leaves out; it takes {Scope::DiscoveryIndex::ENVELOPE_META_SUPERCLASS_MARK}.
4098
+ def record_meta_superclass(node, qualified_prefix, self_base, tables)
4099
+ rvalue = meta_new_rvalue(node)
4100
+ return unless class_new_call?(rvalue) && !(rvalue.arguments&.arguments || []).empty?
4101
+
4102
+ child_prefix = meta_new_child_prefix(node, qualified_prefix, self_base)
4103
+ return if child_prefix.nil? || child_prefix.empty?
4104
+
4105
+ record_surface_mark(tables, child_prefix.join("::"), Scope::DiscoveryIndex::ENVELOPE_META_SUPERCLASS_MARK)
4106
+ end
4107
+
3669
4108
  def record_meta_new_facts(rvalue, child_prefix, tables)
3670
4109
  record_meta_members(rvalue, child_prefix, tables)
4110
+ # `Module.new(&BODY)`: the body runs on the new module but cannot be read, so its surface is unknown (#1816).
4111
+ if rvalue.block.is_a?(Prism::BlockArgumentNode)
4112
+ record_surface_mark(tables, child_prefix.join("::"), Scope::DiscoveryIndex::ENVELOPE_DYNAMIC_MARK)
4113
+ end
3671
4114
  return unless module_new_call?(rvalue)
3672
4115
 
3673
4116
  record_surface_mark(tables, child_prefix.join("::"), Scope::DiscoveryIndex::ENVELOPE_MODULE_MARK)
@@ -3700,7 +4143,7 @@ module Rigor
3700
4143
  def record_alias_method_call(call_node, qualified_prefix, in_singleton_class, accumulator)
3701
4144
  return if qualified_prefix.empty?
3702
4145
 
3703
- names = alias_method_call_names(call_node)
4146
+ names = Source::AliasNames.alias_method_call_names(call_node)
3704
4147
  return if names.nil?
3705
4148
 
3706
4149
  kind = in_singleton_class ? :singleton : :instance
@@ -3807,7 +4250,9 @@ module Rigor
3807
4250
  # other two. {#meta_new_block_call} owns both unwrappings; every walk asks it, so no spelling can be
3808
4251
  # recognised by one pass and missed by another.
3809
4252
  def meta_new_block_body(node)
3810
- meta_new_block_call(node)&.block&.body
4253
+ block = meta_new_block_call(node)&.block
4254
+ # A `&BODY` / `&:sym` block ARGUMENT is not a literal block: its body is unreadable here (#1816).
4255
+ block.body if block.is_a?(Prism::BlockNode)
3811
4256
  end
3812
4257
 
3813
4258
  # The class-creating call a constant write's rvalue ultimately is, or nil when the write is not the idiom.
@@ -4061,16 +4506,57 @@ module Rigor
4061
4506
  return if def_node.body.nil?
4062
4507
 
4063
4508
  Source::NodeWalker.each(def_node.body) do |node|
4509
+ if node.is_a?(Prism::SingletonClassNode)
4510
+ record_singleton_body_prepends(node, qualified_prefix, tables)
4511
+ next
4512
+ end
4064
4513
  next unless node.is_a?(Prism::CallNode)
4065
4514
 
4066
4515
  if node.name == :extend
4067
4516
  record_object_extension(node, qualified_prefix, tables)
4068
4517
  elsif SURFACE_EVAL_CALLS.include?(node.name) || SURFACE_NAMING_CALLS.include?(node.name)
4069
4518
  record_body_rewrite(node, qualified_prefix, tables)
4519
+ else
4520
+ record_singleton_prepend(node, qualified_prefix, tables, self_ok: true)
4070
4521
  end
4071
4522
  end
4072
4523
  end
4073
4524
 
4525
+ # Issue #1820 — `obj.singleton_class.prepend(M)`, or `prepend M` in a `class << obj` body, puts `M` ahead of
4526
+ # `obj`'s class even when the class already includes it (`prepend` re-inserts it; `extend` would not), so
4527
+ # `M`'s refinement answers first for that object. `M` takes {Scope::DiscoveryIndex::ENVELOPE_OBJECT_EXTENDED_MARK},
4528
+ # the mark an `extend` in a method body already leaves: an object of any class may carry its instance methods
4529
+ # ahead of its class's own. A `singleton_class` of `self` (and a `class << self` body, which the caller leaves
4530
+ # out) written directly in a class or module body or at the top level is the class object's or `main`'s, not an
4531
+ # instance's; in a method body or any block (`self_ok`), where `self` may be an object, it counts.
4532
+ # `singleton_body` says the call sits in a `class << obj` body.
4533
+ def record_singleton_prepend(node, qualified_prefix, tables, singleton_body: false, self_ok: false)
4534
+ return unless node.name == :prepend
4535
+
4536
+ receiver = node.receiver
4537
+ singleton = singleton_body && (receiver.nil? || receiver.is_a?(Prism::SelfNode))
4538
+ singleton ||= singleton_class_call?(receiver, self_ok)
4539
+ record_object_extension(node, qualified_prefix, tables) if singleton
4540
+ end
4541
+
4542
+ def singleton_class_call?(receiver, self_ok)
4543
+ return false unless receiver.is_a?(Prism::CallNode) && receiver.name == :singleton_class
4544
+ return false unless receiver.arguments.nil?
4545
+
4546
+ object = receiver.receiver
4547
+ self_ok || !(object.nil? || object.is_a?(Prism::SelfNode))
4548
+ end
4549
+
4550
+ def record_singleton_body_prepends(node, qualified_prefix, tables)
4551
+ return if node.body.nil?
4552
+
4553
+ Source::NodeWalker.each(node.body) do |inner|
4554
+ next unless inner.is_a?(Prism::CallNode)
4555
+
4556
+ record_singleton_prepend(inner, qualified_prefix, tables, singleton_body: true, self_ok: true)
4557
+ end
4558
+ end
4559
+
4074
4560
  def record_body_rewrite(node, qualified_prefix, tables)
4075
4561
  receiver = node.receiver
4076
4562
  if receiver.is_a?(Prism::ConstantReadNode) || receiver.is_a?(Prism::ConstantPathNode)
@@ -5875,6 +6361,7 @@ module Rigor
5875
6361
  def mixin_tables(root)
5876
6362
  accumulator = MixinAccumulator.new
5877
6363
  accumulator.direct_body(program_statements(root))
6364
+ accumulator.program_body(program_statements(root))
5878
6365
  walk_class_includes(root, [], nil, accumulator)
5879
6366
  {
5880
6367
  includes: freeze_mixin_lists(accumulator, :include),
@@ -6029,7 +6516,7 @@ module Rigor
6029
6516
  walk_class_includes(arg, qualified_prefix, current_class, accumulator,
6030
6517
  singleton_self: singleton_self, singleton_cref: singleton_cref)
6031
6518
  end
6032
- if (body = call.block&.body)
6519
+ if (body = meta_new_block_body(node))
6033
6520
  walk_class_includes(body, qualified_prefix, meta_owner, accumulator,
6034
6521
  singleton_cref: body_cref)
6035
6522
  end
@@ -6083,6 +6570,7 @@ module Rigor
6083
6570
  kind, _arguments, via_send = mixin_call_view(node)
6084
6571
  return if kind.nil?
6085
6572
 
6573
+ current_class ||= toplevel_include_owner(node, qualified_prefix, current_class, accumulator)
6086
6574
  effects = mixin_effects(node, kind, qualified_prefix, current_class, accumulator, in_singleton: false)
6087
6575
  .select { |_owner, side| side == :include }
6088
6576
  return if effects.empty?
@@ -6263,6 +6751,25 @@ module Rigor
6263
6751
  node.name == :prepend
6264
6752
  end
6265
6753
 
6754
+ # Issue #1697 — a receiverless `include M` written as a statement of the file's own top level is
6755
+ # `main.include`, which is `Object.include`: it mixes M into `Object`. It is recorded under its own
6756
+ # owner key, `ObjectMixins::TOPLEVEL_INCLUDE_KEY`, rather than on `Object`, so it rides every table an include
6757
+ # rides (the declaration signature, `discovered_class_sources`, the seed bundle) while no ancestry
6758
+ # walk that types a call reaches it: what Ruby reaches first for a name such an include supplies
6759
+ # depends on more than the chain shows (a block that rebinds `self`, an `extend` or a singleton `def`
6760
+ # on `main`), so only the diagnostic silences read it (`Inference::ObjectMixins`), and typing is
6761
+ # issue #1715's. Only a direct program statement counts.
6762
+ # The same call inside a top-level block, a method body or a conditional stays unrecorded, as before:
6763
+ # its `self` may not be `main` (a `describe do include M end` or `class_exec` block rebinds it, while
6764
+ # `[1].each do include M end` does not, and the walk cannot tell them apart), and it may not run. `main`
6765
+ # has no `prepend`, and a top-level `extend M` reaches `main`'s singleton alone, so neither is recorded.
6766
+ def toplevel_include_owner(node, qualified_prefix, current_class, accumulator)
6767
+ return nil unless current_class.nil? && qualified_prefix.empty?
6768
+ return nil unless node.receiver.nil? && node.name == :include
6769
+
6770
+ accumulator.program_statement?(node) ? ObjectMixins::TOPLEVEL_INCLUDE_KEY : nil
6771
+ end
6772
+
6266
6773
  # Issue #1123 — the class a `Recv.prepend(M)` call form targets, as the qualified name the prepend
6267
6774
  # table is keyed by, or nil when the receiver names no static class. The receiver is a constant READ
6268
6775
  # at the call site, so it resolves in the `Module.nesting` the call is written in — the same rule (and
@@ -6581,7 +7088,7 @@ module Rigor
6581
7088
  in_singleton: in_singleton, singleton_self: singleton_self,
6582
7089
  singleton_cref: singleton_cref)
6583
7090
  end
6584
- if (body = call.block&.body)
7091
+ if (body = meta_new_block_body(node))
6585
7092
  walk_class_extends(body, qualified_prefix, meta_owner, accumulator,
6586
7093
  singleton_cref: body_cref)
6587
7094
  end
@@ -7149,9 +7656,7 @@ module Rigor
7149
7656
  end
7150
7657
 
7151
7658
  def visibility_target_name(arg)
7152
- return arg.unescaped.to_sym if arg.is_a?(Prism::SymbolNode) || arg.is_a?(Prism::StringNode)
7153
-
7154
- nil
7659
+ Source::Literals.symbol_or_string(arg)
7155
7660
  end
7156
7661
 
7157
7662
  # Registers the alias name in the `discovered_methods` table so `undefined-method` diagnostics are not emitted for
@@ -7179,10 +7684,24 @@ module Rigor
7179
7684
  class_name = qualified_prefix.join("::")
7180
7685
  undef_node.names.each do |name|
7181
7686
  method_name = literal_method_name(name)
7182
- record_method_envelope_opaque(tables, class_name, method_name) if method_name
7687
+ next unless method_name
7688
+
7689
+ record_method_envelope_opaque(tables, class_name, method_name)
7690
+ record_removed_name(tables, class_name, method_name)
7183
7691
  end
7184
7692
  end
7185
7693
 
7694
+ REMOVING_CALLS = %i[remove_method undef_method].to_set.freeze
7695
+ private_constant :REMOVING_CALLS
7696
+
7697
+ # Issue #1820 — `undef f`, `remove_method :f` or `undef_method :f` on the class, as its own entry beside the
7698
+ # opaque envelopes: an `attr_*` or `define_method` records an opaque envelope too, so only this says the name
7699
+ # is gone.
7700
+ def record_removed_name(tables, class_name, method_name)
7701
+ record_envelope(tables.envelopes, class_name, [Scope::DiscoveryIndex::ENVELOPE_REMOVED_KIND, method_name],
7702
+ Source::ParameterEnvelope::OPAQUE)
7703
+ end
7704
+
7186
7705
  # Both kinds, and the envelope table only: the evidence says something rewrites the name, not which
7187
7706
  # side it lives on, and it is not evidence that the name EXISTS, so the existence table is untouched.
7188
7707
  def record_method_envelope_opaque(tables, class_name, method_name)
@@ -7198,7 +7717,7 @@ module Rigor
7198
7717
  # `def_delegator :@x, :f`) would leave a `def`'s envelope standing for a method that no longer has it.
7199
7718
  NAME_NEUTRAL_MACROS = %i[
7200
7719
  private public protected module_function private_class_method public_class_method
7201
- private_constant public_constant require require_relative autoload
7720
+ private_constant public_constant require require_relative autoload autoload_relative
7202
7721
  ].to_set.freeze
7203
7722
  private_constant :NAME_NEUTRAL_MACROS
7204
7723
 
@@ -7249,13 +7768,21 @@ module Rigor
7249
7768
  if dynamic_surface_call?(node)
7250
7769
  record_surface_mark(tables, class_name, Scope::DiscoveryIndex::ENVELOPE_DYNAMIC_MARK)
7251
7770
  elsif node.name == :refine
7252
- record_refinement(node, qualified_prefix, tables)
7771
+ # Issue #1689 — a `refine` where `self` is a class is the class's own method, and refines nothing.
7772
+ record_refinement(node, qualified_prefix, tables) unless tables.class_body_refine?(node)
7253
7773
  elsif !NAME_NEUTRAL_MACROS.include?(node.name)
7254
- (node.arguments&.arguments || []).each do |argument|
7255
- # `memoize def f(a)` wraps the def it is handed exactly as `memoize :f` does.
7256
- method_name = argument.is_a?(Prism::DefNode) ? argument.name : literal_method_name(argument)
7257
- record_method_envelope_opaque(tables, class_name, method_name) if method_name
7258
- end
7774
+ record_named_arguments_opaque(node, class_name, tables)
7775
+ end
7776
+ end
7777
+
7778
+ def record_named_arguments_opaque(node, class_name, tables)
7779
+ (node.arguments&.arguments || []).each do |argument|
7780
+ # `memoize def f(a)` wraps the def it is handed exactly as `memoize :f` does.
7781
+ method_name = argument.is_a?(Prism::DefNode) ? argument.name : literal_method_name(argument)
7782
+ next unless method_name
7783
+
7784
+ record_method_envelope_opaque(tables, class_name, method_name)
7785
+ record_removed_name(tables, class_name, method_name) if REMOVING_CALLS.include?(node.name)
7259
7786
  end
7260
7787
  end
7261
7788
 
@@ -7414,7 +7941,7 @@ module Rigor
7414
7941
  # `alias_method :new, :old` — the CallNode twin of the `alias` keyword (#533; liquid's i18n
7415
7942
  # `t` alias was the corpus case). Unlike AliasMethodNode a call's children can carry further
7416
7943
  # class bodies (`Class.new do … end`), so the walk continues below it either way.
7417
- names = alias_method_call_names(node)
7944
+ names = Source::AliasNames.alias_method_call_names(node)
7418
7945
  if names && !rec_prefix.empty? && !(singleton_cref && leaf_owner.nil?)
7419
7946
  record_alias_call_entry(accumulator, rec_prefix.join("::"), names, node)
7420
7947
  end
@@ -7475,21 +8002,6 @@ module Rigor
7475
8002
  true
7476
8003
  end
7477
8004
 
7478
- # `[new_name, old_name]` for an implicit-self `alias_method` call with two literal symbol /
7479
- # string arguments, or nil. A variable-named alias is runtime data and stays unrecorded.
7480
- def alias_method_call_names(call_node)
7481
- return nil unless call_node.name == :alias_method && call_node.receiver.nil?
7482
-
7483
- args = call_node.arguments&.arguments
7484
- return nil unless args && args.size == 2
7485
-
7486
- new_name = literal_method_name(args[0])
7487
- old_name = literal_method_name(args[1])
7488
- return nil if new_name.nil? || old_name.nil?
7489
-
7490
- [new_name, old_name]
7491
- end
7492
-
7493
8005
  # The class/module arm of {#collect_class_alias_map}: under an unnameable cref a
7494
8006
  # bare/`self::` header opens `#<singleton>::Name` — the map files nothing for it;
7495
8007
  # a rooted or explicit-base header re-anchors at a nameable prefix.
@@ -7532,11 +8044,12 @@ module Rigor
7532
8044
 
7533
8045
  def record_alias_map_entry(alias_node, qualified_prefix, accumulator)
7534
8046
  return if qualified_prefix.empty?
7535
- return unless alias_node.new_name.is_a?(Prism::SymbolNode) && alias_node.old_name.is_a?(Prism::SymbolNode)
8047
+
8048
+ names = Source::AliasNames.keyword_names(alias_node)
8049
+ return if names.nil?
7536
8050
 
7537
8051
  class_name = qualified_prefix.join("::")
7538
- new_name = alias_node.new_name.unescaped.to_sym
7539
- old_name = alias_node.old_name.unescaped.to_sym
8052
+ new_name, old_name = names
7540
8053
  (accumulator[class_name] ||= {})[new_name] = old_name
7541
8054
  accumulator.wrote(class_name, new_name, alias_node) if accumulator.is_a?(AliasMap)
7542
8055
  end
@@ -7636,9 +8149,7 @@ module Rigor
7636
8149
  end
7637
8150
 
7638
8151
  def literal_method_name(node)
7639
- return nil unless node.is_a?(Prism::SymbolNode) || node.is_a?(Prism::StringNode)
7640
-
7641
- node.unescaped&.to_sym
8152
+ Source::Literals.symbol_or_string(node)
7642
8153
  end
7643
8154
 
7644
8155
  # Walks every file in `paths` (each path is parsed once with `Prism.parse_file`) and returns the unioned
@@ -8766,7 +9277,7 @@ module Rigor
8766
9277
 
8767
9278
  def constant_write_census(root)
8768
9279
  tables = CensusTables.new(writes: {}, seen: Set.new, declared: Set.new, aliases: {},
8769
- write_census: GlobalWriteCensus::Collector.new)
9280
+ write_census: GlobalWriteCensus::Collector.new(InEffectRefinements.new(root)))
8770
9281
  walk_constant_write_census(root, [], tables)
8771
9282
  tables
8772
9283
  end
@@ -9699,7 +10210,7 @@ module Rigor
9699
10210
  record_declarations(part, qualified_prefix, identity_table, discovered,
9700
10211
  EMPTY_RENAMES, self_prefix, singleton_cref: singleton_cref)
9701
10212
  end
9702
- if (body = rvalue.block&.body)
10213
+ if (body = rvalue.block.is_a?(Prism::BlockNode) && rvalue.block.body)
9703
10214
  record_declarations(body, qualified_prefix, identity_table, discovered,
9704
10215
  EMPTY_RENAMES, child_prefix, singleton_cref: singleton_cref)
9705
10216
  end
@@ -9934,11 +10445,22 @@ module Rigor
9934
10445
  propagate(node.expression, table, current_scope)
9935
10446
  propagate(node.rescue_expression, table, current_scope.forget_error_info.forget_last_status)
9936
10447
  when Prism::PostExecutionNode then propagate_end_body(node, table, current_scope)
9937
- else
9938
- node.rigor_each_child { |child| propagate(child, table, current_scope) }
10448
+ else propagate_children(node, table, current_scope)
9939
10449
  end
9940
10450
  end
9941
10451
 
10452
+ SELF_OPENING_BODIES = [Prism::DefNode, Prism::ClassNode, Prism::ModuleNode, Prism::SingletonClassNode].freeze
10453
+ private_constant :SELF_OPENING_BODIES
10454
+
10455
+ # Issue #1717 — a `def` / `class` / `module` body opens its own `self`, so an enclosing unentered block's
10456
+ # unknown-`self` mark ({Scope#block_self_unknown?}) stops at it, as the evaluator's fresh body scope does.
10457
+ # Every other node hands its children the scope as is.
10458
+ def propagate_children(node, table, current_scope)
10459
+ child_scope =
10460
+ SELF_OPENING_BODIES.include?(node.class) ? current_scope.with_block_self_unknown(false) : current_scope
10461
+ node.rigor_each_child { |child| propagate(child, table, child_scope) }
10462
+ end
10463
+
9942
10464
  # Issue #1361 — the block of `Thread.new`, `Fiber.new`, `define_method` and the other calls
9943
10465
  # {FreshFrameBlocks.fresh_entry?} names does not read the match-global narrowing of the body it is written in.
9944
10466
  # The evaluator enters it as {FreshFrameBlocks.entry} gives ({MatchRebinding.block_entry}), but a block in a
@@ -9985,12 +10507,117 @@ module Rigor
9985
10507
  def unentered_block_entry(node, block, table, current_scope)
9986
10508
  return current_scope unless block.is_a?(Prism::BlockNode) && !table.key?(block)
9987
10509
 
10510
+ narrowing = unentered_block_narrowing(node, current_scope)
9988
10511
  current_scope = Narrowing.safe_navigation_block_scope(node, current_scope)
9989
10512
  # Issue #1429 — nor a guard's narrowing of a global or constant the body may run after code rebinds.
9990
10513
  current_scope = GuardRebinding.block_entry(current_scope, block, node)
9991
- return FreshFrameBlocks.entry(current_scope, node) if FreshFrameBlocks.fresh_entry?(node, current_scope)
10514
+ entry =
10515
+ if FreshFrameBlocks.fresh_entry?(node, current_scope)
10516
+ FreshFrameBlocks.entry(current_scope, node)
10517
+ else
10518
+ LastLine.block_entry(FreshFrameBlocks.closure_entry(current_scope, block, node), block, node)
10519
+ end
10520
+ anonymous = AnonymousMetaClass.name_for(node, current_scope.source_path)
10521
+ return unentered_meta_class_body_entry(block, entry, anonymous, table) if anonymous
10522
+
10523
+ refined = refined_body_name(node, current_scope)
10524
+ record_class_body_defs(block, entry, table, Type::Combinator.nominal_of(refined), nil) if refined
10525
+
10526
+ # Issue #1717 — the body's `self` is unknown unless narrowed, as on the evaluator's entry; only the
10527
+ # `call.undefined-method` mark is set here, never #316's opaque mark (its bind gate reads the evaluator's).
10528
+ entry = entry.with_block_self_unknown(true) unless narrowing&.self_type
10529
+ return entry unless narrowing
10530
+
10531
+ MacroBlockSelfType.apply(entry, narrowing, keeps_unknown: current_scope.block_self_narrowing_unknown?(node))
10532
+ end
10533
+
10534
+ # Issue #1120 — the class a `refine X do … end` call's block refines, or nil when the call is not that shape
10535
+ # or is a `refine` where `self` is a class (issue #1689: the class's own method, its block an ordinary one).
10536
+ # The evaluator asks the same two questions ({StatementEvaluator#refined_class_context}).
10537
+ def refined_body_name(node, scope)
10538
+ target = refine_target(node)
10539
+ return nil if target.nil? || InEffectRefinements.class_body_refine?(scope, node)
10540
+
10541
+ refined_class_name(target, scope)
10542
+ end
10543
+
10544
+ # Issue #1120 — the name of the class `refine target do … end` refines: the class `target` types as, or the
10545
+ # constant as written when it does not type as a class object (a gem class with no RBS). Shared by
10546
+ # {StatementEvaluator#refined_class_context} and {#refined_body_name}.
10547
+ def refined_class_name(target, scope)
10548
+ refined = scope.type_of(target)
10549
+ name = refined.is_a?(Type::Singleton) ? refined.class_name : Source::ConstantPath.qualified_name(target)
10550
+ name&.delete_prefix("::")
10551
+ end
10552
+
10553
+ # Issue #1696 — the entry of a `Class.new` / `Module.new` / `Struct.new` / `Data.define` block the evaluator
10554
+ # never entered because it sits in a value position: `Object.const_set(:K, Class.new { attr_reader :x })`,
10555
+ # `[Struct.new(:a) { def b = a }]`. Its `self` is the class `name` ({AnonymousMetaClass.name_for}) names, as
10556
+ # {StatementEvaluator#enter_meta_class_body} gives the statement form; left on the enclosing `self`, a
10557
+ # top-level one read every macro in it as `call.unresolved-toplevel`. Its `def`s are recorded as
10558
+ # {#record_class_body_defs} says.
10559
+ def unentered_meta_class_body_entry(block, entry, name, table)
10560
+ entry = entry.with_block_self_type(Type::Combinator.singleton_of(name)).with_singleton_class_body(false)
10561
+ record_class_body_defs(block, entry, table, Type::Combinator.nominal_of(name), entry.self_type)
10562
+ entry
10563
+ end
10564
+
10565
+ # Issue #1696 — records each `def` a value-position class-body block defines ({#each_class_body_def}) with the
10566
+ # `self` its body runs on, since no evaluator pass enters it: `instance_self` for a plain `def`, `class_self`
10567
+ # for a `def self.m` (skipped when nil). A `refine X` block passes only `Nominal[X]`: Ruby runs its defs on an
10568
+ # instance of X, but the block itself on the refinement module, which Rigor does not model, so the block keeps
10569
+ # the unknown `self` an unnarrowed block has. A `def obj.m` is left to the walk.
10570
+ def record_class_body_defs(block, entry, table, instance_self, class_self)
10571
+ each_class_body_def(block.body) do |def_node|
10572
+ next if table.key?(def_node)
10573
+
10574
+ receiver = def_node.receiver
10575
+ def_self =
10576
+ if receiver.nil? then instance_self
10577
+ elsif receiver.is_a?(Prism::SelfNode) then class_self
10578
+ end
10579
+ table[def_node] = entry.with_block_self_type(def_self) if def_self
10580
+ end
10581
+ end
10582
+
10583
+ # The `def`s a class-body block defines on its class: every one in the body, `private def m` included, short
10584
+ # of a nested block, lambda or `class` / `module` / `class << …` body, each of which opens its own.
10585
+ def each_class_body_def(node, &)
10586
+ return unless node.is_a?(Prism::Node)
10587
+ return yield(node) if node.is_a?(Prism::DefNode)
10588
+ return if CLASS_BODY_DEF_BARRIERS.include?(node.class)
9992
10589
 
9993
- LastLine.block_entry(FreshFrameBlocks.closure_entry(current_scope, block, node), block, node)
10590
+ node.rigor_each_child { |child| each_class_body_def(child, &) }
10591
+ end
10592
+
10593
+ CLASS_BODY_DEF_BARRIERS = [
10594
+ Prism::BlockNode, Prism::LambdaNode, Prism::ClassNode, Prism::ModuleNode, Prism::SingletonClassNode
10595
+ ].freeze
10596
+ private_constant :CLASS_BODY_DEF_BARRIERS
10597
+
10598
+ # The narrowing ({MacroBlockSelfType::Match}) an unentered block's body runs with — its `self` and, for a
10599
+ # `block_as_methods:` entry, the declared refinements (issue #1667) — when the call is one whose block the
10600
+ # engine narrows: an ADR-16 Tier A `block_as_methods:` match ({MacroBlockSelfType}) or `define_method` on the
10601
+ # lexical `self` ({DefineMethodBlockSelf}, issue #963). The evaluator applies the same two narrowings to
10602
+ # the blocks it enters ({StatementEvaluator#build_block_entry_scope}, {StatementEvaluator#enter_call_block})
10603
+ # and the value pass to every block it types ({ExpressionTyper#block_body_self_narrowing}); without this a
10604
+ # block in a value position — `puts(MyApp.get("/x") { redirect "/y" })`, `render json: Alba.serialize(x) {
10605
+ # attributes :id }`, `private define_method(:m) { helper }` — kept the enclosing `self`, and every DSL call
10606
+ # in it read as a call on that. The receiver is typed only when some plugin declares an entry for the
10607
+ # call's name, so a call no entry names costs one table lookup. A miss, or a raise, keeps the entry as
10608
+ # built — the false-positive-safe direction.
10609
+ def unentered_block_narrowing(node, scope)
10610
+ registry = scope.environment&.plugin_registry
10611
+ if registry && !registry.empty? && !registry.contribution_index.block_entries_for(node.name).empty?
10612
+ receiver_type =
10613
+ node.receiver ? scope.type_of(node.receiver) : MacroBlockSelfType.implicit_receiver_type(scope)
10614
+ match = MacroBlockSelfType.match_for(scope: scope, call_node: node, receiver_type: receiver_type)
10615
+ return match if match
10616
+ end
10617
+ defined = DefineMethodBlockSelf.narrow_self_type_for(scope: scope, call_node: node)
10618
+ defined && MacroBlockSelfType::Match.new(self_type: defined, refinements: InEffectRefinements::EMPTY)
10619
+ rescue StandardError
10620
+ nil
9994
10621
  end
9995
10622
 
9996
10623
  # The scope the children of an unentered block or lambda inherit. The evaluator enters a statement-level