rigortype 0.4.0 → 0.4.1

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 (102) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +4 -3
  3. data/docs/handbook/08-understanding-errors.md +3 -2
  4. data/docs/install.md +87 -15
  5. data/docs/manual/02-cli-reference.md +2 -2
  6. data/docs/manual/03-configuration.md +5 -0
  7. data/docs/manual/08-skills.md +4 -3
  8. data/docs/manual/12-caching.md +43 -4
  9. data/docs/manual/14-rails-quickstart.md +19 -11
  10. data/lib/rigor/analysis/check_rules/source_arity.rb +163 -59
  11. data/lib/rigor/analysis/check_rules.rb +118 -87
  12. data/lib/rigor/analysis/dependency_recorder.rb +200 -18
  13. data/lib/rigor/analysis/incremental_run_slot/write_guard.rb +266 -0
  14. data/lib/rigor/analysis/incremental_run_slot.rb +171 -0
  15. data/lib/rigor/analysis/incremental_session.rb +273 -49
  16. data/lib/rigor/analysis/plugin_fact_fingerprint.rb +24 -13
  17. data/lib/rigor/analysis/reachability/scan_cache.rb +2 -1
  18. data/lib/rigor/analysis/run_cache_key.rb +27 -8
  19. data/lib/rigor/analysis/run_stats.rb +2 -2
  20. data/lib/rigor/analysis/runner/diagnostic_aggregator.rb +1 -1
  21. data/lib/rigor/analysis/runner/pool_coordinator.rb +2 -2
  22. data/lib/rigor/analysis/runner/project_pre_passes.rb +21 -13
  23. data/lib/rigor/analysis/runner.rb +337 -46
  24. data/lib/rigor/analysis/shadow_harness.rb +27 -0
  25. data/lib/rigor/analysis/source_rbs_gate.rb +157 -0
  26. data/lib/rigor/analysis/template_unit_paths.rb +6 -4
  27. data/lib/rigor/analysis/template_units.rb +15 -5
  28. data/lib/rigor/analysis/worker_session.rb +4 -3
  29. data/lib/rigor/cache/descriptor.rb +40 -1
  30. data/lib/rigor/cache/file_digest.rb +28 -0
  31. data/lib/rigor/cache/incremental_snapshot.rb +70 -8
  32. data/lib/rigor/cache/store.rb +63 -3
  33. data/lib/rigor/cache/value_digest.rb +325 -0
  34. data/lib/rigor/cli/check_command.rb +83 -13
  35. data/lib/rigor/cli/doctor_command.rb +1 -1
  36. data/lib/rigor/cli/effects_report.rb +2 -2
  37. data/lib/rigor/cli/unused_command.rb +3 -3
  38. data/lib/rigor/cli.rb +1 -1
  39. data/lib/rigor/config_audit.rb +3 -2
  40. data/lib/rigor/config_path.rb +29 -0
  41. data/lib/rigor/configuration/severity_profile.rb +5 -0
  42. data/lib/rigor/configuration.rb +6 -5
  43. data/lib/rigor/effects/plugin_facts.rb +6 -6
  44. data/lib/rigor/environment/bundle_sig_discovery.rb +5 -3
  45. data/lib/rigor/environment/lockfile_resolver.rb +10 -1
  46. data/lib/rigor/environment/rbs_collection_discovery.rb +10 -1
  47. data/lib/rigor/environment/rbs_loader.rb +1 -1
  48. data/lib/rigor/environment/source_rbs_synthesis.rb +147 -0
  49. data/lib/rigor/environment.rb +13 -70
  50. data/lib/rigor/inference/block_parameter_binder.rb +29 -13
  51. data/lib/rigor/inference/captured_locals.rb +7 -1
  52. data/lib/rigor/inference/declaration_walk/context.rb +195 -0
  53. data/lib/rigor/inference/declaration_walk/errors.rb +18 -0
  54. data/lib/rigor/inference/declaration_walk/shadow.rb +147 -0
  55. data/lib/rigor/inference/declaration_walk/traversal.rb +441 -0
  56. data/lib/rigor/inference/declaration_walk.rb +7 -0
  57. data/lib/rigor/inference/def_node_resolver.rb +3 -2
  58. data/lib/rigor/inference/definer_resolution.rb +266 -0
  59. data/lib/rigor/inference/expression_typer.rb +136 -45
  60. data/lib/rigor/inference/external_ancestor_resolution.rb +13 -16
  61. data/lib/rigor/inference/guard_rebinding.rb +53 -12
  62. data/lib/rigor/inference/macro_block_self_type.rb +14 -2
  63. data/lib/rigor/inference/method_dispatcher/rbs_dispatch.rb +46 -26
  64. data/lib/rigor/inference/module_function_state.rb +213 -0
  65. data/lib/rigor/inference/multi_target_binder.rb +21 -16
  66. data/lib/rigor/inference/narrowing.rb +147 -17
  67. data/lib/rigor/inference/parameter_inference_collector.rb +10 -9
  68. data/lib/rigor/inference/scope_indexer/certainty.rb +176 -0
  69. data/lib/rigor/inference/scope_indexer/class_cvars_collector.rb +35 -0
  70. data/lib/rigor/inference/scope_indexer/def_nestings_collector.rb +47 -0
  71. data/lib/rigor/inference/scope_indexer/layered_def_nestings.rb +36 -0
  72. data/lib/rigor/inference/scope_indexer/member_layouts_collector.rb +61 -0
  73. data/lib/rigor/inference/scope_indexer/mixin_accumulator.rb +130 -0
  74. data/lib/rigor/inference/scope_indexer/superclasses_collector.rb +64 -0
  75. data/lib/rigor/inference/scope_indexer.rb +1443 -295
  76. data/lib/rigor/inference/singleton_hook_decline.rb +253 -0
  77. data/lib/rigor/inference/statement_evaluator.rb +80 -14
  78. data/lib/rigor/plugin/base.rb +28 -8
  79. data/lib/rigor/plugin/io_boundary.rb +82 -4
  80. data/lib/rigor/plugin/isolation.rb +1 -1
  81. data/lib/rigor/plugin/trust_policy.rb +3 -3
  82. data/lib/rigor/protection/discovery_seed.rb +18 -9
  83. data/lib/rigor/protection/mutation_cache.rb +3 -3
  84. data/lib/rigor/reflection/constant_ancestors.rb +96 -47
  85. data/lib/rigor/reflection/constant_path.rb +22 -7
  86. data/lib/rigor/reflection.rb +2 -3
  87. data/lib/rigor/scope/discovery_index.rb +218 -2
  88. data/lib/rigor/scope/resolution_chain/relevance.rb +165 -0
  89. data/lib/rigor/scope/resolution_chain.rb +884 -0
  90. data/lib/rigor/scope.rb +295 -322
  91. data/lib/rigor/sig_gen/alias_index.rb +1 -1
  92. data/lib/rigor/sig_gen/effect_annotation.rb +2 -2
  93. data/lib/rigor/sig_gen/generator.rb +22 -16
  94. data/lib/rigor/type/combinator.rb +25 -1
  95. data/lib/rigor/version.rb +1 -1
  96. data/plugins/rigor-ffi/lib/rigor/plugin/ffi.rb +10 -0
  97. data/sig/rigor/environment.rbs +2 -0
  98. data/sig/rigor/plugin/io_boundary.rbs +1 -0
  99. data/sig/rigor/scope.rbs +43 -1
  100. data/skills/rigor-project-init/SKILL.md +37 -11
  101. data/skills/rigor-project-init/references/02-configure.md +7 -2
  102. metadata +25 -1
@@ -0,0 +1,884 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Rigor
4
+ class Scope
5
+ # ADR-24 (amended for #1567, #1568, #1570 and #1571) — Ruby's method resolution order for one project class or
6
+ # module, built from the discovery tables. Internal: `Scope`'s readers, the check rules and `Reflection`
7
+ # read it, and it is not part of the plugin surface `Scope` exposes. This is the ONE place that walks
8
+ # `discovered_includes`,
9
+ # `discovered_prepends`, `discovered_superclasses` and `discovered_extends` transitively to answer "which
10
+ # definer does Ruby call": `Scope#user_def_through_ancestors` and its siblings, the override rules, the
11
+ # constant ladder's ancestor rung and `call.wrong-arity`'s level walk all read it, and
12
+ # `spec/rigor/scope/ancestry_walker_detection_spec.rb` fails on a new method that walks the tables itself.
13
+ #
14
+ # The walks it replaced were breadth-first, which is not Ruby's order: `class C < Base; include A` with `A`
15
+ # including `M` reaches `Base` (depth 1) before `M` (depth 2), while `C.ancestors` is `[C, A, M, Base]`.
16
+ # The chain instead replays CRuby's own insertion rule (`include_modules_at` in `class.c`) over the tables:
17
+ #
18
+ # - A class's chain is the class followed by its superclass's chain; a module has no superclass.
19
+ # - Each `prepend`, in statement order, inserts the module's own chain directly before the class, skipping a
20
+ # module already in the class's prepend region (and ONLY there — `class C < B; prepend W` where `B`
21
+ # includes `W` gives `[W, C, B, W]`).
22
+ # - Each `include`, in statement order (`include M, N` is `include N; include M`), inserts the module's own
23
+ # chain directly after the class, skipping a module already ANYWHERE in the chain — the superclass's
24
+ # included and prepended modules too (#1570: `class C < Base; include M` where `Base` includes `M` is a
25
+ # no-op). A skipped module found between the insertion point and the superclass moves the insertion point
26
+ # past it, which is how `include M; include Z` with `Z` including `M` gives `[C, Z, M]`.
27
+ # - The singleton side is the class object's singleton, its `extend`s and `class << self; include`s
28
+ # inserted by the include rule (each module expanded to its instance chain), then the superclass's
29
+ # singleton chain.
30
+ #
31
+ # The insertion rule is Ruby's for the order the statements RAN in, and the tables hold only their final
32
+ # state. The chain therefore replays every edge once, eagerly, and counts a FORK wherever Ruby's own order
33
+ # could have gone another way. The single-route argument (`class.c`): Ruby skips an insertion only when the
34
+ # module is already present, and that takes a second route to it through the tables. So where the replay
35
+ # inserts every entry exactly once and no owner on the chain is unsettled (below), Ruby's first-occurrence
36
+ # ancestor order equals the replay's under every interleaving of the statements. A fork is a place where a
37
+ # module reached a chain by a second route: an `include` skipped because the chain carries the module, a
38
+ # prepend skipped or an entry of a prepended module's own chain already carried by the class. The ONE
39
+ # decision every first-definer reader makes is {#settle}: with no fork the chain stands; with exactly one
40
+ # include-side fork, at or after the class, on the last entry of the sub-chain being inserted, Ruby has
41
+ # exactly two worlds (the skip made, or the insertion made — `class Base; include M; end` after `class C <
42
+ # Base; include M` keeps both copies, `[C, M, Base, M]`), and the chain stands only where that retro world
43
+ # gives the reader the same answer; with any other fork or two or more the worlds are more than two and
44
+ # the chain never stands. Where it does not stand, a reader answers what the walk this chain replaced
45
+ # answered ({MasterOrder}): the tables cannot say which world ran, and a disagreement is no reason to
46
+ # answer anything new. That leaves #1570's redundant `include` with the answer master gave, since the two
47
+ # worlds disagree there; ADR-119's arity decision point is where it gets fixed.
48
+ #
49
+ # Two things the tables do not record, so the chain cannot reproduce them:
50
+ #
51
+ # - The interleaving of a body's `include` and `prepend` statements, and whether a class both includes and
52
+ # prepends one module (`include M; prepend M` is `[M, C, M]` in Ruby; the tables record it as a plain
53
+ # `prepend M`). Each kind keeps its statement order and the chain processes a body's prepends before its
54
+ # includes. Only the trailing duplicate is ever missing, so a first-occurrence reader is unaffected
55
+ # until a later include propagates into the prepended module (`module M0; prepend M1; end`, `class C;
56
+ # include M0; end`, then `module M0; include M1; end` and `module M1; include M3; end` is `[C, M1, M0,
57
+ # M1, M3]` in Ruby and `[C, M1, M3, M0]` here), which is why an entry of a prepended module's own chain
58
+ # that the class already carries is a fork. The same holds through a module's own includes (`class C2;
59
+ # include A; prepend M` with `A` including `M` is `[M, C2, A, M]` in Ruby and `[M, C2, A]` here).
60
+ # CRuby also propagates a later prepend into the includers of a prepended module and leaves a trailing
61
+ # duplicate (`module M0; prepend M3; end; class Base; prepend M0; end; module M0; prepend M4; end` is
62
+ # `[M4, M3, M0, M4, Base]`); the first-occurrence order is the chain's, and the duplicate is not modelled.
63
+ # - `class << self; prepend P`, which the extends table records (and the extends fold copies) as an
64
+ # `extend`: the chain places `P` after the singleton, where Ruby places it before.
65
+ #
66
+ # An ancestor the project does not declare (`Enumerable`, `< ActiveRecord::Base`) is an EXTERNAL entry at
67
+ # its Ruby position, carrying the name as written and the names that spelling can denote; its own RBS
68
+ # ancestry is not expanded. `Object`, `Kernel` and `BasicObject` are implicit after a chain whose last
69
+ # superclass the project declares. Which as-written names count as project classes is the reader's
70
+ # FLAVOR, because the three consumers asked three different questions before this chain existed and a bug
71
+ # fix does not unify them:
72
+ #
73
+ # - `:methods` — `Scope#known_user_class?`, the method walks' predicate since #723;
74
+ # - `:constants` — a key of `discovered_superclasses`, `discovered_includes` or `discovered_classes`, the
75
+ # constant ladder's (#354), which also admits a module that holds only constants;
76
+ # - `:arity` — `Scope#known_user_class?` or a `discovered_parameter_envelopes` key, `call.wrong-arity`'s
77
+ # (#992), which also expands a name a compact-header rename left ambiguous to BOTH classes it can name
78
+ # (#986): their mutual order is load order, and the arity rule's per-level agreement absorbs it.
79
+ #
80
+ # Building a chain records nothing (ADR-46): it reads the tables raw and is memoised per discovery index.
81
+ # A READ records, through {#search} and {#record}.
82
+ class ResolutionChain
83
+ # One ancestor on a chain. A project entry names a class or module (`name`) on `side` `:instance` (its
84
+ # instance methods) or `:singleton` (the class object's singleton). An external entry has no `name`: it
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.
87
+ # `last_segment` is the unqualified name a project entry files its negative class edge under, computed once
88
+ # when the entry is interned.
89
+ Entry = Data.define(:name, :side, :raw, :candidates, :superclass_edge, :last_segment) do
90
+ def external? = name.nil?
91
+ end
92
+
93
+ # How many project entries a chain may hold before it is cut (ADR-41 WD4). The breadth-first walks
94
+ # counted visited project nodes against the same number.
95
+ LIMIT = 100
96
+
97
+ # ADR-119 WD2 — what marked a chain unsettled, one per marking node and side: `node` is the class or
98
+ # module whose mixin edges' order is not a fact, `kind` the side read (`:include` or `:extend`), `listed`
99
+ # the entries `DiscoveryIndex#unpositioned_mixins` lists for it (`"*"` included), and `multi_file` whether
100
+ # 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.
102
+ Mark = Data.define(:node, :kind, :listed, :multi_file)
103
+
104
+ NO_MARKS = [].freeze
105
+
106
+ attr_reader :root, :side, :entries, :level_starts, :level_classes, :flavor
107
+
108
+ # rubocop:disable-next Metrics/ParameterLists
109
+ def initialize(root:, side:, entries:, level_starts:, level_classes:, levels_end:, truncated:, forks: 0,
110
+ marks: NO_MARKS, flavor: :methods, retro: nil)
111
+ @root = root
112
+ @side = side
113
+ @entries = entries
114
+ @level_starts = level_starts
115
+ @level_classes = level_classes
116
+ @levels_end = levels_end
117
+ @truncated = truncated
118
+ @forks = forks
119
+ @marks = marks
120
+ @flavor = flavor
121
+ @retro = retro
122
+ @memo = {} # the chain is frozen; this table is not, and holds {#levels}
123
+ freeze
124
+ end
125
+
126
+ # True when the chain was cut at {LIMIT} project entries: a reader that found nothing on it cannot tell
127
+ # "no definer" from "a definer past the cut", which is budget uncertainty, not absence.
128
+ def truncated? = @truncated
129
+
130
+ # How many forks the replay met while this chain was built, counted across every module sub-chain it drew
131
+ # on (a memoised sub-chain hands its own count up): each entry that reached the chain by a second route.
132
+ # An over-count only ever sends a reader to master's answer.
133
+ attr_reader :forks
134
+ alias skip_count forks
135
+
136
+ # True when a class on the chain has mixin edges whose order the tables cannot vouch for
137
+ # (`DiscoveryIndex#unpositioned_mixins`, or a class declared in several files with several edges).
138
+ def unsettled? = !@marks.empty?
139
+
140
+ # The ONE decision every first-definer reader makes: does `answer`, read off this chain, stand, or does
141
+ # the reader answer what the walk this chain replaced answered ({MasterOrder})? Returns `:chain` or
142
+ # `:master`.
143
+ #
144
+ # - Unsettled (a class on the chain has a mixin edge whose order is not a fact, or is declared in several
145
+ # files with several edges): master's answer, whatever the fork count, and the block is not called.
146
+ # - No fork: every entry was inserted once by a single route, so Ruby's first-occurrence order is the
147
+ # chain's under every interleaving, and the chain stands.
148
+ # - One fork that is an include-side skip at or after the class on the last entry of the sub-chain: two
149
+ # worlds (the skip made, the insertion made). The chain stands when the retro world gives the same
150
+ # answer, which the block computes from the chain it is handed; where they differ, or the retro world
151
+ # was too large to build, master's answer stands.
152
+ # - Any other fork, or two or more: more than two worlds, so no answer read off two of them is
153
+ # trustworthy. Master's answer stands, and the block is not called.
154
+ #
155
+ # ADR-119 WD2 — `unknown_for: name` is the candidate-set read's option (only {Inference::DefinerResolution}
156
+ # passes it): a `:master` verdict becomes `:unknown`, since a reader that can decline has no use for the
157
+ # replaced walk's answer, and a chain whose every mark is discharged for `name` (see {Relevance}) and that
158
+ # has no fork is read as settled. Without the option the verdict is never `:unknown`: the readers that
159
+ # compare it with `:master` would read a leaked `:unknown` as `:chain`.
160
+ def settle(scope, answer, owner: nil, unknown_for: nil, &)
161
+ # The verdict depends on EVERY node of the chain (a fork, an unpositioned edge, a second declaring file),
162
+ # and `:master` changes the answer, so the entries past the answer are dependencies whichever way it
163
+ # goes; `search` files only the entries ahead of an answer. `owner` names the entry that answered.
164
+ record_beyond(scope, owner, unknown_for.nil?)
165
+ return settle_unknown(scope, answer, unknown_for, &) unless unknown_for.nil?
166
+ return :master unless @marks.empty?
167
+
168
+ case @forks
169
+ when 0 then :chain
170
+ when 1 then !@retro.nil? && answer == yield(@retro) ? :chain : :master
171
+ else :master
172
+ end
173
+ end
174
+
175
+ # {#settle} under `unknown_for:`: a mark that is not discharged for the name, a fork the marks overlap
176
+ # (relevance is argued for the fork-free case only), a second fork or a retro world that answers
177
+ # otherwise are all `:unknown`. The retro world is the one built when the chain had exactly one fork and
178
+ # no mark; a chain with a fork is never narrowed, so none is built on demand.
179
+ def settle_unknown(scope, answer, name)
180
+ return settle_marked(scope, name) unless @marks.empty?
181
+
182
+ case @forks
183
+ when 0 then :chain
184
+ when 1 then !@retro.nil? && answer == yield(@retro) ? :chain : :unknown
185
+ else :unknown
186
+ end
187
+ end
188
+
189
+ # A marked chain stands only with no fork and every mark discharged for the name.
190
+ def settle_marked(scope, name)
191
+ fork_free = @forks.zero?
192
+ return :unknown unless fork_free && @marks.all? { |mark| Relevance.discharged?(scope, self, mark, name) }
193
+
194
+ :chain
195
+ end
196
+ private :settle_marked
197
+ private :settle_unknown
198
+
199
+ # Walks the entries in Ruby's order from `start` up to (not including) `stop`, and returns the first
200
+ # truthy value the block gives for an entry, or nil. `side:` yields only the entries on that side.
201
+ #
202
+ # The ADR-46 contract is the one the breadth-first walks had, in Ruby's order and in the order they filed
203
+ # it: the answer depends on every project entry AHEAD of it — each one's ancestry edges decided where the
204
+ # next one sits — so each records a class edge as the search passes it, and so does the root, first, when
205
+ # it does not head the part searched (its prepends, or its own edges when the search starts past it,
206
+ # placed the rest). The entry that answers records none of its own: its method edge is the caller's
207
+ # `Scope#user_def_for`. A read that finds nothing has passed every entry.
208
+ def search(scope, start = 0, stop = nil, side: nil)
209
+ recording = Analysis::DependencyRecorder.active?
210
+ stop ||= @entries.size
211
+ record_head(scope, start, side) if recording
212
+ index = start
213
+ while index < stop
214
+ entry = @entries[index]
215
+ answer = side.nil? || entry.side == side ? yield(entry) : nil
216
+ return answer if answer
217
+
218
+ ResolutionChain.record_entry(scope, entry, side) if recording
219
+ index += 1
220
+ end
221
+ nil
222
+ end
223
+
224
+ # Records the ADR-46 class edge of the root and of every project entry ahead of `stop` (on `side`, when
225
+ # given; every entry when `stop` is nil) — the dependency of a read that consulted those positions.
226
+ def record(scope, stop = nil, side: nil)
227
+ return unless Analysis::DependencyRecorder.active?
228
+
229
+ ResolutionChain.record_class(scope, @root)
230
+ (stop || @entries.size).times { |index| ResolutionChain.record_entry(scope, @entries[index], side) }
231
+ end
232
+
233
+ # What a {#settle} verdict adds to what the search already filed. The search files the root and every entry
234
+ # AHEAD of the answer; entries strictly after it matter only through the verdict (a fork or an unpositioned
235
+ # edge there sends the reader to master's answer), so they are filed here, and only where master's answer
236
+ # could differ from the chain's:
237
+ #
238
+ # - the answer is the root's own entry and nothing is prepended (the root heads its chain), and the verdict
239
+ # is `:chain` or `:master` (`master_only`): master's first candidate is the root too, so no verdict can
240
+ # move it and nothing more is filed. Under `unknown_for:` a mark or a `"*"` elsewhere on the chain turns
241
+ # it `:unknown`, so the rest of the chain is filed as for any other answer;
242
+ # - otherwise every entry after the answer (the whole chain when `owner` names none) files its class edge
243
+ # 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.
245
+ #
246
+ # What is filed is a function of this chain, the entry the filing starts from and the discovery index the
247
+ # sites are read from, so a consumer that settles the same chain from the same entry again (the common case:
248
+ # one hierarchy read at every call site of a file) files it once ({Analysis::DependencyRecorder.file_chain_once},
249
+ # #1590). The recorded edges are the same; only the repeated filing is gone.
250
+ def record_beyond(scope, owner, master_only)
251
+ return unless Analysis::DependencyRecorder.active?
252
+ return if master_only && !owner.nil? && owner == @root && @entries.first&.name == @root
253
+
254
+ start = owner.nil? ? nil : @entries.index { |entry| entry.name == owner }
255
+ Analysis::DependencyRecorder.file_chain_once(self, start, scope.discovery) { file_beyond(scope, start) }
256
+ end
257
+ private :record_beyond
258
+
259
+ def file_beyond(scope, start)
260
+ ResolutionChain.record_class(scope, @root) if start.nil?
261
+ index = start.nil? ? 0 : start + 1
262
+ while index < @entries.size
263
+ entry = @entries[index]
264
+ ResolutionChain.record_entry(scope, entry, nil)
265
+ Analysis::DependencyRecorder.read_missing(:class, entry.last_segment) unless entry.external?
266
+ index += 1
267
+ end
268
+ end
269
+ private :file_beyond
270
+
271
+ def record_head(scope, start, side)
272
+ ResolutionChain.record_class(scope, @root) unless start.zero? && @entries.first&.name == @root
273
+ start.times { |index| ResolutionChain.record_entry(scope, @entries[index], side) }
274
+ end
275
+ private :record_head
276
+
277
+ # `Scope#record_class_dependency`'s edge, filed from outside `Scope`: every file that declares `name`.
278
+ def self.record_class(scope, name)
279
+ sites = scope.discovery.discovered_class_sources[name.to_s]
280
+ sites&.each { |site| Analysis::DependencyRecorder.read_site(site) }
281
+ end
282
+
283
+ def self.record_entry(scope, entry, side)
284
+ return if side && entry.side != side
285
+
286
+ if entry.external?
287
+ # An ancestor the project does not treat as a class may still be DECLARED (an empty module in another
288
+ # file), and a later edit there turns it into a project entry that moves the answer: file the sites
289
+ # of every name its spelling can denote.
290
+ entry.candidates.each { |candidate| record_class(scope, candidate) }
291
+ else
292
+ record_class(scope, entry.name)
293
+ end
294
+ end
295
+
296
+ # The position of `name`'s own entry on this chain's side, or nil. A module can appear twice (a prepend
297
+ # of a module the superclass includes); a class or the root cannot.
298
+ def index_of(name, side = @side)
299
+ @entries.index { |entry| entry.name == name && entry.side == side }
300
+ end
301
+
302
+ # A LEVEL is one superclass step: the class (or its singleton) with the modules inserted around it,
303
+ # before the next superclass's entries begin. `level_classes[k]` is that class, or nil for a level that
304
+ # is an external superclass. Only COMPLETE levels are counted — a level the {LIMIT} cut in half is not.
305
+ def level_count = @level_starts.size
306
+
307
+ def level_entries(index)
308
+ finish = @level_starts[index + 1] || @levels_end
309
+ @entries[@level_starts[index]...finish]
310
+ end
311
+
312
+ # Each complete level up to the first superclass the project does not declare, as `[class_name,
313
+ # modules, externals]`: the level's class, the project modules around it (each once), and the candidate
314
+ # lists of the ancestors in it the project does not declare — the shape `call.wrong-arity` reads.
315
+ def levels = (@memo[:levels] ||= compute_levels)
316
+
317
+ def compute_levels
318
+ out = []
319
+ level_count.times do |index|
320
+ class_name = @level_classes[index]
321
+ break if class_name.nil?
322
+
323
+ externals, entries = level_entries(index).partition(&:external?)
324
+ modules = entries.filter_map { |entry| entry.name unless entry.name == class_name && entry.side == @side }
325
+ out << [class_name, modules.uniq.freeze, externals.map(&:candidates).freeze].freeze
326
+ end
327
+ out.freeze
328
+ end
329
+ private :compute_levels
330
+
331
+ MEMO_KEY = :__rigor_resolution_chain__
332
+ private_constant :MEMO_KEY
333
+
334
+ FLAVORS = %i[methods constants arity].freeze
335
+
336
+ # The chain for `class_name` on `side` under `flavor`. Memoised in ONE thread-local slot keyed on the
337
+ # discovery index's identity — the same shape as `ExpressionTyper#class_graph_buckets`: the chain is a
338
+ # pure function of that frozen index, and a store keyed on it would pin every file's index for the run.
339
+ def self.for(scope, class_name, side, flavor)
340
+ raise ArgumentError, "unknown resolution-chain flavor #{flavor.inspect}" unless FLAVORS.include?(flavor)
341
+
342
+ bucket = flavor_bucket(scope.discovery, flavor)
343
+ chains = bucket[side]
344
+ chains[class_name] || (chains[class_name] = Builder.new(scope, flavor, bucket).chain(class_name, side))
345
+ end
346
+
347
+ def self.flavor_bucket(discovery, flavor)
348
+ slot = Thread.current[MEMO_KEY]
349
+ unless slot && slot[0].equal?(discovery)
350
+ slot = [discovery, {}]
351
+ Thread.current[MEMO_KEY] = slot
352
+ end
353
+ slot[1][flavor] ||= { instance: {}, singleton: {}, names: {}, interned: {}, externals: {}, master: {},
354
+ marks: {} }
355
+ end
356
+
357
+ # Which project class an as-written ancestor name denotes, under one flavor — shared by the chain and
358
+ # by {MasterOrder}, and memoised per owner and name in the flavor's bucket.
359
+ class Resolver
360
+ def initialize(scope, flavor, bucket)
361
+ @scope = scope
362
+ @flavor = flavor
363
+ @discovery = scope.discovery
364
+ @names = bucket[:names]
365
+ end
366
+
367
+ # The project class `raw` denotes from `owner`'s declaration header, an Array of the classes an
368
+ # ambiguous spelling names (`:arity` only), or nil.
369
+ def resolve(owner, raw)
370
+ by_owner = (@names[owner] ||= {})
371
+ return by_owner[raw] if by_owner.key?(raw)
372
+
373
+ by_owner[raw] = compute(owner, raw)
374
+ end
375
+
376
+ # {#resolve}, with an ambiguous spelling read as unresolved.
377
+ def resolve_one(owner, raw)
378
+ resolved = raw.nil? ? nil : resolve(owner, raw)
379
+ resolved.is_a?(String) ? resolved : nil
380
+ end
381
+
382
+ def candidates(owner, raw) = @scope.ancestor_name_candidates(owner, raw)
383
+
384
+ private
385
+
386
+ def compute(owner, raw)
387
+ found = candidates(owner, raw).find { |candidate| project?(candidate) }
388
+ return found if found || @flavor != :arity
389
+
390
+ alternatives = @scope.ambiguous_ancestor_resolutions(owner, raw)
391
+ alternatives.empty? ? nil : alternatives
392
+ end
393
+
394
+ def project?(name)
395
+ case @flavor
396
+ when :constants
397
+ @discovery.discovered_superclasses.key?(name) || @discovery.discovered_includes.key?(name) ||
398
+ @discovery.discovered_classes.key?(name)
399
+ when :arity
400
+ @scope.known_user_class?(name) || @discovery.discovered_parameter_envelopes.key?(name)
401
+ else
402
+ @scope.known_user_class?(name)
403
+ end
404
+ end
405
+ end
406
+ private_constant :Resolver
407
+
408
+ private_class_method :flavor_bucket
409
+
410
+ # {Relevance}'s verdict memo, one table per flavor bucket.
411
+ def self.relevance_memo(scope, flavor)
412
+ flavor_bucket(scope.discovery, flavor)[:relevance] ||= {}
413
+ end
414
+
415
+ # The singleton-side hook verdicts' memo (`Inference::DefinerResolution`'s positional decline, ADR-119 WD3),
416
+ # one table per flavor bucket.
417
+ def self.hook_memo(scope, flavor)
418
+ flavor_bucket(scope.discovery, flavor)[:hooks] ||= {}
419
+ end
420
+
421
+ # The name resolver of `flavor`, over the same memo the chain builder shares: {Relevance} resolves a
422
+ # mark's listed entry from the node that listed it, exactly as the chain did.
423
+ def self.resolver_for(scope, flavor)
424
+ Resolver.new(scope, flavor, flavor_bucket(scope.discovery, flavor))
425
+ end
426
+
427
+ # The orders the walks this chain replaced searched in, kept for the one case the chain cannot settle
428
+ # alone: where its two worlds put different definers first, a reader answers what master answered
429
+ # (ADR-24 § "Amendment 2026-09-28"), so no reader turns a disagreement into a new answer or a new
430
+ # silence. Each order is the replaced walk's, over the same tables and the same name resolution, and is
431
+ # memoised per root in the flavor's bucket. They are read so rarely — no chain on Rigor's own `lib`
432
+ # needs one, and on Mastodon no read did — that their dependency edges are simply every node listed.
433
+ module MasterOrder
434
+ module_function
435
+
436
+ # `Scope#user_def_through_ancestors`'s breadth-first search, prepend wedge and all: at each class,
437
+ # its prepends' sub-chains, then the class, then its includes and superclass are queued.
438
+ def definer_sequence(scope, root)
439
+ ResolutionChain.master_memo(scope, :methods, [:definers, root]) do |resolver|
440
+ out = []
441
+ queue = [root]
442
+ seen = {}
443
+ until queue.empty? || seen.size > LIMIT
444
+ current = queue.shift
445
+ next if current.nil? || seen[current]
446
+
447
+ seen[current] = true
448
+ wedge(scope, resolver, current, {}).each do |name|
449
+ next if seen[name]
450
+
451
+ seen[name] = true
452
+ out << name
453
+ end
454
+ out << current
455
+ direct_edges(scope, resolver, current).each { |name| queue.push(name) }
456
+ end
457
+ out.freeze
458
+ end
459
+ end
460
+
461
+ # The override rules' and the constant ladder's breadth-first order: every project ancestor after
462
+ # `root`, nearer first, includes and prepends ahead of the superclass at each class.
463
+ def breadth_first(scope, root, flavor)
464
+ ResolutionChain.master_memo(scope, flavor, [:breadth_first, root]) do |resolver|
465
+ out = []
466
+ queue = direct_edges(scope, resolver, root)
467
+ seen = { root => true }
468
+ until queue.empty? || out.size > LIMIT
469
+ current = queue.shift
470
+ next if seen[current]
471
+
472
+ seen[current] = true
473
+ out << current
474
+ queue.concat(direct_edges(scope, resolver, current))
475
+ end
476
+ out.freeze
477
+ end
478
+ end
479
+
480
+ # `Scope#external_ancestor_name_candidates`'s depth-first walk: the candidate lists of the ancestors
481
+ # the project does not declare, a resolved ancestor's own edges before the next sibling's.
482
+ def external_groups(scope, root, mixins)
483
+ ResolutionChain.master_memo(scope, :methods, [:externals, root, mixins]) do |resolver|
484
+ groups = []
485
+ collect_externals(scope, resolver, root, mixins, groups, {})
486
+ groups.freeze
487
+ end
488
+ end
489
+
490
+ def direct_edges(scope, resolver, name)
491
+ discovery = scope.discovery
492
+ raws = (discovery.discovered_includes[name] || EMPTY_NAMES) + [discovery.discovered_superclasses[name]]
493
+ raws.filter_map { |raw| resolver.resolve_one(name, raw) }
494
+ end
495
+
496
+ def wedge(scope, resolver, name, seen)
497
+ (scope.discovery.discovered_prepends[name] || EMPTY_NAMES).each_with_object([]) do |raw, out|
498
+ resolved = resolver.resolve_one(name, raw)
499
+ next if resolved.nil? || seen[resolved]
500
+
501
+ seen[resolved] = true
502
+ out.concat(subchain(scope, resolver, resolved, seen))
503
+ end
504
+ end
505
+
506
+ def subchain(scope, resolver, name, seen)
507
+ out = wedge(scope, resolver, name, seen)
508
+ out << name
509
+ direct_edges(scope, resolver, name).each do |resolved|
510
+ next if seen[resolved]
511
+
512
+ seen[resolved] = true
513
+ out.concat(subchain(scope, resolver, resolved, seen))
514
+ end
515
+ out
516
+ end
517
+
518
+ def collect_externals(scope, resolver, current, mixins, groups, seen)
519
+ return if seen[current] || seen.size > LIMIT
520
+
521
+ seen[current] = true
522
+ discovery = scope.discovery
523
+ raws = mixins ? (discovery.discovered_includes[current] || EMPTY_NAMES).dup : []
524
+ raws << discovery.discovered_superclasses[current]
525
+ raws.compact.each do |raw|
526
+ resolved = resolver.resolve_one(current, raw)
527
+ next groups << resolver.candidates(current, raw) if resolved.nil?
528
+
529
+ collect_externals(scope, resolver, resolved, mixins, groups, seen)
530
+ end
531
+ end
532
+ end
533
+
534
+ EMPTY_NAMES = [].freeze
535
+ private_constant :EMPTY_NAMES
536
+
537
+ # A class's direct project ancestors, as the breadth-first walks queued them: its includes and prepends in
538
+ # `includes_of` order (unless `mixins` is false), then its superclass, each resolved as the method walks
539
+ # resolve a name. `Scope#enqueue_ancestors`, kept for the plugin surface, reads this.
540
+ def self.direct_ancestors(scope, name, mixins)
541
+ resolver = Resolver.new(scope, :methods, flavor_bucket(scope.discovery, :methods))
542
+ return MasterOrder.direct_edges(scope, resolver, name) if mixins
543
+
544
+ resolved = resolver.resolve_one(name, scope.discovery.discovered_superclasses[name])
545
+ resolved ? [resolved] : EMPTY_NAMES
546
+ end
547
+
548
+ # {MasterOrder}'s memo: one value per key in the flavor's bucket.
549
+ def self.master_memo(scope, flavor, key)
550
+ bucket = flavor_bucket(scope.discovery, flavor)
551
+ memo = bucket[:master]
552
+ return memo[key] if memo.key?(key)
553
+
554
+ memo[key] = yield(Resolver.new(scope, flavor, bucket))
555
+ end
556
+
557
+ # The linearisation itself, in one world: Ruby's skip rule (`retro: false`), or every skipped insertion
558
+ # made anyway (`retro: true`). Each node's chain is memoised in the flavor's bucket per world (a module's
559
+ # chain is the same whichever class includes it) unless its computation met a cycle, whose answer depends
560
+ # on where the cycle was entered.
561
+ class Builder # rubocop:disable Metrics/ClassLength
562
+ EMPTY = [].freeze
563
+ RETRO_OVER_BUDGET = :rigor_retro_over_budget
564
+ EMPTY_LIN = [EMPTY, EMPTY, EMPTY, 0, NO_MARKS, true].freeze
565
+ private_constant :EMPTY, :EMPTY_LIN, :RETRO_OVER_BUDGET
566
+
567
+ # What one node's computation met, handed up to the computation that asked for it: how many forks it
568
+ # counted (see {ResolutionChain#settle}), the {Mark}s of the nodes on it whose mixin edges' order is not a
569
+ # fact, and whether it met a cycle or the depth budget (so it is not memoised).
570
+ Frame = Struct.new(:forks, :incomplete, :marks, :retro_ok) do
571
+ # ADR-119 WD2 — mark arrays are frozen and shared: the receiver keeps its array when the other is
572
+ # empty, adopts the other's when it is empty itself, and allocates a union only when both hold marks.
573
+ def absorb(forks, other, retro_ok)
574
+ self.forks += forks
575
+ add_marks(other)
576
+ self.retro_ok &&= retro_ok
577
+ end
578
+
579
+ def add_marks(other)
580
+ return if other.empty? || marks.equal?(other)
581
+
582
+ self.marks = marks.empty? ? other : (marks | other).freeze
583
+ end
584
+ end
585
+ private_constant :Frame
586
+
587
+ def initialize(scope, flavor, bucket, retro: false)
588
+ @scope = scope
589
+ @flavor = flavor
590
+ @bucket = bucket
591
+ @retro = retro
592
+ @discovery = scope.discovery
593
+ @resolver = Resolver.new(scope, flavor, bucket)
594
+ # Per memo table: a module's singleton chain reaches its OWN instance chain through `extend self`,
595
+ # which is not a cycle.
596
+ @stacks = {}
597
+ @frames = []
598
+ @deep = false
599
+ end
600
+
601
+ # The chain for `root`, carrying its retro world when the skip rule skipped anything on the way.
602
+ def chain(root, side)
603
+ 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
605
+ cut_at = cut_position(entries)
606
+ return whole_chain(root, side, entries, starts, classes, [forks, marks], retro) if cut_at.nil?
607
+
608
+ # Keep the prefix for first-definer reads, and only the levels that end inside it.
609
+ complete = starts.count { |start| start < cut_at }
610
+ complete -= 1 if (starts[complete] || entries.size) > cut_at
611
+ ResolutionChain.new(root: root, side: side, entries: entries.first(cut_at).freeze,
612
+ level_starts: starts.first(complete).freeze,
613
+ level_classes: classes.first(complete).freeze,
614
+ levels_end: complete.zero? ? 0 : (starts[complete] || cut_at), truncated: true,
615
+ forks: forks, marks: marks, flavor: @flavor, retro: retro)
616
+ end
617
+
618
+ private
619
+
620
+ def whole_chain(root, side, entries, starts, classes, (forks, marks), retro)
621
+ ResolutionChain.new(root: root, side: side, entries: entries, level_starts: starts,
622
+ level_classes: classes, levels_end: entries.size, truncated: @deep, forks: forks,
623
+ marks: marks, flavor: @flavor, retro: retro)
624
+ end
625
+
626
+ def lin_for(root, side) = side == :singleton ? singleton_lin(root, 0) : instance_lin(root, 0)
627
+
628
+ # The retro world, built only for a chain with exactly one skip (two or more settle to master without
629
+ # it) and abandoned — nil, which settles to master — once it holds more than {LIMIT} project entries.
630
+ # A retro world repeats every skipped module, so a deep diamond grows it geometrically where the
631
+ # chain itself stays small.
632
+ def build_retro(root, side)
633
+ catch(RETRO_OVER_BUDGET) { Builder.new(@scope, @flavor, @bucket, retro: true).chain(root, side) }
634
+ end
635
+
636
+ def guard_retro(entries)
637
+ return unless @retro && entries.size > LIMIT
638
+
639
+ throw RETRO_OVER_BUDGET if entries.count { |entry| !entry.external? } > LIMIT
640
+ end
641
+
642
+ # The index of the first project entry past {LIMIT}, or nil when the chain fits.
643
+ def cut_position(entries)
644
+ seen = 0
645
+ entries.each_with_index do |entry, index|
646
+ next if entry.external?
647
+
648
+ seen += 1
649
+ return index if seen > LIMIT
650
+ end
651
+ nil
652
+ end
653
+
654
+ def instance_lin(name, depth)
655
+ memoised(@retro ? :lin_instance_retro : :lin_instance, name, depth) { compute_instance(name, depth) }
656
+ end
657
+
658
+ def singleton_lin(name, depth)
659
+ memoised(@retro ? :lin_singleton_retro : :lin_singleton, name, depth) { compute_singleton(name, depth) }
660
+ end
661
+
662
+ def memoised(table, name, depth)
663
+ memo = (@bucket[table] ||= {})
664
+ cached = memo[name]
665
+ if cached
666
+ # A memoised chain's forks are the asker's too.
667
+ @frames.last&.absorb(cached[3], cached[4], cached[5])
668
+ return cached
669
+ end
670
+
671
+ # A cycle (`class Foo < Foo` resolved inside its own namespace) or a hierarchy deeper than the budget
672
+ # contributes nothing past that point, and the chains computed while it did are not memoised: they
673
+ # depend on where the walk entered.
674
+ stack = (@stacks[table] ||= [])
675
+ if stack.include?(name) || depth > LIMIT
676
+ @deep ||= depth > LIMIT
677
+ @frames.last&.incomplete = true
678
+ return EMPTY_LIN
679
+ end
680
+
681
+ frame = Frame.new(0, false, NO_MARKS, true)
682
+ @frames.push(frame)
683
+ stack.push(name)
684
+ entries, starts, classes = yield
685
+ stack.pop
686
+ @frames.pop
687
+ lin = [entries, starts, classes, frame.forks, frame.marks, frame.retro_ok].freeze
688
+ memo[name] = lin unless frame.incomplete
689
+ hand_up(frame)
690
+ lin
691
+ end
692
+
693
+ def hand_up(frame)
694
+ parent = @frames.last
695
+ return if parent.nil?
696
+
697
+ parent.absorb(frame.forks, frame.marks, frame.retro_ok)
698
+ parent.incomplete ||= frame.incomplete
699
+ end
700
+
701
+ def compute_instance(name, depth)
702
+ # `discovered_includes` already carries the prepended names, so it counts every instance-side edge once.
703
+ mark_unsettled(name, :include, (@discovery.discovered_includes[name] || EMPTY).size)
704
+ entries = [project_entry(name, :instance)]
705
+ super_lin = superclass_lin(name, :instance, depth)
706
+ entries.concat(super_lin[0])
707
+ prepends = @discovery.discovered_prepends[name] || EMPTY
708
+ boundary = prepend_all(entries, name, prepends, depth)
709
+ includes = @discovery.discovered_includes[name] || EMPTY
710
+ super_start = include_all(entries, name, includes, prepends, depth, boundary)
711
+ finish(entries, name, super_lin, super_start)
712
+ end
713
+
714
+ def compute_singleton(name, depth)
715
+ mark_unsettled(name, :extend, (@discovery.discovered_extends[name] || EMPTY).size)
716
+ entries = [project_entry(name, :singleton)]
717
+ super_lin = superclass_lin(name, :singleton, depth)
718
+ entries.concat(super_lin[0])
719
+ extends = @discovery.discovered_extends[name] || EMPTY
720
+ super_start = include_all(entries, name, extends, EMPTY, depth, [0, 1])
721
+ finish(entries, name, super_lin, super_start)
722
+ end
723
+
724
+ # Marks the node being computed unsettled when the order of its mixin edges on `kind` is not a fact
725
+ # (`DiscoveryIndex#unpositioned_mixins`: an edge written in a conditional, a method, a block or a hook,
726
+ # or a call the walk cannot record, `"*"`), or when its class is declared in more than one file and
727
+ # has two or more such edges, whose order across the files is load order. A node's mark taints every
728
+ # chain that draws on it, so a concern's `included do` edges taint every includer. The {Mark} says why,
729
+ # which is what per-name relevance reads.
730
+ def mark_unsettled(name, kind, edge_count)
731
+ listed = @discovery.unpositioned_mixins[name]&.dig(kind)
732
+ listed = nil if listed && listed.empty?
733
+ multi = edge_count >= 2 && multi_file?(name)
734
+ return if listed.nil? && !multi
735
+
736
+ @frames.last.add_marks(mark_for(name, kind, listed, multi))
737
+ end
738
+
739
+ # The one-element frozen array carrying the {Mark} of `[name, kind]`, interned in the bucket.
740
+ def mark_for(name, kind, listed, multi)
741
+ by_node = (@bucket[:marks][kind] ||= {})
742
+ by_node[name] ||= [ResolutionChain::Mark.new(name, kind, listed.nil? ? EMPTY : listed.to_a.freeze,
743
+ multi)].freeze
744
+ end
745
+
746
+ def multi_file?(name)
747
+ sites = @discovery.discovered_class_sources[name]
748
+ !sites.nil? && sites.size >= 2
749
+ end
750
+
751
+ def finish(entries, name, super_lin, super_start)
752
+ starts = [0]
753
+ super_lin[1].each { |start| starts << (start + super_start) }
754
+ classes = [name].concat(super_lin[2])
755
+ [entries.freeze, starts.freeze, classes.freeze]
756
+ end
757
+
758
+ # The superclass's own chain on `side`, or a one-entry chain holding it as an external.
759
+ def superclass_lin(name, side, depth)
760
+ raw = @discovery.discovered_superclasses[name]
761
+ return EMPTY_LIN if raw.nil?
762
+
763
+ resolved = resolve(name, raw)
764
+ if resolved.is_a?(String)
765
+ side == :singleton ? singleton_lin(resolved, depth + 1) : instance_lin(resolved, depth + 1)
766
+ else
767
+ # A class the project declares that the `:methods` predicate does not admit (one that only `extend`s)
768
+ # is external here, so its extended modules vanish from the singleton chain and a fork among them
769
+ # goes uncounted. That is a hole in what the chain can see, not an order fork, so it settles to
770
+ # master with the weight of two.
771
+ @frames.last.forks += 2 if side == :singleton && declared_class?(name, raw)
772
+ [[external_entry(name, raw, side, true)].freeze, [0].freeze, [nil].freeze, 0, NO_MARKS, true].freeze
773
+ end
774
+ end
775
+
776
+ def declared_class?(owner, raw)
777
+ @scope.ancestor_name_candidates(owner, raw).any? do |candidate|
778
+ @discovery.discovered_extends.key?(candidate)
779
+ end
780
+ end
781
+
782
+ # Inserts every prepend, in statement order (the table stores search order, nearest first), ahead of
783
+ # the class, and returns `[origin, super_start]` — where the class itself now sits and where its
784
+ # superclass's entries begin. Ruby skips a module already in the prepend region (a fork: the module is
785
+ # present through a second route); the retro world inserts it again. A sub-chain entry other than the
786
+ # prepended module that the class or its superclass already carries is a fork too (the prepended
787
+ # module's own includes may have run before or after), and the tables record `include M; prepend M`
788
+ # exactly as a plain `prepend M`. A direct prepend of the module itself is always inserted, in Ruby and
789
+ # here, so it forks nothing.
790
+ def prepend_all(entries, owner, prepends, depth)
791
+ origin = 0
792
+ super_start = 1
793
+ # The table holds every statement, nearest first: a same-owner repeat is a no-op in Ruby (the first
794
+ # statement wins), so it is dropped here rather than met as a fork.
795
+ prepends.reverse.uniq.each do |raw|
796
+ modules = Array(resolve(owner, raw))
797
+ each_mixin_chain(owner, raw, depth) do |sub|
798
+ point = -1
799
+ sub.each do |entry|
800
+ found = entries.index(entry)
801
+ if found && found < origin && !@retro
802
+ fork_without_retro
803
+ point = found if found > point
804
+ else
805
+ fork_without_retro if found && !@retro && !modules.include?(entry.name)
806
+ entries.insert(point + 1, entry)
807
+ point += 1
808
+ origin += 1
809
+ super_start += 1
810
+ end
811
+ end
812
+ end
813
+ guard_retro(entries)
814
+ end
815
+ [origin, super_start]
816
+ end
817
+
818
+ # Inserts every name in `mixins` that `skip` does not hold, in statement order, after the class, and
819
+ # returns where the superclass's entries now begin. Ruby skips a module already anywhere in the chain;
820
+ # that is a fork (the module is present through a second route), and the retro world inserts it
821
+ # again. Exactly one fork, at or after the class, on the last entry of the sub-chain being inserted, is
822
+ # the one shape whose Ruby worlds are exactly two: the skip made or the insertion made.
823
+ def include_all(entries, owner, mixins, skip, depth, boundary)
824
+ origin, super_start = boundary
825
+ mixins.reverse_each do |raw|
826
+ next if skip.include?(raw)
827
+
828
+ each_mixin_chain(owner, raw, depth) do |sub|
829
+ point = origin
830
+ sub.each_with_index do |entry, index|
831
+ found = @retro ? nil : entries.index(entry)
832
+ if found
833
+ @frames.last.forks += 1
834
+ @frames.last.retro_ok = false unless found >= origin && index == sub.size - 1
835
+ point = found if found > point && found < super_start
836
+ else
837
+ entries.insert(point + 1, entry)
838
+ point += 1
839
+ super_start += 1
840
+ end
841
+ end
842
+ end
843
+ guard_retro(entries)
844
+ end
845
+ super_start
846
+ end
847
+
848
+ # A fork whose second Ruby world is not "the insertion made anyway": the chain settles to master.
849
+ def fork_without_retro
850
+ @frames.last.forks += 1
851
+ @frames.last.retro_ok = false
852
+ end
853
+
854
+ # Yields the instance chain of the module `raw` names from `owner` — once per class an ambiguous
855
+ # spelling can name under the `:arity` flavor — or a one-entry chain holding it as an external.
856
+ def each_mixin_chain(owner, raw, depth)
857
+ resolved = resolve(owner, raw)
858
+ case resolved
859
+ when String then yield instance_lin(resolved, depth + 1)[0]
860
+ when Array then resolved.each { |name| yield instance_lin(name, depth + 1)[0] }
861
+ else yield [external_entry(owner, raw, :instance, false)]
862
+ end
863
+ end
864
+
865
+ def resolve(owner, raw) = @resolver.resolve(owner, raw)
866
+
867
+ def project_entry(name, side)
868
+ by_side = (@bucket[:interned][side] ||= {})
869
+ by_side[name] ||= Entry.new(name, side, nil, nil, false, name.to_s.split("::").last.freeze)
870
+ end
871
+
872
+ def external_entry(owner, raw, side, superclass_edge)
873
+ by_edge = ((@bucket[:externals][side] ||= {})[superclass_edge] ||= {})
874
+ by_owner = (by_edge[owner] ||= {})
875
+ by_owner[raw] ||= Entry.new(nil, side, raw, @scope.ancestor_name_candidates(owner, raw).freeze,
876
+ superclass_edge, nil)
877
+ end
878
+ end
879
+ private_constant :Builder
880
+ end
881
+ end
882
+ end
883
+
884
+ require_relative "resolution_chain/relevance"