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
@@ -83,10 +83,12 @@ module Rigor
83
83
  # One ancestor on a chain. A project entry names a class or module (`name`) on `side` `:instance` (its
84
84
  # instance methods) or `:singleton` (the class object's singleton). An external entry has no `name`: it
85
85
  # keeps the spelling (`raw`) and its candidate names (`candidates`, most qualified first, empty for an
86
- # ambiguous spelling), and `superclass_edge` says it was reached through a superclass edge.
86
+ # ambiguous spelling), and `superclass_edge` says it was reached through a superclass edge. `owner` names
87
+ # the class or module whose edge an external entry is (#1698: Ruby resolves an `include`'s argument with
88
+ # that body as the innermost cref, a namespace `candidates` leaves out); it is nil on a project entry.
87
89
  # `last_segment` is the unqualified name a project entry files its negative class edge under, computed once
88
90
  # when the entry is interned.
89
- Entry = Data.define(:name, :side, :raw, :candidates, :superclass_edge, :last_segment) do
91
+ Entry = Data.define(:name, :side, :raw, :candidates, :superclass_edge, :last_segment, :owner) do
90
92
  def external? = name.nil?
91
93
  end
92
94
 
@@ -98,7 +100,9 @@ module Rigor
98
100
  # module whose mixin edges' order is not a fact, `kind` the side read (`:include` or `:extend`), `listed`
99
101
  # the entries `DiscoveryIndex#unpositioned_mixins` lists for it (`"*"` included), and `multi_file` whether
100
102
  # the multi-file rule fired (two or more files declare it and it has two or more edges on that side).
101
- # Interned per `[node, kind]` in the flavor's bucket, so equal marks are one object.
103
+ # Interned per `[node, kind]` in the flavor's bucket, so equal marks are one object. Kind `:extend_hook`
104
+ # (#1730, listing `"*"`) marks a node a module it extends may mix into through a hook; it is not an order mark
105
+ # ({#order_marks}).
102
106
  Mark = Data.define(:node, :kind, :listed, :multi_file)
103
107
 
104
108
  NO_MARKS = [].freeze
@@ -135,7 +139,20 @@ module Rigor
135
139
 
136
140
  # True when a class on the chain has mixin edges whose order the tables cannot vouch for
137
141
  # (`DiscoveryIndex#unpositioned_mixins`, or a class declared in several files with several edges).
138
- def unsettled? = !@marks.empty?
142
+ def unsettled? = !order_marks.empty?
143
+
144
+ # The marks that make the chain's ORDER doubtful. An `:extend_hook` mark (#1730) says only that an extended
145
+ # module's hook may have mixed something in that the chain does not list: it makes a candidate-set read
146
+ # `:unknown` and {#wildcard_mixin?} true, and leaves every other reader on the chain's own order. Sending
147
+ # those readers to master's order would answer a constant lookup breadth-first, which is wrong for the
148
+ # #1567 shape on every class that extends such a module.
149
+ def order_marks
150
+ @memo[:order_marks] ||= @marks.reject { |mark| mark.kind == :extend_hook }.freeze
151
+ end
152
+
153
+ # True when a class on the chain records a mixin the tables cannot name (`"*"`: an `include` of a
154
+ # non-constant, or a call the walk cannot record), so the chain may hold a module it does not list.
155
+ def wildcard_mixin? = @marks.any? { |mark| mark.listed.include?(Relevance::WILDCARD) }
139
156
 
140
157
  # The ONE decision every first-definer reader makes: does `answer`, read off this chain, stand, or does
141
158
  # the reader answer what the walk this chain replaced answered ({MasterOrder})? Returns `:chain` or
@@ -163,7 +180,7 @@ module Rigor
163
180
  # goes; `search` files only the entries ahead of an answer. `owner` names the entry that answered.
164
181
  record_beyond(scope, owner, unknown_for.nil?)
165
182
  return settle_unknown(scope, answer, unknown_for, &) unless unknown_for.nil?
166
- return :master unless @marks.empty?
183
+ return :master unless order_marks.empty?
167
184
 
168
185
  case @forks
169
186
  when 0 then :chain
@@ -208,7 +225,10 @@ module Rigor
208
225
  def search(scope, start = 0, stop = nil, side: nil)
209
226
  recording = Analysis::DependencyRecorder.active?
210
227
  stop ||= @entries.size
211
- record_head(scope, start, side) if recording
228
+ if recording
229
+ record_head(scope, start, side)
230
+ record_extend_hooks(scope)
231
+ end
212
232
  index = start
213
233
  while index < stop
214
234
  entry = @entries[index]
@@ -228,6 +248,7 @@ module Rigor
228
248
 
229
249
  ResolutionChain.record_class(scope, @root)
230
250
  (stop || @entries.size).times { |index| ResolutionChain.record_entry(scope, @entries[index], side) }
251
+ record_extend_hooks(scope)
231
252
  end
232
253
 
233
254
  # What a {#settle} verdict adds to what the search already filed. The search files the root and every entry
@@ -241,7 +262,8 @@ module Rigor
241
262
  # it `:unknown`, so the rest of the chain is filed as for any other answer;
242
263
  # - otherwise every entry after the answer (the whole chain when `owner` names none) files its class edge
243
264
  # and, for a project entry, the negative class edge on its unqualified name, so a NEW file declaring or
244
- # reopening it re-checks the reader. An external entry files only the sites of the names it can denote.
265
+ # reopening it re-checks the reader. An external entry files only the sites of the names it can denote,
266
+ # and every external on the chain files its hook edges ({#record_hooks}, #1641).
245
267
  #
246
268
  # What is filed is a function of this chain, the entry the filing starts from and the discovery index the
247
269
  # sites are read from, so a consumer that settles the same chain from the same entry again (the common case:
@@ -253,6 +275,8 @@ module Rigor
253
275
 
254
276
  start = owner.nil? ? nil : @entries.index { |entry| entry.name == owner }
255
277
  Analysis::DependencyRecorder.file_chain_once(self, start, scope.discovery) { file_beyond(scope, start) }
278
+ record_hooks(scope)
279
+ record_extend_hooks(scope)
256
280
  end
257
281
  private :record_beyond
258
282
 
@@ -268,6 +292,113 @@ module Rigor
268
292
  end
269
293
  private :file_beyond
270
294
 
295
+ # #1641 — the hook edges of every EXTERNAL entry on the chain, ahead of the answer or past it. A project file
296
+ # that gives an external module a hook (`module Comparable; def self.included(base) = base.prepend(P); end`)
297
+ # can put a definer ahead of the answer wherever the module sits, and a candidate-set read declines on it
298
+ # (the hook's mixin lists `"*"` on the module). The entry's class edges are the sites of the names it can
299
+ # denote, which a NEW file is not among, and a negative class edge on its name would re-check every reader of
300
+ # `Kernel` or `Enumerable` on any reopening. So each candidate name files the negative method edge of every
301
+ # {Relevance::HOOKS} name on its singleton side (`method:Comparable.included`, the key a new `def
302
+ # self.included` satisfies); a file adding any other method to the module re-checks nothing, and once the
303
+ # module is declared its sites carry later edits and the removal. The edges are a function of the chain alone,
304
+ # so a consumer files them once per chain ({Analysis::DependencyRecorder.file_chain_once}, #1590). A hook
305
+ # written another way — an instance `def included` in a module extended onto the external, a
306
+ # `define_singleton_method(:included)` — satisfies no such key and is not seen.
307
+ def record_hooks(scope)
308
+ externals = (@memo[:externals] ||= @entries.select(&:external?).freeze)
309
+ return if externals.empty?
310
+
311
+ Analysis::DependencyRecorder.file_chain_once(self, :hooks, scope.discovery) do
312
+ externals.each do |entry|
313
+ entry.candidates.each { |name| Analysis::DependencyRecorder.read_keys(ResolutionChain.hook_keys(name)) }
314
+ end
315
+ end
316
+ end
317
+ private :record_hooks
318
+
319
+ # #1730 — the edges of the {Builder#extend_hook_taint?} decision: an instance chain's project entries may carry
320
+ # a `"*"` mark because a module one of them `extend`s has a hook that mixes into the extender, and the
321
+ # module is often declared in a file the chain records nothing of. Each such module the decision read files
322
+ # its class edges (an edit to its hook moves its declaration signature) and the negative method edges of
323
+ # the hooks the decision reads ({ResolutionChain.extend_hook_keys}), which a NEW file reopening the module
324
+ # to add a hook satisfies; a spelling no project module answers files the singleton hook keys of each name
325
+ # it can denote, as {#record_hooks} does. Filed once per consumer and chain.
326
+ def record_extend_hooks(scope)
327
+ return unless @side == :instance
328
+
329
+ targets = (@memo[:extend_targets] ||= ResolutionChain.extend_targets(scope, @flavor, @entries))
330
+ return if targets.empty?
331
+
332
+ Analysis::DependencyRecorder.file_chain_once(self, :extend_hooks, scope.discovery) do
333
+ targets.each do |raw, resolved, candidates|
334
+ Analysis::DependencyRecorder.read_last_segment(:class, raw)
335
+ if resolved
336
+ ResolutionChain.record_class(scope, resolved)
337
+ Analysis::DependencyRecorder.read_keys(ResolutionChain.extend_hook_keys(resolved))
338
+ else
339
+ candidates.each { |name| Analysis::DependencyRecorder.read_keys(ResolutionChain.hook_keys(name)) }
340
+ end
341
+ end
342
+ end
343
+ end
344
+ private :record_extend_hooks
345
+
346
+ # The hooks that let a module `extend`ed onto N reshape N's INSTANCE side (#1730): its singleton `extended` /
347
+ # `extend_object` run against N at the `extend` and may mix into N; its instance `included` /
348
+ # `append_features` / `prepended` / `prepend_features` / `inherited` become N's own singleton hooks and run
349
+ # against N's includers, prependers and subclasses, which the mark on N reaches through their chains.
350
+ SINGLETON_EXTEND_HOOKS = %i[extended extend_object].freeze
351
+ INSTANCE_EXTEND_HOOKS = %i[included append_features prepended prepend_features inherited].freeze
352
+
353
+ @extend_hook_keys = {}
354
+
355
+ # The frozen negative method keys of `name`'s {SINGLETON_EXTEND_HOOKS} and {INSTANCE_EXTEND_HOOKS}.
356
+ def self.extend_hook_keys(name)
357
+ @extend_hook_keys[name] ||= (
358
+ SINGLETON_EXTEND_HOOKS.map { |hook| "method:#{name}.#{hook}".freeze } +
359
+ INSTANCE_EXTEND_HOOKS.map { |hook| "method:#{name}##{hook}".freeze }
360
+ ).freeze
361
+ end
362
+
363
+ # Whether extending `name` may mix modules into the extender's instance side, or into its includers' (#1730):
364
+ # `name` records a mixin it cannot name on its own instance side (`"*"`, which a hook's `base.include X` or
365
+ # `base.class_eval { … }` lists on the hook's module) and defines one of the hooks that would run it there.
366
+ def self.extend_reshapes_instance?(scope, name)
367
+ listed = scope.discovery.unpositioned_mixins[name]&.dig(:include)
368
+ return false unless listed&.include?(Relevance::WILDCARD)
369
+
370
+ defines_any?(scope.discovered_singleton_def_nodes[name], SINGLETON_EXTEND_HOOKS) ||
371
+ defines_any?(scope.discovered_def_nodes[name], INSTANCE_EXTEND_HOOKS)
372
+ end
373
+
374
+ # Read off the def-node tables: the existence table does not carry a module's own instance hook once the
375
+ # extend fold has copied it onto the extender's singleton.
376
+ def self.defines_any?(defs, hooks)
377
+ !defs.nil? && hooks.any? { |hook| defs.key?(hook) }
378
+ end
379
+
380
+ # `[[spelling, resolved name or nil, candidate names], …]`: every module the project entries of an instance chain
381
+ # `extend`, as {Builder#extend_hook_taint?} resolved them.
382
+ def self.extend_targets(scope, flavor, entries)
383
+ resolver = resolver_for(scope, flavor)
384
+ extends = scope.discovery.discovered_extends
385
+ entries.each_with_object([]) do |entry, out|
386
+ next if entry.external? || entry.side != :instance
387
+
388
+ (extends[entry.name] || EMPTY_NAMES).each do |raw|
389
+ resolved = resolver.resolve_one(entry.name, raw)
390
+ out << [raw, resolved, resolved ? EMPTY_NAMES : resolver.candidates(entry.name, raw)].freeze
391
+ end
392
+ end.uniq.freeze
393
+ end
394
+
395
+ @hook_keys = {}
396
+
397
+ # The frozen `method:Name.hook` negative keys of `name`'s {Relevance::HOOKS}, built once per name.
398
+ def self.hook_keys(name)
399
+ @hook_keys[name] ||= Relevance::HOOKS.map { |hook| "method:#{name}.#{hook}".freeze }.freeze
400
+ end
401
+
271
402
  def record_head(scope, start, side)
272
403
  ResolutionChain.record_class(scope, @root) unless start.zero? && @entries.first&.name == @root
273
404
  start.times { |index| ResolutionChain.record_entry(scope, @entries[index], side) }
@@ -397,7 +528,8 @@ module Rigor
397
528
  @discovery.discovered_superclasses.key?(name) || @discovery.discovered_includes.key?(name) ||
398
529
  @discovery.discovered_classes.key?(name)
399
530
  when :arity
400
- @scope.known_user_class?(name) || @discovery.discovered_parameter_envelopes.key?(name)
531
+ @scope.known_user_class?(name) ||
532
+ DiscoveryIndex.declares_surface?(@discovery.discovered_parameter_envelopes[name])
401
533
  else
402
534
  @scope.known_user_class?(name)
403
535
  end
@@ -601,7 +733,8 @@ module Rigor
601
733
  # The chain for `root`, carrying its retro world when the skip rule skipped anything on the way.
602
734
  def chain(root, side)
603
735
  entries, starts, classes, forks, marks, retro_ok = lin_for(root, side)
604
- retro = build_retro(root, side) if forks == 1 && retro_ok && marks.empty? && !@retro
736
+ order_marked = marks.any? { |mark| mark.kind != :extend_hook }
737
+ retro = build_retro(root, side) if forks == 1 && retro_ok && !order_marked && !@retro
605
738
  cut_at = cut_position(entries)
606
739
  return whole_chain(root, side, entries, starts, classes, [forks, marks], retro) if cut_at.nil?
607
740
 
@@ -701,6 +834,7 @@ module Rigor
701
834
  def compute_instance(name, depth)
702
835
  # `discovered_includes` already carries the prepended names, so it counts every instance-side edge once.
703
836
  mark_unsettled(name, :include, (@discovery.discovered_includes[name] || EMPTY).size)
837
+ @frames.last.add_marks(mark_for(name, :extend_hook, WILDCARD_LIST, false)) if extend_hook_taint?(name)
704
838
  entries = [project_entry(name, :instance)]
705
839
  super_lin = superclass_lin(name, :instance, depth)
706
840
  entries.concat(super_lin[0])
@@ -743,6 +877,20 @@ module Rigor
743
877
  multi)].freeze
744
878
  end
745
879
 
880
+ WILDCARD_LIST = ["*"].freeze # Relevance::WILDCARD, which loads after this file
881
+ private_constant :WILDCARD_LIST
882
+
883
+ # #1730 — whether a module `name` extends has a hook that may mix into `name`'s instance side or its
884
+ # includers' ({ResolutionChain.extend_reshapes_instance?}). Ruby runs it at the `extend`, so the mixin is
885
+ # not a fact of the tables and `name`'s instance side is unpositioned. A spelling no project module
886
+ # answers taints nothing: the project cannot see its hooks.
887
+ def extend_hook_taint?(name)
888
+ (@discovery.discovered_extends[name] || EMPTY).any? do |raw|
889
+ resolved = @resolver.resolve_one(name, raw)
890
+ !resolved.nil? && ResolutionChain.extend_reshapes_instance?(@scope, resolved)
891
+ end
892
+ end
893
+
746
894
  def multi_file?(name)
747
895
  sites = @discovery.discovered_class_sources[name]
748
896
  !sites.nil? && sites.size >= 2
@@ -866,14 +1014,14 @@ module Rigor
866
1014
 
867
1015
  def project_entry(name, side)
868
1016
  by_side = (@bucket[:interned][side] ||= {})
869
- by_side[name] ||= Entry.new(name, side, nil, nil, false, name.to_s.split("::").last.freeze)
1017
+ by_side[name] ||= Entry.new(name, side, nil, nil, false, name.to_s.split("::").last.freeze, nil)
870
1018
  end
871
1019
 
872
1020
  def external_entry(owner, raw, side, superclass_edge)
873
1021
  by_edge = ((@bucket[:externals][side] ||= {})[superclass_edge] ||= {})
874
1022
  by_owner = (by_edge[owner] ||= {})
875
1023
  by_owner[raw] ||= Entry.new(nil, side, raw, @scope.ancestor_name_candidates(owner, raw).freeze,
876
- superclass_edge, nil)
1024
+ superclass_edge, nil, owner)
877
1025
  end
878
1026
  end
879
1027
  private_constant :Builder
data/lib/rigor/scope.rb CHANGED
@@ -5,6 +5,7 @@ require_relative "environment"
5
5
  require_relative "scope/discovery_index"
6
6
  require_relative "scope/resolution_chain"
7
7
  require_relative "inference/definer_resolution"
8
+ require_relative "inference/in_effect_refinements"
8
9
  require_relative "analysis/fact_store"
9
10
  require_relative "analysis/dependency_recorder"
10
11
  require_relative "inference/expression_typer"
@@ -26,12 +27,12 @@ module Rigor
26
27
  :indexed_narrowings, :method_chain_narrowings,
27
28
  :declaration_sourced, :published_constant_sourced,
28
29
  :source_path, :discovery, :struct_fold_safe_locals,
29
- :opaque_block_self, :singleton_class_body, :lexical_nesting,
30
+ :opaque_block_self, :block_self_unknown, :singleton_class_body, :lexical_nesting,
30
31
  :dynamic_origins, :local_origins, :ivar_origins,
31
32
  :void_origins, :plugin_typed_calls,
32
33
  :optimistic_origins, :optimistic_locals, :optimistic_ivars,
33
34
  :repeated_or_writes, :match_frame,
34
- :constant_narrowings, :guard_records, :bot_guard_classes
35
+ :constant_narrowings, :guard_records, :bot_guard_classes, :declared_refinements
35
36
 
36
37
  # ADR-53 Track A — the seed-time discovery tables live on the {DiscoveryIndex} the scope carries by a single
37
38
  # reference; the per-table readers stay on Scope so engine call sites and plugins are unaffected by the
@@ -62,6 +63,20 @@ module Rigor
62
63
  # Issue #1120 — `{refined class name => {method name => [refining module names]}}`: what each
63
64
  # `refine X do … end` block defines, visible only where a `using` of its module is in effect.
64
65
  def discovered_refinements = @discovery.discovered_refinements
66
+
67
+ # Issue #1673 (ADR-121 WD1) — the in-effect refinements at `node`: the refining-module names whose refinements
68
+ # Ruby applies there, ordered so a later activation comes later (the later one wins), each once at its first
69
+ # position, each `using`'d module expanded through its includes and prepends in CRuby's activation order.
70
+ # `declared` is a block source's modules, appended and expanded on the same terms; it defaults to this scope's
71
+ # {#declared_refinements}, a plugin-declared refined block's (#1667). An entry may be
72
+ # `Inference::InEffectRefinements::UNKNOWN`, which means any refinement may be in effect. A node another file
73
+ # wrote (a callee body typed under this file's scope) answers `declared` alone. Reads include edges, so it records
74
+ # the dependencies `Inference::InEffectRefinements.activated_modules` names; a consumer that asks for a method
75
+ # name also records `refinement:<name>` (`Inference::InEffectRefinements.refining_modules`).
76
+ def in_effect_refinements(node, declared = @declared_refinements)
77
+ Inference::InEffectRefinements.for_node(self, node, declared)
78
+ end
79
+
65
80
  # Issue #1367 — the project's `Inference::GlobalWriteCensus`, a `Set` of frozen entries.
66
81
  def discovered_global_write_census = @discovery.discovered_global_write_census
67
82
  def discovered_includes = @discovery.discovered_includes
@@ -182,7 +197,9 @@ module Rigor
182
197
  # today.
183
198
  # - `receiver_name` is the variable's Symbol.
184
199
  # - `key` is the Ruby value of the literal index (Symbol / String / Integer). Non-literal keys
185
- # (`params[field]`) are not recorded; they have no stable address.
200
+ # (`params[field]`) have no stable address for a recorded value. A `key?` guard with a non-literal key
201
+ # (issue #1703) records an {Inference::KeyPresenceGuard::KeyExpr} here instead, with `receiver_kind`
202
+ # `:const` allowed; its entry marks presence only.
186
203
  IndexedKey = Data.define(:receiver_kind, :receiver_name, :key)
187
204
 
188
205
  # Narrowing key for a no-arg / no-block method-call chain `receiver.method_name` (a "single-hop" chain per A1
@@ -320,6 +337,7 @@ module Rigor
320
337
  source_path: nil,
321
338
  struct_fold_safe_locals: EMPTY_FOLD_SAFE,
322
339
  opaque_block_self: false,
340
+ block_self_unknown: false,
323
341
  singleton_class_body: false,
324
342
  lexical_nesting: nil,
325
343
  dynamic_origins: {}.compare_by_identity,
@@ -334,7 +352,8 @@ module Rigor
334
352
  match_frame: nil,
335
353
  constant_narrowings: EMPTY_CONSTANT_NARROWINGS,
336
354
  guard_records: EMPTY_GUARD_RECORDS,
337
- bot_guard_classes: EMPTY_BOT_GUARD_CLASSES
355
+ bot_guard_classes: EMPTY_BOT_GUARD_CLASSES,
356
+ declared_refinements: Inference::InEffectRefinements::EMPTY
338
357
  )
339
358
  @environment = environment
340
359
  @locals = locals
@@ -351,6 +370,7 @@ module Rigor
351
370
  @source_path = source_path
352
371
  @struct_fold_safe_locals = struct_fold_safe_locals
353
372
  @opaque_block_self = opaque_block_self
373
+ @block_self_unknown = block_self_unknown
354
374
  @singleton_class_body = singleton_class_body
355
375
  @lexical_nesting = lexical_nesting
356
376
  @dynamic_origins = dynamic_origins
@@ -366,6 +386,7 @@ module Rigor
366
386
  @constant_narrowings = constant_narrowings
367
387
  @guard_records = guard_records
368
388
  @bot_guard_classes = bot_guard_classes
389
+ @declared_refinements = declared_refinements
369
390
  freeze
370
391
  end
371
392
 
@@ -378,6 +399,14 @@ module Rigor
378
399
  self
379
400
  end
380
401
 
402
+ # Issue #1703 — this scope with private copies of the identity-keyed side tables (dynamic, void and optimistic
403
+ # origins, plugin-typed calls), so a second walk from it — the `key?`-guarded re-walk — records into its own
404
+ # tables and never into the ones the file's analysis reads.
405
+ def with_isolated_side_tables
406
+ rebuild(dynamic_origins: @dynamic_origins.dup, void_origins: @void_origins.dup,
407
+ plugin_typed_calls: @plugin_typed_calls.dup, optimistic_origins: @optimistic_origins.dup)
408
+ end
409
+
381
410
  def optimistic_local(name) = Inference::OptimisticOrigin.bound_cause(@optimistic_locals[name.to_sym])
382
411
  def optimistic_ivar(name) = Inference::OptimisticOrigin.bound_cause(@optimistic_ivars[name.to_sym])
383
412
 
@@ -476,6 +505,39 @@ module Rigor
476
505
  rebuild(self_type: type)
477
506
  end
478
507
 
508
+ # Issue #1717 — installs the `self` a block body runs with when the engine KNOWS it: an ADR-16
509
+ # `block_as_methods:` match, `define_method`, or a `Class.new` / `refine` body (#319). Like
510
+ # {#with_self_type} it leaves the #316 mark alone; it also sets {#block_self_unknown?} to `keeps_unknown`,
511
+ # which a caller passes from {#block_self_narrowing_unknown?}. A nil `type` narrows nothing and keeps it.
512
+ #
513
+ # Issue #1695 — instance variables belong to `self`, so a narrowing onto a DIFFERENT `self` than the enclosing
514
+ # scope's also drops every instance-variable binding the body would otherwise inherit ({#without_ivar_bindings}):
515
+ # a `define_method` block in a class body runs on an instance, and the class body's `@label = nil` is the class
516
+ # object's ivar, not the one the block reads. The body's reads answer `Dynamic` until it writes them. A narrowing
517
+ # that keeps the caller's `self` (an ADR-16 `:lexical` entry) keeps them.
518
+ def with_block_self_type(type, keeps_unknown: false)
519
+ unknown = type.nil? ? @block_self_unknown : keeps_unknown
520
+ return rebuild(self_type: type, block_self_unknown: unknown) if type.nil? || type == @self_type
521
+
522
+ without_ivar_bindings(type, unknown)
523
+ end
524
+
525
+ # Issue #1717 — whether a block-self narrowing for `call_node`, asked from this (the CALLER's) scope, is
526
+ # built on an unknown `self`. `define_method` and an implicit- or `self`-receiver `block_as_methods:` match
527
+ # read the lexical `self`, which inside a block of unknown `self` is the enclosing method's, not the
528
+ # block's; a narrowing keyed on an explicit receiver (`Grape::API.namespace do`) does not depend on it.
529
+ #
530
+ # Issue #1759 — one narrowing is not: a `define_method` written directly in a `refine X` block
531
+ # ({REFINEMENT_BLOCK_SELF}) defines a method of the refinement, which runs on an instance of X, and the
532
+ # `Nominal[X]` it reads off the block's lexical `Singleton[X]` is that `self`.
533
+ def block_self_narrowing_unknown?(call_node)
534
+ return false unless @block_self_unknown
535
+ return false if @block_self_unknown == REFINEMENT_BLOCK_SELF && call_node.name == :define_method
536
+
537
+ receiver = call_node.receiver
538
+ receiver.nil? || receiver.is_a?(Prism::SelfNode)
539
+ end
540
+
479
541
  # Issue #652 — installs the body's REAL `Module.nesting`, innermost first, as recorded at declaration
480
542
  # time by `Inference::StatementEvaluator` (`["Admin::UsersController"]` for a compact
481
543
  # `class Admin::UsersController`, `["Admin::UsersController", "Admin"]` for the nested spelling). A
@@ -518,15 +580,52 @@ module Rigor
518
580
  # `Array#each` without knowing the callee. The flag is set at every block entry that leaves `self_type`
519
581
  # unnarrowed and is inherited by every scope derived inside the block; it never leaks past the block,
520
582
  # because `eval_call` returns the caller's scope unchanged.
583
+ #
584
+ # Entering also sets {#block_self_unknown?} (issue #1717), the separate mark `call.undefined-method` reads.
521
585
  def entering_opaque_block
522
- return self if @opaque_block_self
586
+ return self if @opaque_block_self && @block_self_unknown == true
523
587
 
524
- rebuild(opaque_block_self: true)
588
+ rebuild(opaque_block_self: true, block_self_unknown: true)
525
589
  end
526
590
 
527
591
  # True when this scope sits inside a block whose `self` is unmodelled ({#entering_opaque_block}).
528
592
  def opaque_block_self? = @opaque_block_self
529
593
 
594
+ # Issue #1717 — true when this scope sits in a block whose `self` the engine does not know, so an explicit
595
+ # `self.m` there is no evidence `m` is missing: the yielding method may `instance_exec` the block on another
596
+ # object. Read ONLY by `call.undefined-method`'s explicit-`self` exemption, and kept apart from
597
+ # {#opaque_block_self?} so that mark's reach (the top-level-def bind gate) is untouched. Set with
598
+ # {#entering_opaque_block} on the evaluator's block entries and by the indexer's walk of an unentered block
599
+ # ({#with_block_self_unknown}); cleared where a block's `self` is narrowed ({#with_block_self_type}) and, on
600
+ # the indexer's walk, at a `def` / `class` / `module` body, which the evaluator starts from a fresh scope.
601
+ def block_self_unknown? = @block_self_unknown ? true : false
602
+
603
+ # Issue #1759 — the {#block_self_unknown?} value a `refine X do … end` block body is entered with
604
+ # (`with_block_self_type(Singleton[X], keeps_unknown: REFINEMENT_BLOCK_SELF)`). It reads as unknown everywhere
605
+ # `true` does; what it adds is that the body's lexical `self` is X's singleton, which a `define_method` in it
606
+ # reads correctly ({#block_self_narrowing_unknown?}). A block nested in the body takes plain `true`.
607
+ REFINEMENT_BLOCK_SELF = :refinement
608
+
609
+ # Issue #1763 — true directly in a `refine X do … end` block body ({REFINEMENT_BLOCK_SELF}). The body's
610
+ # `self_type` is the `Singleton[X]` its implicit-self calls and `define_method` read, but the block runs on the
611
+ # refinement module, so a `self` read as a value (`x = self`) types as `Dynamic[top]` there.
612
+ def refinement_block_self? = @block_self_unknown == REFINEMENT_BLOCK_SELF
613
+
614
+ # The join of two arms' {#block_self_unknown?} values: `true` if either has it, else either's mark.
615
+ def merged_block_self_unknown(other)
616
+ return true if @block_self_unknown == true || other == true
617
+
618
+ @block_self_unknown || other
619
+ end
620
+ private :merged_block_self_unknown
621
+
622
+ # Issue #1717 — sets {#block_self_unknown?} to `flag`, leaving every other field (the #316 mark included).
623
+ def with_block_self_unknown(flag)
624
+ return self if @block_self_unknown == flag
625
+
626
+ rebuild(block_self_unknown: flag)
627
+ end
628
+
530
629
  # Issue #963 — marks the body of a `class << ...` as such. Inside it `self` is the SINGLETON class, which
531
630
  # Rigor models with the same `Singleton[X]` carrier a `class X` body gets, so the carrier alone cannot say
532
631
  # which of the two a scope came from. The distinction decides what `define_method` does: in a `class << self`
@@ -544,6 +643,19 @@ module Rigor
544
643
  rebuild(singleton_class_body: flag)
545
644
  end
546
645
 
646
+ # Issue #1667 (ADR-121 WD5) — this scope with `names` appended to {#declared_refinements}, each once at its first
647
+ # position: the modules a plugin-declared refined block (`block_as_methods:` `refinements:`) runs under. Stamped
648
+ # at block entry and inherited by every scope derived inside the body, nested blocks included, and by a `def` or
649
+ # class body written in it (`StatementEvaluator#build_fresh_body_scope`), as CRuby's cref is.
650
+ def with_declared_refinements(names)
651
+ return self if names.empty?
652
+
653
+ merged = (@declared_refinements | names).freeze
654
+ return self if merged == @declared_refinements
655
+
656
+ rebuild(declared_refinements: merged)
657
+ end
658
+
547
659
  # True when this scope IS a `class << ...` body (not merely inside one lexically — a `def` reached from it
548
660
  # answers false).
549
661
  def singleton_class_body? = @singleton_class_body
@@ -600,6 +712,45 @@ module Rigor
600
712
  end
601
713
  private :bind_ivar
602
714
 
715
+ # Issue #1695 — this scope with no instance-variable binding and none of the per-ivar state that rides on one:
716
+ # the narrowings keyed on or rooted at an ivar, its ADR-58 and #667 marks, ADR-82 origins, #286 optimistic marks
717
+ # and #1446 guard records, the state {#bind_ivar} drops for one name. It installs {#with_block_self_type}'s
718
+ # `self_type` and unknown-`self` mark in the same rebuild.
719
+ def without_ivar_bindings(self_type, block_self_unknown)
720
+ rebuild(
721
+ self_type: self_type,
722
+ block_self_unknown: block_self_unknown,
723
+ ivars: EMPTY_VAR_BINDINGS,
724
+ indexed_narrowings: reject_kept(@indexed_narrowings) { |k, _| ivar_indexed_key?(k) },
725
+ method_chain_narrowings: reject_kept(@method_chain_narrowings) { |k, _| k.receiver_kind == :ivar },
726
+ declaration_sourced: reject_kept(@declaration_sourced) { |ref| ref[0] == :ivar },
727
+ published_constant_sourced: reject_kept(@published_constant_sourced) { |ref| ref[0] == :ivar },
728
+ ivar_origins: EMPTY_ORIGINS,
729
+ optimistic_ivars: EMPTY_ORIGINS,
730
+ guard_records: reject_kept(@guard_records) { |key, _| key[0] == :ivar },
731
+ bot_guard_classes: reject_kept(@bot_guard_classes) { |key, _| key[0] == :ivar }
732
+ )
733
+ end
734
+ private :without_ivar_bindings
735
+
736
+ # `collection` without the entries the block selects, or `collection` itself when it selects none. It keeps the
737
+ # collection's class: `Set#reject` answers an Array, which a Set carrier's readers (`merge`, `|`, `==`) reject.
738
+ def reject_kept(collection, &)
739
+ return collection if collection.empty? || collection.none?(&)
740
+
741
+ collection.dup.delete_if(&).freeze
742
+ end
743
+ private :reject_kept
744
+
745
+ # An indexed narrowing of an ivar, or of a `key?` guard's key read from one ({#key_guard_rooted_at?}).
746
+ def ivar_indexed_key?(indexed_key)
747
+ return true if indexed_key.receiver_kind == :ivar
748
+
749
+ key = indexed_key.key
750
+ key.is_a?(Inference::KeyPresenceGuard::KeyExpr) && key.root[0] == :ivar
751
+ end
752
+ private :ivar_indexed_key?
753
+
603
754
  # ADR-58 WD1 — used by the method-entry seed to mark an ivar whose only provenance is the class-ivar index.
604
755
  # Unlike `with_ivar` this binds the type AND records the declaration-sourced mark in one transition.
605
756
  def seed_declaration_sourced_ivar(name, type)
@@ -1954,6 +2105,7 @@ module Rigor
1954
2105
  source_path: @source_path,
1955
2106
  struct_fold_safe_locals: @struct_fold_safe_locals,
1956
2107
  opaque_block_self: @opaque_block_self,
2108
+ block_self_unknown: @block_self_unknown,
1957
2109
  singleton_class_body: @singleton_class_body,
1958
2110
  lexical_nesting: @lexical_nesting,
1959
2111
  dynamic_origins: @dynamic_origins,
@@ -1968,7 +2120,8 @@ module Rigor
1968
2120
  match_frame: @match_frame,
1969
2121
  constant_narrowings: @constant_narrowings,
1970
2122
  guard_records: @guard_records,
1971
- bot_guard_classes: @bot_guard_classes
2123
+ bot_guard_classes: @bot_guard_classes,
2124
+ declared_refinements: @declared_refinements
1972
2125
  )
1973
2126
  self.class.new(
1974
2127
  environment: environment, locals: locals,
@@ -1982,6 +2135,7 @@ module Rigor
1982
2135
  source_path: source_path,
1983
2136
  struct_fold_safe_locals: struct_fold_safe_locals,
1984
2137
  opaque_block_self: opaque_block_self,
2138
+ block_self_unknown: block_self_unknown,
1985
2139
  singleton_class_body: singleton_class_body,
1986
2140
  lexical_nesting: lexical_nesting,
1987
2141
  dynamic_origins: dynamic_origins,
@@ -1996,7 +2150,8 @@ module Rigor
1996
2150
  match_frame: match_frame,
1997
2151
  constant_narrowings: constant_narrowings,
1998
2152
  guard_records: guard_records,
1999
- bot_guard_classes: bot_guard_classes
2153
+ bot_guard_classes: bot_guard_classes,
2154
+ declared_refinements: declared_refinements
2000
2155
  )
2001
2156
  end
2002
2157
 
@@ -2014,7 +2169,8 @@ module Rigor
2014
2169
  result.freeze
2015
2170
  end
2016
2171
 
2017
- def build_joined_scope(joined_locals, joined_ivars, joined_cvars, joined_globals, other)
2172
+ # One keyword per Scope field, each with its join rule beside it; the size is the field count.
2173
+ def build_joined_scope(joined_locals, joined_ivars, joined_cvars, joined_globals, other) # rubocop:disable Metrics/AbcSize, Metrics/MethodLength
2018
2174
  self.class.new(
2019
2175
  environment: @environment,
2020
2176
  locals: joined_locals.freeze,
@@ -2062,6 +2218,9 @@ module Rigor
2062
2218
  # block entry by `entering_opaque_block` and inherited through `rebuild`), so both arms usually carry
2063
2219
  # the same value and the `||` is that value.
2064
2220
  opaque_block_self: @opaque_block_self || other.opaque_block_self,
2221
+ # Issue #1717 — the same body property, and `||` keeps the exemption it grants (the FP-safe side).
2222
+ # A plain `true` outranks the refine body's {REFINEMENT_BLOCK_SELF} for the same reason.
2223
+ block_self_unknown: merged_block_self_unknown(other.block_self_unknown),
2065
2224
  # Issue #963 — a body property like the two above, so both arms of an in-body merge carry the identical
2066
2225
  # value and the `||` is that value. `||` is also the safe direction on its own terms: keeping the mark
2067
2226
  # declines the `define_method` narrowing, which is the pre-#963 answer.
@@ -2086,12 +2245,24 @@ module Rigor
2086
2245
  # of a merge inside one body carry the same one; `||` keeps it should either arm lack it, since dropping
2087
2246
  # it only loses the frame's resets.
2088
2247
  match_frame: @match_frame || other.match_frame,
2248
+ # Issue #1667 — a body property stamped at block entry like the frame above, so both arms of a merge carry
2249
+ # the same list; the union keeps either arm's modules, the declining direction.
2250
+ declared_refinements: join_declared_refinements(other),
2089
2251
  # Issue #1429 — the guard narrowings of globals and constants and their pre-guard records, and of instance
2090
2252
  # variables (#1446).
2091
2253
  **join_guard_narrowings(other, joined_globals, joined_ivars)
2092
2254
  )
2093
2255
  end
2094
2256
 
2257
+ def join_declared_refinements(other)
2258
+ mine = @declared_refinements
2259
+ theirs = other.declared_refinements
2260
+ return mine if theirs.equal?(mine) || theirs.empty?
2261
+ return theirs if mine.empty?
2262
+
2263
+ (mine | theirs).freeze
2264
+ end
2265
+
2095
2266
  def join_guard_narrowings(other, joined_globals, joined_ivars)
2096
2267
  joined_constants = join_constant_narrowings(other)
2097
2268
  joined = { global: joined_globals, ivar: joined_ivars, constant: joined_constants }
@@ -2246,11 +2417,19 @@ module Rigor
2246
2417
  sym_kind = receiver_kind.to_sym
2247
2418
  sym_name = receiver_name.to_sym
2248
2419
  filtered = @indexed_narrowings.reject do |k, _|
2249
- k.receiver_kind == sym_kind && k.receiver_name == sym_name
2420
+ (k.receiver_kind == sym_kind && k.receiver_name == sym_name) || key_guard_rooted_at?(k.key, sym_kind, sym_name)
2250
2421
  end
2251
2422
  filtered.size == @indexed_narrowings.size ? @indexed_narrowings : filtered.freeze
2252
2423
  end
2253
2424
 
2425
+ # Issue #1703 — whether `key` is a `key?` guard's key that is the variable `kind`/`name`, or a chain read from it.
2426
+ def key_guard_rooted_at?(key, kind, name)
2427
+ return false unless key.is_a?(Inference::KeyPresenceGuard::KeyExpr)
2428
+
2429
+ root = key.root
2430
+ root[0] == kind && root[1] == name
2431
+ end
2432
+
2254
2433
  # ADR-58 WD1 — set/clear the declaration-sourced provenance mark.
2255
2434
  # Issue #667 — the #667 carrier's add/drop pair. Deliberately not reusing
2256
2435
  # {#add_declaration_sourced} / {#drop_declaration_sourced_for}: the two sets are joined by opposite
@@ -57,7 +57,10 @@ module Rigor
57
57
  inline_generic_class: "sig.skipped.inline-generic-class",
58
58
  # ADR-112 WD4 — the member is declared inline and in `sig/`, and the two disagree. A refusal, not a
59
59
  # skip: `--write` and `--check` exit 1 until a person reconciles them or passes `--overwrite`.
60
- inline_differs: "sig.skipped.inline-differs"
60
+ inline_differs: "sig.skipped.inline-differs",
61
+ # #1676 — a plain top-level `def` defines a private `Object` method and has no class to write a
62
+ # declaration under, so it is reported rather than silently dropped. Whether to emit it stays open.
63
+ top_level_def: "sig.skipped.top-level-def"
61
64
  }.freeze
62
65
  end
63
66
  end