rigortype 0.4.1 → 0.4.3

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 (230) 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 +18 -5
  25. data/docs/handbook/appendix-liskov.md +5 -0
  26. data/docs/manual/02-cli-reference.md +19 -1
  27. data/docs/manual/03-configuration.md +7 -5
  28. data/docs/manual/04-diagnostics.md +2 -1
  29. data/docs/manual/05-inspecting-types.md +50 -0
  30. data/docs/manual/06-baseline.md +2 -0
  31. data/docs/manual/07-plugins.md +1 -1
  32. data/docs/manual/12-caching.md +18 -1
  33. data/docs/manual/14-rails-quickstart.md +2 -2
  34. data/docs/manual/18-removing-dead-code.md +41 -4
  35. data/docs/manual/plugins/README.md +6 -1
  36. data/docs/manual/plugins/rigor-activerecord.md +21 -0
  37. data/docs/manual/plugins/rigor-alba.md +60 -0
  38. data/docs/manual/plugins/rigor-minitest.md +22 -4
  39. data/docs/manual/plugins/rigor-typelizer.md +71 -0
  40. data/lib/rigor/analysis/check_rules/ivar_write_collector.rb +27 -3
  41. data/lib/rigor/analysis/check_rules/lexical_method_sites.rb +21 -92
  42. data/lib/rigor/analysis/check_rules/ruby_deprecations.rb +275 -0
  43. data/lib/rigor/analysis/check_rules/rule_ids.rb +11 -0
  44. data/lib/rigor/analysis/check_rules/source_arity.rb +21 -2
  45. data/lib/rigor/analysis/check_rules.rb +292 -59
  46. data/lib/rigor/analysis/dependency_recorder.rb +15 -0
  47. data/lib/rigor/analysis/dependency_source_inference/builder.rb +8 -1
  48. data/lib/rigor/analysis/dependency_source_inference/gem_resolver.rb +21 -0
  49. data/lib/rigor/analysis/dependency_source_inference/index.rb +36 -11
  50. data/lib/rigor/analysis/dependency_source_inference/walker.rb +196 -27
  51. data/lib/rigor/analysis/effects_cache_probe.rb +1 -1
  52. data/lib/rigor/analysis/erb_template_detector.rb +2 -1
  53. data/lib/rigor/analysis/incremental.rb +29 -9
  54. data/lib/rigor/analysis/incremental_run_slot/write_guard.rb +30 -5
  55. data/lib/rigor/analysis/incremental_run_slot.rb +6 -4
  56. data/lib/rigor/analysis/incremental_session.rb +103 -23
  57. data/lib/rigor/analysis/path_expansion.rb +62 -6
  58. data/lib/rigor/analysis/plugin_fact_fingerprint.rb +124 -37
  59. data/lib/rigor/analysis/reachability/graph.rb +241 -37
  60. data/lib/rigor/analysis/reachability/scan.rb +490 -77
  61. data/lib/rigor/analysis/reachability/scan_cache.rb +4 -2
  62. data/lib/rigor/analysis/rule_catalog.rb +43 -0
  63. data/lib/rigor/analysis/run_cache_key.rb +16 -1
  64. data/lib/rigor/analysis/run_cache_probe.rb +1 -1
  65. data/lib/rigor/analysis/run_stats.rb +28 -6
  66. data/lib/rigor/analysis/runner/diagnostic_aggregator.rb +113 -6
  67. data/lib/rigor/analysis/runner/pool_coordinator.rb +77 -19
  68. data/lib/rigor/analysis/runner/project_pre_passes.rb +21 -1
  69. data/lib/rigor/analysis/runner/run_snapshots.rb +6 -1
  70. data/lib/rigor/analysis/runner.rb +62 -13
  71. data/lib/rigor/analysis/worker_session.rb +8 -5
  72. data/lib/rigor/cache/engine_source.rb +21 -7
  73. data/lib/rigor/cache/incremental_snapshot.rb +55 -5
  74. data/lib/rigor/cli/annotate_command.rb +5 -0
  75. data/lib/rigor/cli/baseline_command.rb +7 -18
  76. data/lib/rigor/cli/check_command.rb +140 -40
  77. data/lib/rigor/cli/docs_command.rb +5 -4
  78. data/lib/rigor/cli/lens_command.rb +144 -0
  79. data/lib/rigor/cli/lsp_command.rb +5 -2
  80. data/lib/rigor/cli/skill_command.rb +3 -3
  81. data/lib/rigor/cli/skill_describe.rb +1 -1
  82. data/lib/rigor/cli/trace_command.rb +5 -0
  83. data/lib/rigor/cli/type_of_command.rb +6 -3
  84. data/lib/rigor/cli/unused_command.rb +166 -27
  85. data/lib/rigor/cli.rb +39 -5
  86. data/lib/rigor/configuration/severity_profile.rb +6 -0
  87. data/lib/rigor/configuration.rb +61 -2
  88. data/lib/rigor/environment/bundle_sig_discovery.rb +11 -7
  89. data/lib/rigor/environment/gated_signature_guard.rb +43 -0
  90. data/lib/rigor/environment/gated_signature_plan.rb +339 -0
  91. data/lib/rigor/environment/installed_gem_set.rb +38 -11
  92. data/lib/rigor/environment/lockfile_resolver.rb +37 -6
  93. data/lib/rigor/environment/missing_gem_constant_index.rb +22 -7
  94. data/lib/rigor/environment/rbs_coverage_report.rb +1 -1
  95. data/lib/rigor/environment/rbs_loader.rb +316 -14
  96. data/lib/rigor/environment/required_features.rb +208 -0
  97. data/lib/rigor/environment.rb +33 -8
  98. data/lib/rigor/inference/acceptance.rb +28 -3
  99. data/lib/rigor/inference/builtins/cruby_definers.rb +78 -0
  100. data/lib/rigor/inference/builtins/method_catalog.rb +3 -0
  101. data/lib/rigor/inference/captured_locals.rb +43 -3
  102. data/lib/rigor/inference/def_node_resolver.rb +23 -1
  103. data/lib/rigor/inference/expression_typer.rb +261 -29
  104. data/lib/rigor/inference/global_write_census.rb +7 -4
  105. data/lib/rigor/inference/guard_rebinding.rb +1 -1
  106. data/lib/rigor/inference/in_effect_refinements/proc_literals.rb +83 -0
  107. data/lib/rigor/inference/in_effect_refinements/refine_defs.rb +54 -0
  108. data/lib/rigor/inference/in_effect_refinements/scope_reads.rb +286 -0
  109. data/lib/rigor/inference/in_effect_refinements/walk.rb +276 -0
  110. data/lib/rigor/inference/in_effect_refinements.rb +256 -0
  111. data/lib/rigor/inference/key_presence_guard/scan.rb +281 -0
  112. data/lib/rigor/inference/key_presence_guard.rb +303 -0
  113. data/lib/rigor/inference/macro_block_self_type.rb +59 -12
  114. data/lib/rigor/inference/match_rebinding.rb +1 -1
  115. data/lib/rigor/inference/method_dispatcher/alias_strict_nominals.rb +36 -0
  116. data/lib/rigor/inference/method_dispatcher/argument_nodes.rb +25 -0
  117. data/lib/rigor/inference/method_dispatcher/arithmetic_sequence_elements.rb +73 -0
  118. data/lib/rigor/inference/method_dispatcher/imprecise_argument.rb +68 -0
  119. data/lib/rigor/inference/method_dispatcher/integer_step_block_params.rb +81 -0
  120. data/lib/rigor/inference/method_dispatcher/iterator_dispatch.rb +6 -0
  121. data/lib/rigor/inference/method_dispatcher/kernel_dispatch.rb +3 -2
  122. data/lib/rigor/inference/method_dispatcher/keyword_arguments.rb +267 -0
  123. data/lib/rigor/inference/method_dispatcher/match_data_folding.rb +2 -2
  124. data/lib/rigor/inference/method_dispatcher/member_shape_projection.rb +3 -3
  125. data/lib/rigor/inference/method_dispatcher/method_folding.rb +2 -1
  126. data/lib/rigor/inference/method_dispatcher/optional_count.rb +63 -0
  127. data/lib/rigor/inference/method_dispatcher/overload_selector.rb +122 -105
  128. data/lib/rigor/inference/method_dispatcher/rbs_dispatch.rb +247 -28
  129. data/lib/rigor/inference/method_dispatcher/splat_arity.rb +71 -0
  130. data/lib/rigor/inference/method_dispatcher/struct_folding.rb +4 -7
  131. data/lib/rigor/inference/method_dispatcher/struct_materialization.rb +8 -26
  132. data/lib/rigor/inference/method_dispatcher/unit_denominator_rational.rb +67 -0
  133. data/lib/rigor/inference/method_dispatcher.rb +96 -14
  134. data/lib/rigor/inference/module_function_state.rb +2 -1
  135. data/lib/rigor/inference/narrowing.rb +20 -6
  136. data/lib/rigor/inference/object_mixins.rb +180 -0
  137. data/lib/rigor/inference/project_patched_definedness_guard.rb +86 -0
  138. data/lib/rigor/inference/project_patched_methods.rb +12 -7
  139. data/lib/rigor/inference/project_patched_scanner.rb +108 -26
  140. data/lib/rigor/inference/refine_census.rb +239 -0
  141. data/lib/rigor/inference/refine_self.rb +317 -0
  142. data/lib/rigor/inference/refined_dispatch/class_object.rb +350 -0
  143. data/lib/rigor/inference/refined_dispatch/own_method.rb +475 -0
  144. data/lib/rigor/inference/refined_dispatch.rb +320 -0
  145. data/lib/rigor/inference/repeated_or_writes.rb +2 -0
  146. data/lib/rigor/inference/scope_indexer/mixin_accumulator.rb +13 -0
  147. data/lib/rigor/inference/scope_indexer.rb +827 -65
  148. data/lib/rigor/inference/statement_evaluator.rb +294 -51
  149. data/lib/rigor/inference/string_mutation.rb +9 -3
  150. data/lib/rigor/inference/synthetic_method_scanner.rb +2 -1
  151. data/lib/rigor/inference/untyped_rejoin.rb +75 -0
  152. data/lib/rigor/inference/version_guard.rb +19 -10
  153. data/lib/rigor/language_server/project_context.rb +87 -5
  154. data/lib/rigor/language_server/server.rb +18 -0
  155. data/lib/rigor/lens/anchor.rb +115 -0
  156. data/lib/rigor/lens/contract_reader.rb +118 -0
  157. data/lib/rigor/lens/document.rb +113 -0
  158. data/lib/rigor/lens/file_reader.rb +433 -0
  159. data/lib/rigor/lens/members.rb +153 -0
  160. data/lib/rigor/lens/parameters.rb +91 -0
  161. data/lib/rigor/lens/plugins.rb +40 -0
  162. data/lib/rigor/lens/run.rb +280 -0
  163. data/lib/rigor/lens/slot.rb +79 -0
  164. data/lib/rigor/lens.rb +40 -0
  165. data/lib/rigor/plugin/inflector.rb +1 -1
  166. data/lib/rigor/plugin/io_boundary.rb +42 -3
  167. data/lib/rigor/plugin/macro/block_as_method.rb +51 -8
  168. data/lib/rigor/project_environment.rb +48 -6
  169. data/lib/rigor/protection/mutation_cache.rb +3 -2
  170. data/lib/rigor/protection/mutator.rb +1 -1
  171. data/lib/rigor/reflection/constant_ancestors.rb +127 -11
  172. data/lib/rigor/reflection/constant_path.rb +13 -2
  173. data/lib/rigor/reflection.rb +40 -8
  174. data/lib/rigor/scope/discovery_index.rb +40 -1
  175. data/lib/rigor/scope/resolution_chain.rb +159 -11
  176. data/lib/rigor/scope.rb +211 -21
  177. data/lib/rigor/sig_gen/classification.rb +4 -1
  178. data/lib/rigor/sig_gen/generator.rb +100 -10
  179. data/lib/rigor/sig_gen/method_candidate.rb +2 -2
  180. data/lib/rigor/sig_gen/skip_reason_catalog.rb +12 -0
  181. data/lib/rigor/source/alias_names.rb +52 -0
  182. data/lib/rigor/source/literals.rb +3 -2
  183. data/lib/rigor/source/symbol_name.rb +20 -0
  184. data/lib/rigor/type/combinator.rb +34 -2
  185. data/lib/rigor/type/float_range.rb +4 -1
  186. data/lib/rigor/type/nominal.rb +6 -1
  187. data/lib/rigor/version.rb +1 -1
  188. data/plugins/rigor-ac-library-rb/lib/rigor/plugin/ac_library_rb.rb +28 -0
  189. data/plugins/rigor-ac-library-rb/lib/rigor-ac-library-rb.rb +3 -0
  190. data/plugins/rigor-ac-library-rb/sig/ac_library_rb/convolution.rbs +7 -0
  191. data/plugins/rigor-ac-library-rb/sig/ac_library_rb/data_structures.rbs +160 -0
  192. data/plugins/rigor-ac-library-rb/sig/ac_library_rb/graph.rbs +94 -0
  193. data/plugins/rigor-ac-library-rb/sig/ac_library_rb/math.rbs +42 -0
  194. data/plugins/rigor-ac-library-rb/sig/ac_library_rb/modint.rbs +71 -0
  195. data/plugins/rigor-ac-library-rb/sig/ac_library_rb/object.rbs +37 -0
  196. data/plugins/rigor-ac-library-rb/sig/ac_library_rb/segtree.rbs +83 -0
  197. data/plugins/rigor-actionpack/lib/rigor/plugin/actionpack/analyzer.rb +2 -1
  198. data/plugins/rigor-active-model-serializers/lib/rigor/plugin/active_model_serializers/serializer_discoverer.rb +29 -3
  199. data/plugins/rigor-active-model-serializers/lib/rigor/plugin/active_model_serializers.rb +5 -1
  200. data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord/association_roots.rb +100 -0
  201. data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord/model_discoverer.rb +94 -10
  202. data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord.rb +43 -2
  203. data/plugins/rigor-activerecord/sig/active_record/relation.rbs +3 -1
  204. data/plugins/rigor-alba/lib/rigor/plugin/alba/resource_collector.rb +149 -0
  205. data/plugins/rigor-alba/lib/rigor/plugin/alba/resource_discoverer.rb +74 -0
  206. data/plugins/rigor-alba/lib/rigor/plugin/alba/resource_index.rb +104 -0
  207. data/plugins/rigor-alba/lib/rigor/plugin/alba.rb +85 -0
  208. data/plugins/rigor-alba/lib/rigor-alba.rb +3 -0
  209. data/plugins/rigor-alba/sig/alba.rbs +4 -0
  210. data/plugins/rigor-ffi/lib/rigor/plugin/ffi/analyzer.rb +2 -2
  211. data/plugins/rigor-ffi/lib/rigor/plugin/ffi/target_detector.rb +29 -12
  212. data/plugins/rigor-ffi/lib/rigor/plugin/ffi.rb +7 -6
  213. data/plugins/rigor-minitest/lib/rigor/plugin/minitest/assertion_analyzer.rb +130 -21
  214. data/plugins/rigor-minitest/lib/rigor/plugin/minitest.rb +6 -4
  215. data/plugins/rigor-pundit/lib/rigor/plugin/pundit/analyzer.rb +4 -4
  216. data/plugins/rigor-rails-i18n/lib/rigor/plugin/rails_i18n/analyzer.rb +1 -1
  217. data/plugins/rigor-rails-routes/lib/rigor/plugin/rails_routes/routes_parser.rb +1 -1
  218. data/plugins/rigor-rbs-inline/lib/rigor/plugin/rbs_inline.rb +31 -0
  219. data/plugins/rigor-typelizer/lib/rigor/plugin/typelizer/serializer_collector.rb +102 -0
  220. data/plugins/rigor-typelizer/lib/rigor/plugin/typelizer/serializer_discoverer.rb +87 -0
  221. data/plugins/rigor-typelizer/lib/rigor/plugin/typelizer/serializer_index.rb +76 -0
  222. data/plugins/rigor-typelizer/lib/rigor/plugin/typelizer.rb +73 -0
  223. data/plugins/rigor-typelizer/lib/rigor-typelizer.rb +3 -0
  224. data/sig/rigor/analysis/dependency_source_inference/index.rbs +1 -1
  225. data/sig/rigor/plugin/io_boundary.rbs +3 -0
  226. data/sig/rigor/scope.rbs +30 -4
  227. data/sig/rigor/source.rbs +4 -0
  228. data/sig/rigor.rbs +9 -4
  229. data/skills/rigor-project-init/references/02-configure.md +2 -2
  230. metadata +83 -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"
@@ -95,13 +101,17 @@ module Rigor
95
101
  # bodies from their CONVERGED bindings so per-line probes
96
102
  # (`rigor annotate`) reflect the post-writeback state, not the
97
103
  # cap-N intermediate constants. Off for the check path.
104
+ # @param widen_clauses — check-path flag (issue #1231): a rescue or
105
+ # `ensure` clause records the variables the `begin` body writes as
106
+ # `Dynamic[top]`. Off for every other reader, so parameter inference
107
+ # and sig-gen observe the clause's plain entry.
98
108
  # @return identity-comparing
99
109
  # table whose default value is `default_scope`.
100
110
  # The no-op re-anchoring table (#722 residue 2): shared so the overwhelmingly common
101
111
  # nothing-to-re-anchor walk allocates none.
102
112
  EMPTY_RENAMES = {}.freeze
103
113
 
104
- def index(root, default_scope:, converged_loop_recording: false) # rubocop:disable Metrics/AbcSize
114
+ def index(root, default_scope:, converged_loop_recording: false, widen_clauses: false) # rubocop:disable Metrics/AbcSize
105
115
  # Slice A-declarations. Build the declaration overrides first so every scope handed to the StatementEvaluator
106
116
  # already carries the table; structural sharing through `Scope#with_local` / `#with_fact` / `#with_self_type`
107
117
  # propagates it across every derived scope.
@@ -188,7 +198,8 @@ module Rigor
188
198
 
189
199
  on_enter = ->(node, scope) { table[node] = scope }
190
200
  StatementEvaluator.new(scope: seeded_scope, on_enter: on_enter,
191
- converged_loop_recording: converged_loop_recording).evaluate(root)
201
+ converged_loop_recording: converged_loop_recording,
202
+ widen_clauses: widen_clauses).evaluate(root)
192
203
 
193
204
  propagate(root, table, seeded_scope)
194
205
  table
@@ -2249,11 +2260,13 @@ module Rigor
2249
2260
  # @return the `program_globals` table and the census, keyed by the discovery index members it fills
2250
2261
  def build_program_global_index(root, default_scope)
2251
2262
  accumulator = {}
2263
+ in_effect = InEffectRefinements.new(root)
2252
2264
  census = { patched_line_readers: Set.new, clears_last_status: false, defines_case_equality: false,
2253
- write_census: GlobalWriteCensus::Collector.new }
2265
+ write_census: GlobalWriteCensus::Collector.new(in_effect) }
2254
2266
  gather_global_writes(root, default_scope, accumulator, census)
2255
2267
  census[:patched_line_readers] = census[:patched_line_readers].freeze
2256
2268
  census[:implicit_self_evidence] = LastLine::SelfEvidence.new(root)
2269
+ census[:in_effect_refinements] = in_effect
2257
2270
  [accumulator.freeze, census]
2258
2271
  end
2259
2272
 
@@ -3245,8 +3258,10 @@ module Rigor
3245
3258
  # `possible_discovered_methods`, `contested_discovered_def_nodes` and
3246
3259
  # `contested_discovered_parameter_envelopes`, from the {Certainty} classification `certainty` (computed here
3247
3260
  # when the caller has none).
3248
- def build_methods_and_def_nodes(root, source_path = nil, certainty: Certainty.possible_nodes(root))
3261
+ def build_methods_and_def_nodes(root, source_path = nil, certainty: Certainty.possible_nodes(root), census: nil)
3249
3262
  tables = MethodTables.new({}, {}, nil, certainty, true)
3263
+ tables.root = root
3264
+ tables.census = census
3250
3265
  def_nodes = {}
3251
3266
  walk_methods_and_def_nodes(root, [], false, tables, def_nodes, source_path)
3252
3267
  apply_alias_def_nodes(root, def_nodes, tables)
@@ -3263,8 +3278,79 @@ module Rigor
3263
3278
  # ADR-119 WD3 — `certainty` is the file's {Certainty} answer and `certain` whether the contribution being
3264
3279
  # recorded is certain ({#at} sets it before each recorder). `possible`, `contested_envelopes` and
3265
3280
  # `contested_def_nodes` are the siblings the recorders fill, each nil until a possible contribution writes it.
3281
+ #
3282
+ # Issue #1689 — `root` is the walked tree, and `in_effect` its {InEffectRefinements}, built the first time a
3283
+ # `refine`-shaped call asks {#refine_target_of}, so a file without one never builds it.
3284
+ #
3285
+ # ADR-121 WD7 — `census`, when an Array, collects `[start offset, outcome]` for every `refine`-shaped node the
3286
+ # walk accounts for ({ScopeIndexer.refine_census}); `refine_text` memoises the byte offsets of the `refine`
3287
+ # words in the file's text, so a file with none skips every census scan and a `def` with none is not scanned;
3288
+ # `literal_seen` holds the offsets of the refine literals a call's arguments already accounted for;
3289
+ # `source_bytes` memoises the byte offsets of the file's heredoc openers; `loose_constants` memoises
3290
+ # {ScopeIndexer.loose_constant_segments}. The module a `refine`-shaped node is charged to is what the in-effect
3291
+ # walk worked out for it ({#refine_context}).
3266
3292
  MethodTables = Struct.new(:existence, :envelopes, :refinements, :certainty, :certain, :possible,
3267
- :contested_envelopes, :contested_def_nodes) do
3293
+ :contested_envelopes, :contested_def_nodes, :root, :in_effect, :census,
3294
+ :refine_text, :literal_seen, :source_bytes, :loose_constants, :block_spans) do
3295
+ # {ScopeIndexer.module_refine_target} against this tree's {InEffectRefinements}.
3296
+ def refine_target_of(node)
3297
+ return nil if ScopeIndexer.refine_target(node).nil?
3298
+
3299
+ ScopeIndexer.module_refine_target(node, in_effect_query)
3300
+ end
3301
+
3302
+ # {InEffectRefinements#class_body_refine?} for a `refine` call of this tree.
3303
+ def class_body_refine?(node) = ScopeIndexer.refine_call?(node) && in_effect_query.class_body_refine?(node)
3304
+
3305
+ def in_effect_query = (self.in_effect ||= InEffectRefinements.new(root))
3306
+
3307
+ # Does the walked file's text name `refine` as a word? Every census scan asks first, so a file that only says
3308
+ # `refined` or `refinement` pays nothing for it.
3309
+ def mentions_refine? = !refine_offsets.empty?
3310
+
3311
+ # Does the text in `[start, stop)` name `refine` as a word, or open a heredoc whose body (which follows the
3312
+ # line, and so may end after `stop`) could? A `def` that does neither is not scanned.
3313
+ def refine_in?(start, stop)
3314
+ ScopeIndexer.offset_in?(refine_offsets, start, stop) ||
3315
+ ScopeIndexer.offset_in?(self.source_bytes ||= ScopeIndexer.heredoc_offsets(root), start, stop)
3316
+ end
3317
+
3318
+ # ADR-121 WD7 — the `self` the `refine`-shaped `node` runs on, as the in-effect walk worked it out
3319
+ # ({InEffectRefinements#refine_context}); a module the walk cannot name for a node it did not record.
3320
+ def refine_context(node) = in_effect_query.refine_context(node) || RefineSelf.unknown
3321
+
3322
+ # The byte offsets of each `refine` word in the file's text, ascending.
3323
+ def refine_offsets
3324
+ self.refine_text ||= ScopeIndexer.refine_word_offsets(root)
3325
+ end
3326
+
3327
+ # Records that the walk accounted for the `refine`-shaped `node` with `outcome` (and the module it charged),
3328
+ # when a census is taken.
3329
+ def account(node, outcome, charged = nil)
3330
+ census&.push([node.location.start_offset, outcome, charged])
3331
+ end
3332
+
3333
+ # Marks a refine literal accounted for by its call, so the walk's descent does not account for it again.
3334
+ def see_literal(node) = (self.literal_seen ||= Set.new) << node.location.start_offset
3335
+
3336
+ def literal_seen?(node) = !literal_seen.nil? && literal_seen.include?(node.location.start_offset)
3337
+
3338
+ # A6 — the last segments of the constants this file binds by `||=` or by a literal `const_set`, which the
3339
+ # project-wide `constant_writers` a reader consults leaves out.
3340
+ def loose_constant?(segment)
3341
+ self.loose_constants ||= ScopeIndexer.loose_constant_segments(root)
3342
+ loose_constants.include?(segment)
3343
+ end
3344
+
3345
+ # Issue #1820 — does `node` sit inside a block or lambda of this tree? A block's `self` may be an object even in
3346
+ # a class body or at the top level (`s.instance_eval { … }`, a `define_method` block). The spans are found once
3347
+ # per file, on the first ask.
3348
+ def in_block?(node)
3349
+ self.block_spans ||= ScopeIndexer.block_spans(root)
3350
+ offset = node.location.start_offset
3351
+ block_spans.any? { |start, stop| offset >= start && offset < stop }
3352
+ end
3353
+
3268
3354
  # Sets {#certain} for the contribution `node` and returns the tables.
3269
3355
  def at(node)
3270
3356
  self.certain = !Certainty.possible?(certainty, node)
@@ -3295,7 +3381,121 @@ module Rigor
3295
3381
  end
3296
3382
 
3297
3383
  EMPTY_REFINEMENTS = {}.freeze
3298
- private_constant :EMPTY_REFINEMENTS
3384
+ REFINEMENT_WILDCARD = Scope::DiscoveryIndex::REFINEMENT_WILDCARD
3385
+ REFINE_WORD = /\brefine\b/
3386
+ HEREDOC_OPENER = /<<[~-]?["'`A-Za-z_]/
3387
+ private_constant :EMPTY_REFINEMENTS, :REFINEMENT_WILDCARD, :REFINE_WORD, :HEREDOC_OPENER
3388
+
3389
+ NO_OFFSETS = [].freeze
3390
+ private_constant :NO_OFFSETS
3391
+
3392
+ # The byte offsets of the `refine` words in `root`'s text (none for no tree), which `MethodTables#refine_in?`
3393
+ # matches node offsets against.
3394
+ def refine_word_offsets(root) = pattern_offsets(root, REFINE_WORD)
3395
+
3396
+ # The byte offsets of the heredoc openers (`<<~ID`, `<<-ID`, `<<ID`, `<<"ID"`) in `root`'s text: a heredoc's body
3397
+ # follows its line, so it may end after the `def` that opens it.
3398
+ def heredoc_offsets(root) = pattern_offsets(root, HEREDOC_OPENER)
3399
+
3400
+ def pattern_offsets(root, pattern)
3401
+ return NO_OFFSETS if root.nil?
3402
+
3403
+ # Byte-wise: a file may hold bytes its encoding does not (binary `__END__` data, a stray comment byte), and a
3404
+ # regexp over such a String raises.
3405
+ binary = root.send(:source).source.b
3406
+ return NO_OFFSETS unless pattern.match?(binary)
3407
+
3408
+ offsets = []
3409
+ position = 0
3410
+ while (match = pattern.match(binary, position))
3411
+ offsets << match.begin(0)
3412
+ position = match.end(0)
3413
+ end
3414
+ offsets.freeze
3415
+ end
3416
+
3417
+ # Is one of the ascending `offsets` in `[start, stop)`?
3418
+ # The `[start, end)` byte span of every block and lambda in `root`.
3419
+ def block_spans(root)
3420
+ spans = []
3421
+ return spans if root.nil?
3422
+
3423
+ Source::NodeWalker.each(root) do |node|
3424
+ next unless node.is_a?(Prism::BlockNode) || node.is_a?(Prism::LambdaNode)
3425
+
3426
+ location = node.location
3427
+ spans << [location.start_offset, location.end_offset]
3428
+ end
3429
+ spans
3430
+ end
3431
+
3432
+ def offset_in?(offsets, start, stop)
3433
+ index = offsets.bsearch_index { |offset| offset >= start }
3434
+ !index.nil? && offsets[index] < stop
3435
+ end
3436
+
3437
+ # A6 — the last segments of the constants `root` binds by `||=` (`K ||= String`, `A::K ||= …`) or by a
3438
+ # `const_set` whose name is a literal (`Object.const_set(:K, String)`), and (issue #1749) by any write in a
3439
+ # `class << …` body: the project-wide constant census files a write there under no name a `refine` argument
3440
+ # spells, but a `def` in that body reads it lexically (`class << self; K = String; def setup = refine(K) {…}`).
3441
+ # Scanned only for a file with a literal-target `refine`.
3442
+ def loose_constant_segments(root)
3443
+ segments = Set.new
3444
+ Source::NodeWalker.each(root) do |node|
3445
+ case node
3446
+ when Prism::ConstantOrWriteNode then segments << node.name.to_s
3447
+ when Prism::ConstantPathOrWriteNode then segments << node.target.name.to_s
3448
+ when Prism::SingletonClassNode then singleton_constant_segments(node.body, segments)
3449
+ when Prism::CallNode
3450
+ name = node.name == :const_set && node.arguments&.arguments&.first
3451
+ segments << name.unescaped if name.is_a?(Prism::SymbolNode) || name.is_a?(Prism::StringNode)
3452
+ end
3453
+ end
3454
+ segments
3455
+ end
3456
+
3457
+ # The last segment of every constant a write, an operator write or a multiple assignment target in `body` binds.
3458
+ def singleton_constant_segments(body, segments)
3459
+ return if body.nil?
3460
+
3461
+ Source::NodeWalker.each(body) do |node|
3462
+ case node
3463
+ when Prism::ConstantWriteNode, Prism::ConstantTargetNode, Prism::ConstantOperatorWriteNode,
3464
+ Prism::ConstantAndWriteNode, Prism::ConstantPathTargetNode
3465
+ segments << node.name.to_s
3466
+ when Prism::ConstantPathWriteNode, Prism::ConstantPathOperatorWriteNode, Prism::ConstantPathAndWriteNode
3467
+ segments << node.target.name.to_s
3468
+ end
3469
+ end
3470
+ end
3471
+
3472
+ # ADR-121 WD7 — the census gate's view of one parsed file (`root`): `[refinement table, [[start offset,
3473
+ # outcome], …]]`, one pair for every `refine`-shaped node the method walk accounted for. The outcomes are
3474
+ # `:recorded` (a literal target and block, its names recorded; also one in a module's own singleton method,
3475
+ # #1749), `:class_unknown` (a block whose target the walk cannot name), `:targets_wildcard` (a `refine` that
3476
+ # runs once a method is called, other than a module's own singleton method's, or in a block that may rebind
3477
+ # `self`, charged to its module or the wildcard), `:literal` (A3's `:refine` / eval-string literal), `:nested` (a
3478
+ # `refine` in a refine body, or in a singleton method of one), `:dsl` (where `self` is a class, #1689) and
3479
+ # `:raises` (no block, a Proc as the block, another receiver, or a computed target where `self` is `main`), and
3480
+ # `:data` (a `:refine` literal where `self` is a plain class or one of its instances).
3481
+ def refine_census(root)
3482
+ census = []
3483
+ refinements = build_methods_and_def_nodes(root, census: census)[3]
3484
+ [refinements, census]
3485
+ end
3486
+
3487
+ # ADR-121 WD7 (issue #1814) — the refinement table one parsed file contributes: the census of
3488
+ # {#build_methods_and_def_nodes}, whose fourth table is the project walk's own (`seed_discovered_methods` and
3489
+ # `walk_def_contributions` take it from the same call), run for a caller that wants nothing else. The gem source
3490
+ # walker reads a gem file this way, so a gem module's rows carry the wildcards a project module's would
3491
+ # (`walker_walk_census_parity_spec.rb`, beside the walker's specs, holds the two to one table). A file whose text
3492
+ # never names `refine` as a word records no row, so it is not walked. `source_path` names an anonymous module's
3493
+ # row (`#<Module:<path>:L:C>`) as the project walk does, so two files' bare `Module.new` blocks never share one.
3494
+ def refinement_table(root, source_path = nil)
3495
+ return EMPTY_REFINEMENTS if refine_word_offsets(root).empty?
3496
+
3497
+ build_methods_and_def_nodes(root, source_path)[3]
3498
+ end
3299
3499
 
3300
3500
  # Issue #1120 — a refinement table, deep-frozen; the shared empty table when nothing was recorded.
3301
3501
  def freeze_refinements(table)
@@ -3319,8 +3519,7 @@ module Rigor
3319
3519
  # implicit- or `self`-receiver `refine` with one constant argument and a literal block. A computed target
3320
3520
  # (`refine(klass) { … }`) names no class this walk can key, so its body walks as it did before.
3321
3521
  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)
3522
+ return nil unless refine_call?(node)
3324
3523
 
3325
3524
  arguments = node.arguments&.arguments
3326
3525
  return nil unless arguments&.size == 1
@@ -3329,27 +3528,239 @@ module Rigor
3329
3528
  target if target.is_a?(Prism::ConstantReadNode) || target.is_a?(Prism::ConstantPathNode)
3330
3529
  end
3331
3530
 
3531
+ # An implicit- or `self`-receiver `refine` call with a literal block, whatever its argument.
3532
+ def refine_call?(node)
3533
+ node.name == :refine && node.block.is_a?(Prism::BlockNode) &&
3534
+ (node.receiver.nil? || node.receiver.is_a?(Prism::SelfNode))
3535
+ end
3536
+
3537
+ # Issue #1689 — {#refine_target}, for a call that `in_effect` (its file's {InEffectRefinements}) does not place
3538
+ # where `self` is a class. `Class` undefines `refine`, so `refine X do … end` directly in a `class` body is the
3539
+ # class's own method (a DSL), and its block walks as any other block. Every walk that treats a refine body
3540
+ # specially asks this, so the method tables, the in-effect refinements and the typer agree on which calls are
3541
+ # refine bodies.
3542
+ def module_refine_target(node, in_effect)
3543
+ target = refine_target(node)
3544
+ target unless target.nil? || in_effect.class_body_refine?(node)
3545
+ end
3546
+
3332
3547
  # 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
3548
+ # the refining module (`refining`, the `self` the `refine` call runs on). X is resolved LEXICALLY
3334
3549
  # (`qualified_prefix`), which is where Ruby resolves the argument even inside a `Module.new { … }` block
3335
3550
  # whose `self` is anonymous; every name it can denote is recorded, as {#constant_receiver_candidates}
3336
3551
  # explains. Nothing in the body reaches the class tables: `refine String do def shout` does not give
3337
3552
  # `String` or the refining module a `shout` outside a `using`.
3338
- def record_refinement_defs(node, target, qualified_prefix, owner_prefix, tables)
3553
+ #
3554
+ # ADR-121 WD7 (issue #1799) — the names are what {RefineCensus.read_body} reads (`def`, `alias`,
3555
+ # `alias_method`, `define_method`, `attr_*`, `undef`), and a body that may define a name it cannot spell adds a
3556
+ # names-wildcard row per target. `refining` is `""` at the top level (where Ruby raises). The body is not
3557
+ # walked any further, so the census scans it here.
3558
+ #
3559
+ # A6 — a target this file binds by `||=` or a literal `const_set` (`K ||= String; refine(K)`) names whatever value
3560
+ # the write holds, so its names go under the wildcard class, as a computed target's do.
3561
+ def record_refinement_defs(node, target, qualified_prefix, refining, tables)
3562
+ loose = tables.loose_constant?(target.name.to_s)
3563
+ tables.account(node, loose ? :class_unknown : :recorded, census_charge(refining))
3339
3564
  body = node.block.body
3340
3565
  return if body.nil?
3341
3566
 
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)
3567
+ targets = loose ? [REFINEMENT_WILDCARD] : constant_receiver_candidates(target, qualified_prefix)
3568
+ reading = RefineCensus.read_body(body)
3569
+ refining.each do |owner|
3570
+ tables.refinements = RefineCensus.record_rows(tables.refinements, targets, reading, owner)
3571
+ end
3572
+ census_scan(body, qualified_prefix, tables, refine_body: true) if tables.mentions_refine?
3573
+ end
3574
+
3575
+ # What the census records as charged for `owners`: the one module, or every name.
3576
+ def census_charge(owners) = owners.size == 1 ? owners.first : owners
3577
+
3578
+ # ADR-121 WD7 — a literal-target `refine X do … end` the module-body walk reaches, charged where the in-effect
3579
+ # walk says it runs ({MethodTables#refine_context}): rows for the module it names (the walk's own name for an
3580
+ # anonymous `Module.new` block, `""` at the top level), or a targets-wildcard for that module (the wildcard
3581
+ # where it cannot be named) when it runs only once a method is called or on a module the walk cannot name.
3582
+ def record_literal_target_refine(node, target, qualified_prefix, owner_prefix, tables)
3583
+ context = tables.refine_context(node)
3584
+ here = context.here
3585
+ return record_refinement_defs(node, target, qualified_prefix, [""], tables) if here == RefineSelf::MAIN
3586
+ if context.deferred || here == REFINEMENT_WILDCARD
3587
+ return record_deferred_refine(node, RefineSelf.charged_modules(context), qualified_prefix, tables)
3588
+ end
3589
+
3590
+ record_refinement_defs(node, target, qualified_prefix, census_owners(context, owner_prefix), tables)
3591
+ end
3592
+
3593
+ # The modules a `refine` charged to `context` refines for; an anonymous `Module.new` block's is named by its
3594
+ # position (`owner_prefix`).
3595
+ def census_owners(context, owner_prefix)
3596
+ context.here == RefineSelf::ANONYMOUS ? [owner_prefix.join("::")] : RefineSelf.charged_modules(context)
3597
+ end
3598
+
3599
+ # ADR-121 WD7 — a targets-wildcard for each of `owners`, as a `refine` that runs once a method is called records
3600
+ # (an instance `def`, a `define_method` or `define_singleton_method` block; a module's own singleton method
3601
+ # records rows instead, #1749). The body is not walked any further, so the census scans it here.
3602
+ def record_deferred_refine(node, owners, qualified_prefix, tables)
3603
+ tables.account(node, :targets_wildcard, census_charge(owners))
3604
+ owners.each { |owner| record_targets_wildcard(tables, owner) }
3605
+ body = node.block.body
3606
+ census_scan(body, qualified_prefix, tables, refine_body: true) if body && tables.mentions_refine?
3607
+ end
3608
+
3609
+ # ADR-121 WD7 — a `refine`-shaped call the canonical arm ({#refine_target}) does not take: on another receiver
3610
+ # (`Module#refine` is private, so it raises), where `self` is a class (#1689's DSL), with no literal block (no
3611
+ # block raises, and so does a Proc passed as one), or with a target the walk cannot name (`refine(k)`,
3612
+ # `refine(self)`, `refine(K.dup)`). The last records the body's names under the wildcard class, so they decline
3613
+ # on every receiver, and a names-wildcard row when they are incomplete, which makes the module opaque; one that
3614
+ # runs once a method is called, or on a module the walk cannot name, records a targets-wildcard. At the top
3615
+ # level `self` is `main`, which has no `refine`.
3616
+ def account_other_refine(node, owner_prefix, tables)
3617
+ context = tables.refine_context(node)
3618
+ outcome, charged =
3619
+ if tables.class_body_refine?(node) || context.here == RefineSelf::CLASS then :dsl
3620
+ elsif !RefineCensus.self_call?(node) || !node.block.is_a?(Prism::BlockNode) ||
3621
+ context.here == RefineSelf::MAIN
3622
+ :raises
3623
+ elsif context.deferred || context.here == REFINEMENT_WILDCARD
3624
+ owners = RefineSelf.charged_modules(context)
3625
+ owners.each { |owner| record_targets_wildcard(tables, owner) }
3626
+ [:targets_wildcard, census_charge(owners)]
3627
+ else
3628
+ owners = census_owners(context, owner_prefix)
3629
+ owners.each { |owner| record_unreadable_target(node, owner, tables) }
3630
+ [:class_unknown, census_charge(owners)]
3631
+ end
3632
+ tables.account(node, outcome, charged)
3633
+ end
3634
+
3635
+ def record_unreadable_target(node, owner, tables)
3636
+ tables.refinements = RefineCensus.record_rows(tables.refinements, [REFINEMENT_WILDCARD],
3637
+ RefineCensus.read_body(node.block.body), owner)
3638
+ end
3639
+
3640
+ # `{"*" => {"*" => [owner]}}`: some `refine` of `owner` (every module, for the wildcard) is unreadable.
3641
+ def record_targets_wildcard(tables, owner)
3642
+ tables.refinements = RefineCensus.add_row(tables.refinements || {}, REFINEMENT_WILDCARD, REFINEMENT_WILDCARD,
3643
+ owner)
3644
+ end
3645
+
3646
+ # ADR-121 WD7 — the census over a subtree the method walk does not descend (a `def` body, a refine body), each
3647
+ # node charged where the in-effect walk says it runs. `refine_body` says `self` is the refinement itself, where a
3648
+ # nested `refine` refines for the refinement and only makes the body's names incomplete (§2.2).
3649
+ def census_scan(node, qualified_prefix, tables, refine_body: false)
3650
+ case node
3651
+ when Prism::DefNode, Prism::ClassNode, Prism::ModuleNode, Prism::SingletonClassNode
3652
+ refine_body = false
3653
+ when Prism::CallNode
3654
+ if (target = own_method_refine_target(node, tables))
3655
+ return record_own_method_refine(node, target, qualified_prefix, tables)
3656
+ end
3657
+
3658
+ census_call(node, qualified_prefix, tables, refine_body)
3659
+ when Prism::SymbolNode
3660
+ census_symbol(node, tables)
3661
+ end
3662
+ node.rigor_each_child { |child| census_scan(child, qualified_prefix, tables, refine_body: refine_body) }
3663
+ end
3664
+
3665
+ # ADR-121 WD7 (issue #1749) — the target of a canonical `refine X do … end` ({#refine_target}) in a module's own
3666
+ # singleton method (`def self.x`, a `def` in the module's `class << self`, or a block there that keeps `self`),
3667
+ # else nil. `self` is the module itself there, so the call refines X for it once the method is called. Anything
3668
+ # else in a method body (an instance `def`, a `define_method` or `define_singleton_method` block, a computed
3669
+ # target, a block that may rebind `self`) charges a targets-wildcard in {#census_call}, and a `refine` in a refine
3670
+ # body is nested there, whichever method it sits in: its context ({RefineSelf.refinement_body}) is not an
3671
+ # own method's.
3672
+ def own_method_refine_target(node, tables)
3673
+ return nil if node.name != :refine
3674
+
3675
+ target = tables.refine_target_of(node)
3676
+ target if target && tables.refine_context(node).own_method
3677
+ end
3678
+
3679
+ # Records `node` as the refine of its module, as a `refine` in the module body is recorded, with the refined
3680
+ # class marked as that one is ({#record_refinement}). The call's block is scanned here, so {#census_scan} does
3681
+ # not descend into it.
3682
+ def record_own_method_refine(node, target, qualified_prefix, tables)
3683
+ record_refinement(node, qualified_prefix, tables)
3684
+ owners = RefineSelf.charged_modules(tables.refine_context(node))
3685
+ record_refinement_defs(node, target, qualified_prefix, owners, tables)
3686
+ end
3687
+
3688
+ def census_call(node, qualified_prefix, tables, refine_body)
3689
+ census_literal_arguments(node, qualified_prefix, tables)
3690
+ return unless node.name == :refine
3691
+
3692
+ context = tables.refine_context(node)
3693
+ nested = refine_body || context.here == RefineSelf::REFINEMENT
3694
+ outcome, charged =
3695
+ if tables.class_body_refine?(node) || context.here == RefineSelf::CLASS then :dsl
3696
+ elsif nested && RefineCensus.self_call?(node) then :nested
3697
+ elsif !RefineCensus.self_call?(node) || node.block.nil? || context.here == RefineSelf::MAIN then :raises
3698
+ else
3699
+ owners = RefineSelf.charged_modules(context)
3700
+ owners.each { |owner| record_targets_wildcard(tables, owner) }
3701
+ [:targets_wildcard, census_charge(owners)]
3349
3702
  end
3703
+ tables.account(node, outcome, charged)
3704
+ end
3705
+
3706
+ # A3 — a `:refine` Symbol argument of any call, or a String argument holding `refine` of an eval, `send` or
3707
+ # method-naming call, records a targets-wildcard for the module the call runs `refine` on: the receiver of a
3708
+ # `send` or `*_eval` (each name a constant one can denote), else the module the in-effect walk says `self` is
3709
+ # there. Where `self` is a plain class or one of its instances, a literal charged to `self` is data.
3710
+ def census_literal_arguments(node, qualified_prefix, tables)
3711
+ arguments = node.arguments&.arguments
3712
+ return if arguments.nil?
3713
+
3714
+ strings = RefineCensus.string_literal_call?(node)
3715
+ arguments.each do |argument|
3716
+ next unless RefineCensus.refine_symbol?(argument) || (strings && RefineCensus.refine_string?(argument))
3717
+ next if tables.literal_seen?(argument)
3718
+
3719
+ tables.see_literal(argument)
3720
+ census_literal_argument(node, argument, qualified_prefix, tables)
3721
+ end
3722
+ end
3723
+
3724
+ def census_literal_argument(call, literal, qualified_prefix, tables)
3725
+ target = RefineCensus.literal_target(call)
3726
+ context = tables.refine_context(literal)
3727
+ return tables.account(literal, :data) if target == :self && context.literal == RefineSelf::CLASS
3728
+ return tables.account(literal, :nested) if target == :self && context.literal == RefineSelf::REFINEMENT
3729
+
3730
+ owners = literal_owners(call, target, RefineSelf.charged_modules(context, literal: true), qualified_prefix)
3731
+ owners.each { |owner| record_targets_wildcard(tables, owner) }
3732
+ tables.account(literal, :literal, target == :self ? census_charge(owners) : [:receiver, owners])
3733
+ end
3734
+
3735
+ def literal_owners(call, target, self_modules, qualified_prefix)
3736
+ case target
3737
+ when :self then self_modules
3738
+ when :unknown then [REFINEMENT_WILDCARD]
3739
+ else
3740
+ receiver = call.receiver
3741
+ names = constant_shaped?(receiver) ? constant_receiver_candidates(receiver, qualified_prefix) : []
3742
+ names.empty? ? [REFINEMENT_WILDCARD] : names
3350
3743
  end
3351
3744
  end
3352
3745
 
3746
+ def constant_shaped?(node) = node.is_a?(Prism::ConstantReadNode) || node.is_a?(Prism::ConstantPathNode)
3747
+
3748
+ # A3 — a `:refine` Symbol anywhere else (`alias my_refine refine`, `%i[refine]`, a hash) names `Module#refine`
3749
+ # for code the walk cannot follow, run on the module the in-effect walk says `self` is there; where `self` is a
3750
+ # plain class or one of its instances it is data.
3751
+ def census_symbol(node, tables)
3752
+ return unless RefineCensus.refine_symbol?(node) && !tables.literal_seen?(node)
3753
+
3754
+ tables.see_literal(node)
3755
+ context = tables.refine_context(node)
3756
+ return tables.account(node, :data) if context.literal == RefineSelf::CLASS
3757
+ return tables.account(node, :nested) if context.literal == RefineSelf::REFINEMENT
3758
+
3759
+ owners = RefineSelf.charged_modules(context, literal: true)
3760
+ owners.each { |owner| record_targets_wildcard(tables, owner) }
3761
+ tables.account(node, :literal, census_charge(owners))
3762
+ end
3763
+
3353
3764
  # The instance `def`s a refine body defines on the refined class: nested declarations open their own
3354
3765
  # scope, and a `def self.x` or a `def` inside another `def` defines nothing on it.
3355
3766
  def each_refinement_def(node, &)
@@ -3503,6 +3914,9 @@ module Rigor
3503
3914
  # `self::` header under a REBOUND self (eval/meta-new body) names
3504
3915
  # `owner::Name` instead.
3505
3916
  child_cref = unnameable_decl?(node, self_decl, singleton_cref)
3917
+ if node.is_a?(Prism::ClassNode) && node.superclass && methods_acc.mentions_refine?
3918
+ census_scan(node.superclass, qualified_prefix, methods_acc)
3919
+ end
3506
3920
  record_declaration_facts(node, child_prefix, methods_acc.at(node)) unless child_cref
3507
3921
  body_prefix = child_cref ? [] : child_prefix
3508
3922
  if node.body
@@ -3519,6 +3933,10 @@ module Rigor
3519
3933
  # still decline. The lexical cref below is unnameable in every case.
3520
3934
  singleton_prefix = singleton_body_prefix(node, in_singleton_class, owner_prefix,
3521
3935
  qualified_prefix)
3936
+ tables = methods_acc.at(node)
3937
+ if !node.expression.is_a?(Prism::SelfNode) || tables.in_block?(node)
3938
+ record_singleton_body_prepends(node, qualified_prefix, tables)
3939
+ end
3522
3940
  walk_methods_and_def_nodes(node.expression, qualified_prefix, in_singleton_class,
3523
3941
  methods_acc, def_nodes_acc, source_path, def_owner_prefix,
3524
3942
  singleton_cref: singleton_cref,
@@ -3536,6 +3954,7 @@ module Rigor
3536
3954
  return
3537
3955
  when Prism::ConstantWriteNode, Prism::ConstantPathWriteNode, Prism::ConstantOrWriteNode,
3538
3956
  Prism::ConstantPathOrWriteNode
3957
+ record_meta_superclass(node, qualified_prefix, def_owner_prefix, methods_acc.at(node))
3539
3958
  if (split = meta_new_block_split(node, qualified_prefix, def_owner_prefix, singleton_cref))
3540
3959
  enclosing, body, body_self = split
3541
3960
  # A meta-new block rebinds only `self` — `Module.nesting` stays lexical — so
@@ -3572,6 +3991,11 @@ module Rigor
3572
3991
  # receiver, a singleton nothing names) or any ownerless prefix under an unnameable
3573
3992
  # cref — is NOT top level; `record_def_node` would file the def under `<toplevel>`
3574
3993
  # where an implicit-self call could find a method Ruby never installed there.
3994
+ location = node.location
3995
+ if methods_acc.mentions_refine? && methods_acc.refine_in?(location.start_offset, location.end_offset)
3996
+ node.rigor_each_child { |child| census_scan(child, qualified_prefix, methods_acc) }
3997
+ end
3998
+ record_foreign_singleton_def(node, qualified_prefix, owner_prefix, methods_acc.at(node))
3575
3999
  unless def_owner_prefix&.empty? || defs_singleton == :unnameable ||
3576
4000
  (singleton_cref && owner_prefix.empty?)
3577
4001
  singleton_def = in_singleton_class || defs_singleton
@@ -3581,13 +4005,17 @@ module Rigor
3581
4005
  end
3582
4006
  return
3583
4007
  when Prism::AliasMethodNode, Prism::UndefNode
4008
+ census_scan(node, qualified_prefix, methods_acc) if methods_acc.mentions_refine?
3584
4009
  unless def_owner_prefix&.empty? || defs_singleton == :unnameable ||
3585
4010
  (singleton_cref && owner_prefix.empty?)
3586
4011
  record_alias_or_undef(node, owner_prefix, in_singleton_class || defs_singleton,
3587
4012
  methods_acc.at(node))
3588
4013
  end
3589
4014
  return
4015
+ when Prism::SymbolNode
4016
+ census_symbol(node, methods_acc) if methods_acc.mentions_refine?
3590
4017
  when Prism::CallNode
4018
+ census_literal_arguments(node, qualified_prefix, methods_acc) if methods_acc.mentions_refine?
3591
4019
  if receiver_eval_call?(node)
3592
4020
  return walk_eval_methods_and_defs(node, qualified_prefix, in_singleton_class, methods_acc,
3593
4021
  def_nodes_acc, source_path, def_owner_prefix,
@@ -3596,11 +4024,16 @@ module Rigor
3596
4024
  end
3597
4025
  anonymous = record_call_node_methods(node, owner_prefix, in_singleton_class, methods_acc.at(node),
3598
4026
  source_path)
4027
+ tables = methods_acc.at(node)
4028
+ record_singleton_prepend(node, qualified_prefix, tables,
4029
+ self_ok: node.name == :prepend && tables.in_block?(node))
3599
4030
  # 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)
4031
+ if (target = methods_acc.refine_target_of(node))
4032
+ return record_literal_target_refine(node, target, qualified_prefix, owner_prefix, methods_acc)
3602
4033
  end
3603
4034
 
4035
+ account_other_refine(node, owner_prefix, methods_acc) if node.name == :refine
4036
+
3604
4037
  if anonymous
3605
4038
  walk_anonymous_meta_block(node, anonymous, qualified_prefix, in_singleton_class, methods_acc,
3606
4039
  def_nodes_acc, source_path, def_owner_prefix,
@@ -3666,8 +4099,24 @@ module Rigor
3666
4099
  end
3667
4100
 
3668
4101
  # The block form of a meta-new constant write: its members, and the module mark `Module.new` earns.
4102
+ # Issue #1820 — `K = Class.new(Parent)`, with or without a block, names a class whose superclass the superclass
4103
+ # table leaves out; it takes {Scope::DiscoveryIndex::ENVELOPE_META_SUPERCLASS_MARK}.
4104
+ def record_meta_superclass(node, qualified_prefix, self_base, tables)
4105
+ rvalue = meta_new_rvalue(node)
4106
+ return unless class_new_call?(rvalue) && !(rvalue.arguments&.arguments || []).empty?
4107
+
4108
+ child_prefix = meta_new_child_prefix(node, qualified_prefix, self_base)
4109
+ return if child_prefix.nil? || child_prefix.empty?
4110
+
4111
+ record_surface_mark(tables, child_prefix.join("::"), Scope::DiscoveryIndex::ENVELOPE_META_SUPERCLASS_MARK)
4112
+ end
4113
+
3669
4114
  def record_meta_new_facts(rvalue, child_prefix, tables)
3670
4115
  record_meta_members(rvalue, child_prefix, tables)
4116
+ # `Module.new(&BODY)`: the body runs on the new module but cannot be read, so its surface is unknown (#1816).
4117
+ if rvalue.block.is_a?(Prism::BlockArgumentNode)
4118
+ record_surface_mark(tables, child_prefix.join("::"), Scope::DiscoveryIndex::ENVELOPE_DYNAMIC_MARK)
4119
+ end
3671
4120
  return unless module_new_call?(rvalue)
3672
4121
 
3673
4122
  record_surface_mark(tables, child_prefix.join("::"), Scope::DiscoveryIndex::ENVELOPE_MODULE_MARK)
@@ -3700,7 +4149,7 @@ module Rigor
3700
4149
  def record_alias_method_call(call_node, qualified_prefix, in_singleton_class, accumulator)
3701
4150
  return if qualified_prefix.empty?
3702
4151
 
3703
- names = alias_method_call_names(call_node)
4152
+ names = Source::AliasNames.alias_method_call_names(call_node)
3704
4153
  return if names.nil?
3705
4154
 
3706
4155
  kind = in_singleton_class ? :singleton : :instance
@@ -3807,7 +4256,9 @@ module Rigor
3807
4256
  # other two. {#meta_new_block_call} owns both unwrappings; every walk asks it, so no spelling can be
3808
4257
  # recognised by one pass and missed by another.
3809
4258
  def meta_new_block_body(node)
3810
- meta_new_block_call(node)&.block&.body
4259
+ block = meta_new_block_call(node)&.block
4260
+ # A `&BODY` / `&:sym` block ARGUMENT is not a literal block: its body is unreadable here (#1816).
4261
+ block.body if block.is_a?(Prism::BlockNode)
3811
4262
  end
3812
4263
 
3813
4264
  # The class-creating call a constant write's rvalue ultimately is, or nil when the write is not the idiom.
@@ -4061,16 +4512,57 @@ module Rigor
4061
4512
  return if def_node.body.nil?
4062
4513
 
4063
4514
  Source::NodeWalker.each(def_node.body) do |node|
4515
+ if node.is_a?(Prism::SingletonClassNode)
4516
+ record_singleton_body_prepends(node, qualified_prefix, tables)
4517
+ next
4518
+ end
4064
4519
  next unless node.is_a?(Prism::CallNode)
4065
4520
 
4066
4521
  if node.name == :extend
4067
4522
  record_object_extension(node, qualified_prefix, tables)
4068
4523
  elsif SURFACE_EVAL_CALLS.include?(node.name) || SURFACE_NAMING_CALLS.include?(node.name)
4069
4524
  record_body_rewrite(node, qualified_prefix, tables)
4525
+ else
4526
+ record_singleton_prepend(node, qualified_prefix, tables, self_ok: true)
4070
4527
  end
4071
4528
  end
4072
4529
  end
4073
4530
 
4531
+ # Issue #1820 — `obj.singleton_class.prepend(M)`, or `prepend M` in a `class << obj` body, puts `M` ahead of
4532
+ # `obj`'s class even when the class already includes it (`prepend` re-inserts it; `extend` would not), so
4533
+ # `M`'s refinement answers first for that object. `M` takes {Scope::DiscoveryIndex::ENVELOPE_OBJECT_EXTENDED_MARK},
4534
+ # the mark an `extend` in a method body already leaves: an object of any class may carry its instance methods
4535
+ # ahead of its class's own. A `singleton_class` of `self` (and a `class << self` body, which the caller leaves
4536
+ # out) written directly in a class or module body or at the top level is the class object's or `main`'s, not an
4537
+ # instance's; in a method body or any block (`self_ok`), where `self` may be an object, it counts.
4538
+ # `singleton_body` says the call sits in a `class << obj` body.
4539
+ def record_singleton_prepend(node, qualified_prefix, tables, singleton_body: false, self_ok: false)
4540
+ return unless node.name == :prepend
4541
+
4542
+ receiver = node.receiver
4543
+ singleton = singleton_body && (receiver.nil? || receiver.is_a?(Prism::SelfNode))
4544
+ singleton ||= singleton_class_call?(receiver, self_ok)
4545
+ record_object_extension(node, qualified_prefix, tables) if singleton
4546
+ end
4547
+
4548
+ def singleton_class_call?(receiver, self_ok)
4549
+ return false unless receiver.is_a?(Prism::CallNode) && receiver.name == :singleton_class
4550
+ return false unless receiver.arguments.nil?
4551
+
4552
+ object = receiver.receiver
4553
+ self_ok || !(object.nil? || object.is_a?(Prism::SelfNode))
4554
+ end
4555
+
4556
+ def record_singleton_body_prepends(node, qualified_prefix, tables)
4557
+ return if node.body.nil?
4558
+
4559
+ Source::NodeWalker.each(node.body) do |inner|
4560
+ next unless inner.is_a?(Prism::CallNode)
4561
+
4562
+ record_singleton_prepend(inner, qualified_prefix, tables, singleton_body: true, self_ok: true)
4563
+ end
4564
+ end
4565
+
4074
4566
  def record_body_rewrite(node, qualified_prefix, tables)
4075
4567
  receiver = node.receiver
4076
4568
  if receiver.is_a?(Prism::ConstantReadNode) || receiver.is_a?(Prism::ConstantPathNode)
@@ -4243,6 +4735,50 @@ module Rigor
4243
4735
  acc[:deferred_ranges][path] = build_deferred_ranges(root)
4244
4736
  record_class_sources(acc[:class_sources], path, root, superclasses, includes, file_def_nodes,
4245
4737
  acc[:compact_headers])
4738
+ record_singleton_class_sources(acc[:singleton_class_sources], path, root)
4739
+ end
4740
+
4741
+ # `{name => Set[path]}` for a bundle's name list (a bundle written before the list existed has none).
4742
+ def bundle_path_sets(names, path) = (names || []).to_h { |name| [name, Set[path]] }
4743
+
4744
+ # Issue #1752 — the constants a `class << Const` body opens in this file, and those whose singleton side a call
4745
+ # there may rewrite ({#singleton_side_call?}). Such a body records facts about
4746
+ # `Const`'s singleton class (a `remove_method`, a `def`) without declaring `Const`, so it stays out of
4747
+ # `class_sources` (whose readers take a name there as one the project declares) and joins only the per-file
4748
+ # class-declaration sets the incremental `class:` edges diff (`Runner#class_declarations`).
4749
+ def record_singleton_class_sources(sources, path, root)
4750
+ collect_singleton_class_targets(root, []) { |name| (sources[name] ||= Set.new) << path }
4751
+ end
4752
+
4753
+ def collect_singleton_class_targets(node, lexical_prefix, &)
4754
+ case node
4755
+ when Prism::ClassNode, Prism::ModuleNode
4756
+ prefix = Source::ConstantPath.declaration_prefix(lexical_prefix, node.constant_path)
4757
+ node.body && prefix && collect_singleton_class_targets(node.body, prefix, &)
4758
+ return
4759
+ when Prism::SingletonClassNode
4760
+ rendered = singleton_receiver_constant_name(node.expression)
4761
+ yield eval_constant_receiver_prefix(node, node.expression, rendered, lexical_prefix).join("::") if rendered
4762
+ when Prism::CallNode
4763
+ yield_singleton_side_constant(node, node.receiver, lexical_prefix, &) if singleton_side_call?(node)
4764
+ end
4765
+ node.rigor_each_child { |child| collect_singleton_class_targets(child, lexical_prefix, &) }
4766
+ end
4767
+
4768
+ # A call on a constant that may rewrite its singleton side (`Const.singleton_class`,
4769
+ # `Const.define_singleton_method`, `Const.extend(M)`, `Const.instance_eval { … }`) records facts about `Const`'s
4770
+ # singleton methods the refinement walk reads from a file that need not declare `Const`, so the file joins the
4771
+ # constant's set too. Written inside `Const`'s own body, the name is in the file's set already; a `def Const.x`
4772
+ # files its own `method:` key.
4773
+ def singleton_side_call?(node)
4774
+ node.name == :singleton_class || surface_rewriting_call?(node)
4775
+ end
4776
+
4777
+ def yield_singleton_side_constant(node, receiver, lexical_prefix)
4778
+ return unless receiver.is_a?(Prism::ConstantReadNode) || receiver.is_a?(Prism::ConstantPathNode)
4779
+
4780
+ rendered = singleton_receiver_constant_name(receiver)
4781
+ yield eval_constant_receiver_prefix(node, receiver, rendered, lexical_prefix).join("::") if rendered
4246
4782
  end
4247
4783
 
4248
4784
  def build_deferred_ranges(root)
@@ -5875,6 +6411,7 @@ module Rigor
5875
6411
  def mixin_tables(root)
5876
6412
  accumulator = MixinAccumulator.new
5877
6413
  accumulator.direct_body(program_statements(root))
6414
+ accumulator.program_body(program_statements(root))
5878
6415
  walk_class_includes(root, [], nil, accumulator)
5879
6416
  {
5880
6417
  includes: freeze_mixin_lists(accumulator, :include),
@@ -6029,7 +6566,7 @@ module Rigor
6029
6566
  walk_class_includes(arg, qualified_prefix, current_class, accumulator,
6030
6567
  singleton_self: singleton_self, singleton_cref: singleton_cref)
6031
6568
  end
6032
- if (body = call.block&.body)
6569
+ if (body = meta_new_block_body(node))
6033
6570
  walk_class_includes(body, qualified_prefix, meta_owner, accumulator,
6034
6571
  singleton_cref: body_cref)
6035
6572
  end
@@ -6083,6 +6620,7 @@ module Rigor
6083
6620
  kind, _arguments, via_send = mixin_call_view(node)
6084
6621
  return if kind.nil?
6085
6622
 
6623
+ current_class ||= toplevel_include_owner(node, qualified_prefix, current_class, accumulator)
6086
6624
  effects = mixin_effects(node, kind, qualified_prefix, current_class, accumulator, in_singleton: false)
6087
6625
  .select { |_owner, side| side == :include }
6088
6626
  return if effects.empty?
@@ -6263,6 +6801,25 @@ module Rigor
6263
6801
  node.name == :prepend
6264
6802
  end
6265
6803
 
6804
+ # Issue #1697 — a receiverless `include M` written as a statement of the file's own top level is
6805
+ # `main.include`, which is `Object.include`: it mixes M into `Object`. It is recorded under its own
6806
+ # owner key, `ObjectMixins::TOPLEVEL_INCLUDE_KEY`, rather than on `Object`, so it rides every table an include
6807
+ # rides (the declaration signature, `discovered_class_sources`, the seed bundle) while no ancestry
6808
+ # walk that types a call reaches it: what Ruby reaches first for a name such an include supplies
6809
+ # depends on more than the chain shows (a block that rebinds `self`, an `extend` or a singleton `def`
6810
+ # on `main`), so only the diagnostic silences read it (`Inference::ObjectMixins`), and typing is
6811
+ # issue #1715's. Only a direct program statement counts.
6812
+ # The same call inside a top-level block, a method body or a conditional stays unrecorded, as before:
6813
+ # its `self` may not be `main` (a `describe do include M end` or `class_exec` block rebinds it, while
6814
+ # `[1].each do include M end` does not, and the walk cannot tell them apart), and it may not run. `main`
6815
+ # has no `prepend`, and a top-level `extend M` reaches `main`'s singleton alone, so neither is recorded.
6816
+ def toplevel_include_owner(node, qualified_prefix, current_class, accumulator)
6817
+ return nil unless current_class.nil? && qualified_prefix.empty?
6818
+ return nil unless node.receiver.nil? && node.name == :include
6819
+
6820
+ accumulator.program_statement?(node) ? ObjectMixins::TOPLEVEL_INCLUDE_KEY : nil
6821
+ end
6822
+
6266
6823
  # Issue #1123 — the class a `Recv.prepend(M)` call form targets, as the qualified name the prepend
6267
6824
  # table is keyed by, or nil when the receiver names no static class. The receiver is a constant READ
6268
6825
  # at the call site, so it resolves in the `Module.nesting` the call is written in — the same rule (and
@@ -6581,7 +7138,7 @@ module Rigor
6581
7138
  in_singleton: in_singleton, singleton_self: singleton_self,
6582
7139
  singleton_cref: singleton_cref)
6583
7140
  end
6584
- if (body = call.block&.body)
7141
+ if (body = meta_new_block_body(node))
6585
7142
  walk_class_extends(body, qualified_prefix, meta_owner, accumulator,
6586
7143
  singleton_cref: body_cref)
6587
7144
  end
@@ -6653,6 +7210,7 @@ module Rigor
6653
7210
  return record_module_function(node, current_class, accumulator, in_singleton) if node.name == :module_function
6654
7211
 
6655
7212
  taint_opaque_eval(node, current_class, accumulator)
7213
+ return if singleton_class_rewrite_tainted?(node, qualified_prefix, current_class, accumulator)
6656
7214
 
6657
7215
  kind, _arguments, via_send = mixin_call_view(node)
6658
7216
  return if kind.nil?
@@ -6665,6 +7223,49 @@ module Rigor
6665
7223
  effects.each { |owner, side| accumulator.taint(owner, side) }
6666
7224
  end
6667
7225
 
7226
+ # Issue #1752 — a call that rewrites a singleton class reached through `singleton_class` (`singleton_class.
7227
+ # class_eval { def x }`, `singleton_class.send(:define_method, :x)`, `Const.singleton_class.prepend(M)`,
7228
+ # `Const.singleton_class.send(:include, M)`) puts methods or modules on that singleton class that no edge
7229
+ # records, so its singleton side is unpositioned: the owner is the enclosing class for a bare or `self.`
7230
+ # receiver, else the constant. True when it tainted.
7231
+ def singleton_class_rewrite_tainted?(node, qualified_prefix, current_class, accumulator)
7232
+ receiver = node.receiver
7233
+ return false unless singleton_class_of_receiver?(receiver) && surface_rewriting_call?(node)
7234
+ return false if singleton_class_named_call?(node)
7235
+
7236
+ owner = singleton_class_owner(node, receiver.receiver, qualified_prefix, current_class)
7237
+ return false if owner.nil?
7238
+
7239
+ accumulator.taint(owner, :extend)
7240
+ true
7241
+ end
7242
+
7243
+ # A bare, `self.` or constant receiver's `singleton_class` call.
7244
+ def singleton_class_of_receiver?(receiver)
7245
+ receiver.is_a?(Prism::CallNode) && receiver.name == :singleton_class && receiver.arguments.nil?
7246
+ end
7247
+
7248
+ # An `attr_*` on a singleton class (`singleton_class.attr_accessor :conf`) rewrites only the names it spells, so
7249
+ # it makes those names' singleton envelopes opaque ({#record_singleton_class_names}) and leaves the rest of the
7250
+ # singleton side readable. An `attr_*` with a name it does not spell as a literal (`attr_accessor(*KEYS)`, a
7251
+ # block's variable), and an `alias_method` or `define_method` there, still taint the side: the own-definer read
7252
+ # would otherwise answer the body Ruby replaced (`definer_resolution_witness_spec.rb`).
7253
+ def singleton_class_named_call?(node)
7254
+ arguments = node.arguments&.arguments || []
7255
+ ATTR_MACROS.include?(node.name) && !arguments.empty? &&
7256
+ arguments.all? { |argument| literal_method_name(argument) }
7257
+ end
7258
+
7259
+ def singleton_class_owner(node, object, qualified_prefix, current_class)
7260
+ return current_class if object.nil? || object.is_a?(Prism::SelfNode)
7261
+
7262
+ rendered = Source::ConstantPath.qualified_name_or_nil(object)
7263
+ return nil if rendered.nil?
7264
+ return rendered if Source::ConstantPath.rooted?(object) || qualified_prefix.empty?
7265
+
7266
+ eval_constant_receiver_prefix(node, object, rendered, qualified_prefix).join("::")
7267
+ end
7268
+
6668
7269
  # The bare `module_function` toggle: the module extends itself. A direct statement only.
6669
7270
  def record_module_function(node, current_class, accumulator, in_singleton)
6670
7271
  return unless current_class && node.receiver.nil? && !in_singleton && ModuleFunctionState.extends_self?(node)
@@ -7149,9 +7750,7 @@ module Rigor
7149
7750
  end
7150
7751
 
7151
7752
  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
7753
+ Source::Literals.symbol_or_string(arg)
7155
7754
  end
7156
7755
 
7157
7756
  # Registers the alias name in the `discovered_methods` table so `undefined-method` diagnostics are not emitted for
@@ -7179,10 +7778,24 @@ module Rigor
7179
7778
  class_name = qualified_prefix.join("::")
7180
7779
  undef_node.names.each do |name|
7181
7780
  method_name = literal_method_name(name)
7182
- record_method_envelope_opaque(tables, class_name, method_name) if method_name
7781
+ next unless method_name
7782
+
7783
+ record_method_envelope_opaque(tables, class_name, method_name)
7784
+ record_removed_name(tables, class_name, method_name)
7183
7785
  end
7184
7786
  end
7185
7787
 
7788
+ REMOVING_CALLS = %i[remove_method undef_method].to_set.freeze
7789
+ private_constant :REMOVING_CALLS
7790
+
7791
+ # Issue #1820 — `undef f`, `remove_method :f` or `undef_method :f` on the class, as its own entry beside the
7792
+ # opaque envelopes: an `attr_*` or `define_method` records an opaque envelope too, so only this says the name
7793
+ # is gone.
7794
+ def record_removed_name(tables, class_name, method_name)
7795
+ record_envelope(tables.envelopes, class_name, [Scope::DiscoveryIndex::ENVELOPE_REMOVED_KIND, method_name],
7796
+ Source::ParameterEnvelope::OPAQUE)
7797
+ end
7798
+
7186
7799
  # Both kinds, and the envelope table only: the evidence says something rewrites the name, not which
7187
7800
  # side it lives on, and it is not evidence that the name EXISTS, so the existence table is untouched.
7188
7801
  def record_method_envelope_opaque(tables, class_name, method_name)
@@ -7198,7 +7811,7 @@ module Rigor
7198
7811
  # `def_delegator :@x, :f`) would leave a `def`'s envelope standing for a method that no longer has it.
7199
7812
  NAME_NEUTRAL_MACROS = %i[
7200
7813
  private public protected module_function private_class_method public_class_method
7201
- private_constant public_constant require require_relative autoload
7814
+ private_constant public_constant require require_relative autoload autoload_relative
7202
7815
  ].to_set.freeze
7203
7816
  private_constant :NAME_NEUTRAL_MACROS
7204
7817
 
@@ -7239,6 +7852,42 @@ module Rigor
7239
7852
  constant_receiver_candidates(receiver, qualified_prefix).each do |name|
7240
7853
  record_surface_mark(tables, name, Scope::DiscoveryIndex::ENVELOPE_DYNAMIC_MARK)
7241
7854
  end
7855
+ elsif singleton_class_of_receiver?(receiver) && singleton_class_named_call?(node)
7856
+ record_singleton_class_names(node, receiver.receiver, qualified_prefix, tables)
7857
+ end
7858
+ end
7859
+
7860
+ # Issue #1752 — the names a {#singleton_class_named_call?} rewrites on its singleton class's owner, each
7861
+ # singleton envelope opaque: the evidence the refinement typer reads as a rewritten singleton name.
7862
+ def record_singleton_class_names(node, object, qualified_prefix, tables)
7863
+ current_class = qualified_prefix.empty? ? nil : qualified_prefix.join("::")
7864
+ owner = singleton_class_owner(node, object, qualified_prefix, current_class)
7865
+ return if owner.nil?
7866
+
7867
+ (node.arguments&.arguments || []).each do |argument|
7868
+ method_name = literal_method_name(argument)
7869
+ next unless method_name
7870
+
7871
+ names = [method_name]
7872
+ names << :"#{method_name}=" if %i[attr_writer attr_accessor].include?(node.name)
7873
+ names.each do |name|
7874
+ record_envelope(tables.envelopes, owner, [:singleton, name], Source::ParameterEnvelope::OPAQUE)
7875
+ contest_envelope(tables, owner, [:singleton, name]) unless tables.certain
7876
+ end
7877
+ end
7878
+ end
7879
+
7880
+ # Issue #1752 — `def Const.x` whose receiver is not the lexical `self` (written at the top level or in another
7881
+ # class's body) defines `Const`'s singleton method, which no table files under `Const`: its singleton name's
7882
+ # envelope turns opaque, the evidence the refinement typer reads as a rewritten singleton name.
7883
+ def record_foreign_singleton_def(node, qualified_prefix, owner_prefix, tables)
7884
+ receiver = node.receiver
7885
+ return unless receiver.is_a?(Prism::ConstantReadNode) || receiver.is_a?(Prism::ConstantPathNode)
7886
+ return if def_receiver_targets_lexical_self?(receiver, owner_prefix)
7887
+
7888
+ constant_receiver_candidates(receiver, qualified_prefix).each do |name|
7889
+ record_envelope(tables.envelopes, name, [:singleton, node.name], Source::ParameterEnvelope::OPAQUE)
7890
+ contest_envelope(tables, name, [:singleton, node.name]) unless tables.certain
7242
7891
  end
7243
7892
  end
7244
7893
 
@@ -7249,13 +7898,21 @@ module Rigor
7249
7898
  if dynamic_surface_call?(node)
7250
7899
  record_surface_mark(tables, class_name, Scope::DiscoveryIndex::ENVELOPE_DYNAMIC_MARK)
7251
7900
  elsif node.name == :refine
7252
- record_refinement(node, qualified_prefix, tables)
7901
+ # Issue #1689 — a `refine` where `self` is a class is the class's own method, and refines nothing.
7902
+ record_refinement(node, qualified_prefix, tables) unless tables.class_body_refine?(node)
7253
7903
  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
7904
+ record_named_arguments_opaque(node, class_name, tables)
7905
+ end
7906
+ end
7907
+
7908
+ def record_named_arguments_opaque(node, class_name, tables)
7909
+ (node.arguments&.arguments || []).each do |argument|
7910
+ # `memoize def f(a)` wraps the def it is handed exactly as `memoize :f` does.
7911
+ method_name = argument.is_a?(Prism::DefNode) ? argument.name : literal_method_name(argument)
7912
+ next unless method_name
7913
+
7914
+ record_method_envelope_opaque(tables, class_name, method_name)
7915
+ record_removed_name(tables, class_name, method_name) if REMOVING_CALLS.include?(node.name)
7259
7916
  end
7260
7917
  end
7261
7918
 
@@ -7414,7 +8071,7 @@ module Rigor
7414
8071
  # `alias_method :new, :old` — the CallNode twin of the `alias` keyword (#533; liquid's i18n
7415
8072
  # `t` alias was the corpus case). Unlike AliasMethodNode a call's children can carry further
7416
8073
  # class bodies (`Class.new do … end`), so the walk continues below it either way.
7417
- names = alias_method_call_names(node)
8074
+ names = Source::AliasNames.alias_method_call_names(node)
7418
8075
  if names && !rec_prefix.empty? && !(singleton_cref && leaf_owner.nil?)
7419
8076
  record_alias_call_entry(accumulator, rec_prefix.join("::"), names, node)
7420
8077
  end
@@ -7475,21 +8132,6 @@ module Rigor
7475
8132
  true
7476
8133
  end
7477
8134
 
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
8135
  # The class/module arm of {#collect_class_alias_map}: under an unnameable cref a
7494
8136
  # bare/`self::` header opens `#<singleton>::Name` — the map files nothing for it;
7495
8137
  # a rooted or explicit-base header re-anchors at a nameable prefix.
@@ -7532,11 +8174,12 @@ module Rigor
7532
8174
 
7533
8175
  def record_alias_map_entry(alias_node, qualified_prefix, accumulator)
7534
8176
  return if qualified_prefix.empty?
7535
- return unless alias_node.new_name.is_a?(Prism::SymbolNode) && alias_node.old_name.is_a?(Prism::SymbolNode)
8177
+
8178
+ names = Source::AliasNames.keyword_names(alias_node)
8179
+ return if names.nil?
7536
8180
 
7537
8181
  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
8182
+ new_name, old_name = names
7540
8183
  (accumulator[class_name] ||= {})[new_name] = old_name
7541
8184
  accumulator.wrote(class_name, new_name, alias_node) if accumulator.is_a?(AliasMap)
7542
8185
  end
@@ -7636,9 +8279,7 @@ module Rigor
7636
8279
  end
7637
8280
 
7638
8281
  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
8282
+ Source::Literals.symbol_or_string(node)
7642
8283
  end
7643
8284
 
7644
8285
  # Walks every file in `paths` (each path is parsed once with `Prism.parse_file`) and returns the unioned
@@ -8293,6 +8934,9 @@ module Rigor
8293
8934
  merge_header_nestings(acc[:header_nestings], file_index[:header_nestings] || {})
8294
8935
  fold_mixin_lists(acc, file_index)
8295
8936
  file_index[:class_sources].each { |cn, files| (acc[:class_sources][cn] ||= Set.new).merge(files) }
8937
+ (file_index[:singleton_class_sources] || {}).each do |cn, files|
8938
+ (acc[:singleton_class_sources][cn] ||= Set.new).merge(files)
8939
+ end
8296
8940
  # Issue #722 residue 2 — a pre-#722 seed bundle carries no candidates; the SCHEMA bump makes such a
8297
8941
  # blob a cold rebuild, but default so any in-flight fold stays total.
8298
8942
  acc[:compact_headers].merge!(file_index[:compact_headers] || {})
@@ -8381,6 +9025,7 @@ module Rigor
8381
9025
  # data, so the bundle stays Marshal-clean and a warm file joins the envelopes its cold walk records.
8382
9026
  parameter_envelopes: file_index[:parameter_envelopes],
8383
9027
  class_source_names: file_index[:class_sources].keys,
9028
+ singleton_class_source_names: (file_index[:singleton_class_sources] || {}).keys,
8384
9029
  # Issue #722 residue 2 — plain data, so the bundle stays Marshal-clean and a warm incremental file
8385
9030
  # re-anchors its compact headers the way a cold walk of it does.
8386
9031
  compact_headers: file_index[:compact_headers],
@@ -8405,7 +9050,7 @@ module Rigor
8405
9050
  # ADR-85 WD2 — reconstitutes a cached bundle into a single-file index {#fold_file_index} folds: the
8406
9051
  # def-node triples become {DefHandle}s bound to this file's `path`, and the class-source names become a
8407
9052
  # `{name => Set[path]}` table (the shape `accumulate_project_index` produces).
8408
- def bundle_to_file_index(bundle, path) # rubocop:disable Metrics/CyclomaticComplexity,Metrics/PerceivedComplexity -- one read per bundle slot
9053
+ def bundle_to_file_index(bundle, path) # rubocop:disable Metrics/AbcSize -- one read per bundle slot
8409
9054
  {
8410
9055
  def_nodes: bundle_defs_to_handles(bundle[:def_nodes], path),
8411
9056
  singleton_def_nodes: bundle_defs_to_handles(bundle[:singleton_def_nodes], path),
@@ -8426,7 +9071,8 @@ module Rigor
8426
9071
  method_visibilities: bundle[:method_visibilities],
8427
9072
  methods: bundle[:methods],
8428
9073
  parameter_envelopes: bundle[:parameter_envelopes] || {},
8429
- class_sources: bundle[:class_source_names].to_h { |name| [name, Set[path]] },
9074
+ class_sources: bundle_path_sets(bundle[:class_source_names], path),
9075
+ singleton_class_sources: bundle_path_sets(bundle[:singleton_class_source_names], path),
8430
9076
  compact_headers: bundle[:compact_headers] || {},
8431
9077
  # Issue #644 — a pre-#644 bundle carries no census; the SCHEMA bump makes such a blob a cold
8432
9078
  # rebuild, but default so any in-flight fold stays total.
@@ -8476,7 +9122,7 @@ module Rigor
8476
9122
  method_visibilities: {},
8477
9123
  methods: {},
8478
9124
  deferred_ranges: {},
8479
- parameter_envelopes: {}, class_sources: {},
9125
+ parameter_envelopes: {}, class_sources: {}, singleton_class_sources: {},
8480
9126
  # Issue #722 residue 2 — compact-header re-anchor candidates, adjudicated in {#finalize_def_index}.
8481
9127
  compact_headers: {},
8482
9128
  constant_writes: {},
@@ -8766,7 +9412,7 @@ module Rigor
8766
9412
 
8767
9413
  def constant_write_census(root)
8768
9414
  tables = CensusTables.new(writes: {}, seen: Set.new, declared: Set.new, aliases: {},
8769
- write_census: GlobalWriteCensus::Collector.new)
9415
+ write_census: GlobalWriteCensus::Collector.new(InEffectRefinements.new(root)))
8770
9416
  walk_constant_write_census(root, [], tables)
8771
9417
  tables
8772
9418
  end
@@ -9699,7 +10345,7 @@ module Rigor
9699
10345
  record_declarations(part, qualified_prefix, identity_table, discovered,
9700
10346
  EMPTY_RENAMES, self_prefix, singleton_cref: singleton_cref)
9701
10347
  end
9702
- if (body = rvalue.block&.body)
10348
+ if (body = rvalue.block.is_a?(Prism::BlockNode) && rvalue.block.body)
9703
10349
  record_declarations(body, qualified_prefix, identity_table, discovered,
9704
10350
  EMPTY_RENAMES, child_prefix, singleton_cref: singleton_cref)
9705
10351
  end
@@ -9934,11 +10580,22 @@ module Rigor
9934
10580
  propagate(node.expression, table, current_scope)
9935
10581
  propagate(node.rescue_expression, table, current_scope.forget_error_info.forget_last_status)
9936
10582
  when Prism::PostExecutionNode then propagate_end_body(node, table, current_scope)
9937
- else
9938
- node.rigor_each_child { |child| propagate(child, table, current_scope) }
10583
+ else propagate_children(node, table, current_scope)
9939
10584
  end
9940
10585
  end
9941
10586
 
10587
+ SELF_OPENING_BODIES = [Prism::DefNode, Prism::ClassNode, Prism::ModuleNode, Prism::SingletonClassNode].freeze
10588
+ private_constant :SELF_OPENING_BODIES
10589
+
10590
+ # Issue #1717 — a `def` / `class` / `module` body opens its own `self`, so an enclosing unentered block's
10591
+ # unknown-`self` mark ({Scope#block_self_unknown?}) stops at it, as the evaluator's fresh body scope does.
10592
+ # Every other node hands its children the scope as is.
10593
+ def propagate_children(node, table, current_scope)
10594
+ child_scope =
10595
+ SELF_OPENING_BODIES.include?(node.class) ? current_scope.with_block_self_unknown(false) : current_scope
10596
+ node.rigor_each_child { |child| propagate(child, table, child_scope) }
10597
+ end
10598
+
9942
10599
  # Issue #1361 — the block of `Thread.new`, `Fiber.new`, `define_method` and the other calls
9943
10600
  # {FreshFrameBlocks.fresh_entry?} names does not read the match-global narrowing of the body it is written in.
9944
10601
  # The evaluator enters it as {FreshFrameBlocks.entry} gives ({MatchRebinding.block_entry}), but a block in a
@@ -9985,12 +10642,117 @@ module Rigor
9985
10642
  def unentered_block_entry(node, block, table, current_scope)
9986
10643
  return current_scope unless block.is_a?(Prism::BlockNode) && !table.key?(block)
9987
10644
 
10645
+ narrowing = unentered_block_narrowing(node, current_scope)
9988
10646
  current_scope = Narrowing.safe_navigation_block_scope(node, current_scope)
9989
10647
  # Issue #1429 — nor a guard's narrowing of a global or constant the body may run after code rebinds.
9990
10648
  current_scope = GuardRebinding.block_entry(current_scope, block, node)
9991
- return FreshFrameBlocks.entry(current_scope, node) if FreshFrameBlocks.fresh_entry?(node, current_scope)
10649
+ entry =
10650
+ if FreshFrameBlocks.fresh_entry?(node, current_scope)
10651
+ FreshFrameBlocks.entry(current_scope, node)
10652
+ else
10653
+ LastLine.block_entry(FreshFrameBlocks.closure_entry(current_scope, block, node), block, node)
10654
+ end
10655
+ anonymous = AnonymousMetaClass.name_for(node, current_scope.source_path)
10656
+ return unentered_meta_class_body_entry(block, entry, anonymous, table) if anonymous
10657
+
10658
+ refined = refined_body_name(node, current_scope)
10659
+ record_class_body_defs(block, entry, table, Type::Combinator.nominal_of(refined), nil) if refined
10660
+
10661
+ # Issue #1717 — the body's `self` is unknown unless narrowed, as on the evaluator's entry; only the
10662
+ # `call.undefined-method` mark is set here, never #316's opaque mark (its bind gate reads the evaluator's).
10663
+ entry = entry.with_block_self_unknown(true) unless narrowing&.self_type
10664
+ return entry unless narrowing
10665
+
10666
+ MacroBlockSelfType.apply(entry, narrowing, keeps_unknown: current_scope.block_self_narrowing_unknown?(node))
10667
+ end
10668
+
10669
+ # Issue #1120 — the class a `refine X do … end` call's block refines, or nil when the call is not that shape
10670
+ # or is a `refine` where `self` is a class (issue #1689: the class's own method, its block an ordinary one).
10671
+ # The evaluator asks the same two questions ({StatementEvaluator#refined_class_context}).
10672
+ def refined_body_name(node, scope)
10673
+ target = refine_target(node)
10674
+ return nil if target.nil? || InEffectRefinements.class_body_refine?(scope, node)
10675
+
10676
+ refined_class_name(target, scope)
10677
+ end
10678
+
10679
+ # Issue #1120 — the name of the class `refine target do … end` refines: the class `target` types as, or the
10680
+ # constant as written when it does not type as a class object (a gem class with no RBS). Shared by
10681
+ # {StatementEvaluator#refined_class_context} and {#refined_body_name}.
10682
+ def refined_class_name(target, scope)
10683
+ refined = scope.type_of(target)
10684
+ name = refined.is_a?(Type::Singleton) ? refined.class_name : Source::ConstantPath.qualified_name(target)
10685
+ name&.delete_prefix("::")
10686
+ end
10687
+
10688
+ # Issue #1696 — the entry of a `Class.new` / `Module.new` / `Struct.new` / `Data.define` block the evaluator
10689
+ # never entered because it sits in a value position: `Object.const_set(:K, Class.new { attr_reader :x })`,
10690
+ # `[Struct.new(:a) { def b = a }]`. Its `self` is the class `name` ({AnonymousMetaClass.name_for}) names, as
10691
+ # {StatementEvaluator#enter_meta_class_body} gives the statement form; left on the enclosing `self`, a
10692
+ # top-level one read every macro in it as `call.unresolved-toplevel`. Its `def`s are recorded as
10693
+ # {#record_class_body_defs} says.
10694
+ def unentered_meta_class_body_entry(block, entry, name, table)
10695
+ entry = entry.with_block_self_type(Type::Combinator.singleton_of(name)).with_singleton_class_body(false)
10696
+ record_class_body_defs(block, entry, table, Type::Combinator.nominal_of(name), entry.self_type)
10697
+ entry
10698
+ end
10699
+
10700
+ # Issue #1696 — records each `def` a value-position class-body block defines ({#each_class_body_def}) with the
10701
+ # `self` its body runs on, since no evaluator pass enters it: `instance_self` for a plain `def`, `class_self`
10702
+ # for a `def self.m` (skipped when nil). A `refine X` block passes only `Nominal[X]`: Ruby runs its defs on an
10703
+ # instance of X, but the block itself on the refinement module, which Rigor does not model, so the block keeps
10704
+ # the unknown `self` an unnarrowed block has. A `def obj.m` is left to the walk.
10705
+ def record_class_body_defs(block, entry, table, instance_self, class_self)
10706
+ each_class_body_def(block.body) do |def_node|
10707
+ next if table.key?(def_node)
10708
+
10709
+ receiver = def_node.receiver
10710
+ def_self =
10711
+ if receiver.nil? then instance_self
10712
+ elsif receiver.is_a?(Prism::SelfNode) then class_self
10713
+ end
10714
+ table[def_node] = entry.with_block_self_type(def_self) if def_self
10715
+ end
10716
+ end
9992
10717
 
9993
- LastLine.block_entry(FreshFrameBlocks.closure_entry(current_scope, block, node), block, node)
10718
+ # The `def`s a class-body block defines on its class: every one in the body, `private def m` included, short
10719
+ # of a nested block, lambda or `class` / `module` / `class << …` body, each of which opens its own.
10720
+ def each_class_body_def(node, &)
10721
+ return unless node.is_a?(Prism::Node)
10722
+ return yield(node) if node.is_a?(Prism::DefNode)
10723
+ return if CLASS_BODY_DEF_BARRIERS.include?(node.class)
10724
+
10725
+ node.rigor_each_child { |child| each_class_body_def(child, &) }
10726
+ end
10727
+
10728
+ CLASS_BODY_DEF_BARRIERS = [
10729
+ Prism::BlockNode, Prism::LambdaNode, Prism::ClassNode, Prism::ModuleNode, Prism::SingletonClassNode
10730
+ ].freeze
10731
+ private_constant :CLASS_BODY_DEF_BARRIERS
10732
+
10733
+ # The narrowing ({MacroBlockSelfType::Match}) an unentered block's body runs with — its `self` and, for a
10734
+ # `block_as_methods:` entry, the declared refinements (issue #1667) — when the call is one whose block the
10735
+ # engine narrows: an ADR-16 Tier A `block_as_methods:` match ({MacroBlockSelfType}) or `define_method` on the
10736
+ # lexical `self` ({DefineMethodBlockSelf}, issue #963). The evaluator applies the same two narrowings to
10737
+ # the blocks it enters ({StatementEvaluator#build_block_entry_scope}, {StatementEvaluator#enter_call_block})
10738
+ # and the value pass to every block it types ({ExpressionTyper#block_body_self_narrowing}); without this a
10739
+ # block in a value position — `puts(MyApp.get("/x") { redirect "/y" })`, `render json: Alba.serialize(x) {
10740
+ # attributes :id }`, `private define_method(:m) { helper }` — kept the enclosing `self`, and every DSL call
10741
+ # in it read as a call on that. The receiver is typed only when some plugin declares an entry for the
10742
+ # call's name, so a call no entry names costs one table lookup. A miss, or a raise, keeps the entry as
10743
+ # built — the false-positive-safe direction.
10744
+ def unentered_block_narrowing(node, scope)
10745
+ registry = scope.environment&.plugin_registry
10746
+ if registry && !registry.empty? && !registry.contribution_index.block_entries_for(node.name).empty?
10747
+ receiver_type =
10748
+ node.receiver ? scope.type_of(node.receiver) : MacroBlockSelfType.implicit_receiver_type(scope)
10749
+ match = MacroBlockSelfType.match_for(scope: scope, call_node: node, receiver_type: receiver_type)
10750
+ return match if match
10751
+ end
10752
+ defined = DefineMethodBlockSelf.narrow_self_type_for(scope: scope, call_node: node)
10753
+ defined && MacroBlockSelfType::Match.new(self_type: defined, refinements: InEffectRefinements::EMPTY)
10754
+ rescue StandardError
10755
+ nil
9994
10756
  end
9995
10757
 
9996
10758
  # The scope the children of an unentered block or lambda inherit. The evaluator enters a statement-level