rigortype 0.4.0 → 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 (237) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +4 -3
  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/08-understanding-errors.md +3 -2
  25. data/docs/handbook/11-sig-gen.md +6 -0
  26. data/docs/handbook/appendix-liskov.md +5 -0
  27. data/docs/install.md +87 -15
  28. data/docs/manual/02-cli-reference.md +2 -2
  29. data/docs/manual/03-configuration.md +6 -1
  30. data/docs/manual/04-diagnostics.md +2 -1
  31. data/docs/manual/06-baseline.md +2 -0
  32. data/docs/manual/07-plugins.md +1 -1
  33. data/docs/manual/08-skills.md +4 -3
  34. data/docs/manual/12-caching.md +43 -4
  35. data/docs/manual/14-rails-quickstart.md +19 -11
  36. data/docs/manual/18-removing-dead-code.md +32 -2
  37. data/docs/manual/plugins/README.md +6 -1
  38. data/docs/manual/plugins/rigor-activerecord.md +21 -0
  39. data/docs/manual/plugins/rigor-alba.md +60 -0
  40. data/docs/manual/plugins/rigor-minitest.md +22 -4
  41. data/docs/manual/plugins/rigor-typelizer.md +71 -0
  42. data/lib/rigor/analysis/check_rules/ivar_write_collector.rb +27 -3
  43. data/lib/rigor/analysis/check_rules/lexical_method_sites.rb +21 -92
  44. data/lib/rigor/analysis/check_rules/ruby_deprecations.rb +275 -0
  45. data/lib/rigor/analysis/check_rules/rule_ids.rb +4 -0
  46. data/lib/rigor/analysis/check_rules/source_arity.rb +184 -61
  47. data/lib/rigor/analysis/check_rules.rb +407 -144
  48. data/lib/rigor/analysis/dependency_recorder.rb +215 -18
  49. data/lib/rigor/analysis/dependency_source_inference/builder.rb +8 -1
  50. data/lib/rigor/analysis/dependency_source_inference/gem_resolver.rb +21 -0
  51. data/lib/rigor/analysis/dependency_source_inference/index.rb +36 -11
  52. data/lib/rigor/analysis/dependency_source_inference/walker.rb +196 -27
  53. data/lib/rigor/analysis/erb_template_detector.rb +2 -1
  54. data/lib/rigor/analysis/incremental.rb +29 -9
  55. data/lib/rigor/analysis/incremental_run_slot/write_guard.rb +291 -0
  56. data/lib/rigor/analysis/incremental_run_slot.rb +173 -0
  57. data/lib/rigor/analysis/incremental_session.rb +287 -50
  58. data/lib/rigor/analysis/plugin_fact_fingerprint.rb +24 -13
  59. data/lib/rigor/analysis/reachability/graph.rb +123 -23
  60. data/lib/rigor/analysis/reachability/scan.rb +290 -36
  61. data/lib/rigor/analysis/reachability/scan_cache.rb +2 -1
  62. data/lib/rigor/analysis/rule_catalog.rb +43 -0
  63. data/lib/rigor/analysis/run_cache_key.rb +43 -9
  64. data/lib/rigor/analysis/run_stats.rb +2 -2
  65. data/lib/rigor/analysis/runner/diagnostic_aggregator.rb +88 -6
  66. data/lib/rigor/analysis/runner/pool_coordinator.rb +52 -13
  67. data/lib/rigor/analysis/runner/project_pre_passes.rb +21 -13
  68. data/lib/rigor/analysis/runner/run_snapshots.rb +6 -1
  69. data/lib/rigor/analysis/runner.rb +371 -49
  70. data/lib/rigor/analysis/shadow_harness.rb +27 -0
  71. data/lib/rigor/analysis/source_rbs_gate.rb +157 -0
  72. data/lib/rigor/analysis/template_unit_paths.rb +6 -4
  73. data/lib/rigor/analysis/template_units.rb +15 -5
  74. data/lib/rigor/analysis/worker_session.rb +11 -7
  75. data/lib/rigor/cache/descriptor.rb +40 -1
  76. data/lib/rigor/cache/file_digest.rb +28 -0
  77. data/lib/rigor/cache/incremental_snapshot.rb +92 -8
  78. data/lib/rigor/cache/store.rb +63 -3
  79. data/lib/rigor/cache/value_digest.rb +325 -0
  80. data/lib/rigor/cli/annotate_command.rb +5 -0
  81. data/lib/rigor/cli/baseline_command.rb +7 -18
  82. data/lib/rigor/cli/check_command.rb +85 -14
  83. data/lib/rigor/cli/doctor_command.rb +1 -1
  84. data/lib/rigor/cli/effects_report.rb +2 -2
  85. data/lib/rigor/cli/trace_command.rb +5 -0
  86. data/lib/rigor/cli/type_of_command.rb +5 -3
  87. data/lib/rigor/cli/unused_command.rb +16 -10
  88. data/lib/rigor/cli.rb +11 -3
  89. data/lib/rigor/config_audit.rb +3 -2
  90. data/lib/rigor/config_path.rb +29 -0
  91. data/lib/rigor/configuration/severity_profile.rb +11 -0
  92. data/lib/rigor/configuration.rb +52 -7
  93. data/lib/rigor/effects/plugin_facts.rb +6 -6
  94. data/lib/rigor/environment/bundle_sig_discovery.rb +5 -3
  95. data/lib/rigor/environment/gated_signature_guard.rb +43 -0
  96. data/lib/rigor/environment/gated_signature_plan.rb +339 -0
  97. data/lib/rigor/environment/lockfile_resolver.rb +10 -1
  98. data/lib/rigor/environment/rbs_collection_discovery.rb +10 -1
  99. data/lib/rigor/environment/rbs_coverage_report.rb +1 -1
  100. data/lib/rigor/environment/rbs_loader.rb +284 -15
  101. data/lib/rigor/environment/required_features.rb +206 -0
  102. data/lib/rigor/environment/source_rbs_synthesis.rb +147 -0
  103. data/lib/rigor/environment.rb +34 -73
  104. data/lib/rigor/inference/acceptance.rb +28 -3
  105. data/lib/rigor/inference/block_parameter_binder.rb +29 -13
  106. data/lib/rigor/inference/builtins/cruby_definers.rb +61 -0
  107. data/lib/rigor/inference/builtins/method_catalog.rb +3 -0
  108. data/lib/rigor/inference/captured_locals.rb +7 -1
  109. data/lib/rigor/inference/declaration_walk/context.rb +195 -0
  110. data/lib/rigor/inference/declaration_walk/errors.rb +18 -0
  111. data/lib/rigor/inference/declaration_walk/shadow.rb +147 -0
  112. data/lib/rigor/inference/declaration_walk/traversal.rb +441 -0
  113. data/lib/rigor/inference/declaration_walk.rb +7 -0
  114. data/lib/rigor/inference/def_node_resolver.rb +26 -3
  115. data/lib/rigor/inference/definer_resolution.rb +266 -0
  116. data/lib/rigor/inference/expression_typer.rb +351 -61
  117. data/lib/rigor/inference/external_ancestor_resolution.rb +13 -16
  118. data/lib/rigor/inference/global_write_census.rb +7 -4
  119. data/lib/rigor/inference/guard_rebinding.rb +54 -13
  120. data/lib/rigor/inference/in_effect_refinements/proc_literals.rb +83 -0
  121. data/lib/rigor/inference/in_effect_refinements/refine_defs.rb +54 -0
  122. data/lib/rigor/inference/in_effect_refinements/scope_reads.rb +282 -0
  123. data/lib/rigor/inference/in_effect_refinements/walk.rb +276 -0
  124. data/lib/rigor/inference/in_effect_refinements.rb +256 -0
  125. data/lib/rigor/inference/key_presence_guard/scan.rb +281 -0
  126. data/lib/rigor/inference/key_presence_guard.rb +303 -0
  127. data/lib/rigor/inference/macro_block_self_type.rb +73 -14
  128. data/lib/rigor/inference/method_dispatcher/alias_strict_nominals.rb +36 -0
  129. data/lib/rigor/inference/method_dispatcher/arithmetic_sequence_elements.rb +73 -0
  130. data/lib/rigor/inference/method_dispatcher/imprecise_argument.rb +68 -0
  131. data/lib/rigor/inference/method_dispatcher/integer_step_block_params.rb +81 -0
  132. data/lib/rigor/inference/method_dispatcher/iterator_dispatch.rb +6 -0
  133. data/lib/rigor/inference/method_dispatcher/keyword_arguments.rb +267 -0
  134. data/lib/rigor/inference/method_dispatcher/method_folding.rb +2 -1
  135. data/lib/rigor/inference/method_dispatcher/overload_selector.rb +122 -105
  136. data/lib/rigor/inference/method_dispatcher/rbs_dispatch.rb +305 -54
  137. data/lib/rigor/inference/method_dispatcher/splat_arity.rb +71 -0
  138. data/lib/rigor/inference/method_dispatcher.rb +44 -11
  139. data/lib/rigor/inference/module_function_state.rb +214 -0
  140. data/lib/rigor/inference/multi_target_binder.rb +21 -16
  141. data/lib/rigor/inference/narrowing.rb +166 -22
  142. data/lib/rigor/inference/object_mixins.rb +180 -0
  143. data/lib/rigor/inference/parameter_inference_collector.rb +10 -9
  144. data/lib/rigor/inference/project_patched_methods.rb +10 -5
  145. data/lib/rigor/inference/project_patched_scanner.rb +71 -3
  146. data/lib/rigor/inference/refine_census.rb +239 -0
  147. data/lib/rigor/inference/refine_self.rb +317 -0
  148. data/lib/rigor/inference/refined_dispatch/own_method.rb +403 -0
  149. data/lib/rigor/inference/refined_dispatch.rb +272 -0
  150. data/lib/rigor/inference/scope_indexer/certainty.rb +176 -0
  151. data/lib/rigor/inference/scope_indexer/class_cvars_collector.rb +35 -0
  152. data/lib/rigor/inference/scope_indexer/def_nestings_collector.rb +47 -0
  153. data/lib/rigor/inference/scope_indexer/layered_def_nestings.rb +36 -0
  154. data/lib/rigor/inference/scope_indexer/member_layouts_collector.rb +61 -0
  155. data/lib/rigor/inference/scope_indexer/mixin_accumulator.rb +143 -0
  156. data/lib/rigor/inference/scope_indexer/superclasses_collector.rb +64 -0
  157. data/lib/rigor/inference/scope_indexer.rb +2129 -354
  158. data/lib/rigor/inference/singleton_hook_decline.rb +253 -0
  159. data/lib/rigor/inference/statement_evaluator.rb +149 -32
  160. data/lib/rigor/inference/string_mutation.rb +9 -3
  161. data/lib/rigor/inference/synthetic_method_scanner.rb +2 -1
  162. data/lib/rigor/inference/version_guard.rb +19 -10
  163. data/lib/rigor/language_server/project_context.rb +31 -0
  164. data/lib/rigor/language_server/server.rb +18 -0
  165. data/lib/rigor/plugin/base.rb +28 -8
  166. data/lib/rigor/plugin/inflector.rb +1 -1
  167. data/lib/rigor/plugin/io_boundary.rb +124 -7
  168. data/lib/rigor/plugin/isolation.rb +1 -1
  169. data/lib/rigor/plugin/macro/block_as_method.rb +51 -8
  170. data/lib/rigor/plugin/trust_policy.rb +3 -3
  171. data/lib/rigor/project_environment.rb +29 -6
  172. data/lib/rigor/protection/discovery_seed.rb +18 -9
  173. data/lib/rigor/protection/mutation_cache.rb +4 -4
  174. data/lib/rigor/protection/mutator.rb +1 -1
  175. data/lib/rigor/reflection/constant_ancestors.rb +211 -46
  176. data/lib/rigor/reflection/constant_path.rb +33 -7
  177. data/lib/rigor/reflection.rb +41 -10
  178. data/lib/rigor/scope/discovery_index.rb +258 -3
  179. data/lib/rigor/scope/resolution_chain/relevance.rb +165 -0
  180. data/lib/rigor/scope/resolution_chain.rb +1032 -0
  181. data/lib/rigor/scope.rb +480 -328
  182. data/lib/rigor/sig_gen/alias_index.rb +1 -1
  183. data/lib/rigor/sig_gen/classification.rb +4 -1
  184. data/lib/rigor/sig_gen/effect_annotation.rb +2 -2
  185. data/lib/rigor/sig_gen/generator.rb +77 -21
  186. data/lib/rigor/sig_gen/method_candidate.rb +2 -2
  187. data/lib/rigor/sig_gen/skip_reason_catalog.rb +12 -0
  188. data/lib/rigor/source/alias_names.rb +52 -0
  189. data/lib/rigor/source/literals.rb +3 -2
  190. data/lib/rigor/source/symbol_name.rb +20 -0
  191. data/lib/rigor/type/combinator.rb +59 -3
  192. data/lib/rigor/type/float_range.rb +4 -1
  193. data/lib/rigor/type/nominal.rb +6 -1
  194. data/lib/rigor/version.rb +1 -1
  195. data/plugins/rigor-ac-library-rb/lib/rigor/plugin/ac_library_rb.rb +28 -0
  196. data/plugins/rigor-ac-library-rb/lib/rigor-ac-library-rb.rb +3 -0
  197. data/plugins/rigor-ac-library-rb/sig/ac_library_rb/convolution.rbs +7 -0
  198. data/plugins/rigor-ac-library-rb/sig/ac_library_rb/data_structures.rbs +160 -0
  199. data/plugins/rigor-ac-library-rb/sig/ac_library_rb/graph.rbs +94 -0
  200. data/plugins/rigor-ac-library-rb/sig/ac_library_rb/math.rbs +42 -0
  201. data/plugins/rigor-ac-library-rb/sig/ac_library_rb/modint.rbs +71 -0
  202. data/plugins/rigor-ac-library-rb/sig/ac_library_rb/object.rbs +37 -0
  203. data/plugins/rigor-ac-library-rb/sig/ac_library_rb/segtree.rbs +83 -0
  204. data/plugins/rigor-actionpack/lib/rigor/plugin/actionpack/analyzer.rb +2 -1
  205. data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord/association_roots.rb +100 -0
  206. data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord/model_discoverer.rb +94 -10
  207. data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord.rb +43 -2
  208. data/plugins/rigor-activerecord/sig/active_record/relation.rbs +3 -1
  209. data/plugins/rigor-alba/lib/rigor/plugin/alba/resource_collector.rb +149 -0
  210. data/plugins/rigor-alba/lib/rigor/plugin/alba/resource_discoverer.rb +74 -0
  211. data/plugins/rigor-alba/lib/rigor/plugin/alba/resource_index.rb +104 -0
  212. data/plugins/rigor-alba/lib/rigor/plugin/alba.rb +85 -0
  213. data/plugins/rigor-alba/lib/rigor-alba.rb +3 -0
  214. data/plugins/rigor-alba/sig/alba.rbs +4 -0
  215. data/plugins/rigor-ffi/lib/rigor/plugin/ffi/analyzer.rb +2 -2
  216. data/plugins/rigor-ffi/lib/rigor/plugin/ffi/target_detector.rb +29 -12
  217. data/plugins/rigor-ffi/lib/rigor/plugin/ffi.rb +13 -2
  218. data/plugins/rigor-minitest/lib/rigor/plugin/minitest/assertion_analyzer.rb +130 -21
  219. data/plugins/rigor-minitest/lib/rigor/plugin/minitest.rb +6 -4
  220. data/plugins/rigor-pundit/lib/rigor/plugin/pundit/analyzer.rb +4 -4
  221. data/plugins/rigor-rails-i18n/lib/rigor/plugin/rails_i18n/analyzer.rb +1 -1
  222. data/plugins/rigor-rails-routes/lib/rigor/plugin/rails_routes/routes_parser.rb +1 -1
  223. data/plugins/rigor-rbs-inline/lib/rigor/plugin/rbs_inline.rb +31 -0
  224. data/plugins/rigor-typelizer/lib/rigor/plugin/typelizer/serializer_collector.rb +102 -0
  225. data/plugins/rigor-typelizer/lib/rigor/plugin/typelizer/serializer_discoverer.rb +87 -0
  226. data/plugins/rigor-typelizer/lib/rigor/plugin/typelizer/serializer_index.rb +76 -0
  227. data/plugins/rigor-typelizer/lib/rigor/plugin/typelizer.rb +73 -0
  228. data/plugins/rigor-typelizer/lib/rigor-typelizer.rb +3 -0
  229. data/sig/rigor/analysis/dependency_source_inference/index.rbs +1 -1
  230. data/sig/rigor/environment.rbs +2 -0
  231. data/sig/rigor/plugin/io_boundary.rbs +4 -0
  232. data/sig/rigor/scope.rbs +69 -4
  233. data/sig/rigor/source.rbs +4 -0
  234. data/sig/rigor.rbs +3 -1
  235. data/skills/rigor-project-init/SKILL.md +37 -11
  236. data/skills/rigor-project-init/references/02-configure.md +7 -2
  237. metadata +90 -1
data/lib/rigor/scope.rb CHANGED
@@ -3,6 +3,9 @@
3
3
  require_relative "type"
4
4
  require_relative "environment"
5
5
  require_relative "scope/discovery_index"
6
+ require_relative "scope/resolution_chain"
7
+ require_relative "inference/definer_resolution"
8
+ require_relative "inference/in_effect_refinements"
6
9
  require_relative "analysis/fact_store"
7
10
  require_relative "analysis/dependency_recorder"
8
11
  require_relative "inference/expression_typer"
@@ -24,12 +27,12 @@ module Rigor
24
27
  :indexed_narrowings, :method_chain_narrowings,
25
28
  :declaration_sourced, :published_constant_sourced,
26
29
  :source_path, :discovery, :struct_fold_safe_locals,
27
- :opaque_block_self, :singleton_class_body, :lexical_nesting,
30
+ :opaque_block_self, :block_self_unknown, :singleton_class_body, :lexical_nesting,
28
31
  :dynamic_origins, :local_origins, :ivar_origins,
29
32
  :void_origins, :plugin_typed_calls,
30
33
  :optimistic_origins, :optimistic_locals, :optimistic_ivars,
31
34
  :repeated_or_writes, :match_frame,
32
- :constant_narrowings, :guard_records
35
+ :constant_narrowings, :guard_records, :bot_guard_classes, :declared_refinements
33
36
 
34
37
  # ADR-53 Track A — the seed-time discovery tables live on the {DiscoveryIndex} the scope carries by a single
35
38
  # reference; the per-table readers stay on Scope so engine call sites and plugins are unaffected by the
@@ -60,13 +63,30 @@ module Rigor
60
63
  # Issue #1120 — `{refined class name => {method name => [refining module names]}}`: what each
61
64
  # `refine X do … end` block defines, visible only where a `using` of its module is in effect.
62
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
+
63
80
  # Issue #1367 — the project's `Inference::GlobalWriteCensus`, a `Set` of frozen entries.
64
81
  def discovered_global_write_census = @discovery.discovered_global_write_census
65
82
  def discovered_includes = @discovery.discovered_includes
66
83
  # Issue #1123 — `{qualified class or module name => [module names it `prepend`s, as written]}`, in
67
- # instance-ancestor search order (nearest prepend first). Feeds {#prepends_of}, the one table that tells
68
- # a `prepend` from an `include` at `def`-priority level.
69
- def discovered_prepends = @discovery.discovered_prepends
84
+ # instance-ancestor search order (nearest prepend first). The one table that tells a `prepend` from an
85
+ # `include`: {ResolutionChain} reads it to put a prepended module ahead of the class.
86
+ # The plugin-facing view: each class's prepends once, nearest first. The discovery index itself keeps every
87
+ # statement (a repeated `prepend` is a no-op in Ruby, the first statement winning), and the resolution chain
88
+ # reads that raw list.
89
+ def discovered_prepends = @discovery.discovered_prepends.transform_values { |names| names.uniq.freeze }.freeze
70
90
  def discovered_extends = @discovery.discovered_extends
71
91
  def discovered_class_sources = @discovery.discovered_class_sources
72
92
  # Issue #644 — `{qualified constant name => Set[declaring file]}`; seeded only on an ADR-46 recording run.
@@ -85,9 +105,13 @@ module Rigor
85
105
  # Issue #1290 — the census names sharing `name`'s last segment that some write other than a memo `||=`
86
106
  # assigns: the ones that shadow an outer constant of the name, so the lexical ladder stops at them
87
107
  # (`Reflection.resolve_constant_type`). That asks once per constant reference, so a bare name is looked up
88
- # as it stands rather than split.
108
+ # as it stands rather than split, and a qualified one is sliced after its last `::` rather than split into
109
+ # an Array of every segment ([#1502](https://github.com/rigortype/rigor/issues/1502)). The two agree on every
110
+ # name a parsed constant path spells. Only the `Foo::` Prism recovers from a syntax error differs, and no
111
+ # file with a parse error is typed.
89
112
  def shadowing_constant_names(name)
90
- segment = name.include?("::") ? name.split("::").last : name
113
+ separator = name.rindex("::")
114
+ segment = separator ? name[(separator + 2)..] : name
91
115
  @discovery.constant_shadowers[segment] || EMPTY_BOUND_CONSTANT_NAMES
92
116
  end
93
117
 
@@ -173,7 +197,9 @@ module Rigor
173
197
  # today.
174
198
  # - `receiver_name` is the variable's Symbol.
175
199
  # - `key` is the Ruby value of the literal index (Symbol / String / Integer). Non-literal keys
176
- # (`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.
177
203
  IndexedKey = Data.define(:receiver_kind, :receiver_name, :key)
178
204
 
179
205
  # Narrowing key for a no-arg / no-block method-call chain `receiver.method_name` (a "single-hop" chain per A1
@@ -241,12 +267,21 @@ module Rigor
241
267
  # analysis cannot see runs, so a call or block that may run project or unresolved code restores each recorded
242
268
  # binding to the union of this pre-guard type and its narrowed one ({#forget_guard_narrowings}). A write drops
243
269
  # the record ({#with_global}), and a name the frame-local special-variable machinery owns is never recorded.
270
+ # Issue #1446 — a class guard's narrowing of an instance variable is recorded too, keyed `[:ivar, :@name]`
271
+ # ({#with_guarded_ivar}), since that code may reach `self` and rebind it; a write drops it ({#without_ivar_guard}).
244
272
  EMPTY_GUARD_RECORDS = {}.freeze
273
+ # Issue #1446 — the classes a class guard narrowed a receiver to where the narrowing left it `bot`, keyed like
274
+ # {EMPTY_GUARD_RECORDS} with `[:local, :name]` too: `return unless @cb.is_a?(Proc)` binds `@cb` to `bot` when the
275
+ # guard is disjoint from its binding, but the value that passes it is a `Proc` (or a subclass of one), and a call
276
+ # on it dispatches on that class. `Inference::GuardRebinding` reads the class a `bot` receiver runs its methods
277
+ # on from here ({#bot_guard_classes_for}); a `bot` no class guard produced has no entry. A write or another
278
+ # narrowing of the name drops its entry, and a join keeps one only both arms hold.
279
+ EMPTY_BOT_GUARD_CLASSES = {}.freeze
245
280
  private_constant :EMPTY_VAR_BINDINGS, :EMPTY_INDEXED_NARROWINGS,
246
281
  :EMPTY_CHAIN_NARROWINGS, :EMPTY_DECLARATION_SOURCED,
247
282
  :EMPTY_FOLD_SAFE, :EMPTY_ORIGINS, :EMPTY_PUBLISHED_CONSTANT_SOURCED,
248
283
  :EMPTY_PUBLISHED_CONSTANT_IVARS, :EMPTY_REPEATED_OR_WRITES,
249
- :EMPTY_CONSTANT_NARROWINGS, :EMPTY_GUARD_RECORDS
284
+ :EMPTY_CONSTANT_NARROWINGS, :EMPTY_GUARD_RECORDS, :EMPTY_BOT_GUARD_CLASSES
250
285
 
251
286
  class << self
252
287
  def empty(environment: Environment.default, source_path: nil)
@@ -287,7 +322,7 @@ module Rigor
287
322
  @plugin_typed_calls.key?(node)
288
323
  end
289
324
 
290
- def initialize(
325
+ def initialize( # rubocop:disable Metrics/AbcSize, Metrics/MethodLength -- one keyword and assignment per field
291
326
  environment:, locals:,
292
327
  fact_store: Analysis::FactStore.empty,
293
328
  self_type: nil,
@@ -302,6 +337,7 @@ module Rigor
302
337
  source_path: nil,
303
338
  struct_fold_safe_locals: EMPTY_FOLD_SAFE,
304
339
  opaque_block_self: false,
340
+ block_self_unknown: false,
305
341
  singleton_class_body: false,
306
342
  lexical_nesting: nil,
307
343
  dynamic_origins: {}.compare_by_identity,
@@ -315,7 +351,9 @@ module Rigor
315
351
  repeated_or_writes: EMPTY_REPEATED_OR_WRITES,
316
352
  match_frame: nil,
317
353
  constant_narrowings: EMPTY_CONSTANT_NARROWINGS,
318
- guard_records: EMPTY_GUARD_RECORDS
354
+ guard_records: EMPTY_GUARD_RECORDS,
355
+ bot_guard_classes: EMPTY_BOT_GUARD_CLASSES,
356
+ declared_refinements: Inference::InEffectRefinements::EMPTY
319
357
  )
320
358
  @environment = environment
321
359
  @locals = locals
@@ -332,6 +370,7 @@ module Rigor
332
370
  @source_path = source_path
333
371
  @struct_fold_safe_locals = struct_fold_safe_locals
334
372
  @opaque_block_self = opaque_block_self
373
+ @block_self_unknown = block_self_unknown
335
374
  @singleton_class_body = singleton_class_body
336
375
  @lexical_nesting = lexical_nesting
337
376
  @dynamic_origins = dynamic_origins
@@ -346,6 +385,8 @@ module Rigor
346
385
  @match_frame = match_frame
347
386
  @constant_narrowings = constant_narrowings
348
387
  @guard_records = guard_records
388
+ @bot_guard_classes = bot_guard_classes
389
+ @declared_refinements = declared_refinements
349
390
  freeze
350
391
  end
351
392
 
@@ -358,6 +399,14 @@ module Rigor
358
399
  self
359
400
  end
360
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
+
361
410
  def optimistic_local(name) = Inference::OptimisticOrigin.bound_cause(@optimistic_locals[name.to_sym])
362
411
  def optimistic_ivar(name) = Inference::OptimisticOrigin.bound_cause(@optimistic_ivars[name.to_sym])
363
412
 
@@ -440,7 +489,8 @@ module Rigor
440
489
  # rvalue is one.
441
490
  published_constant_sourced: drop_published_constant_sourced_for(:local, name),
442
491
  local_origins: drop_origin(@local_origins, name),
443
- optimistic_locals: keep_marks ? @optimistic_locals : drop_origin(@optimistic_locals, name))
492
+ optimistic_locals: keep_marks ? @optimistic_locals : drop_origin(@optimistic_locals, name),
493
+ bot_guard_classes: drop_bot_guard_class(:local, name))
444
494
  end
445
495
  private :bind_local
446
496
 
@@ -455,6 +505,39 @@ module Rigor
455
505
  rebuild(self_type: type)
456
506
  end
457
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
+
458
541
  # Issue #652 — installs the body's REAL `Module.nesting`, innermost first, as recorded at declaration
459
542
  # time by `Inference::StatementEvaluator` (`["Admin::UsersController"]` for a compact
460
543
  # `class Admin::UsersController`, `["Admin::UsersController", "Admin"]` for the nested spelling). A
@@ -497,15 +580,52 @@ module Rigor
497
580
  # `Array#each` without knowing the callee. The flag is set at every block entry that leaves `self_type`
498
581
  # unnarrowed and is inherited by every scope derived inside the block; it never leaks past the block,
499
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.
500
585
  def entering_opaque_block
501
- return self if @opaque_block_self
586
+ return self if @opaque_block_self && @block_self_unknown == true
502
587
 
503
- rebuild(opaque_block_self: true)
588
+ rebuild(opaque_block_self: true, block_self_unknown: true)
504
589
  end
505
590
 
506
591
  # True when this scope sits inside a block whose `self` is unmodelled ({#entering_opaque_block}).
507
592
  def opaque_block_self? = @opaque_block_self
508
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
+
509
629
  # Issue #963 — marks the body of a `class << ...` as such. Inside it `self` is the SINGLETON class, which
510
630
  # Rigor models with the same `Singleton[X]` carrier a `class X` body gets, so the carrier alone cannot say
511
631
  # which of the two a scope came from. The distinction decides what `define_method` does: in a `class << self`
@@ -523,6 +643,19 @@ module Rigor
523
643
  rebuild(singleton_class_body: flag)
524
644
  end
525
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
+
526
659
  # True when this scope IS a `class << ...` body (not merely inside one lexically — a `def` reached from it
527
660
  # answers false).
528
661
  def singleton_class_body? = @singleton_class_body
@@ -557,6 +690,11 @@ module Rigor
557
690
  end
558
691
 
559
692
  def with_ivar(name, type)
693
+ bind_ivar(name, type, @guard_records)
694
+ end
695
+
696
+ # The one body behind {#with_ivar} and {#with_guarded_ivar}, which differ only in the guard records they leave.
697
+ def bind_ivar(name, type, guard_records)
560
698
  new_indexed_narrowings = drop_indexed_narrowings_for(:ivar, name)
561
699
  new_chain_narrowings = drop_chain_narrowings_for(:ivar, name)
562
700
  # ADR-58 WD1 — a method-local ivar write or narrowing is flow-live: drop any declaration-sourced mark so
@@ -568,8 +706,50 @@ module Rigor
568
706
  declaration_sourced: drop_declaration_sourced_for(:ivar, name),
569
707
  published_constant_sourced: drop_published_constant_sourced_for(:ivar, name),
570
708
  ivar_origins: drop_origin(@ivar_origins, name),
571
- optimistic_ivars: drop_origin(@optimistic_ivars, name))
709
+ optimistic_ivars: drop_origin(@optimistic_ivars, name),
710
+ guard_records: guard_records,
711
+ bot_guard_classes: drop_bot_guard_class(:ivar, name))
712
+ end
713
+ private :bind_ivar
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
+ )
572
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?
573
753
 
574
754
  # ADR-58 WD1 — used by the method-entry seed to mark an ivar whose only provenance is the class-ivar index.
575
755
  # Unlike `with_ivar` this binds the type AND records the declaration-sourced mark in one transition.
@@ -680,7 +860,8 @@ module Rigor
680
860
  name = name.to_sym
681
861
  written = rebuild(globals: @globals.merge(name => type).freeze,
682
862
  declaration_sourced: drop_declaration_sourced_for(:global, name),
683
- guard_records: drop_guard_record(:global, name))
863
+ guard_records: drop_guard_record(:global, name),
864
+ bot_guard_classes: drop_bot_guard_class(:global, name))
684
865
  alias_name = STDOUT_ALIASES[name]
685
866
  alias_name ? written.forget_guard_narrowing_of(:global, alias_name) : written
686
867
  end
@@ -717,7 +898,28 @@ module Rigor
717
898
  records = record ? add_guard_record([:global, name].freeze, pre_guard) : drop_guard_record(:global, name)
718
899
  rebuild(globals: @globals.merge(name => type).freeze,
719
900
  declaration_sourced: drop_declaration_sourced_for(:global, name),
720
- guard_records: records)
901
+ guard_records: records,
902
+ bot_guard_classes: drop_bot_guard_class(:global, name))
903
+ end
904
+
905
+ # Issue #1446 — binds the instance variable `name` to `type` on a class guard's edge, recording `pre_guard` as
906
+ # {#with_guarded_global} does. The truthiness, `nil?`, `&.` and `respond_to?` guards narrow an instance variable
907
+ # through {#with_ivar} and record nothing.
908
+ def with_guarded_ivar(name, type, pre_guard)
909
+ name = name.to_sym
910
+ bind_ivar(name, type, add_guard_record([:ivar, name].freeze, pre_guard))
911
+ end
912
+
913
+ # Issue #1446 — this scope with no guard record for the instance variable `name`: what a write to it leaves.
914
+ # {#with_ivar} keeps the record, since a narrowing binds through it too.
915
+ def without_ivar_guard(name)
916
+ records = drop_guard_record(:ivar, name.to_sym)
917
+ records.equal?(@guard_records) ? self : rebuild(guard_records: records)
918
+ end
919
+
920
+ # Issue #1446 — true when a class guard's narrowing of the instance variable `name` is live.
921
+ def guard_narrowed_ivar?(name)
922
+ !@guard_records.empty? && @guard_records.key?([:ivar, name.to_sym])
721
923
  end
722
924
 
723
925
  # Issue #1429 — the type a guard narrowed the constant reference `key` to, or nil.
@@ -731,7 +933,23 @@ module Rigor
731
933
  # {#with_guarded_global} does.
732
934
  def with_constant_narrowing(key, type, pre_guard)
733
935
  rebuild(constant_narrowings: @constant_narrowings.merge(key => type).freeze,
734
- guard_records: add_guard_record([:constant, key].freeze, pre_guard))
936
+ guard_records: add_guard_record([:constant, key].freeze, pre_guard),
937
+ bot_guard_classes: drop_bot_guard_class(:constant, key))
938
+ end
939
+
940
+ # Issue #1446 — this scope with `class_names` recorded as the classes a class guard narrowed the receiver `[kind,
941
+ # name]` to, where the guard left it `bot` ({EMPTY_BOT_GUARD_CLASSES}). Call it after the narrowing, which drops
942
+ # any earlier entry.
943
+ def with_bot_guard_classes(kind, name, class_names)
944
+ key = [kind, kind == :constant ? name : name.to_sym].freeze
945
+ rebuild(bot_guard_classes: @bot_guard_classes.merge(key => class_names.dup.freeze).freeze)
946
+ end
947
+
948
+ # Issue #1446 — the classes a class guard narrowed the `bot` receiver `[kind, name]` to, or nil.
949
+ def bot_guard_classes_for(kind, name)
950
+ return nil if @bot_guard_classes.empty?
951
+
952
+ @bot_guard_classes[[kind, name]]
735
953
  end
736
954
 
737
955
  # Issue #1429 — every constant reference whose last segment is `name` with no narrowing: what a write to a constant
@@ -741,7 +959,9 @@ module Rigor
741
959
  return self if keys.empty?
742
960
 
743
961
  records = @guard_records.reject { |(kind, key), _| kind == :constant && keys.include?(key) }
744
- rebuild(constant_narrowings: @constant_narrowings.except(*keys).freeze, guard_records: records.freeze)
962
+ classes = @bot_guard_classes.except(*keys.map { |key| [:constant, key] }).freeze
963
+ rebuild(constant_narrowings: @constant_narrowings.except(*keys).freeze, guard_records: records.freeze,
964
+ bot_guard_classes: classes)
745
965
  end
746
966
 
747
967
  # Issue #1429 — the constant reference `key` with no narrowing.
@@ -749,11 +969,12 @@ module Rigor
749
969
  return self unless @constant_narrowings.key?(key)
750
970
 
751
971
  rebuild(constant_narrowings: @constant_narrowings.except(key).freeze,
752
- guard_records: @guard_records.except([:constant, key]).freeze)
972
+ guard_records: @guard_records.except([:constant, key]).freeze,
973
+ bot_guard_classes: drop_bot_guard_class(:constant, key))
753
974
  end
754
975
 
755
- # True when a guard's narrowing of a global or constant is live, the state {#forget_guard_narrowings} drops and
756
- # so the gate on every scan that decides whether to.
976
+ # True when a guard's narrowing of a global, constant or instance variable is live, the state
977
+ # {#forget_guard_narrowings} drops and so the gate on every scan that decides whether to.
757
978
  def guard_narrowed?
758
979
  !@guard_records.empty?
759
980
  end
@@ -762,23 +983,19 @@ module Rigor
762
983
  # the union of its narrowed type and the binding the guard narrowed, and no record is left. The union, not the
763
984
  # pre-guard binding alone, because the code may leave the value as it was: `$stdout.is_a?(StringIO)`, then a
764
985
  # helper, reads `IO | StringIO`, which keeps `$stdout.string` quiet as the guard intended.
986
+ #
987
+ # Issue #1446 — an instance variable a class guard narrowed is restored the same way.
765
988
  def forget_guard_narrowings
766
989
  return self if @guard_records.empty?
767
990
 
768
- globals = @globals
769
- constants = @constant_narrowings
991
+ tables = { global: @globals, constant: @constant_narrowings, ivar: @ivars }
770
992
  @guard_records.each do |(kind, name), pre_guard|
771
- if kind == :global
772
- current = globals[name]
773
- globals = globals.merge(name => Type::Combinator.union(pre_guard, current)) if current
774
- else
775
- current = constants[name]
776
- constants = constants.merge(name => Type::Combinator.union(pre_guard, current)) if current
777
- end
993
+ table = tables.fetch(kind)
994
+ current = table[name]
995
+ tables[kind] = table.merge(name => Type::Combinator.union(pre_guard, current)) if current
778
996
  end
779
- rebuild(globals: globals.frozen? ? globals : globals.freeze,
780
- constant_narrowings: constants.frozen? ? constants : constants.freeze,
781
- guard_records: EMPTY_GUARD_RECORDS)
997
+ globals, constants, ivars = tables.values_at(:global, :constant, :ivar).map { |t| t.frozen? ? t : t.freeze }
998
+ rebuild(globals: globals, constant_narrowings: constants, ivars: ivars, guard_records: EMPTY_GUARD_RECORDS)
782
999
  end
783
1000
 
784
1001
  # Issue #1362 (ADR-58 parity, ADR-117 Decision point 2) — used by the method-entry and top-level seeds to bind a
@@ -998,6 +1215,18 @@ module Rigor
998
1215
  @self_type.nil?
999
1216
  end
1000
1217
 
1218
+ # ADR-119 WD1 — the RAW slot of the def tables for `class_name` and `method_name`: the instance side by default,
1219
+ # the singleton side for `kind: :singleton`. The slot is a live `Prism::DefNode` or a {Inference::DefHandle}
1220
+ # (ADR-85 WD3), returned as stored: no dependency recording and no handle resolution. ADR-119 C1d/C2 decide how
1221
+ # a contested slot answers here. Callers that need a node go through {#user_def_for} or {#singleton_def_for}.
1222
+ # The `same_slot` census reads of this slot stay justified: certainty reaches them through the declaration
1223
+ # signature the producer fingerprints (C1d-a), not through this accessor.
1224
+ def def_node_slot(class_name, method_name, kind = :instance)
1225
+ table = kind == :singleton ? discovered_singleton_def_nodes : discovered_def_nodes
1226
+ per_class = table[class_name]
1227
+ per_class && per_class[method_name]
1228
+ end
1229
+
1001
1230
  # v0.0.2 #5 — per-class table mapping `method_name (Symbol) → Prism::DefNode`. Populated by `ScopeIndexer`
1002
1231
  # alongside `discovered_methods` for instance-side defs only (singleton-side and `define_method`-introduced
1003
1232
  # methods do not contribute a static body the engine can re-type). Consumed by `ExpressionTyper` to do
@@ -1085,12 +1314,15 @@ module Rigor
1085
1314
  same_file_top_level_def?(method_name) ? node : nil
1086
1315
  end
1087
1316
 
1317
+ # ADR-119 WD1 errata, deliberately unchanged: this compares the recorded site's FILE with the call's file, and
1318
+ # which file a def sits in does not depend on whether it executes. The `<toplevel>` slots are contested
1319
+ # truthfully (ADR-119 C1d-a) but unread until C2 makes the top-level reader decline on them.
1088
1320
  def same_file_top_level_def?(method_name)
1089
1321
  key = Inference::ScopeIndexer::TOP_LEVEL_DEF_KEY
1090
1322
  site = discovered_def_sources.dig(key, method_name.to_sym)
1091
1323
  return true if site.nil? || @source_path.nil?
1092
1324
 
1093
- File.expand_path(site.sub(/:\d+\z/, "")) == File.expand_path(@source_path)
1325
+ File.absolute_path(site.sub(/:\d+\z/, "")) == File.absolute_path(@source_path)
1094
1326
  end
1095
1327
  private :same_file_top_level_def?
1096
1328
 
@@ -1272,32 +1504,6 @@ module Rigor
1272
1504
  @discovery.discovered_includes[class_name.to_s] || []
1273
1505
  end
1274
1506
 
1275
- # Issue #1123 — the modules `class_name` `prepend`s, in instance-ancestor SEARCH order: the nearest
1276
- # prepend first, so `prepend A; prepend B` answers `["B", "A"]` and `prepend A, B` keeps `["A", "B"]`
1277
- # (each Ruby statement's argument list lands as one unit, ahead of the earlier statements'). Read by
1278
- # {#user_def_through_ancestors}'s prepend wedge — Ruby inserts a prepended module, and its own ancestry,
1279
- # IMMEDIATELY BEFORE the class that prepends it, so these names are searched ahead of the class's own
1280
- # `def`s. Empty for a class the project never prepends, which is the behaviour every class had before
1281
- # `prepend` was ordered at all.
1282
- #
1283
- # The same names ALSO appear in {#includes_of} — that table answers "which modules does this class
1284
- # carry", which is what the arity, visibility, reflection and constant-scope consumers ask, and a
1285
- # prepended module does contribute instance methods. This table adds the ORDER and the KIND; it never
1286
- # removes a name from the other one.
1287
- #
1288
- # The class dependency is recorded only on a HIT, as {#data_member_layout} does: the wedge asks this
1289
- # for every class the walk visits, so recording on a miss would give a caller file an ancestry edge on
1290
- # a class that prepends nothing — an edge where ADR-46 slice 4 keeps method-body reads at
1291
- # symbol granularity.
1292
- def prepends_of(class_name)
1293
- names = @discovery.discovered_prepends[class_name.to_s]
1294
- return EMPTY_MIXIN_NAMES if names.nil?
1295
-
1296
- record_class_dependency(class_name) if Analysis::DependencyRecorder.active?
1297
- names
1298
- end
1299
- private :prepends_of
1300
-
1301
1507
  # Issue #898 — the module names `extend`ed onto `class_name`'s SINGLETON, as written, gathered up the
1302
1508
  # as-written superclass chain because a singleton class inherits its superclass's singleton class
1303
1509
  # (`class Base; extend Comparable; end; class Widget < Base; end` leaves `Widget.is_a?(Comparable)`
@@ -1326,224 +1532,145 @@ module Rigor
1326
1532
  names.uniq
1327
1533
  end
1328
1534
 
1329
- # ADR-24 slice 2 — the user-side ancestor walk: resolves `method_name` against `class_name`'s own `def`s,
1330
- # then breadth-first through its included / prepended modules (mixins first, as Ruby orders them) and its
1331
- # superclass chain. Returns `[def_node, owner]`, or `[nil, nil]` when nothing in the project defines it.
1332
- # RBS-known / third-party ancestors resolve to no discovered class and end that branch.
1535
+ # ADR-24, amended for #1567 / #1568 / #1570 / #1571 — every reader below answers in Ruby's linearised
1536
+ # ancestor order, read from {ResolutionChain} (an internal class: `Scope`'s own surface is the plugin API
1537
+ # `spec/rigor/public_api_drift_spec.rb` pins, and the chain is not part of it). None walks `includes_of` /
1538
+ # `superclass_of` / the extends table itself; `spec/rigor/scope/ancestry_walker_detection_spec.rb` fails on
1539
+ # a method that does. Each keeps its signature and its return type exactly: only WHICH definer it answers
1540
+ # moved. `name_memo:` is still accepted and no longer needed — the chain memoises name resolution itself.
1541
+ UNUSED_NAME_MEMO = {}.freeze
1542
+ private_constant :UNUSED_NAME_MEMO
1333
1543
  #
1334
- # It lives HERE, not in a consumer, because it reads nothing but this scope's frozen discovery index
1335
- # (`user_def_for` / `includes_of` / `superclass_of` / the `discovered_*` tables) — the same property that
1336
- # lets `ExpressionTyper` memoise its results run-wide. Two callers depend on that single answer:
1337
- # `ExpressionTyper#compute_user_def_with_owner`, which wraps it in the run-scoped memo for the dispatch hot
1338
- # path, and {Inference::MethodDispatcher::StructMaterialization}, whose `.with` guard must see a `with`
1339
- # contributed by an included module. They forked once — the guard resolved own-class-only while the
1340
- # dispatcher walked ancestors — and answered differently about the same receiver expression (#598 review).
1544
+ # Every reader below asks {ResolutionChain#settle} whether its chain answer stands, and where it does not
1545
+ # answers what the walk it replaced answered ({ResolutionChain::MasterOrder}) — the tables cannot say which
1546
+ # world ran, and a disagreement is no reason to answer anything new.
1547
+
1548
+ # ADR-24 slice 2 — the user-side method lookup: the first project `def` of `method_name` along
1549
+ # `class_name`'s instance chain, as `[def_node, owner]`, or `[nil, nil]` when nothing the project declares
1550
+ # defines it. An external ancestor is passed over, as before: a caller asking "does the project define
1551
+ # this" must not read an RBS module's declaration as absence (#1572 is the typing read that will stop
1552
+ # there).
1341
1553
  #
1342
- # `name_memo` is a CACHE, not semantics: the per-edge as-written-name resolutions, so a caller resolving
1343
- # many methods against one class pays each ancestor edge once. Omit it and the walk is identical, just
1344
- # uncached.
1554
+ # It lives HERE, not in a consumer, because it reads nothing but this scope's frozen discovery index — the
1555
+ # property that lets `ExpressionTyper` memoise its results run-wide. `ExpressionTyper#compute_user_def_with_owner`
1556
+ # wraps it in that memo for the dispatch hot path, and {Inference::MethodDispatcher::StructMaterialization}'s
1557
+ # `.with` guard reads the same answer (#598 review).
1345
1558
  #
1346
- # Issue #1123 — a prepended module is searched through {#prepends_of} AHEAD of the class's own `def`s
1347
- # ({#prepend_wedge_names}), because Ruby inserts it, and its own ancestry, immediately before the class
1348
- # that prepends it: `class Base; def speak = "base"; end; Base.prepend(Loud)` answers `Loud#speak`, and
1349
- # the `super` in that body then reaches `Base#speak`. An included module still
1350
- # sits AFTER the class in this walk, but — since #1173 — `includes_of` feeds the mixin step in
1351
- # instance-ancestor search order, so `include A; include B` searches B first the way CRuby does. The
1352
- # wedge is searched at EVERY node, not just the entry class: a prepend on an ancestor (`class Sub <
1353
- # Base` where `Base` prepends a module) wins for the subclass too, exactly as Ruby dispatches it.
1354
- def user_def_through_ancestors(class_name, method_name, name_memo: {})
1355
- queue = [class_name.to_s]
1356
- seen = {}
1357
- visited = 0
1358
- until queue.empty?
1359
- current = queue.shift
1360
- next if current.nil? || seen[current]
1361
-
1362
- seen[current] = true
1363
- visited += 1
1364
- return ancestor_walk_gave_up if visited > ANCESTOR_WALK_LIMIT
1559
+ # #1567 — the walk was breadth-first until the chain replaced it: `class C < Base; include A` with `A`
1560
+ # including `M` answered `Base#foo` where Ruby calls `M#foo`. The prepend wedge #1123 added (a prepended
1561
+ # module searched ahead of the class's own `def`s) and the include search order #1173 fixed are both
1562
+ # positions on the chain now, at every class on it.
1563
+ def user_def_through_ancestors(class_name, method_name, name_memo: UNUSED_NAME_MEMO) # rubocop:disable Lint/UnusedMethodArgument
1564
+ chain = ResolutionChain.for(self, class_name.to_s, :instance, :methods)
1565
+ found = first_user_def(chain, method_name)
1566
+ owner = found&.last
1567
+ if chain.settle(self, owner, owner: owner) { |retro| first_user_def(retro, method_name)&.last } == :master
1568
+ found = master_user_def(class_name.to_s, method_name)
1569
+ end
1570
+ return found if found
1365
1571
 
1366
- wedge_hit = search_prepend_wedge(current, method_name, name_memo, seen)
1367
- return wedge_hit if wedge_hit
1572
+ chain.truncated? ? ancestor_walk_gave_up : [nil, nil]
1573
+ end
1368
1574
 
1369
- found = user_def_for(current, method_name)
1370
- return [found, current] if found
1575
+ def first_user_def(chain, method_name)
1576
+ chain.search(self) do |entry|
1577
+ next if entry.external?
1371
1578
 
1372
- enqueue_ancestors(current, queue, name_memo)
1579
+ node = user_def_for(entry.name, method_name)
1580
+ [node, entry.name] if node
1373
1581
  end
1374
- [nil, nil]
1375
1582
  end
1376
1583
 
1377
- # Issue #1123 — the `[def_node, owner]` pair a class's prepend wedge answers `method_name` with, or nil
1378
- # when nothing in the wedge defines it and the walk should ask the class's own `def`s next.
1379
- #
1380
- # Every name the wedge takes is marked on `seen`, the SAME set the walk's breadth-first step uses: a
1381
- # prepended module also sits in `includes_of`, so the mixin step would otherwise re-ask a name the wedge
1382
- # already answered — and, more importantly, marking it there keeps a `prepend` graph that loops back on
1383
- # the walk (a prepended module reached again from somewhere else) from being expanded twice.
1384
- def search_prepend_wedge(class_name, method_name, name_memo, seen)
1385
- prepend_wedge_names(class_name, name_memo).each do |name|
1386
- next if seen[name]
1387
-
1388
- seen[name] = true
1389
- found = user_def_for(name, method_name)
1390
- return [found, name] if found
1584
+ def master_user_def(class_name, method_name)
1585
+ names = ResolutionChain::MasterOrder.definer_sequence(self, class_name)
1586
+ names.each { |name| record_class_dependency(name) } if Analysis::DependencyRecorder.active?
1587
+ names.each do |name|
1588
+ node = user_def_for(name, method_name)
1589
+ return [node, name] if node
1391
1590
  end
1392
1591
  nil
1393
1592
  end
1394
-
1395
- # Issue #1123 — the qualified names the instance MRO searches ahead of `class_name`'s own definitions,
1396
- # in the order it searches them: for each of `class_name`'s prepends, nearest first, that module's own
1397
- # prepend wedge, then the module itself, then its includes and superclass.
1398
- #
1399
- # A prepended module's own ancestry is part of the wedge because Ruby inserts the whole sub-chain with
1400
- # it — `module Loud; include Loud::Prefix; end` prepended into `Base` puts `Loud::Prefix` ahead of
1401
- # `Base`, so the wedge has to reach it. `seen` is shared across one wedge computation and guards a cyclic
1402
- # `prepend`/`include` graph (and the overlap with `includes_of`, which carries the prepend names too);
1403
- # the walk's own set is applied by {#search_prepend_wedge}, and each name is expanded at most once.
1404
- def prepend_wedge_names(class_name, name_memo, seen = {})
1405
- out = []
1406
- prepends_of(class_name).each do |raw|
1407
- resolved = resolve_ancestor_class_name(class_name, raw, name_memo)
1408
- next if resolved.nil? || seen[resolved]
1409
-
1410
- seen[resolved] = true
1411
- out.concat(subchain_names(resolved, name_memo, seen))
1412
- end
1413
- out
1414
- end
1415
-
1416
- # Issue #1123 — appends `node`'s own instance search order to `out`: its prepend wedge, the node
1417
- # itself, then its includes and superclass, each expanded the same way. Since #1173 the include
1418
- # half is `includes_of`'s search order — the ORDER inside a prepended module's own include list is
1419
- # not re-derived here, only read.
1420
- def subchain_names(node, name_memo, seen)
1421
- out = prepend_wedge_names(node, name_memo, seen)
1422
- out << node
1423
- (includes_of(node) + [superclass_of(node)].compact).each do |raw|
1424
- resolved = resolve_ancestor_class_name(node, raw, name_memo)
1425
- next if resolved.nil? || seen[resolved]
1426
-
1427
- seen[resolved] = true
1428
- out.concat(subchain_names(resolved, name_memo, seen))
1593
+ private :first_user_def, :master_user_def
1594
+
1595
+ # Issue #731 — the singleton-side twin of {#user_def_through_ancestors}: the first `def self.` /
1596
+ # `class << self` body of `method_name` along `class_name`'s singleton chain, as `[def_node, owner]`, or
1597
+ # `[nil, nil]` — each class object's own table, then the modules it extends in Ruby's order, then the
1598
+ # superclass's. `ScopeIndexer` folds an extended module's own `def`s into the extender's table, so those
1599
+ # answer at the extender; a module the extended one includes is its own entry (#1567's singleton shape:
1600
+ # `class C < Base; extend A` with `A` including `M` is `M#foo`, not `Base.foo`). Where the chain does
1601
+ # not stand, the answer is the class objects' alone, as the superclass-only walk this replaced gave.
1602
+ def singleton_def_through_ancestors(class_name, method_name, name_memo: UNUSED_NAME_MEMO) # rubocop:disable Lint/UnusedMethodArgument
1603
+ chain = ResolutionChain.for(self, class_name.to_s, :singleton, :methods)
1604
+ found = first_singleton_def(chain, method_name)
1605
+ owner = found&.last
1606
+ if chain.settle(self, owner, owner: owner) { |retro| first_singleton_def(retro, method_name)&.last } == :master
1607
+ found = first_singleton_def(chain, method_name, :singleton)
1429
1608
  end
1430
- out
1431
- end
1432
- private :search_prepend_wedge, :prepend_wedge_names, :subchain_names
1433
-
1434
- # Issue #731 — the singleton-side twin of {#user_def_through_ancestors}: resolves `method_name` against
1435
- # `class_name`'s own `def self.` / `class << self` bodies, then up the SUPERCLASS chain. Returns
1436
- # `[def_node, owner]`, or `[nil, nil]`.
1437
- #
1438
- # Superclasses only, and that is the whole shape of class-method inheritance in Ruby: a subclass
1439
- # inherits its parent's class methods, while an `include`d module contributes INSTANCE methods and its
1440
- # own `def self.x` is not callable on the includer. `extend M` needs no walk either — `ScopeIndexer`
1441
- # folds an extend into the extender's own singleton entries before the table is frozen — so the one
1442
- # edge this misses is a module's singleton surface reached through some other route, which nothing
1443
- # resolves today.
1444
- def singleton_def_through_ancestors(class_name, method_name, name_memo: {})
1445
- queue = [class_name.to_s]
1446
- seen = {}
1447
- visited = 0
1448
- until queue.empty?
1449
- current = queue.shift
1450
- next if current.nil? || seen[current]
1609
+ return found if found
1451
1610
 
1452
- seen[current] = true
1453
- visited += 1
1454
- return ancestor_walk_gave_up if visited > ANCESTOR_WALK_LIMIT
1611
+ chain.truncated? ? ancestor_walk_gave_up : [nil, nil]
1612
+ end
1455
1613
 
1456
- found = singleton_def_for(current, method_name)
1457
- return [found, current] if found
1614
+ def first_singleton_def(chain, method_name, side = nil)
1615
+ chain.search(self, side: side) do |entry|
1616
+ next if entry.external?
1458
1617
 
1459
- enqueue_ancestors(current, queue, name_memo, mixins: false)
1618
+ name = entry.name
1619
+ node = entry.side == :singleton ? singleton_def_for(name, method_name) : user_def_for(name, method_name)
1620
+ [node, entry.name] if node
1460
1621
  end
1461
- [nil, nil]
1462
1622
  end
1623
+ private :first_singleton_def
1463
1624
 
1464
1625
  # Issue #633 — the ancestors a project class reaches that the project itself does NOT declare: the
1465
- # `< StandardError` / `< Array` superclasses and the `include Comparable` mixins the two walks above
1466
- # deliberately drop, gathered depth-first over the project ancestry (an included module's own
1467
- # sub-chain precedes the includer's superclass edge) so an inherited edge counts too — and, since
1468
- # #1173, so the groups arrive in instance-ancestor search order for the caller that adopts the
1469
- # FIRST answering one.
1626
+ # `< StandardError` / `< Array` superclasses and the `include Comparable` mixins, as the CANDIDATE LIST for
1627
+ # each as-written name ({#ancestor_name_candidates}: the nesting spellings first, the bare name last), in
1628
+ # Ruby's order along the instance chain — the order the caller that adopts the FIRST answering one (#1173)
1629
+ # depends on. Resolving a candidate means asking the RBS environment, which this frozen-index read does
1630
+ # not do; the caller takes the first candidate its own oracle knows. Where the chain does not stand, the
1631
+ # groups are the depth-first ones the walk this replaced emitted.
1470
1632
  #
1471
- # Each entry is the CANDIDATE LIST for one as-written name ({#ancestor_name_candidates}: the nesting
1472
- # spellings first, the bare name last), not a resolved class — resolving it means asking the RBS
1473
- # environment, which this frozen-index walk does not read. The caller takes the first candidate its own
1474
- # oracle knows and ignores the rest, the same "most-qualified first" order the project-side resolution
1475
- # uses.
1476
- # Issue #527 slice 1 — `mixins: false` follows the SUPERCLASS chain only. Ruby reaches an included
1477
- # module's methods too, so the default answers the whole ancestry; a consumer resolving one KIND of
1478
- # inheritance edge at a time (the dispatch arm lands `< Hash` before `include Enumerable`) narrows
1479
- # it, the same way {#singleton_def_through_ancestors} narrows {#enqueue_ancestors}.
1480
- def external_ancestor_name_candidates(class_name, name_memo: {}, mixins: true)
1481
- groups = []
1482
- collect_external_ancestors(class_name.to_s, groups, name_memo, mixins, {}, [0])
1633
+ # Issue #527 slice 1 — `mixins: false` answers the superclass edge only: a consumer resolving one KIND of
1634
+ # inheritance edge at a time (the dispatch arm lands `< Hash` before `include Enumerable`) narrows it, and
1635
+ # records only the superclass chain it read.
1636
+ def external_ancestor_name_candidates(class_name, name_memo: UNUSED_NAME_MEMO, mixins: true) # rubocop:disable Lint/UnusedMethodArgument
1637
+ chain = ResolutionChain.for(self, class_name.to_s, :instance, :methods)
1638
+ groups = external_groups(chain, mixins)
1639
+ if chain.settle(self, groups) { |retro| external_groups(retro, mixins) } == :master
1640
+ groups = ResolutionChain::MasterOrder.external_groups(self, class_name.to_s, mixins)
1641
+ end
1642
+ if Analysis::DependencyRecorder.active?
1643
+ mixins ? chain.record(self) : chain.level_classes.each { |name| record_class_dependency(name) if name }
1644
+ end
1645
+ # Issue #527 — the cut is a budget event, not an answer: consumers read the groups as "the ancestors
1646
+ # this class reaches", so a short list must be visible in `--stats` beside the other walks' exhaustions.
1647
+ Inference::BudgetTrace.hit(Inference::BudgetTrace::ANCESTOR_WALK_LIMIT) if chain.truncated?
1483
1648
  groups
1484
1649
  end
1485
1650
 
1486
- # One node of {#external_ancestor_name_candidates}: the project-declared ancestors continue the walk,
1487
- # everything else is reported as a candidate list.
1488
- #
1489
- # Issue #1173 — the walk is depth-first pre-order, not breadth-first: a resolved ancestor's own
1490
- # edges are followed BEFORE the next sibling edge, because Ruby inserts an included module's whole
1491
- # sub-chain ahead of the includer's superclass (`class C < Hash; include M` where `M` itself
1492
- # includes `Widen` chains `C → M → Widen → Hash`; BFS would report `Hash` ahead of `Widen`). Each
1493
- # node's edges still go includes-then-superclass, and `includes_of` is already search order, so the
1494
- # emitted groups arrive in MRO order — which the #1173 include arm depends on when two ancestors
1495
- # both declare the name.
1496
- def collect_external_ancestors(current, groups, name_memo, mixins, seen, visited)
1497
- return if current.nil? || seen[current]
1498
-
1499
- seen[current] = true
1500
- visited[0] += 1
1501
- if visited[0] > ANCESTOR_WALK_LIMIT
1502
- # Issue #527 — the give-up is a budget event, not an answer. Consumers read the groups as
1503
- # "the ancestors this class reaches", so a truncated list must be visible in `--stats`
1504
- # alongside the other walks' exhaustions rather than silently short. Hit once: every node
1505
- # visited past the limit returns here too, and only the first should count.
1506
- Inference::BudgetTrace.hit(Inference::BudgetTrace::ANCESTOR_WALK_LIMIT) if visited[0] == ANCESTOR_WALK_LIMIT + 1
1507
- return
1508
- end
1509
-
1510
- raw_names = mixins ? includes_of(current).dup : []
1511
- raw_super = superclass_of(current)
1512
- raw_names << raw_super if raw_super
1513
- raw_names.each do |raw|
1514
- resolved = resolve_ancestor_class_name(current, raw, name_memo)
1515
- if resolved
1516
- collect_external_ancestors(resolved, groups, name_memo, mixins, seen, visited)
1517
- else
1518
- groups << ancestor_name_candidates(current, raw)
1519
- end
1520
- end
1651
+ def external_groups(chain, mixins)
1652
+ chain.entries.filter_map { |entry| entry.candidates if entry.external? && (mixins || entry.superclass_edge) }
1521
1653
  end
1522
- private :collect_external_ancestors
1654
+ private :external_groups
1523
1655
 
1524
- # The BFS node cap; a hierarchy past it gives up rather than walking unboundedly (ADR-41 WD4).
1525
- ANCESTOR_WALK_LIMIT = 100
1656
+ # The budget {ResolutionChain::LIMIT} enforces; a hierarchy past it gives up rather than reading an answer
1657
+ # off a cut chain (ADR-41 WD4).
1658
+ ANCESTOR_WALK_LIMIT = ResolutionChain::LIMIT
1526
1659
 
1527
1660
  EMPTY_HEADER_NESTING = [].freeze
1528
1661
  private_constant :EMPTY_HEADER_NESTING
1529
1662
 
1530
- # The answer for a class that prepends nothing — the common case, so the wedge does not allocate a
1531
- # fresh empty list per visited node. Shared and frozen, and never handed to a caller that could
1532
- # mutate it ({#prepends_of} is private to this walk).
1533
- EMPTY_MIXIN_NAMES = [].freeze
1534
- private_constant :EMPTY_MIXIN_NAMES
1535
-
1536
1663
  def ancestor_walk_gave_up
1537
1664
  Inference::BudgetTrace.hit(Inference::BudgetTrace::ANCESTOR_WALK_LIMIT)
1538
1665
  [nil, nil]
1539
1666
  end
1667
+ private :ancestor_walk_gave_up
1540
1668
 
1541
- # Issue #723 — the same ancestor walk as {#user_def_through_ancestors}, asked of the DISCOVERY table
1542
- # rather than the def-node table: does the project define `method_name` on `class_name` or on any
1543
- # ancestor the project itself declares? The two tables are not interchangeable —
1544
- # `discovered_methods` also carries `define_method`, `attr_*` and the whole singleton side, none of
1545
- # which contributes a `Prism::DefNode` — and the suppression probe shared by the `call.*` check rules
1546
- # needs the broader one.
1669
+ # Issue #723 — the question {#user_def_through_ancestors} asks, asked of the DISCOVERY table rather than
1670
+ # the def-node table: does the project define `method_name` on `class_name` or on any ancestor the project
1671
+ # itself declares? The two tables are not interchangeable — `discovered_methods` also carries
1672
+ # `define_method`, `attr_*` and the whole singleton side, none of which contributes a `Prism::DefNode` —
1673
+ # and the suppression probe shared by the `call.*` check rules needs the broader one.
1547
1674
  #
1548
1675
  # Why it must walk at all: `Analysis::CheckRules#source_declared_method?` asked `discovered_method?`,
1549
1676
  # which is keyed on the receiver's OWN name, while the typer resolves the same call through this
@@ -1551,72 +1678,47 @@ module Rigor
1551
1678
  # `call.undefined-method` on a method `dump_type` resolved on the same line of the same run — writing
1552
1679
  # MORE RBS made the run worse, the incentive #653 removed for plugin-typed calls.
1553
1680
  #
1554
- # `kind: :singleton` follows the superclass chain ONLY: a class method is inherited by a subclass, but
1555
- # an `include`d module contributes instance methods and its own `def self.x` is not callable on the
1556
- # includer. `extend M` is not lost to that narrowing — `Inference::ScopeIndexer` folds an extend into
1557
- # the extender's own singleton entries before this table is frozen.
1558
- def discovered_method_through_ancestors?(class_name, method_name, kind, name_memo: {})
1681
+ # `kind: :singleton` reads the singleton chain: the class objects' own methods (an extended module's are
1682
+ # folded into its extender's) and the instance methods of the modules they extend and those modules'
1683
+ # includes. Existence is a union, so the chain's two worlds, which hold the same modules, agree on it.
1684
+ def discovered_method_through_ancestors?(class_name, method_name, kind, name_memo: UNUSED_NAME_MEMO) # rubocop:disable Lint/UnusedMethodArgument
1559
1685
  return false if class_name.nil?
1560
1686
 
1561
- queue = [class_name.to_s]
1562
- seen = {}
1563
- visited = 0
1564
- until queue.empty?
1565
- current = queue.shift
1566
- next if current.nil? || seen[current]
1687
+ chain = ResolutionChain.for(self, class_name.to_s, kind == :singleton ? :singleton : :instance, :methods)
1688
+ found = chain.search(self) do |entry|
1689
+ next false if entry.external?
1567
1690
 
1568
- seen[current] = true
1569
- visited += 1
1570
- # Budget exhaustion is uncertainty, not absence: answering "not declared" here would hand a
1571
- # `call.undefined-method` a fired verdict it has no evidence for. Suppress and record the hit.
1572
- if visited > ANCESTOR_WALK_LIMIT
1573
- Inference::BudgetTrace.hit(Inference::BudgetTrace::ANCESTOR_WALK_LIMIT)
1574
- return true
1575
- end
1576
-
1577
- return true if discovered_method?(current, method_name, kind)
1578
-
1579
- enqueue_ancestors(current, queue, name_memo, mixins: kind != :singleton)
1691
+ discovered_method?(entry.name, method_name, entry.side == :singleton ? :singleton : kind_for(kind))
1580
1692
  end
1581
- false
1582
- end
1583
-
1584
- # Pushes `current`'s direct ancestors onto the BFS queue: included / prepended modules first (Ruby places
1585
- # mixins nearer than the superclass), then the superclass. Each as-written name is resolved against the
1586
- # nesting `current`'s declaration header is written in ({#ancestor_name_candidates}); names that resolve
1587
- # to no project class/module are dropped. `mixins: false` walks the superclass chain alone, for the
1588
- # singleton-side question where an `include` contributes nothing.
1589
- def enqueue_ancestors(current, queue, name_memo, mixins: true)
1590
- if mixins
1591
- includes_of(current).each do |raw|
1592
- resolved = resolve_ancestor_class_name(current, raw, name_memo)
1593
- queue.push(resolved) if resolved
1594
- end
1595
- end
1596
- raw_super = superclass_of(current)
1597
- return if raw_super.nil?
1693
+ return true if found
1694
+ # Budget exhaustion is uncertainty, not absence: answering "not declared" here would hand a
1695
+ # `call.undefined-method` a fired verdict it has no evidence for. Suppress and record the hit.
1696
+ return false unless chain.truncated?
1598
1697
 
1599
- resolved_super = resolve_ancestor_class_name(current, raw_super, name_memo)
1600
- queue.push(resolved_super) if resolved_super
1698
+ Inference::BudgetTrace.hit(Inference::BudgetTrace::ANCESTOR_WALK_LIMIT)
1699
+ true
1601
1700
  end
1602
1701
 
1603
- # Resolves an ancestor name AS WRITTEN (`"Base"`, or a qualified `"A::B"`) to a project-discovered class,
1604
- # following Ruby's `Module.nesting` constant lookup from the subclass's declaration HEADER, innermost
1605
- # first, then bare. nil when no candidate names a discovered user class.
1606
- def resolve_ancestor_class_name(subclass_qualified, raw_ancestor, name_memo)
1607
- by_subclass = (name_memo[subclass_qualified] ||= {})
1608
- return by_subclass[raw_ancestor] if by_subclass.key?(raw_ancestor)
1702
+ # A module entry on the singleton chain answers with its INSTANCE methods; on the instance chain, the
1703
+ # caller's kind stands.
1704
+ def kind_for(kind) = kind == :singleton ? :instance : kind
1705
+ private :kind_for
1609
1706
 
1610
- by_subclass[raw_ancestor] = compute_ancestor_class_name(subclass_qualified, raw_ancestor)
1707
+ # Pushes `current`'s direct ancestors onto a breadth-first queue: included / prepended modules first, then
1708
+ # the superclass, each resolved against the nesting `current`'s declaration header is written in; names
1709
+ # that resolve to no project class/module are dropped. `mixins: false` gives the superclass alone. No
1710
+ # reader in the engine walks this way any more (they read {ResolutionChain}); it stays for the plugin
1711
+ # surface, with the ADR-46 class edge the walk it served always filed.
1712
+ def enqueue_ancestors(current, queue, name_memo, mixins: true) # rubocop:disable Lint/UnusedMethodArgument
1713
+ record_class_dependency(current) if Analysis::DependencyRecorder.active?
1714
+ queue.concat(ResolutionChain.direct_ancestors(self, current.to_s, mixins))
1611
1715
  end
1612
- private :ancestor_walk_gave_up, :resolve_ancestor_class_name
1613
1716
 
1614
1717
  # Issue #682 — the candidate names an ancestor spelled `raw_ancestor` can denote in `subclass_qualified`,
1615
1718
  # in Ruby's lookup order, most-qualified first and the bare name last. The single owner of the question:
1616
- # three walks resolved a superclass / include name apiece ({#enqueue_ancestors} for method lookup,
1617
- # `Reflection.resolve_ancestor_name` for the constant ladder's ancestor rung, and
1618
- # `Analysis::CheckRules`' override-visibility rule), and each derived the order by PEELING the
1619
- # subclass's qualified name one `::` segment at a time.
1719
+ # three walks resolved a superclass / include name apiece (the method walk, the constant ladder's
1720
+ # ancestor rung, and `Analysis::CheckRules`' override-visibility rule — all three now {ResolutionChain}),
1721
+ # and each derived the order by PEELING the subclass's qualified name one `::` segment at a time.
1620
1722
  #
1621
1723
  # That peel is the NESTED spelling's answer, given to both spellings. Ruby evaluates a superclass
1622
1724
  # expression before entering the body, so the cref that governs it is the one the declaration's HEADER
@@ -1628,9 +1730,9 @@ module Rigor
1628
1730
  # The header nesting is a property of the DECLARATION, not of the reader, so it is read from the
1629
1731
  # discovery table `Inference::ScopeIndexer` records it in rather than from this scope's own
1630
1732
  # `#lexical_nesting`. Answering off the reader's chain would be wrong past the first hop of the ancestor
1631
- # BFS (the chain describes the reader's class, not the ancestor being resolved) and would silently
1632
- # corrupt the run-scoped ancestor memos in `Inference::ExpressionTyper#class_graph_buckets` and
1633
- # `Reflection.ancestor_constant_scopes`, both of which key on the class name alone because this walk is
1733
+ # chain (the chain describes the reader's class, not the ancestor being resolved) and would silently
1734
+ # corrupt the {ResolutionChain} memo and the ones layered on it (`Inference::ExpressionTyper#class_graph_buckets`,
1735
+ # `Reflection.ancestor_constant_scopes`), all of which key on the class name alone because this walk is
1634
1736
  # a pure function of the frozen discovery tables.
1635
1737
  #
1636
1738
  # The peel survives as the FALLBACK, for a class no declaration walk recorded — a scope seeded without
@@ -1737,10 +1839,6 @@ module Rigor
1737
1839
  end
1738
1840
  private :peeled_header_nesting
1739
1841
 
1740
- def compute_ancestor_class_name(subclass_qualified, raw_ancestor)
1741
- ancestor_name_candidates(subclass_qualified, raw_ancestor).find { |c| known_user_class?(c) }
1742
- end
1743
-
1744
1842
  # Issue #723 — `discovered_methods` is in the list because the other three miss a class whose only
1745
1843
  # project-side content is CLASS methods: `class Base; def self.build = :built; end` records no instance
1746
1844
  # def node, no superclass and no include, so the ancestor-name resolver did not recognise `Base` as a
@@ -1755,13 +1853,13 @@ module Rigor
1755
1853
  # narrower predicate (`discovered_classes` alone) would read a project class as having left the project
1756
1854
  # and attribute its root name to a same-named locked gem — a WRONG provenance label, which is the one
1757
1855
  # direction ADR-82 forbids.
1758
- private :ancestor_walk_gave_up, :compute_ancestor_class_name
1759
1856
 
1760
1857
  # Records, for a resolved cross-class ancestry read, every file that declares `class_name` (its declaration /
1761
- # reopening / superclass / include sites). The `discovered_class_sources` table it reads is populated only by
1762
- # the cross-file project pre-pass ({Inference::ScopeIndexer.discovered_def_index_for_paths}) and only when
1763
- # dependency recording is active. No-op when the class is not a project class (core / stdlib / gem names
1764
- # never appear in the source map). Gated by the caller on the recorder being active.
1858
+ # reopening / superclass / include sites). The `discovered_class_sources` table it reads is populated by the
1859
+ # cross-file project pre-pass ({Inference::ScopeIndexer.discovered_def_index_for_paths}); a scope built
1860
+ # without one holds none, and the read then files nothing. No-op when the class is not a project class
1861
+ # (core / stdlib / gem names never appear in the source map). Gated by the caller on the recorder being
1862
+ # active.
1765
1863
  def record_class_dependency(class_name)
1766
1864
  sites = @discovery.discovered_class_sources[class_name.to_s]
1767
1865
  return if sites.nil?
@@ -1781,8 +1879,7 @@ module Rigor
1781
1879
  # so a bare referent is re-checked when the class appears or disappears and at no other time. A file edge
1782
1880
  # would have re-checked every bare referent of every class the file declares on any edit to it.
1783
1881
  def record_class_existence(class_name)
1784
- segment = class_name.to_s.delete_prefix("::").split("::").last
1785
- Analysis::DependencyRecorder.read_name(:class, segment) if segment
1882
+ Analysis::DependencyRecorder.read_last_segment(:class, class_name.to_s)
1786
1883
  end
1787
1884
 
1788
1885
  # Issue #644 — the positive ADR-46 edge for a cross-file VALUE constant. `Reflection.constant_type_at`
@@ -1938,17 +2035,17 @@ module Rigor
1938
2035
 
1939
2036
  # Issue #1429 — a join keeps a guard record while the joined scope still narrows its name: a global both arms
1940
2037
  # bind, a constant both arms narrow (one an arm does not narrow reads its resolved type after the join, so its
1941
- # narrowing is gone). An arm without a record contributes nothing to the restore target, whose union with the
1942
- # joined binding {#forget_guard_narrowings} takes, so it still covers that arm's binding.
1943
- def join_guard_records(other, joined_globals, joined_constants)
2038
+ # narrowing is gone), and an instance variable both arms bind (#1446). An arm without a record contributes nothing
2039
+ # to the restore target, whose union with the joined binding {#forget_guard_narrowings} takes, so it still covers
2040
+ # that arm's binding.
2041
+ # `joined` maps each record kind to the joined scope's table of that kind.
2042
+ def join_guard_records(other, joined)
1944
2043
  mine = @guard_records
1945
2044
  theirs = other.guard_records
1946
2045
  return EMPTY_GUARD_RECORDS if mine.empty? && theirs.empty?
1947
2046
 
1948
2047
  merged = mine.merge(theirs) { |_key, left, right| Type::Combinator.union(left, right) }
1949
- kept = merged.select do |(kind, name), _|
1950
- kind == :global ? joined_globals.key?(name) : joined_constants.key?(name)
1951
- end
2048
+ kept = merged.select { |(kind, name), _| joined.fetch(kind).key?(name) }
1952
2049
  kept.empty? ? EMPTY_GUARD_RECORDS : kept.freeze
1953
2050
  end
1954
2051
  private :join_guard_records
@@ -1993,7 +2090,8 @@ module Rigor
1993
2090
  end
1994
2091
 
1995
2092
  def same_guard_state?(other)
1996
- @constant_narrowings == other.constant_narrowings && @guard_records == other.guard_records
2093
+ @constant_narrowings == other.constant_narrowings && @guard_records == other.guard_records &&
2094
+ @bot_guard_classes == other.bot_guard_classes
1997
2095
  end
1998
2096
 
1999
2097
  def rebuild(
@@ -2007,6 +2105,7 @@ module Rigor
2007
2105
  source_path: @source_path,
2008
2106
  struct_fold_safe_locals: @struct_fold_safe_locals,
2009
2107
  opaque_block_self: @opaque_block_self,
2108
+ block_self_unknown: @block_self_unknown,
2010
2109
  singleton_class_body: @singleton_class_body,
2011
2110
  lexical_nesting: @lexical_nesting,
2012
2111
  dynamic_origins: @dynamic_origins,
@@ -2020,7 +2119,9 @@ module Rigor
2020
2119
  repeated_or_writes: @repeated_or_writes,
2021
2120
  match_frame: @match_frame,
2022
2121
  constant_narrowings: @constant_narrowings,
2023
- guard_records: @guard_records
2122
+ guard_records: @guard_records,
2123
+ bot_guard_classes: @bot_guard_classes,
2124
+ declared_refinements: @declared_refinements
2024
2125
  )
2025
2126
  self.class.new(
2026
2127
  environment: environment, locals: locals,
@@ -2034,6 +2135,7 @@ module Rigor
2034
2135
  source_path: source_path,
2035
2136
  struct_fold_safe_locals: struct_fold_safe_locals,
2036
2137
  opaque_block_self: opaque_block_self,
2138
+ block_self_unknown: block_self_unknown,
2037
2139
  singleton_class_body: singleton_class_body,
2038
2140
  lexical_nesting: lexical_nesting,
2039
2141
  dynamic_origins: dynamic_origins,
@@ -2047,7 +2149,9 @@ module Rigor
2047
2149
  repeated_or_writes: repeated_or_writes,
2048
2150
  match_frame: match_frame,
2049
2151
  constant_narrowings: constant_narrowings,
2050
- guard_records: guard_records
2152
+ guard_records: guard_records,
2153
+ bot_guard_classes: bot_guard_classes,
2154
+ declared_refinements: declared_refinements
2051
2155
  )
2052
2156
  end
2053
2157
 
@@ -2065,7 +2169,8 @@ module Rigor
2065
2169
  result.freeze
2066
2170
  end
2067
2171
 
2068
- 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
2069
2174
  self.class.new(
2070
2175
  environment: @environment,
2071
2176
  locals: joined_locals.freeze,
@@ -2113,6 +2218,9 @@ module Rigor
2113
2218
  # block entry by `entering_opaque_block` and inherited through `rebuild`), so both arms usually carry
2114
2219
  # the same value and the `||` is that value.
2115
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),
2116
2224
  # Issue #963 — a body property like the two above, so both arms of an in-body merge carry the identical
2117
2225
  # value and the `||` is that value. `||` is also the safe direction on its own terms: keeping the mark
2118
2226
  # declines the `define_method` narrowing, which is the pre-#963 answer.
@@ -2137,15 +2245,44 @@ module Rigor
2137
2245
  # of a merge inside one body carry the same one; `||` keeps it should either arm lack it, since dropping
2138
2246
  # it only loses the frame's resets.
2139
2247
  match_frame: @match_frame || other.match_frame,
2140
- # Issue #1429 — the guard narrowings of globals and constants and their pre-guard records.
2141
- **join_guard_narrowings(other, joined_globals)
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),
2251
+ # Issue #1429 — the guard narrowings of globals and constants and their pre-guard records, and of instance
2252
+ # variables (#1446).
2253
+ **join_guard_narrowings(other, joined_globals, joined_ivars)
2142
2254
  )
2143
2255
  end
2144
2256
 
2145
- def join_guard_narrowings(other, joined_globals)
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
+
2266
+ def join_guard_narrowings(other, joined_globals, joined_ivars)
2146
2267
  joined_constants = join_constant_narrowings(other)
2147
- { constant_narrowings: joined_constants,
2148
- guard_records: join_guard_records(other, joined_globals, joined_constants) }
2268
+ joined = { global: joined_globals, ivar: joined_ivars, constant: joined_constants }
2269
+ { constant_narrowings: joined_constants, guard_records: join_guard_records(other, joined),
2270
+ bot_guard_classes: join_bot_guard_classes(other) }
2271
+ end
2272
+
2273
+ # Issue #1446 — an entry both arms hold, with the classes of either: the joined receiver reads `bot` only where
2274
+ # both arms do. One only an arm holds is dropped, so a call on the joined `bot` reads as on an unknown one.
2275
+ def join_bot_guard_classes(other)
2276
+ mine = @bot_guard_classes
2277
+ theirs = other.bot_guard_classes
2278
+ return mine if mine.equal?(theirs)
2279
+ return EMPTY_BOT_GUARD_CLASSES if mine.empty? || theirs.empty?
2280
+
2281
+ joined = mine.each_with_object({}) do |(key, names), acc|
2282
+ other_names = theirs[key]
2283
+ acc[key] = (names | other_names).freeze if other_names
2284
+ end
2285
+ joined.empty? ? EMPTY_BOT_GUARD_CLASSES : joined.freeze
2149
2286
  end
2150
2287
 
2151
2288
  # Issue #1429 — a constant reference both arms narrow reads the union; one only an arm narrows reads its resolved
@@ -2280,11 +2417,19 @@ module Rigor
2280
2417
  sym_kind = receiver_kind.to_sym
2281
2418
  sym_name = receiver_name.to_sym
2282
2419
  filtered = @indexed_narrowings.reject do |k, _|
2283
- 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)
2284
2421
  end
2285
2422
  filtered.size == @indexed_narrowings.size ? @indexed_narrowings : filtered.freeze
2286
2423
  end
2287
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
+
2288
2433
  # ADR-58 WD1 — set/clear the declaration-sourced provenance mark.
2289
2434
  # Issue #667 — the #667 carrier's add/drop pair. Deliberately not reusing
2290
2435
  # {#add_declaration_sourced} / {#drop_declaration_sourced_for}: the two sets are joined by opposite
@@ -2359,6 +2504,13 @@ module Rigor
2359
2504
  @guard_records.merge(key => pre_guard).freeze
2360
2505
  end
2361
2506
 
2507
+ def drop_bot_guard_class(kind, name)
2508
+ return @bot_guard_classes if @bot_guard_classes.empty?
2509
+
2510
+ key = [kind, kind == :constant ? name : name.to_sym]
2511
+ @bot_guard_classes.key?(key) ? @bot_guard_classes.except(key).freeze : @bot_guard_classes
2512
+ end
2513
+
2362
2514
  def drop_guard_record(kind, name)
2363
2515
  return @guard_records if @guard_records.empty?
2364
2516