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.
- checksums.yaml +4 -4
- data/README.md +4 -3
- data/docs/handbook/08-understanding-errors.md +3 -2
- data/docs/install.md +87 -15
- data/docs/manual/02-cli-reference.md +2 -2
- data/docs/manual/03-configuration.md +5 -0
- data/docs/manual/08-skills.md +4 -3
- data/docs/manual/12-caching.md +43 -4
- data/docs/manual/14-rails-quickstart.md +19 -11
- data/lib/rigor/analysis/check_rules/source_arity.rb +163 -59
- data/lib/rigor/analysis/check_rules.rb +118 -87
- data/lib/rigor/analysis/dependency_recorder.rb +200 -18
- data/lib/rigor/analysis/incremental_run_slot/write_guard.rb +266 -0
- data/lib/rigor/analysis/incremental_run_slot.rb +171 -0
- data/lib/rigor/analysis/incremental_session.rb +273 -49
- data/lib/rigor/analysis/plugin_fact_fingerprint.rb +24 -13
- data/lib/rigor/analysis/reachability/scan_cache.rb +2 -1
- data/lib/rigor/analysis/run_cache_key.rb +27 -8
- data/lib/rigor/analysis/run_stats.rb +2 -2
- data/lib/rigor/analysis/runner/diagnostic_aggregator.rb +1 -1
- data/lib/rigor/analysis/runner/pool_coordinator.rb +2 -2
- data/lib/rigor/analysis/runner/project_pre_passes.rb +21 -13
- data/lib/rigor/analysis/runner.rb +337 -46
- data/lib/rigor/analysis/shadow_harness.rb +27 -0
- data/lib/rigor/analysis/source_rbs_gate.rb +157 -0
- data/lib/rigor/analysis/template_unit_paths.rb +6 -4
- data/lib/rigor/analysis/template_units.rb +15 -5
- data/lib/rigor/analysis/worker_session.rb +4 -3
- data/lib/rigor/cache/descriptor.rb +40 -1
- data/lib/rigor/cache/file_digest.rb +28 -0
- data/lib/rigor/cache/incremental_snapshot.rb +70 -8
- data/lib/rigor/cache/store.rb +63 -3
- data/lib/rigor/cache/value_digest.rb +325 -0
- data/lib/rigor/cli/check_command.rb +83 -13
- data/lib/rigor/cli/doctor_command.rb +1 -1
- data/lib/rigor/cli/effects_report.rb +2 -2
- data/lib/rigor/cli/unused_command.rb +3 -3
- data/lib/rigor/cli.rb +1 -1
- data/lib/rigor/config_audit.rb +3 -2
- data/lib/rigor/config_path.rb +29 -0
- data/lib/rigor/configuration/severity_profile.rb +5 -0
- data/lib/rigor/configuration.rb +6 -5
- data/lib/rigor/effects/plugin_facts.rb +6 -6
- data/lib/rigor/environment/bundle_sig_discovery.rb +5 -3
- data/lib/rigor/environment/lockfile_resolver.rb +10 -1
- data/lib/rigor/environment/rbs_collection_discovery.rb +10 -1
- data/lib/rigor/environment/rbs_loader.rb +1 -1
- data/lib/rigor/environment/source_rbs_synthesis.rb +147 -0
- data/lib/rigor/environment.rb +13 -70
- data/lib/rigor/inference/block_parameter_binder.rb +29 -13
- data/lib/rigor/inference/captured_locals.rb +7 -1
- data/lib/rigor/inference/declaration_walk/context.rb +195 -0
- data/lib/rigor/inference/declaration_walk/errors.rb +18 -0
- data/lib/rigor/inference/declaration_walk/shadow.rb +147 -0
- data/lib/rigor/inference/declaration_walk/traversal.rb +441 -0
- data/lib/rigor/inference/declaration_walk.rb +7 -0
- data/lib/rigor/inference/def_node_resolver.rb +3 -2
- data/lib/rigor/inference/definer_resolution.rb +266 -0
- data/lib/rigor/inference/expression_typer.rb +136 -45
- data/lib/rigor/inference/external_ancestor_resolution.rb +13 -16
- data/lib/rigor/inference/guard_rebinding.rb +53 -12
- data/lib/rigor/inference/macro_block_self_type.rb +14 -2
- data/lib/rigor/inference/method_dispatcher/rbs_dispatch.rb +46 -26
- data/lib/rigor/inference/module_function_state.rb +213 -0
- data/lib/rigor/inference/multi_target_binder.rb +21 -16
- data/lib/rigor/inference/narrowing.rb +147 -17
- data/lib/rigor/inference/parameter_inference_collector.rb +10 -9
- data/lib/rigor/inference/scope_indexer/certainty.rb +176 -0
- data/lib/rigor/inference/scope_indexer/class_cvars_collector.rb +35 -0
- data/lib/rigor/inference/scope_indexer/def_nestings_collector.rb +47 -0
- data/lib/rigor/inference/scope_indexer/layered_def_nestings.rb +36 -0
- data/lib/rigor/inference/scope_indexer/member_layouts_collector.rb +61 -0
- data/lib/rigor/inference/scope_indexer/mixin_accumulator.rb +130 -0
- data/lib/rigor/inference/scope_indexer/superclasses_collector.rb +64 -0
- data/lib/rigor/inference/scope_indexer.rb +1443 -295
- data/lib/rigor/inference/singleton_hook_decline.rb +253 -0
- data/lib/rigor/inference/statement_evaluator.rb +80 -14
- data/lib/rigor/plugin/base.rb +28 -8
- data/lib/rigor/plugin/io_boundary.rb +82 -4
- data/lib/rigor/plugin/isolation.rb +1 -1
- data/lib/rigor/plugin/trust_policy.rb +3 -3
- data/lib/rigor/protection/discovery_seed.rb +18 -9
- data/lib/rigor/protection/mutation_cache.rb +3 -3
- data/lib/rigor/reflection/constant_ancestors.rb +96 -47
- data/lib/rigor/reflection/constant_path.rb +22 -7
- data/lib/rigor/reflection.rb +2 -3
- data/lib/rigor/scope/discovery_index.rb +218 -2
- data/lib/rigor/scope/resolution_chain/relevance.rb +165 -0
- data/lib/rigor/scope/resolution_chain.rb +884 -0
- data/lib/rigor/scope.rb +295 -322
- data/lib/rigor/sig_gen/alias_index.rb +1 -1
- data/lib/rigor/sig_gen/effect_annotation.rb +2 -2
- data/lib/rigor/sig_gen/generator.rb +22 -16
- data/lib/rigor/type/combinator.rb +25 -1
- data/lib/rigor/version.rb +1 -1
- data/plugins/rigor-ffi/lib/rigor/plugin/ffi.rb +10 -0
- data/sig/rigor/environment.rbs +2 -0
- data/sig/rigor/plugin/io_boundary.rbs +1 -0
- data/sig/rigor/scope.rbs +43 -1
- data/skills/rigor-project-init/SKILL.md +37 -11
- data/skills/rigor-project-init/references/02-configure.md +7 -2
- metadata +25 -1
|
@@ -20,6 +20,7 @@ require_relative "last_status"
|
|
|
20
20
|
require_relative "error_info"
|
|
21
21
|
require_relative "hash_lookup_mutation"
|
|
22
22
|
require_relative "index_write_widening"
|
|
23
|
+
require_relative "module_function_state"
|
|
23
24
|
require_relative "multi_target_binder"
|
|
24
25
|
require_relative "mutation_widening"
|
|
25
26
|
require_relative "narrowing"
|
|
@@ -27,6 +28,14 @@ require_relative "operand_effects"
|
|
|
27
28
|
require_relative "statement_evaluator"
|
|
28
29
|
require_relative "struct_fold_safety"
|
|
29
30
|
require_relative "unknown_store_widening"
|
|
31
|
+
require_relative "declaration_walk/traversal"
|
|
32
|
+
require_relative "scope_indexer/class_cvars_collector"
|
|
33
|
+
require_relative "scope_indexer/superclasses_collector"
|
|
34
|
+
require_relative "scope_indexer/def_nestings_collector"
|
|
35
|
+
require_relative "scope_indexer/layered_def_nestings"
|
|
36
|
+
require_relative "scope_indexer/mixin_accumulator"
|
|
37
|
+
require_relative "scope_indexer/certainty"
|
|
38
|
+
require_relative "scope_indexer/member_layouts_collector"
|
|
30
39
|
|
|
31
40
|
module Rigor
|
|
32
41
|
module Inference
|
|
@@ -98,8 +107,8 @@ module Rigor
|
|
|
98
107
|
# propagates it across every derived scope.
|
|
99
108
|
# Issue #722 residue 2 — the project's settled declaration names are the oracle the file's own
|
|
100
109
|
# compact headers are re-anchored against. `discovered_class_sources` would be the more direct
|
|
101
|
-
# reading, but it
|
|
102
|
-
#
|
|
110
|
+
# reading, but it names files rather than settled declarations, and the fold has already applied its
|
|
111
|
+
# own re-anchoring to this table.
|
|
103
112
|
declared_types, discovered_classes =
|
|
104
113
|
build_declaration_artifacts(root, default_scope.discovered_classes.keys.to_set)
|
|
105
114
|
# Merge the indexer's findings on top of whatever the base scope already carries so callers that seed cross-file
|
|
@@ -152,7 +161,9 @@ module Rigor
|
|
|
152
161
|
# discovered-methods existence table and the instance def-node table — see {#build_methods_and_def_nodes}.
|
|
153
162
|
# `seed_discovered_methods` seeds the former onto the scope and returns the def-node table for
|
|
154
163
|
# `merge_project_method_indexes` below.
|
|
155
|
-
|
|
164
|
+
# ADR-119 WD3 — the file's possible contributions, classified once for both def-contribution walks.
|
|
165
|
+
certainty = Certainty.possible_nodes(root)
|
|
166
|
+
seeded_scope, file_tables = seed_discovered_methods(seeded_scope, default_scope, root, certainty)
|
|
156
167
|
|
|
157
168
|
# v0.0.2 #5 + ADR-24 slice 2 — record per-instance-method def nodes, the class -> superclass map, and the
|
|
158
169
|
# class/module -> included-modules map, each merged under the cross-file pre-pass seed (see below). v0.1.2 —
|
|
@@ -160,7 +171,7 @@ module Rigor
|
|
|
160
171
|
# `def.method-visibility-mismatch` and ADR-35 `def.override-visibility-reduced` CheckRules consult the table.
|
|
161
172
|
# Seeded inside `merge_project_method_indexes` so the per-file visibilities merge OVER the cross-file project
|
|
162
173
|
# seed rather than overwriting it.
|
|
163
|
-
seeded_scope = merge_project_method_indexes(seeded_scope, default_scope, root,
|
|
174
|
+
seeded_scope = merge_project_method_indexes(seeded_scope, default_scope, root, file_tables, certainty)
|
|
164
175
|
|
|
165
176
|
table = {}.compare_by_identity
|
|
166
177
|
table.default = seeded_scope
|
|
@@ -187,11 +198,22 @@ module Rigor
|
|
|
187
198
|
# onto `seeded_scope` (merged UNDER the cross-file pre-pass seed `default_scope` carries), and returns `[scope,
|
|
188
199
|
# file_def_nodes, file_envelopes]` so the caller can thread the def-node and issue #992 envelope tables into
|
|
189
200
|
# {#merge_project_method_indexes} without walking the file a second time.
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
201
|
+
#
|
|
202
|
+
# ADR-119 WD3 — the file's `possible_discovered_methods` folds over the seed's by {#fold_possible_methods}, so a
|
|
203
|
+
# name the file defines certainly is no longer possible and a name only the file's possible contribution
|
|
204
|
+
# supplies becomes possible. The file's other siblings are returned for {#merge_project_method_indexes}.
|
|
205
|
+
def seed_discovered_methods(seeded_scope, default_scope, root, certainty = Certainty.possible_nodes(root))
|
|
206
|
+
file_methods, file_def_nodes, file_envelopes, file_refinements, file_siblings =
|
|
207
|
+
build_methods_and_def_nodes(root, default_scope.source_path, certainty: certainty)
|
|
208
|
+
seed_methods = default_scope.discovered_methods
|
|
209
|
+
discovered_methods = deep_merge_class_methods(seed_methods, file_methods)
|
|
210
|
+
discovery = seeded_scope.discovery.with(
|
|
211
|
+
discovered_methods: discovered_methods,
|
|
212
|
+
possible_discovered_methods: fold_possible_methods(
|
|
213
|
+
seeded_scope.discovery.possible_discovered_methods, seed_methods,
|
|
214
|
+
file_siblings[:possible_discovered_methods], file_methods
|
|
215
|
+
)
|
|
216
|
+
)
|
|
195
217
|
# Issue #1120 — only a file that refines something pays the overlay; the cross-file seed already carries
|
|
196
218
|
# every other file's refinements.
|
|
197
219
|
unless file_refinements.empty?
|
|
@@ -199,7 +221,7 @@ module Rigor
|
|
|
199
221
|
discovered_refinements: merge_refinement_tables(default_scope.discovered_refinements, file_refinements)
|
|
200
222
|
)
|
|
201
223
|
end
|
|
202
|
-
[seeded_scope.with_discovery(discovery), file_def_nodes, file_envelopes]
|
|
224
|
+
[seeded_scope.with_discovery(discovery), [file_def_nodes, file_envelopes, file_siblings]]
|
|
203
225
|
end
|
|
204
226
|
|
|
205
227
|
# ADR-48 Struct slice 3 — installs the top-level fold-safe-local set ({Inference::StructFoldSafety}). Struct
|
|
@@ -220,19 +242,22 @@ module Rigor
|
|
|
220
242
|
# Issue #992 — the envelope table is JOINED with the seed rather than overlaid: the seed already carries this
|
|
221
243
|
# file's own contribution (identical, so the join keeps it), and a reopening in a sibling file must still
|
|
222
244
|
# make a disagreeing name opaque here, which "same-file declarations win" would silently undo.
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
245
|
+
#
|
|
246
|
+
# `file_tables` is `[file_def_nodes, file_envelopes, file_siblings]` from {#seed_discovered_methods}.
|
|
247
|
+
def merge_project_method_indexes(seeded_scope, default_scope, root, file_tables,
|
|
248
|
+
certainty = Certainty.possible_nodes(root))
|
|
249
|
+
# ADR-116 WD5 — the superclass, def-nesting and member-layout tables come from ONE shared walk.
|
|
250
|
+
walked = declaration_walk_tables(root, default_scope.source_path)
|
|
251
|
+
def_nodes, def_nestings = merge_def_node_tables(default_scope, walked, file_tables[0])
|
|
252
|
+
singleton_def_nodes, file_singleton = merge_singleton_def_nodes(default_scope, root, certainty)
|
|
253
|
+
superclasses, header_nestings = merge_ancestry_tables(default_scope, walked)
|
|
254
|
+
includes, prepends, *mixin_unpositioned = merge_mixin_tables(default_scope, root)
|
|
228
255
|
# ADR-35 — per-file visibilities merged OVER the cross-file seed (the current file is authoritative for its own
|
|
229
256
|
# classes; sibling-file ancestors are preserved from the project seed).
|
|
230
|
-
method_visibilities = default_scope
|
|
231
|
-
build_discovered_method_visibilities(root)
|
|
232
|
-
) { |_class, cross_file, per_file| cross_file.merge(per_file) }
|
|
257
|
+
method_visibilities, file_visibilities = merge_method_visibilities(default_scope, root, certainty)
|
|
233
258
|
# ADR-48 — per-file Data + Struct member layouts merged OVER the cross-file seed (same-file declaration is
|
|
234
259
|
# authoritative).
|
|
235
|
-
data_member_layouts, struct_member_layouts = merge_member_layouts(default_scope,
|
|
260
|
+
data_member_layouts, struct_member_layouts = merge_member_layouts(default_scope, walked)
|
|
236
261
|
|
|
237
262
|
# #526 — this file's extends folded against the MERGED instance def-nodes (so `extend M` sees a
|
|
238
263
|
# sibling-file M through the cross-file seed). The project-wide fold in {#finalize_def_index}
|
|
@@ -240,8 +265,11 @@ module Rigor
|
|
|
240
265
|
#
|
|
241
266
|
# Issue #898 — and the same walk's table is now kept, merged over the cross-file seed the way
|
|
242
267
|
# `includes` is: `Narrowing` asks it what a class object's singleton ancestry holds.
|
|
243
|
-
|
|
244
|
-
|
|
268
|
+
siblings = per_file_siblings(seeded_scope.discovery.sibling_tables, file_tables[0], file_tables[2],
|
|
269
|
+
file_singleton, file_visibilities)
|
|
270
|
+
extends, methods_table, unpositioned =
|
|
271
|
+
merge_and_fold_extends(default_scope, root, def_nodes, singleton_def_nodes, seeded_scope, siblings,
|
|
272
|
+
mixin_unpositioned)
|
|
245
273
|
|
|
246
274
|
seeded_scope.with_discovery(
|
|
247
275
|
seeded_scope.discovery.with(
|
|
@@ -252,23 +280,60 @@ module Rigor
|
|
|
252
280
|
discovered_superclasses: superclasses,
|
|
253
281
|
discovered_header_nestings: header_nestings,
|
|
254
282
|
discovered_includes: includes,
|
|
255
|
-
discovered_prepends: prepends,
|
|
256
|
-
|
|
283
|
+
discovered_prepends: prepends, discovered_extends: extends,
|
|
284
|
+
unpositioned_mixins: unpositioned,
|
|
257
285
|
discovered_method_visibilities: method_visibilities,
|
|
258
|
-
discovered_parameter_envelopes: merge_envelope_seed(default_scope,
|
|
259
|
-
data_member_layouts: data_member_layouts,
|
|
260
|
-
|
|
261
|
-
|
|
286
|
+
discovered_parameter_envelopes: merge_envelope_seed(default_scope, file_tables[1]),
|
|
287
|
+
data_member_layouts: data_member_layouts, struct_member_layouts: struct_member_layouts,
|
|
288
|
+
discovered_deferred_ranges: merge_deferred_ranges_seed(default_scope, root),
|
|
289
|
+
# ADR-119 WD1/WD3 — the seed's siblings with this file's folded in ({#per_file_siblings}: the file wins
|
|
290
|
+
# every slot it writes), which the extends fold above then followed (copy on write, so the seed's
|
|
291
|
+
# tables are never written).
|
|
292
|
+
**siblings
|
|
262
293
|
)
|
|
263
294
|
)
|
|
264
295
|
end
|
|
265
296
|
|
|
297
|
+
# Returns `[merged table, [file table, file contested slots]]` ({#build_method_visibility_tables}).
|
|
298
|
+
def merge_method_visibilities(default_scope, root, certainty)
|
|
299
|
+
file_visibilities = build_method_visibility_tables(root, certainty: certainty)
|
|
300
|
+
merged = default_scope.discovered_method_visibilities.merge(
|
|
301
|
+
file_visibilities.first
|
|
302
|
+
) { |_class, cross_file, per_file| cross_file.merge(per_file) }
|
|
303
|
+
[merged, file_visibilities]
|
|
304
|
+
end
|
|
305
|
+
|
|
266
306
|
# Per-file singleton def nodes merged OVER the cross-file seed (same-file declaration is
|
|
267
307
|
# authoritative for its own classes, sibling-file defs are preserved).
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
308
|
+
#
|
|
309
|
+
# Returns `[merged table, [file table, file contested slots]]` ({#build_singleton_def_tables}).
|
|
310
|
+
def merge_singleton_def_nodes(default_scope, root, certainty)
|
|
311
|
+
file_singleton = build_singleton_def_tables(root, certainty: certainty)
|
|
312
|
+
merged = default_scope.discovered_singleton_def_nodes.merge(
|
|
313
|
+
file_singleton.first
|
|
271
314
|
) { |_class, cross_file, per_file| cross_file.merge(per_file) }
|
|
315
|
+
[merged, file_singleton]
|
|
316
|
+
end
|
|
317
|
+
|
|
318
|
+
# ADR-119 WD3 — the per-file merge of the slot siblings, under the members' own merges: this file's
|
|
319
|
+
# `def_nodes`, singleton defs and visibilities win every slot they write, so the seed's contest on those keys
|
|
320
|
+
# is replaced by the file's ({#fold_slot_contests}); the envelopes are joined, so their contests union. The
|
|
321
|
+
# possible methods were folded already ({#seed_discovered_methods}).
|
|
322
|
+
def per_file_siblings(siblings, file_def_nodes, file_siblings, file_singleton, file_visibilities)
|
|
323
|
+
file_singleton_def_nodes, file_singleton_contested = file_singleton
|
|
324
|
+
siblings[:contested_discovered_method_visibilities] = fold_slot_contests(
|
|
325
|
+
siblings[:contested_discovered_method_visibilities], *file_visibilities
|
|
326
|
+
)
|
|
327
|
+
siblings[:contested_discovered_def_nodes] = fold_slot_contests(
|
|
328
|
+
siblings[:contested_discovered_def_nodes], file_def_nodes, file_siblings[:contested_discovered_def_nodes]
|
|
329
|
+
)
|
|
330
|
+
siblings[:contested_discovered_singleton_def_nodes] = fold_slot_contests(
|
|
331
|
+
siblings[:contested_discovered_singleton_def_nodes], file_singleton_def_nodes, file_singleton_contested
|
|
332
|
+
)
|
|
333
|
+
siblings[:contested_discovered_parameter_envelopes] = union_paths(
|
|
334
|
+
siblings[:contested_discovered_parameter_envelopes], file_siblings[:contested_discovered_parameter_envelopes]
|
|
335
|
+
)
|
|
336
|
+
siblings
|
|
272
337
|
end
|
|
273
338
|
|
|
274
339
|
# Issue #1123 — the two instance-side mixin tables, from ONE descent of this file. Each merges over
|
|
@@ -284,28 +349,60 @@ module Rigor
|
|
|
284
349
|
end,
|
|
285
350
|
default_scope.discovered_prepends.merge(file[:prepends]) do |_class, cross_file, per_file|
|
|
286
351
|
(per_file + cross_file).uniq
|
|
287
|
-
end
|
|
352
|
+
end,
|
|
353
|
+
file[:unpositioned]
|
|
288
354
|
]
|
|
289
355
|
end
|
|
290
356
|
|
|
357
|
+
# The union of `{class => {side => [names]}}` tables — how {Scope::DiscoveryIndex#unpositioned_mixins}
|
|
358
|
+
# folds: an edge any contribution cannot position stays unpositioned. The per-file index folds its own
|
|
359
|
+
# walk over a seed that already carries this file's edges, so the first table itself comes back unless a
|
|
360
|
+
# contribution holds a name the seed lacks: the whole project table is copied only then.
|
|
361
|
+
def union_unpositioned(first, *others)
|
|
362
|
+
result = nil
|
|
363
|
+
others.each do |table|
|
|
364
|
+
table.each do |owner, sides|
|
|
365
|
+
sides.each do |side, names|
|
|
366
|
+
have = (result || first).dig(owner, side)
|
|
367
|
+
next if have && names.all? { |name| have.include?(name) }
|
|
368
|
+
|
|
369
|
+
result ||= first.dup
|
|
370
|
+
result[owner] = (result[owner] || {}).merge(side => ((have || []) | names).freeze).freeze
|
|
371
|
+
end
|
|
372
|
+
end
|
|
373
|
+
end
|
|
374
|
+
result ? result.freeze : first
|
|
375
|
+
end
|
|
376
|
+
|
|
291
377
|
# The `extend`-edge half of {#merge_project_method_indexes}: merges this file's `extend`s over the
|
|
292
378
|
# cross-file seed AND folds them against the merged def tables — the #526 fold that turns an
|
|
293
379
|
# extended module's instance defs into singleton-side method entries on the extending class.
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
380
|
+
#
|
|
381
|
+
# ADR-119 WD1 — the unpositioned union (the seed's, the mixin walks' and this file's extend side) is settled
|
|
382
|
+
# first, because the fold reads it to tell a certain extend edge from a listed one. `siblings` is written by
|
|
383
|
+
# the fold.
|
|
384
|
+
def merge_and_fold_extends(default_scope, root, def_nodes, singleton_def_nodes, seeded_scope, siblings,
|
|
385
|
+
mixin_unpositioned)
|
|
386
|
+
file_extends, extends, extend_unpositioned = merge_extend_tables(default_scope, root)
|
|
387
|
+
unpositioned = union_unpositioned(default_scope.discovery.unpositioned_mixins, *mixin_unpositioned,
|
|
388
|
+
extend_unpositioned)
|
|
389
|
+
methods_table = fold_per_file_extends(file_extends, def_nodes, singleton_def_nodes, seeded_scope, siblings,
|
|
390
|
+
unpositioned)
|
|
391
|
+
[extends, methods_table, unpositioned]
|
|
298
392
|
end
|
|
299
393
|
|
|
300
394
|
# Issue #1097 — this file's def / block / lambda ranges merged over the cross-file seed; the
|
|
301
395
|
# ordering predicates key the table by `source_path`, so the entry must exist even on a run
|
|
302
|
-
# that never built the project pre-pass.
|
|
303
|
-
#
|
|
396
|
+
# that never built the project pre-pass. A seeded entry is only the identical walk product when
|
|
397
|
+
# the pre-pass read the same bytes (a mutant analysed under a seed of the unmutated files does
|
|
398
|
+
# not), so the file is always re-walked and the seed is kept only when the rows are equal — which
|
|
399
|
+
# spares the project-wide Hash copy per file. Issue #1548.
|
|
304
400
|
def merge_deferred_ranges_seed(default_scope, root)
|
|
305
401
|
seeded = default_scope.discovered_deferred_ranges
|
|
306
|
-
|
|
402
|
+
fresh = build_deferred_ranges(root)
|
|
403
|
+
return seeded if seeded[default_scope.source_path] == fresh
|
|
307
404
|
|
|
308
|
-
seeded.merge(default_scope.source_path =>
|
|
405
|
+
seeded.merge(default_scope.source_path => fresh)
|
|
309
406
|
end
|
|
310
407
|
|
|
311
408
|
def merge_envelope_seed(default_scope, file_envelopes)
|
|
@@ -315,70 +412,69 @@ module Rigor
|
|
|
315
412
|
# The as-written superclass table and its issue #682 header-nesting twin, each merged over the cross-file
|
|
316
413
|
# seed. Returned as a pair for the same reason {#merge_def_node_tables} is: both come from ONE walk of the
|
|
317
414
|
# file, so a caller cannot pair a superclass name with a nesting recorded by a different parse.
|
|
318
|
-
def merge_ancestry_tables(default_scope,
|
|
319
|
-
|
|
320
|
-
[default_scope.discovered_superclasses.merge(file_superclasses),
|
|
415
|
+
def merge_ancestry_tables(default_scope, walked)
|
|
416
|
+
[default_scope.discovered_superclasses.merge(walked.fetch(:superclasses)),
|
|
321
417
|
merge_header_nestings(default_scope.discovery.discovered_header_nestings.dup,
|
|
322
|
-
|
|
418
|
+
walked.fetch(:header_nestings)).freeze]
|
|
323
419
|
end
|
|
324
420
|
|
|
325
421
|
# The instance-side def-node table and its issue #681 nesting twin, each merged over the cross-file seed.
|
|
326
422
|
# Returned as a pair so the two stay written together: a node the merge keeps must be the same object the
|
|
327
423
|
# nesting table keys, and they are only that if both halves take the same file's walk.
|
|
328
|
-
def merge_def_node_tables(default_scope,
|
|
424
|
+
def merge_def_node_tables(default_scope, walked, file_def_nodes)
|
|
329
425
|
def_nodes = default_scope.discovered_def_nodes.merge(
|
|
330
426
|
file_def_nodes
|
|
331
427
|
) { |_class, cross_file, per_file| cross_file.merge(per_file) }
|
|
332
428
|
[def_nodes,
|
|
333
|
-
merge_def_nestings(default_scope.discovery.discovered_def_nestings,
|
|
429
|
+
merge_def_nestings(default_scope.discovery.discovered_def_nestings, walked.fetch(:def_nestings))]
|
|
334
430
|
end
|
|
335
431
|
|
|
336
432
|
# Issue #681 — the per-file nesting table over the cross-file seed. Both are keyed by node identity, so
|
|
337
|
-
# a same-file declaration and its cross-file twin are distinct keys and the
|
|
338
|
-
#
|
|
339
|
-
#
|
|
340
|
-
#
|
|
433
|
+
# a same-file declaration and its cross-file twin are distinct keys and the layering order is immaterial.
|
|
434
|
+
# Issue #1507 — layered by lookup ({LayeredDefNestings}) instead of copied: the copy was one insert per
|
|
435
|
+
# `def` in the project for every analysed file. Either table alone is returned as it is when the other is
|
|
436
|
+
# empty — since issue #716 a top-level `def` records its empty chain, so only a file declaring no `def` at
|
|
437
|
+
# all hands back the seed itself.
|
|
341
438
|
def merge_def_nestings(seed, file_nestings)
|
|
342
439
|
return seed if file_nestings.empty?
|
|
343
440
|
return file_nestings if seed.empty?
|
|
344
441
|
|
|
345
|
-
|
|
346
|
-
merged.merge!(seed)
|
|
347
|
-
merged.merge!(file_nestings)
|
|
348
|
-
merged.freeze
|
|
442
|
+
LayeredDefNestings.new(file_nestings, seed)
|
|
349
443
|
end
|
|
350
444
|
|
|
351
445
|
# Issue #898 — one walk, two consumers: the raw per-file table the #526 method fold reads, and the
|
|
352
446
|
# same table merged over the cross-file seed for the scope. Returned as a pair so a caller cannot
|
|
353
447
|
# pair the fold with a table a different parse produced, as {#merge_ancestry_tables} is.
|
|
354
448
|
def merge_extend_tables(default_scope, root)
|
|
355
|
-
|
|
449
|
+
file = extend_tables(root)
|
|
450
|
+
file_extends = file.fetch(:extends)
|
|
356
451
|
# The table stores singleton-ancestor search order, so the file under analysis — the
|
|
357
452
|
# later-loading contribution for a reopened class — prepends over the cross-file seed.
|
|
358
453
|
merged = default_scope.discovered_extends.merge(
|
|
359
454
|
file_extends
|
|
360
455
|
) { |_class, cross_file, per_file| (per_file + cross_file).uniq }
|
|
361
|
-
[file_extends, merged]
|
|
456
|
+
[file_extends, merged, file.fetch(:unpositioned)]
|
|
362
457
|
end
|
|
363
458
|
|
|
364
459
|
# The per-file half of the #526 fold: mutable copies of the merged tables take the extends, and the
|
|
365
460
|
# existence table (already seeded onto the scope) is rebuilt only when the fold touched it.
|
|
366
|
-
def fold_per_file_extends(extends, def_nodes, singleton_def_nodes, seeded_scope)
|
|
461
|
+
def fold_per_file_extends(extends, def_nodes, singleton_def_nodes, seeded_scope, siblings, unpositioned)
|
|
367
462
|
methods_table = seeded_scope.discovered_methods
|
|
368
463
|
return methods_table if extends.empty?
|
|
369
464
|
|
|
370
465
|
mutable_methods = methods_table.transform_values(&:dup)
|
|
371
|
-
fold_extends_into_singleton_tables(extends, def_nodes, singleton_def_nodes, mutable_methods
|
|
466
|
+
fold_extends_into_singleton_tables(extends, def_nodes, singleton_def_nodes, mutable_methods,
|
|
467
|
+
siblings: siblings, unpositioned: unpositioned)
|
|
372
468
|
mutable_methods
|
|
373
469
|
end
|
|
374
470
|
|
|
375
471
|
# ADR-48 — the per-file Data + Struct member-layout tables, each merged OVER the cross-file seed so a same-file
|
|
376
472
|
# declaration wins for its own classes. Returned as a pair to keep {#merge_project_method_indexes} under the
|
|
377
473
|
# method-size budget.
|
|
378
|
-
def merge_member_layouts(default_scope,
|
|
474
|
+
def merge_member_layouts(default_scope, walked)
|
|
379
475
|
[
|
|
380
|
-
default_scope.data_member_layouts.merge(
|
|
381
|
-
default_scope.struct_member_layouts.merge(
|
|
476
|
+
default_scope.data_member_layouts.merge(walked.fetch(:data_member_layouts)),
|
|
477
|
+
default_scope.struct_member_layouts.merge(walked.fetch(:struct_member_layouts))
|
|
382
478
|
]
|
|
383
479
|
end
|
|
384
480
|
|
|
@@ -1946,7 +2042,20 @@ module Rigor
|
|
|
1946
2042
|
# `Prism::ClassVariableWriteNode` writes inside ANY def body (instance or singleton) of the enclosing class,
|
|
1947
2043
|
# because Ruby cvars are shared across both facets. The resulting table is seeded into both instance and singleton
|
|
1948
2044
|
# method bodies through `Scope#class_cvars_for`.
|
|
2045
|
+
#
|
|
2046
|
+
# ADR-116 WD5 — built by {ClassCvarsCollector} on the shared {DeclarationWalk}; `RIGOR_SHADOW_RULE_WALK`
|
|
2047
|
+
# checks it against {#legacy_class_cvar_index}.
|
|
1949
2048
|
def build_class_cvar_index(root, default_scope)
|
|
2049
|
+
collector = ClassCvarsCollector.new
|
|
2050
|
+
DeclarationWalk.run(root, [collector], DeclarationWalk::Context.root(scope: default_scope))
|
|
2051
|
+
DeclarationWalk::Shadow.verified(:class_cvars, default_scope.source_path, collector.table) do
|
|
2052
|
+
legacy_class_cvar_index(root, default_scope)
|
|
2053
|
+
end
|
|
2054
|
+
end
|
|
2055
|
+
|
|
2056
|
+
# The walker {ClassCvarsCollector} replaced, kept as the shadow harness's oracle until ADR-116 WD5 has
|
|
2057
|
+
# ported every table walker onto {DeclarationWalk}.
|
|
2058
|
+
def legacy_class_cvar_index(root, default_scope)
|
|
1950
2059
|
accumulator = {}
|
|
1951
2060
|
walk_class_cvars(root, [], default_scope, accumulator)
|
|
1952
2061
|
accumulator.transform_values(&:freeze).freeze
|
|
@@ -2867,8 +2976,7 @@ module Rigor
|
|
|
2867
2976
|
# `ArgumentsNode`, and a `&blk` pass-through is a `BlockArgumentNode`.
|
|
2868
2977
|
def walk_constant_write_children(node, qualified_prefix, default_scope, accumulator, self_owner,
|
|
2869
2978
|
singleton_cref)
|
|
2870
|
-
rebound = rebound_block_self(node, qualified_prefix, default_scope, nil,
|
|
2871
|
-
rebound_self_base(self_owner))
|
|
2979
|
+
rebound = rebound_block_self(node, qualified_prefix, default_scope, nil, self_owner)
|
|
2872
2980
|
node.rigor_each_child do |child|
|
|
2873
2981
|
rebinds_self = rebound && child.is_a?(Prism::BlockNode)
|
|
2874
2982
|
owner = rebinds_self ? rebound : self_owner
|
|
@@ -2894,14 +3002,18 @@ module Rigor
|
|
|
2894
3002
|
# caller ({#meta_new_block_owner}) ([#710](https://github.com/rigortype/rigor/issues/710)). It is
|
|
2895
3003
|
# supplied only by the publication census; the typed walk returns at a `ConstantWriteNode` without
|
|
2896
3004
|
# descending into its rvalue, so no `self::` write inside one reaches that walk at all.
|
|
3005
|
+
#
|
|
3006
|
+
# `self_owner` is the enclosing body's self as the walk carries it, unsplit. Every walk asks this at
|
|
3007
|
+
# every node it visits, and only an eval call reads the self, so the split waits until one does
|
|
3008
|
+
# ([#1502](https://github.com/rigortype/rigor/issues/1502)).
|
|
2897
3009
|
def rebound_block_self(node, qualified_prefix, default_scope = nil, meta_owner = nil,
|
|
2898
|
-
|
|
3010
|
+
self_owner = nil)
|
|
2899
3011
|
return nil unless node.is_a?(Prism::CallNode) && node.block.is_a?(Prism::BlockNode)
|
|
2900
3012
|
return OPAQUE_SELF if OPAQUE_SELF_BLOCK_CALLS.include?(node.name)
|
|
2901
3013
|
return meta_owner || OPAQUE_SELF if meta_new_constant_rvalue?(node)
|
|
2902
3014
|
return nil unless RECEIVER_EVAL_CALLS.include?(node.name)
|
|
2903
3015
|
|
|
2904
|
-
eval_receiver_self(node.receiver, qualified_prefix, default_scope,
|
|
3016
|
+
eval_receiver_self(node.receiver, qualified_prefix, default_scope, rebound_self_base(self_owner))
|
|
2905
3017
|
end
|
|
2906
3018
|
|
|
2907
3019
|
# Issue #710 — the qualified name a `Klass = Class.new { … }` gives the block's class, or nil when
|
|
@@ -3128,20 +3240,59 @@ module Rigor
|
|
|
3128
3240
|
# Issue #1120 — and a fourth, `refinements`: `{refined class => {method => [refining modules]}}`, the
|
|
3129
3241
|
# `def`s of every `refine X do … end` body ({#record_refinement_defs}). Those are kept OUT of the first
|
|
3130
3242
|
# three tables, which answer everywhere: a refined method exists only where a `using` is in effect.
|
|
3131
|
-
|
|
3132
|
-
|
|
3243
|
+
#
|
|
3244
|
+
# ADR-119 WD3 — and a fifth, the file's def-contribution siblings ({MethodTables#siblings}):
|
|
3245
|
+
# `possible_discovered_methods`, `contested_discovered_def_nodes` and
|
|
3246
|
+
# `contested_discovered_parameter_envelopes`, from the {Certainty} classification `certainty` (computed here
|
|
3247
|
+
# when the caller has none).
|
|
3248
|
+
def build_methods_and_def_nodes(root, source_path = nil, certainty: Certainty.possible_nodes(root))
|
|
3249
|
+
tables = MethodTables.new({}, {}, nil, certainty, true)
|
|
3133
3250
|
def_nodes = {}
|
|
3134
3251
|
walk_methods_and_def_nodes(root, [], false, tables, def_nodes, source_path)
|
|
3135
|
-
apply_alias_def_nodes(root, def_nodes)
|
|
3252
|
+
apply_alias_def_nodes(root, def_nodes, tables)
|
|
3136
3253
|
[tables.existence.transform_values(&:freeze).freeze, def_nodes.transform_values(&:freeze).freeze,
|
|
3137
|
-
tables.envelopes.transform_values(&:freeze).freeze, freeze_refinements(tables.refinements)
|
|
3254
|
+
tables.envelopes.transform_values(&:freeze).freeze, freeze_refinements(tables.refinements),
|
|
3255
|
+
tables.siblings]
|
|
3138
3256
|
end
|
|
3139
3257
|
|
|
3140
3258
|
# The accumulator {#walk_methods_and_def_nodes} threads: the existence table and its issue #992
|
|
3141
3259
|
# envelope twin, which only {#record_method} and {#record_surface_mark} write, and the issue #1120
|
|
3142
3260
|
# refinement table, which only {#record_refinement_defs} writes. That last one stays nil until a
|
|
3143
3261
|
# `refine` block is seen, so a file without one allocates nothing for it.
|
|
3144
|
-
|
|
3262
|
+
#
|
|
3263
|
+
# ADR-119 WD3 — `certainty` is the file's {Certainty} answer and `certain` whether the contribution being
|
|
3264
|
+
# recorded is certain ({#at} sets it before each recorder). `possible`, `contested_envelopes` and
|
|
3265
|
+
# `contested_def_nodes` are the siblings the recorders fill, each nil until a possible contribution writes it.
|
|
3266
|
+
MethodTables = Struct.new(:existence, :envelopes, :refinements, :certainty, :certain, :possible,
|
|
3267
|
+
:contested_envelopes, :contested_def_nodes) do
|
|
3268
|
+
# Sets {#certain} for the contribution `node` and returns the tables.
|
|
3269
|
+
def at(node)
|
|
3270
|
+
self.certain = !Certainty.possible?(certainty, node)
|
|
3271
|
+
self
|
|
3272
|
+
end
|
|
3273
|
+
|
|
3274
|
+
# The `{sibling name => table}` part this walk fills, frozen, with the shared empty value for each
|
|
3275
|
+
# sibling nothing wrote.
|
|
3276
|
+
def siblings
|
|
3277
|
+
empty = Scope::DiscoveryIndex::EMPTY_SIBLINGS
|
|
3278
|
+
{
|
|
3279
|
+
possible_discovered_methods:
|
|
3280
|
+
possible ? possible.transform_values(&:freeze).freeze : empty[:possible_discovered_methods],
|
|
3281
|
+
contested_discovered_def_nodes:
|
|
3282
|
+
frozen_paths(contested_def_nodes) || empty[:contested_discovered_def_nodes],
|
|
3283
|
+
contested_discovered_parameter_envelopes:
|
|
3284
|
+
frozen_paths(contested_envelopes) || empty[:contested_discovered_parameter_envelopes]
|
|
3285
|
+
}.freeze
|
|
3286
|
+
end
|
|
3287
|
+
|
|
3288
|
+
private
|
|
3289
|
+
|
|
3290
|
+
def frozen_paths(set)
|
|
3291
|
+
return nil if set.nil? || set.empty?
|
|
3292
|
+
|
|
3293
|
+
set.each(&:freeze).freeze
|
|
3294
|
+
end
|
|
3295
|
+
end
|
|
3145
3296
|
|
|
3146
3297
|
EMPTY_REFINEMENTS = {}.freeze
|
|
3147
3298
|
private_constant :EMPTY_REFINEMENTS
|
|
@@ -3215,9 +3366,58 @@ module Rigor
|
|
|
3215
3366
|
|
|
3216
3367
|
# The walk's single existence writer. Everything except a `def` passes no envelope and so records
|
|
3217
3368
|
# {Source::ParameterEnvelope::OPAQUE}.
|
|
3369
|
+
#
|
|
3370
|
+
# ADR-119 WD3 — the existence table's `possible` sibling follows the contribution's certainty
|
|
3371
|
+
# ({MethodTables#certain}): a certain contribution removes its kind from the name's possible kind, and a
|
|
3372
|
+
# possible one adds its kind unless the table already answered that kind certainly. A possible contribution
|
|
3373
|
+
# also contests the envelope entry it joins (envelopes are joined, so one possible joiner is enough).
|
|
3218
3374
|
def record_method(tables, class_name, method_name, kind, envelope = Source::ParameterEnvelope::OPAQUE)
|
|
3375
|
+
follow_method_certainty(tables, class_name, method_name, kind) unless tables.certain && tables.possible.nil?
|
|
3219
3376
|
record_method_kind(tables.existence, class_name, method_name, kind)
|
|
3220
3377
|
record_envelope(tables.envelopes, class_name, [kind, method_name], envelope)
|
|
3378
|
+
contest_envelope(tables, class_name, [kind, method_name]) unless tables.certain
|
|
3379
|
+
end
|
|
3380
|
+
|
|
3381
|
+
# The `possible_discovered_methods` half of {#record_method}, read BEFORE the existence table is written.
|
|
3382
|
+
def follow_method_certainty(tables, class_name, method_name, kind)
|
|
3383
|
+
if tables.certain
|
|
3384
|
+
settle_possible_kind(tables.possible, class_name, method_name, kind)
|
|
3385
|
+
else
|
|
3386
|
+
sitting = tables.possible&.dig(class_name, method_name)
|
|
3387
|
+
return if kind_covers?(tables.existence[class_name]&.[](method_name), kind) && !kind_covers?(sitting, kind)
|
|
3388
|
+
|
|
3389
|
+
row = ((tables.possible ||= {})[class_name] ||= {})
|
|
3390
|
+
row[method_name] = merge_sibling_kind(sitting, kind)
|
|
3391
|
+
end
|
|
3392
|
+
end
|
|
3393
|
+
|
|
3394
|
+
# A certain contribution: `kind` leaves the name's possible kind, and an emptied row goes.
|
|
3395
|
+
def settle_possible_kind(possible, class_name, method_name, kind)
|
|
3396
|
+
row = possible[class_name]
|
|
3397
|
+
return if row.nil?
|
|
3398
|
+
|
|
3399
|
+
remaining = without_kind(row[method_name], kind)
|
|
3400
|
+
return if remaining == row[method_name]
|
|
3401
|
+
|
|
3402
|
+
remaining.nil? ? row.delete(method_name) : (row[method_name] = remaining)
|
|
3403
|
+
possible.delete(class_name) if row.empty?
|
|
3404
|
+
end
|
|
3405
|
+
|
|
3406
|
+
# Whether a recorded kind (`:instance`, `:singleton`, `:both`, or nil) answers `kind`.
|
|
3407
|
+
def kind_covers?(recorded, kind)
|
|
3408
|
+
!recorded.nil? && (recorded == kind || recorded == Scope::DiscoveryIndex::METHOD_KIND_BOTH)
|
|
3409
|
+
end
|
|
3410
|
+
|
|
3411
|
+
# `recorded` less `kind`: nil when nothing remains.
|
|
3412
|
+
def without_kind(recorded, kind)
|
|
3413
|
+
return nil if recorded.nil? || recorded == kind
|
|
3414
|
+
return recorded unless recorded == Scope::DiscoveryIndex::METHOD_KIND_BOTH
|
|
3415
|
+
|
|
3416
|
+
kind == :instance ? :singleton : :instance
|
|
3417
|
+
end
|
|
3418
|
+
|
|
3419
|
+
def contest_envelope(tables, class_name, key)
|
|
3420
|
+
(tables.contested_envelopes ||= Set.new) << [class_name, key]
|
|
3221
3421
|
end
|
|
3222
3422
|
|
|
3223
3423
|
def record_envelope(envelopes, class_name, key, envelope)
|
|
@@ -3303,7 +3503,7 @@ module Rigor
|
|
|
3303
3503
|
# `self::` header under a REBOUND self (eval/meta-new body) names
|
|
3304
3504
|
# `owner::Name` instead.
|
|
3305
3505
|
child_cref = unnameable_decl?(node, self_decl, singleton_cref)
|
|
3306
|
-
record_declaration_facts(node, child_prefix, methods_acc) unless child_cref
|
|
3506
|
+
record_declaration_facts(node, child_prefix, methods_acc.at(node)) unless child_cref
|
|
3307
3507
|
body_prefix = child_cref ? [] : child_prefix
|
|
3308
3508
|
if node.body
|
|
3309
3509
|
walk_methods_and_def_nodes(node.body, body_prefix, false, methods_acc, def_nodes_acc,
|
|
@@ -3346,7 +3546,9 @@ module Rigor
|
|
|
3346
3546
|
child_prefix = meta_new_child_prefix(node, qualified_prefix, def_owner_prefix)
|
|
3347
3547
|
meta_ownerless = singleton_cref &&
|
|
3348
3548
|
!meta_new_path_target_nameable?(node, def_owner_prefix)
|
|
3349
|
-
|
|
3549
|
+
if child_prefix && !meta_ownerless
|
|
3550
|
+
record_meta_new_facts(meta_new_rvalue(node), child_prefix, methods_acc.at(node))
|
|
3551
|
+
end
|
|
3350
3552
|
enclosing.each do |part|
|
|
3351
3553
|
walk_methods_and_def_nodes(part, qualified_prefix, in_singleton_class, methods_acc,
|
|
3352
3554
|
def_nodes_acc, source_path, def_owner_prefix,
|
|
@@ -3373,16 +3575,16 @@ module Rigor
|
|
|
3373
3575
|
unless def_owner_prefix&.empty? || defs_singleton == :unnameable ||
|
|
3374
3576
|
(singleton_cref && owner_prefix.empty?)
|
|
3375
3577
|
singleton_def = in_singleton_class || defs_singleton
|
|
3376
|
-
record_def_method(node, owner_prefix, singleton_def, methods_acc)
|
|
3578
|
+
record_def_method(node, owner_prefix, singleton_def, methods_acc.at(node))
|
|
3377
3579
|
record_def_body_evidence(node, owner_prefix, methods_acc)
|
|
3378
|
-
record_def_node(node, owner_prefix, singleton_def, def_nodes_acc)
|
|
3580
|
+
record_def_node(node, owner_prefix, singleton_def, def_nodes_acc, methods_acc)
|
|
3379
3581
|
end
|
|
3380
3582
|
return
|
|
3381
3583
|
when Prism::AliasMethodNode, Prism::UndefNode
|
|
3382
3584
|
unless def_owner_prefix&.empty? || defs_singleton == :unnameable ||
|
|
3383
3585
|
(singleton_cref && owner_prefix.empty?)
|
|
3384
3586
|
record_alias_or_undef(node, owner_prefix, in_singleton_class || defs_singleton,
|
|
3385
|
-
methods_acc)
|
|
3587
|
+
methods_acc.at(node))
|
|
3386
3588
|
end
|
|
3387
3589
|
return
|
|
3388
3590
|
when Prism::CallNode
|
|
@@ -3392,7 +3594,8 @@ module Rigor
|
|
|
3392
3594
|
singleton_cref: singleton_cref,
|
|
3393
3595
|
defs_singleton: defs_singleton)
|
|
3394
3596
|
end
|
|
3395
|
-
anonymous = record_call_node_methods(node, owner_prefix, in_singleton_class, methods_acc,
|
|
3597
|
+
anonymous = record_call_node_methods(node, owner_prefix, in_singleton_class, methods_acc.at(node),
|
|
3598
|
+
source_path)
|
|
3396
3599
|
# Issue #1120 — a refine body's defs go to the refinement table and nowhere else.
|
|
3397
3600
|
if (target = refine_target(node))
|
|
3398
3601
|
return record_refinement_defs(node, target, qualified_prefix, owner_prefix, methods_acc)
|
|
@@ -3514,7 +3717,7 @@ module Rigor
|
|
|
3514
3717
|
def walk_anonymous_meta_block(call_node, name, qualified_prefix, in_singleton_class, methods_acc, # rubocop:disable Metrics/ParameterLists
|
|
3515
3718
|
def_nodes_acc, source_path, def_owner_prefix = nil,
|
|
3516
3719
|
singleton_cref: false, defs_singleton: false)
|
|
3517
|
-
record_meta_members(call_node, [name], methods_acc)
|
|
3720
|
+
record_meta_members(call_node, [name], methods_acc.at(call_node))
|
|
3518
3721
|
call_node.rigor_each_child do |child|
|
|
3519
3722
|
if child.equal?(call_node.block)
|
|
3520
3723
|
body = call_node.block.body
|
|
@@ -3928,13 +4131,30 @@ module Rigor
|
|
|
3928
4131
|
# same-named local method.
|
|
3929
4132
|
TOP_LEVEL_DEF_KEY = "<toplevel>"
|
|
3930
4133
|
|
|
3931
|
-
|
|
4134
|
+
#
|
|
4135
|
+
# ADR-119 WD3 — the slot follows its writer (later-wins): a certain write clears the key from
|
|
4136
|
+
# `contested_discovered_def_nodes` and a possible one adds it ({MethodTables#certain}, set for `def_node`).
|
|
4137
|
+
def record_def_node(def_node, qualified_prefix, in_singleton_class, accumulator, tables = nil)
|
|
3932
4138
|
return if def_singleton?(def_node, qualified_prefix, in_singleton_class)
|
|
3933
4139
|
|
|
3934
4140
|
class_name = qualified_prefix.empty? ? TOP_LEVEL_DEF_KEY : qualified_prefix.join("::")
|
|
3935
4141
|
accumulator[class_name] ||= {}
|
|
3936
4142
|
accumulator[class_name][def_node.name] = def_node
|
|
3937
|
-
|
|
4143
|
+
follow_slot_writer(tables, :contested_def_nodes, [class_name, def_node.name], tables.nil? || tables.certain)
|
|
4144
|
+
record_anonymous_body_def_as_toplevel(def_node, qualified_prefix, accumulator, tables)
|
|
4145
|
+
end
|
|
4146
|
+
|
|
4147
|
+
# ADR-119 WD3 — a single-valued slot's contest follows the write that holds it: `certain` clears `key` from
|
|
4148
|
+
# the `slot` Set of `tables`, a possible write adds it. Nil `tables` (a caller with no certainty) is a no-op.
|
|
4149
|
+
def follow_slot_writer(tables, slot, key, certain)
|
|
4150
|
+
return if tables.nil?
|
|
4151
|
+
|
|
4152
|
+
if certain
|
|
4153
|
+
set = tables[slot]
|
|
4154
|
+
set&.delete(key)
|
|
4155
|
+
else
|
|
4156
|
+
(tables[slot] ||= Set.new) << key
|
|
4157
|
+
end
|
|
3938
4158
|
end
|
|
3939
4159
|
|
|
3940
4160
|
# #319 — a `def` inside an anonymous `Class.new` / `Module.new` body ALSO stays in the `<toplevel>` table.
|
|
@@ -3946,12 +4166,16 @@ module Rigor
|
|
|
3946
4166
|
# `spawn(...) { start }` block. Giving the body a class of its own must not silently retract it: the call
|
|
3947
4167
|
# resolves at runtime, and `call.unresolved-toplevel` firing on it would be a new false positive traded
|
|
3948
4168
|
# for the ones this change retires. Never clobbers a real top-level `def` of the same name.
|
|
3949
|
-
def record_anonymous_body_def_as_toplevel(def_node, qualified_prefix, accumulator)
|
|
4169
|
+
def record_anonymous_body_def_as_toplevel(def_node, qualified_prefix, accumulator, tables = nil)
|
|
3950
4170
|
return unless qualified_prefix.length == 1
|
|
3951
4171
|
return unless Type::AnonymousClassName.match?(qualified_prefix.first)
|
|
3952
4172
|
|
|
3953
4173
|
table = (accumulator[TOP_LEVEL_DEF_KEY] ||= {})
|
|
3954
|
-
table
|
|
4174
|
+
return if table.key?(def_node.name)
|
|
4175
|
+
|
|
4176
|
+
table[def_node.name] = def_node
|
|
4177
|
+
follow_slot_writer(tables, :contested_def_nodes, [TOP_LEVEL_DEF_KEY, def_node.name],
|
|
4178
|
+
tables.nil? || tables.certain)
|
|
3955
4179
|
end
|
|
3956
4180
|
|
|
3957
4181
|
# Module-singleton call resolution (ADR-57 follow-up) — the SINGLETON-side mirror of `build_discovered_def_nodes`.
|
|
@@ -3963,9 +4187,43 @@ module Rigor
|
|
|
3963
4187
|
# singleton defs (`def self.x` outside any class — `self` is `main`) are not recorded; they have no constant
|
|
3964
4188
|
# receiver to dispatch through.
|
|
3965
4189
|
def build_discovered_singleton_def_nodes(root)
|
|
3966
|
-
|
|
4190
|
+
build_singleton_def_tables(root).first
|
|
4191
|
+
end
|
|
4192
|
+
|
|
4193
|
+
# ADR-119 WD3 — {#build_discovered_singleton_def_nodes} with its sibling: `[table,
|
|
4194
|
+
# contested_discovered_singleton_def_nodes]`, the second a frozen Set of `[class, name]` keys whose slot a
|
|
4195
|
+
# possible `def self.x`, `class << self` def or `module_function` copy wrote last ({Certainty} `certainty`).
|
|
4196
|
+
def build_singleton_def_tables(root, certainty: Certainty.possible_nodes(root))
|
|
4197
|
+
accumulator = SingletonDefTable.new.track(certainty)
|
|
3967
4198
|
walk_singleton_def_nodes(root, [], false, accumulator)
|
|
3968
|
-
accumulator.transform_values(&:freeze).freeze
|
|
4199
|
+
[accumulator.transform_values(&:freeze).freeze, accumulator.contested_paths]
|
|
4200
|
+
end
|
|
4201
|
+
|
|
4202
|
+
# The singleton walk's accumulator: the `{class => {name => DefNode}}` table, plus the contested slots, which
|
|
4203
|
+
# follow each slot's writer ({#follow_slot_writer}'s rule).
|
|
4204
|
+
class SingletonDefTable < Hash
|
|
4205
|
+
def track(certainty)
|
|
4206
|
+
@certainty = certainty
|
|
4207
|
+
@contested = nil
|
|
4208
|
+
self
|
|
4209
|
+
end
|
|
4210
|
+
|
|
4211
|
+
# Writes the slot; `node` and `other` are the contributions the write rests on, possible when either is.
|
|
4212
|
+
def write(class_name, name, def_node, node, other = nil)
|
|
4213
|
+
(self[class_name] ||= {})[name] = def_node
|
|
4214
|
+
if Certainty.possible?(@certainty, node) || (other && Certainty.possible?(@certainty, other))
|
|
4215
|
+
(@contested ||= Set.new) << [class_name, name]
|
|
4216
|
+
else
|
|
4217
|
+
@contested&.delete([class_name, name])
|
|
4218
|
+
end
|
|
4219
|
+
end
|
|
4220
|
+
|
|
4221
|
+
def contested_paths
|
|
4222
|
+
contested = @contested
|
|
4223
|
+
return contested.each(&:freeze).freeze if contested && !contested.empty?
|
|
4224
|
+
|
|
4225
|
+
Scope::DiscoveryIndex::EMPTY_SIBLINGS[:contested_discovered_singleton_def_nodes]
|
|
4226
|
+
end
|
|
3969
4227
|
end
|
|
3970
4228
|
|
|
3971
4229
|
# Issue #1097 — `[[start_offset, end_offset, name, kind, owner], ...]` for every `def` / block /
|
|
@@ -4004,12 +4262,14 @@ module Rigor
|
|
|
4004
4262
|
defs_singleton: false)
|
|
4005
4263
|
return unless node.is_a?(Prism::Node)
|
|
4006
4264
|
|
|
4007
|
-
|
|
4265
|
+
# Tested by exact class (Prism's node classes have no subclasses): the three `any?` scans this
|
|
4266
|
+
# replaced ran a block per list member at every node ([#1507](https://github.com/rigortype/rigor/issues/1507)).
|
|
4267
|
+
if CLASS_BODY_NODES.include?(node.class)
|
|
4008
4268
|
return walk_deferred_lexical_body(node, qualified_prefix, in_singleton_class,
|
|
4009
4269
|
inside_deferred, mf_offsets, ranges, def_owner_prefix,
|
|
4010
4270
|
singleton_cref: singleton_cref)
|
|
4011
4271
|
end
|
|
4012
|
-
if META_WRITE_NODES.
|
|
4272
|
+
if META_WRITE_NODES.include?(node.class)
|
|
4013
4273
|
return walk_deferred_meta_new(node, qualified_prefix, in_singleton_class, inside_deferred,
|
|
4014
4274
|
mf_offsets, ranges, def_owner_prefix,
|
|
4015
4275
|
singleton_cref: singleton_cref)
|
|
@@ -4019,7 +4279,7 @@ module Rigor
|
|
|
4019
4279
|
mf_offsets, ranges, def_owner_prefix, singleton_cref,
|
|
4020
4280
|
defs_singleton)
|
|
4021
4281
|
end
|
|
4022
|
-
if DEFERRED_RANGE_NODES.
|
|
4282
|
+
if DEFERRED_RANGE_NODES.include?(node.class)
|
|
4023
4283
|
# Deferred: a call inside runs at invocation / interpreter-exit time. `BEGIN`
|
|
4024
4284
|
# (PreExecutionNode) is the opposite — eager, before the class body — and stays unlisted.
|
|
4025
4285
|
ranges << [node.location.start_offset, node.location.end_offset, nil, nil, nil]
|
|
@@ -4052,10 +4312,10 @@ module Rigor
|
|
|
4052
4312
|
defs_singleton: defs_singleton)
|
|
4053
4313
|
end
|
|
4054
4314
|
|
|
4055
|
-
DEFERRED_RANGE_NODES = [Prism::BlockNode, Prism::LambdaNode, Prism::PostExecutionNode].freeze
|
|
4056
|
-
META_WRITE_NODES = [Prism::ConstantWriteNode, Prism::ConstantPathWriteNode,
|
|
4057
|
-
|
|
4058
|
-
CLASS_BODY_NODES = [Prism::ClassNode, Prism::ModuleNode, Prism::SingletonClassNode].freeze
|
|
4315
|
+
DEFERRED_RANGE_NODES = Set[Prism::BlockNode, Prism::LambdaNode, Prism::PostExecutionNode].freeze
|
|
4316
|
+
META_WRITE_NODES = Set[Prism::ConstantWriteNode, Prism::ConstantPathWriteNode,
|
|
4317
|
+
Prism::ConstantOrWriteNode, Prism::ConstantPathOrWriteNode].freeze
|
|
4318
|
+
CLASS_BODY_NODES = Set[Prism::ClassNode, Prism::ModuleNode, Prism::SingletonClassNode].freeze
|
|
4059
4319
|
private_constant :DEFERRED_RANGE_NODES, :META_WRITE_NODES, :CLASS_BODY_NODES
|
|
4060
4320
|
|
|
4061
4321
|
def walk_deferred_children(node, qualified_prefix, in_singleton_class, inside_deferred, # rubocop:disable Metrics/ParameterLists
|
|
@@ -4156,7 +4416,7 @@ module Rigor
|
|
|
4156
4416
|
end
|
|
4157
4417
|
|
|
4158
4418
|
# Body-level entry for a class / module / `class <<` / meta-`new` / eval-block body: prescans
|
|
4159
|
-
# the body's `module_function` state first ({
|
|
4419
|
+
# the body's `module_function` state first ({ModuleFunctionState.prescan_deferred} — bare-call
|
|
4160
4420
|
# offsets plus the retro-install and `module_function def x` rows), then walks each statement
|
|
4161
4421
|
# with the offsets in hand. The prescan looks through nested containers (`if` / `begin` /
|
|
4162
4422
|
# `rescue` / blocks) because a `module_function` that RAN there still flips later defs —
|
|
@@ -4173,8 +4433,8 @@ module Rigor
|
|
|
4173
4433
|
# `module_function` under an unnameable definee (`class <<` + `instance_eval`)
|
|
4174
4434
|
# toggles the metaclass — its rows must not name the enclosing class.
|
|
4175
4435
|
prescan_owner = defs_singleton == :unnameable ? EMPTY_PREFIX : owner_prefix
|
|
4176
|
-
|
|
4177
|
-
|
|
4436
|
+
ModuleFunctionState.prescan_deferred(body, prescan_owner, in_singleton_class, mf_offsets,
|
|
4437
|
+
ranges)
|
|
4178
4438
|
end
|
|
4179
4439
|
statements_of(body).each do |stmt|
|
|
4180
4440
|
walk_deferred_ranges(stmt, qualified_prefix, in_singleton_class, inside_deferred,
|
|
@@ -4183,49 +4443,6 @@ module Rigor
|
|
|
4183
4443
|
end
|
|
4184
4444
|
end
|
|
4185
4445
|
|
|
4186
|
-
# The `module_function` prescan over one body's subtree: bare-call offsets (defs starting after
|
|
4187
|
-
# one are module functions), a `:singleton` row at each `module_function :name` call — the
|
|
4188
|
-
# retro-install happens AT THE CALL, not the def — and a `:both` / `:singleton` row for a
|
|
4189
|
-
# `module_function def x` argument. Nested class / module / `class <<` / def bodies are skipped:
|
|
4190
|
-
# `module_function` there targets a different module. An `END` body is skipped too — it runs at
|
|
4191
|
-
# interpreter exit, after every def — and so is a `*_eval` / `*_exec` block, whose `self`
|
|
4192
|
-
# rebinds to the receiver's module. Blocks, lambdas and control-flow containers are entered —
|
|
4193
|
-
# their `self` is still this module, so the call really can toggle.
|
|
4194
|
-
def collect_module_function_state(node, qualified_prefix, in_singleton_class, mf_offsets, ranges)
|
|
4195
|
-
return unless node.is_a?(Prism::Node)
|
|
4196
|
-
return if node.is_a?(Prism::ClassNode) || node.is_a?(Prism::ModuleNode) ||
|
|
4197
|
-
node.is_a?(Prism::SingletonClassNode) || node.is_a?(Prism::DefNode) ||
|
|
4198
|
-
node.is_a?(Prism::PostExecutionNode)
|
|
4199
|
-
|
|
4200
|
-
if node.is_a?(Prism::CallNode)
|
|
4201
|
-
if module_function_toggle?(node)
|
|
4202
|
-
return collect_module_function_call(node, qualified_prefix, in_singleton_class,
|
|
4203
|
-
mf_offsets, ranges)
|
|
4204
|
-
end
|
|
4205
|
-
if receiver_eval_call?(node)
|
|
4206
|
-
return collect_eval_call_state(node, qualified_prefix, in_singleton_class, mf_offsets,
|
|
4207
|
-
ranges)
|
|
4208
|
-
end
|
|
4209
|
-
end
|
|
4210
|
-
|
|
4211
|
-
node.rigor_each_child do |child|
|
|
4212
|
-
collect_module_function_state(child, qualified_prefix, in_singleton_class, mf_offsets, ranges)
|
|
4213
|
-
end
|
|
4214
|
-
end
|
|
4215
|
-
|
|
4216
|
-
# The prescan's eval arm: a `*_eval` / `*_exec` block's self rebinds to the receiver's module,
|
|
4217
|
-
# so only the call's receiver and arguments keep this module's `module_function` context.
|
|
4218
|
-
def collect_eval_call_state(node, qualified_prefix, in_singleton_class, mf_offsets, ranges)
|
|
4219
|
-
if node.receiver
|
|
4220
|
-
collect_module_function_state(node.receiver, qualified_prefix, in_singleton_class,
|
|
4221
|
-
mf_offsets, ranges)
|
|
4222
|
-
end
|
|
4223
|
-
node.arguments&.arguments&.each do |arg|
|
|
4224
|
-
collect_module_function_state(arg, qualified_prefix, in_singleton_class, mf_offsets,
|
|
4225
|
-
ranges)
|
|
4226
|
-
end
|
|
4227
|
-
end
|
|
4228
|
-
|
|
4229
4446
|
# The wider form: any call whose block rebinds `self` to the receiver — `instance_eval` and
|
|
4230
4447
|
# `instance_exec` included. Walks that read `self`-anchored facts use this gate; walks that
|
|
4231
4448
|
# own `def` leaves additionally split on {INSTANCE_EVAL_CALLS}, whose `def`s bind on the
|
|
@@ -4234,29 +4451,6 @@ module Rigor
|
|
|
4234
4451
|
node.block.is_a?(Prism::BlockNode) && RECEIVER_EVAL_CALLS.include?(node.name)
|
|
4235
4452
|
end
|
|
4236
4453
|
|
|
4237
|
-
def collect_module_function_call(node, qualified_prefix, in_singleton_class, mf_offsets, ranges)
|
|
4238
|
-
if bare_module_function?(node)
|
|
4239
|
-
mf_offsets << node.location.start_offset
|
|
4240
|
-
else
|
|
4241
|
-
owner = qualified_prefix.empty? ? nil : qualified_prefix.join("::")
|
|
4242
|
-
node.arguments&.arguments&.each do |arg|
|
|
4243
|
-
if arg.is_a?(Prism::DefNode)
|
|
4244
|
-
kind = def_singleton?(arg, qualified_prefix, in_singleton_class) ? :singleton : :both
|
|
4245
|
-
ranges << [arg.location.start_offset, arg.location.end_offset, arg.name, kind, owner]
|
|
4246
|
-
elsif (name = symbol_argument_name(arg))
|
|
4247
|
-
ranges << [node.location.start_offset, node.location.end_offset, name, :singleton, owner]
|
|
4248
|
-
else
|
|
4249
|
-
collect_module_function_state(arg, qualified_prefix, in_singleton_class, mf_offsets,
|
|
4250
|
-
ranges)
|
|
4251
|
-
end
|
|
4252
|
-
end
|
|
4253
|
-
end
|
|
4254
|
-
return unless node.block
|
|
4255
|
-
|
|
4256
|
-
collect_module_function_state(node.block, qualified_prefix, in_singleton_class, mf_offsets,
|
|
4257
|
-
ranges)
|
|
4258
|
-
end
|
|
4259
|
-
|
|
4260
4454
|
# A `class_eval` / `module_eval` / `class_exec` / `module_exec` block is NOT deferred: it runs
|
|
4261
4455
|
# eagerly during the receiver's call, as a class-ish body — its `module_function` state gets
|
|
4262
4456
|
# its own prescan, and its defs belong to the RECEIVER's surface. A nameable receiver gives
|
|
@@ -4620,7 +4814,7 @@ module Rigor
|
|
|
4620
4814
|
|
|
4621
4815
|
kind = if def_singleton?(def_node, qualified_prefix, in_singleton_class)
|
|
4622
4816
|
:singleton
|
|
4623
|
-
elsif
|
|
4817
|
+
elsif ModuleFunctionState.deferred_module_function?(mf_offsets, start)
|
|
4624
4818
|
:both
|
|
4625
4819
|
else
|
|
4626
4820
|
:instance
|
|
@@ -4773,12 +4967,27 @@ module Rigor
|
|
|
4773
4967
|
# CompactBase` calls it, and the bare `Post` inside `make` names `::Post` — the constant the compact
|
|
4774
4968
|
# declaration that OWNS the body reaches, not the one the receiver's spelling would suggest.
|
|
4775
4969
|
#
|
|
4776
|
-
#
|
|
4777
|
-
#
|
|
4778
|
-
#
|
|
4779
|
-
#
|
|
4780
|
-
#
|
|
4970
|
+
# Not a leaf of the fused methods/def-nodes walk: it needs the CHAIN threaded, which the fused walk does
|
|
4971
|
+
# not carry (a singleton-class body and a `Class.new` block body both push a qualified prefix while
|
|
4972
|
+
# pushing no `Module.nesting` entry). {DefNestingsCollector} declines at every `Prism::DefNode`, so a
|
|
4973
|
+
# def-dense file pays only the declaration spine.
|
|
4974
|
+
#
|
|
4975
|
+
# ADR-116 WD5 — production builds this table in {#declaration_walk_tables}' shared run, and nothing in
|
|
4976
|
+
# `lib` calls this builder. It runs the collector alone and stays for the specs, which compare it with
|
|
4977
|
+
# the shared run; `RIGOR_SHADOW_RULE_WALK` checks it against {#legacy_def_nestings}.
|
|
4781
4978
|
def build_def_nestings(root)
|
|
4979
|
+
collector = DefNestingsCollector.new
|
|
4980
|
+
DeclarationWalk.run(root, [collector], superclass_walk_root(nil))
|
|
4981
|
+
verified_def_nestings(root, collector.table)
|
|
4982
|
+
end
|
|
4983
|
+
|
|
4984
|
+
def verified_def_nestings(root, table, source_path = nil)
|
|
4985
|
+
DeclarationWalk::Shadow.verified(:def_nestings, source_path, table) { legacy_def_nestings(root) }
|
|
4986
|
+
end
|
|
4987
|
+
|
|
4988
|
+
# The walker {DefNestingsCollector} replaced, kept as the shadow harness's oracle until ADR-116 WD5 has
|
|
4989
|
+
# ported every table walker onto {DeclarationWalk}.
|
|
4990
|
+
def legacy_def_nestings(root)
|
|
4782
4991
|
accumulator = {}.compare_by_identity
|
|
4783
4992
|
walk_def_nestings(root, EMPTY_NESTING, accumulator)
|
|
4784
4993
|
accumulator.freeze
|
|
@@ -4904,24 +5113,21 @@ module Rigor
|
|
|
4904
5113
|
end
|
|
4905
5114
|
|
|
4906
5115
|
# Walks a class/module/singleton-class body's direct statements in source order, threading the
|
|
4907
|
-
# bare-`module_function` toggle: once a bare
|
|
4908
|
-
#
|
|
4909
|
-
#
|
|
5116
|
+
# bare-`module_function` toggle ({ModuleFunctionState.each_singleton_sibling}): once a bare
|
|
5117
|
+
# `module_function` is seen, every subsequent `def` in the body registers as a singleton method. Nested
|
|
5118
|
+
# classes/modules/defs recurse through the general walker so the toggle stays scoped to its own body, and a
|
|
5119
|
+
# `module_function :a, :b` named form records through {#record_module_function_names}.
|
|
4910
5120
|
def walk_singleton_body(body, qualified_prefix, in_singleton_class, accumulator,
|
|
4911
5121
|
def_owner_prefix = nil, singleton_cref: false,
|
|
4912
5122
|
defs_singleton: false)
|
|
4913
5123
|
owner_prefix = def_owner_prefix || qualified_prefix
|
|
4914
|
-
|
|
4915
|
-
|
|
4916
|
-
if
|
|
4917
|
-
|
|
4918
|
-
|
|
4919
|
-
|
|
4920
|
-
|
|
4921
|
-
# which this table cannot name either.
|
|
4922
|
-
record_module_function_names(stmt, owner_prefix, body, accumulator) unless
|
|
4923
|
-
defs_singleton == :unnameable
|
|
4924
|
-
end
|
|
5124
|
+
statements = statements_of(body)
|
|
5125
|
+
ModuleFunctionState.each_singleton_sibling(statements) do |stmt, module_function_on, named_call|
|
|
5126
|
+
if named_call
|
|
5127
|
+
# `:unnameable` — the named defs the call copies live on the metaclass,
|
|
5128
|
+
# which this table cannot name either.
|
|
5129
|
+
record_module_function_names(stmt, owner_prefix, statements, accumulator) unless
|
|
5130
|
+
defs_singleton == :unnameable
|
|
4925
5131
|
next
|
|
4926
5132
|
end
|
|
4927
5133
|
if stmt.is_a?(Prism::DefNode)
|
|
@@ -4972,42 +5178,30 @@ module Rigor
|
|
|
4972
5178
|
return if qualified_prefix.empty?
|
|
4973
5179
|
|
|
4974
5180
|
class_name = qualified_prefix.join("::")
|
|
4975
|
-
(accumulator
|
|
4976
|
-
end
|
|
4977
|
-
|
|
4978
|
-
# A bare `module_function` (no arguments) flips every following `def` in the module body to module-function
|
|
4979
|
-
# (instance + singleton) mode.
|
|
4980
|
-
def module_function_toggle?(node)
|
|
4981
|
-
node.name == :module_function && node.receiver.nil?
|
|
5181
|
+
write_singleton_slot(accumulator, class_name, def_node.name, def_node, def_node)
|
|
4982
5182
|
end
|
|
4983
5183
|
|
|
4984
|
-
|
|
4985
|
-
|
|
5184
|
+
# One singleton slot write, through {SingletonDefTable#write} when the walk tracks certainty.
|
|
5185
|
+
def write_singleton_slot(accumulator, class_name, name, def_node, node, other = nil)
|
|
5186
|
+
if accumulator.is_a?(SingletonDefTable)
|
|
5187
|
+
accumulator.write(class_name, name, def_node, node, other)
|
|
5188
|
+
else
|
|
5189
|
+
(accumulator[class_name] ||= {})[name] = def_node
|
|
5190
|
+
end
|
|
4986
5191
|
end
|
|
4987
5192
|
|
|
4988
|
-
# `module_function :a, :b` retro-marks named siblings
|
|
4989
|
-
#
|
|
4990
|
-
#
|
|
4991
|
-
|
|
4992
|
-
def record_module_function_names(node, qualified_prefix, body, accumulator)
|
|
5193
|
+
# `module_function :a, :b` retro-marks named siblings as module-functions: registers on the module's singleton
|
|
5194
|
+
# side each `DefNode` {ModuleFunctionState.each_singleton_copy} resolves among the body's direct `statements`.
|
|
5195
|
+
# Non-symbol arguments and names with no matching `def` are skipped (a miss degrades to today's `Dynamic`).
|
|
5196
|
+
def record_module_function_names(node, qualified_prefix, statements, accumulator)
|
|
4993
5197
|
return if qualified_prefix.empty?
|
|
4994
5198
|
|
|
4995
|
-
defs_by_name = statements_of(body).each_with_object({}) do |stmt, acc|
|
|
4996
|
-
acc[stmt.name] = stmt if stmt.is_a?(Prism::DefNode) && stmt.receiver.nil?
|
|
4997
|
-
end
|
|
4998
5199
|
class_name = qualified_prefix.join("::")
|
|
4999
|
-
node
|
|
5000
|
-
name
|
|
5001
|
-
def_node = name && defs_by_name[name]
|
|
5002
|
-
(accumulator[class_name] ||= {})[name] = def_node if def_node
|
|
5200
|
+
ModuleFunctionState.each_singleton_copy(node, statements) do |name, def_node|
|
|
5201
|
+
write_singleton_slot(accumulator, class_name, name, def_node, node, def_node)
|
|
5003
5202
|
end
|
|
5004
5203
|
end
|
|
5005
5204
|
|
|
5006
|
-
# The Symbol value of a `:name` / `"name"` literal argument, or nil.
|
|
5007
|
-
def symbol_argument_name(arg)
|
|
5008
|
-
arg.unescaped.to_sym if arg.is_a?(Prism::SymbolNode) || arg.is_a?(Prism::StringNode)
|
|
5009
|
-
end
|
|
5010
|
-
|
|
5011
5205
|
# ADR-24 slice 2 — per-class table mapping a fully qualified user class to its superclass name AS WRITTEN at the
|
|
5012
5206
|
# `class Foo < Bar` declaration. Only constant superclasses are recorded (`class Foo < Struct.new(...)` and other
|
|
5013
5207
|
# non-constant superclasses produce no entry). The as-written name is resolved to a qualified class at the call
|
|
@@ -5021,13 +5215,57 @@ module Rigor
|
|
|
5021
5215
|
# the enclosing cref — `[]` for the first spelling, `["Admin"]` for the second. A class reopened under two
|
|
5022
5216
|
# spellings keeps the LAST one walked, matching how `superclasses` itself merges.
|
|
5023
5217
|
#
|
|
5218
|
+
# ADR-116 WD5 — built by {SuperclassesCollector} on the shared {DeclarationWalk}, following the two
|
|
5219
|
+
# legacy variants it declares; `RIGOR_SHADOW_RULE_WALK` checks both tables against
|
|
5220
|
+
# {#legacy_superclass_tables}. Production builds both in {#declaration_walk_tables}' shared run, and
|
|
5221
|
+
# nothing in `lib` calls this builder: it runs the collector alone and stays for the specs, which
|
|
5222
|
+
# compare it with the shared run.
|
|
5223
|
+
#
|
|
5024
5224
|
# @return the `[superclasses, header_nestings]` pair
|
|
5025
5225
|
def build_superclass_tables(root, source_path = nil)
|
|
5226
|
+
collector = SuperclassesCollector.new
|
|
5227
|
+
DeclarationWalk.run(root, [collector], superclass_walk_root(source_path))
|
|
5228
|
+
verified_superclass_tables(root, source_path, collector.tables)
|
|
5229
|
+
end
|
|
5230
|
+
|
|
5231
|
+
# The root context the superclass tables are walked from: the ancestry nesting starts empty and the
|
|
5232
|
+
# anonymous-class key carries the file's path.
|
|
5233
|
+
def superclass_walk_root(source_path, scope: nil)
|
|
5234
|
+
DeclarationWalk::Context.root(scope: scope, nesting: EMPTY_NESTING, source_path: source_path)
|
|
5235
|
+
end
|
|
5236
|
+
|
|
5237
|
+
# `tables` once the shadow harness has compared them with the legacy walker's, when it is on.
|
|
5238
|
+
def verified_superclass_tables(root, source_path, tables)
|
|
5239
|
+
DeclarationWalk::Shadow.verified(:superclass_tables, source_path, tables) do
|
|
5240
|
+
legacy_superclass_tables(root, source_path)
|
|
5241
|
+
end
|
|
5242
|
+
end
|
|
5243
|
+
|
|
5244
|
+
# The walker {SuperclassesCollector} replaced, kept as the shadow harness's oracle until ADR-116 WD5 has
|
|
5245
|
+
# ported every table walker onto {DeclarationWalk}.
|
|
5246
|
+
def legacy_superclass_tables(root, source_path = nil)
|
|
5026
5247
|
accumulator = { superclasses: {}, header_nestings: {} }
|
|
5027
5248
|
walk_class_superclasses(root, [], accumulator, source_path)
|
|
5028
5249
|
[accumulator[:superclasses].freeze, accumulator[:header_nestings].freeze]
|
|
5029
5250
|
end
|
|
5030
5251
|
|
|
5252
|
+
# ADR-116 WD5 — the file's superclass, header-nesting, def-nesting and member-layout tables from ONE
|
|
5253
|
+
# shared {DeclarationWalk}: the file is descended once for all of them, where the legacy walkers
|
|
5254
|
+
# descended it once per table. `RIGOR_SHADOW_RULE_WALK` checks each table against its legacy walker.
|
|
5255
|
+
#
|
|
5256
|
+
# @return `{ superclasses:, header_nestings:, def_nestings:, data_member_layouts:, struct_member_layouts: }`
|
|
5257
|
+
def declaration_walk_tables(root, source_path = nil)
|
|
5258
|
+
superclasses = SuperclassesCollector.new
|
|
5259
|
+
layouts = MemberLayoutsCollector.new
|
|
5260
|
+
def_nestings = DefNestingsCollector.new
|
|
5261
|
+
DeclarationWalk.run(root, [superclasses, layouts, def_nestings], superclass_walk_root(source_path))
|
|
5262
|
+
supers, header_nestings = verified_superclass_tables(root, source_path, superclasses.tables)
|
|
5263
|
+
data, struct = verified_member_layouts(root, layouts.tables, source_path)
|
|
5264
|
+
{ superclasses: supers, header_nestings: header_nestings,
|
|
5265
|
+
def_nestings: verified_def_nestings(root, def_nestings.table, source_path),
|
|
5266
|
+
data_member_layouts: data, struct_member_layouts: struct }
|
|
5267
|
+
end
|
|
5268
|
+
|
|
5031
5269
|
# `self_base` names a rebound `self` for `self::`-anchored headers — a meta-new
|
|
5032
5270
|
# block's class; `[]` marks a self no name covers.
|
|
5033
5271
|
def walk_class_superclasses(node, qualified_prefix, accumulator, source_path = nil,
|
|
@@ -5342,7 +5580,31 @@ module Rigor
|
|
|
5342
5580
|
# `Data.define` is recorded: `Struct.new` instances are mutable, so member-value folding would be unsound (the
|
|
5343
5581
|
# Struct follow-up is deferred — see ADR-48 § "Struct follow-up"). Consumed by
|
|
5344
5582
|
# {Inference::MethodDispatcher::DataFolding} via {Scope#data_member_layout}.
|
|
5583
|
+
#
|
|
5584
|
+
# ADR-116 WD5 — built with its Struct sibling by {MemberLayoutsCollector}. Production builds both in
|
|
5585
|
+
# {#declaration_walk_tables}' shared run, and nothing in `lib` calls this builder: it stays for the specs,
|
|
5586
|
+
# and walks both layout tables to return this one.
|
|
5345
5587
|
def build_data_member_layouts(root)
|
|
5588
|
+
member_layout_tables(root).first
|
|
5589
|
+
end
|
|
5590
|
+
|
|
5591
|
+
# `[data_member_layouts, struct_member_layouts]` from a {MemberLayoutsCollector} run of its own, checked by
|
|
5592
|
+
# `RIGOR_SHADOW_RULE_WALK` against the two legacy walkers. Spec-only, like the two builders on it.
|
|
5593
|
+
def member_layout_tables(root)
|
|
5594
|
+
collector = MemberLayoutsCollector.new
|
|
5595
|
+
DeclarationWalk.run(root, [collector], superclass_walk_root(nil))
|
|
5596
|
+
verified_member_layouts(root, collector.tables)
|
|
5597
|
+
end
|
|
5598
|
+
|
|
5599
|
+
def verified_member_layouts(root, tables, source_path = nil)
|
|
5600
|
+
DeclarationWalk::Shadow.verified(:member_layouts, source_path, tables) do
|
|
5601
|
+
[legacy_data_member_layouts(root), legacy_struct_member_layouts(root)]
|
|
5602
|
+
end
|
|
5603
|
+
end
|
|
5604
|
+
|
|
5605
|
+
# The walker {MemberLayoutsCollector} replaced for the `Data.define` table, kept as the shadow harness's
|
|
5606
|
+
# oracle until ADR-116 WD5 has ported every table walker onto {DeclarationWalk}.
|
|
5607
|
+
def legacy_data_member_layouts(root)
|
|
5346
5608
|
accumulator = {}
|
|
5347
5609
|
walk_data_member_layouts(root, [], accumulator)
|
|
5348
5610
|
accumulator.freeze
|
|
@@ -5445,7 +5707,16 @@ module Rigor
|
|
|
5445
5707
|
# table so the existing `Data.define` value-shape contract (a bare `[Symbol]`) is untouched: a Struct entry
|
|
5446
5708
|
# carries `{ members:, keyword_init: }` because the dispatcher needs the flag to fold the matching `.new` call
|
|
5447
5709
|
# form (positional vs keyword) without manufacturing a wrong map.
|
|
5710
|
+
#
|
|
5711
|
+
# ADR-116 WD5 — production builds it in {#declaration_walk_tables}' shared run; this builder is spec-only,
|
|
5712
|
+
# like {#build_data_member_layouts}.
|
|
5448
5713
|
def build_struct_member_layouts(root)
|
|
5714
|
+
member_layout_tables(root).last
|
|
5715
|
+
end
|
|
5716
|
+
|
|
5717
|
+
# The walker {MemberLayoutsCollector} replaced for the `Struct.new` table, kept as the shadow harness's
|
|
5718
|
+
# oracle until ADR-116 WD5 has ported every table walker onto {DeclarationWalk}.
|
|
5719
|
+
def legacy_struct_member_layouts(root)
|
|
5449
5720
|
accumulator = {}
|
|
5450
5721
|
walk_struct_member_layouts(root, [], accumulator)
|
|
5451
5722
|
accumulator.freeze
|
|
@@ -5589,7 +5860,7 @@ module Rigor
|
|
|
5589
5860
|
# stored in instance-ancestor SEARCH order: a later `prepend` statement puts its module NEARER than
|
|
5590
5861
|
# an earlier one (`prepend A; prepend B` searches B before A) while the arguments of ONE
|
|
5591
5862
|
# `prepend A, B` keep call order — exactly the convention {#record_extend_targets} documents for
|
|
5592
|
-
# `extend`, and the order `Scope
|
|
5863
|
+
# `extend`, and the order `Scope::ResolutionChain` reads a body's prepends in. Ruby inserts a
|
|
5593
5864
|
# prepended module, and its own ancestry, immediately BEFORE the class that prepends it, so these
|
|
5594
5865
|
# names outrank the class's own `def`s; without the kind recorded separately from {#build_discovered_includes}
|
|
5595
5866
|
# there was nothing to order on and `prepend` was silently an `include`.
|
|
@@ -5599,13 +5870,16 @@ module Rigor
|
|
|
5599
5870
|
|
|
5600
5871
|
# One descent, both instance-side mixin tables: the walk classifies each mixin call it sees, so the
|
|
5601
5872
|
# two tables cost one walk rather than two. Each value is frozen and de-duplicated per class, as the
|
|
5602
|
-
# single-table builder always did.
|
|
5873
|
+
# single-table builder always did. `:unpositioned` names the recorded edges whose order is not a fact
|
|
5874
|
+
# ({MixinAccumulator}).
|
|
5603
5875
|
def mixin_tables(root)
|
|
5604
|
-
accumulator =
|
|
5876
|
+
accumulator = MixinAccumulator.new
|
|
5877
|
+
accumulator.direct_body(program_statements(root))
|
|
5605
5878
|
walk_class_includes(root, [], nil, accumulator)
|
|
5606
5879
|
{
|
|
5607
5880
|
includes: freeze_mixin_lists(accumulator, :include),
|
|
5608
|
-
prepends: freeze_mixin_lists(accumulator, :prepend)
|
|
5881
|
+
prepends: freeze_mixin_lists(accumulator, :prepend),
|
|
5882
|
+
unpositioned: accumulator.unpositioned
|
|
5609
5883
|
}
|
|
5610
5884
|
end
|
|
5611
5885
|
|
|
@@ -5620,7 +5894,10 @@ module Rigor
|
|
|
5620
5894
|
# already store nearest-first (see {#write_mixin_targets}), so the concat preserves MRO.
|
|
5621
5895
|
def freeze_mixin_lists(accumulator, kind)
|
|
5622
5896
|
accumulator.each_with_object({}) do |(class_name, kinds), out|
|
|
5623
|
-
names = mixin_names_for(kinds, kind)
|
|
5897
|
+
names = mixin_names_for(kinds, kind)
|
|
5898
|
+
# The instance-side projection is a SET in search order; the prepend table keeps every statement.
|
|
5899
|
+
names = names.uniq if kind == :include
|
|
5900
|
+
names = names.freeze
|
|
5624
5901
|
out[class_name] = names unless names.empty?
|
|
5625
5902
|
end.freeze
|
|
5626
5903
|
end
|
|
@@ -5635,6 +5912,12 @@ module Rigor
|
|
|
5635
5912
|
end
|
|
5636
5913
|
end
|
|
5637
5914
|
|
|
5915
|
+
# The top-level statements of a parsed file: the one body whose declarations need no enclosing
|
|
5916
|
+
# declaration to be direct ({MixinAccumulator}).
|
|
5917
|
+
def program_statements(root)
|
|
5918
|
+
root.is_a?(Prism::ProgramNode) ? root.statements : root
|
|
5919
|
+
end
|
|
5920
|
+
|
|
5638
5921
|
def walk_class_includes(node, qualified_prefix, current_class, accumulator,
|
|
5639
5922
|
singleton_self: false, singleton_cref: false)
|
|
5640
5923
|
return unless node.is_a?(Prism::Node)
|
|
@@ -5665,6 +5948,25 @@ module Rigor
|
|
|
5665
5948
|
singleton_self, singleton_cref)
|
|
5666
5949
|
end
|
|
5667
5950
|
|
|
5951
|
+
walk_includes_children(node, qualified_prefix, current_class, accumulator, singleton_self, singleton_cref)
|
|
5952
|
+
end
|
|
5953
|
+
|
|
5954
|
+
# The generic descent of {#walk_class_includes}; a `self.included(base)`-style hook's body walks with
|
|
5955
|
+
# its parameters in scope ({MixinAccumulator#with_hook_params}).
|
|
5956
|
+
def walk_includes_children(node, qualified_prefix, current_class, accumulator, singleton_self, singleton_cref)
|
|
5957
|
+
hook_params = hook_def_params(node)
|
|
5958
|
+
if hook_params
|
|
5959
|
+
return accumulator.with_hook_params(hook_params) do
|
|
5960
|
+
walk_includes_children_plain(node, qualified_prefix, current_class, accumulator, singleton_self,
|
|
5961
|
+
singleton_cref)
|
|
5962
|
+
end
|
|
5963
|
+
end
|
|
5964
|
+
|
|
5965
|
+
walk_includes_children_plain(node, qualified_prefix, current_class, accumulator, singleton_self, singleton_cref)
|
|
5966
|
+
end
|
|
5967
|
+
|
|
5968
|
+
def walk_includes_children_plain(node, qualified_prefix, current_class, accumulator, singleton_self,
|
|
5969
|
+
singleton_cref)
|
|
5668
5970
|
node.rigor_each_child do |child|
|
|
5669
5971
|
walk_class_includes(child, qualified_prefix, current_class, accumulator,
|
|
5670
5972
|
singleton_self: singleton_self, singleton_cref: singleton_cref)
|
|
@@ -5682,6 +5984,7 @@ module Rigor
|
|
|
5682
5984
|
return true unless node.body
|
|
5683
5985
|
|
|
5684
5986
|
_self_decl, child_prefix, child_cref = ctx
|
|
5987
|
+
accumulator.direct_body(node.body) if accumulator.direct?(node)
|
|
5685
5988
|
walk_class_includes(node.body, child_cref ? [] : child_prefix,
|
|
5686
5989
|
child_cref ? nil : child_prefix.join("::"), accumulator,
|
|
5687
5990
|
singleton_cref: child_cref)
|
|
@@ -5752,7 +6055,7 @@ module Rigor
|
|
|
5752
6055
|
# names, so it declines rather than re-anchoring to the lexical class.
|
|
5753
6056
|
unnameable_self = singleton_self || (singleton_cref && current_class.nil?)
|
|
5754
6057
|
rebound = rebound_block_self(node, qualified_prefix, nil, nil,
|
|
5755
|
-
|
|
6058
|
+
unnameable_self ? OPAQUE_SELF : current_class)
|
|
5756
6059
|
node.rigor_each_child do |child|
|
|
5757
6060
|
rebinds_self = rebound && child.is_a?(Prism::BlockNode)
|
|
5758
6061
|
owner =
|
|
@@ -5776,15 +6079,150 @@ module Rigor
|
|
|
5776
6079
|
# it or the set-shaped consumers (arity, visibility, undefined-method suppression) would answer
|
|
5777
6080
|
# differently for the two spellings of one edge.
|
|
5778
6081
|
def record_mixin_call(node, qualified_prefix, current_class, accumulator)
|
|
5779
|
-
|
|
6082
|
+
taint_opaque_eval(node, current_class, accumulator)
|
|
6083
|
+
kind, _arguments, via_send = mixin_call_view(node)
|
|
6084
|
+
return if kind.nil?
|
|
6085
|
+
|
|
6086
|
+
effects = mixin_effects(node, kind, qualified_prefix, current_class, accumulator, in_singleton: false)
|
|
6087
|
+
.select { |_owner, side| side == :include }
|
|
6088
|
+
return if effects.empty?
|
|
6089
|
+
return if !via_send && instance_mixin_recorded?(node, qualified_prefix, current_class, accumulator)
|
|
6090
|
+
|
|
6091
|
+
# A mixin call the tables cannot hold still reshapes the ancestry: taint the whole owner side.
|
|
6092
|
+
effects.each { |owner, side| accumulator.taint(owner, side) }
|
|
6093
|
+
end
|
|
6094
|
+
|
|
6095
|
+
# Records the instance-side edges of a call {#mixin_call_recorded?} accepts. False when it recorded
|
|
6096
|
+
# nothing — the caller then taints the owner instead.
|
|
6097
|
+
def instance_mixin_recorded?(node, qualified_prefix, current_class, accumulator)
|
|
6098
|
+
return false unless mixin_call_recorded?(node, current_class)
|
|
5780
6099
|
|
|
5781
|
-
|
|
5782
|
-
|
|
6100
|
+
arguments = node.arguments&.arguments || []
|
|
6101
|
+
targets = arguments.filter_map { |arg| Source::ConstantPath.qualified_name(arg) }
|
|
6102
|
+
return false if targets.empty?
|
|
5783
6103
|
|
|
5784
6104
|
owner = node.receiver.nil? ? current_class : prepend_call_receiver(node, qualified_prefix)
|
|
5785
|
-
return if owner.nil?
|
|
6105
|
+
return false if owner.nil?
|
|
5786
6106
|
|
|
5787
6107
|
write_mixin_targets(accumulator, owner, targets, prepend: node.name == :prepend)
|
|
6108
|
+
accumulator.note(node, owner, :include, targets, complete: targets.size == arguments.size)
|
|
6109
|
+
true
|
|
6110
|
+
end
|
|
6111
|
+
|
|
6112
|
+
MIXIN_KINDS = %i[include prepend extend].freeze
|
|
6113
|
+
SEND_CALL_NAMES = %i[send public_send __send__].freeze
|
|
6114
|
+
private_constant :MIXIN_KINDS, :SEND_CALL_NAMES
|
|
6115
|
+
|
|
6116
|
+
# `[kind, arguments, via_send]` for a call that mixes modules in — `include` / `prepend` / `extend`, or
|
|
6117
|
+
# `send(:include, M)` and its `public_send` / `__send__` twins with a literal first argument — else nil.
|
|
6118
|
+
# A computed `send` name is not read as a mixin: it names nothing this walk could taint by.
|
|
6119
|
+
def mixin_call_view(node)
|
|
6120
|
+
return [node.name, node.arguments&.arguments || [], false] if MIXIN_KINDS.include?(node.name)
|
|
6121
|
+
return unless SEND_CALL_NAMES.include?(node.name)
|
|
6122
|
+
|
|
6123
|
+
arguments = node.arguments&.arguments || []
|
|
6124
|
+
first = arguments.first
|
|
6125
|
+
return unless first.is_a?(Prism::SymbolNode) || first.is_a?(Prism::StringNode)
|
|
6126
|
+
|
|
6127
|
+
# By string: a name literal need not be a valid symbol in its encoding.
|
|
6128
|
+
kind = MIXIN_KINDS.find { |candidate| candidate.name == first.unescaped }
|
|
6129
|
+
[kind, arguments.drop(1), true] if kind
|
|
6130
|
+
end
|
|
6131
|
+
|
|
6132
|
+
# `[[owner, side], …]` — the ancestry sides a mixin call reshapes. `self` and the implicit receiver
|
|
6133
|
+
# are the enclosing class (`:include` for `include` / `prepend`, `:extend` for `extend`, and both
|
|
6134
|
+
# flip to `:extend` inside `class << self`); `singleton_class` is the singleton side; a constant is
|
|
6135
|
+
# that class; any other receiver is unknown, so it may be the class that includes this one from a
|
|
6136
|
+
# hook (`base.include M`) and taints both sides of the enclosing class.
|
|
6137
|
+
def mixin_effects(node, kind, qualified_prefix, current_class, accumulator, in_singleton:)
|
|
6138
|
+
receiver = node.receiver
|
|
6139
|
+
side = kind == :extend ? :extend : :include
|
|
6140
|
+
pairs =
|
|
6141
|
+
if receiver.nil? || receiver.is_a?(Prism::SelfNode)
|
|
6142
|
+
own_mixin_effects(kind, side, current_class, in_singleton)
|
|
6143
|
+
else
|
|
6144
|
+
receiver_mixin_effects(node, side, qualified_prefix, current_class, accumulator)
|
|
6145
|
+
end
|
|
6146
|
+
pairs.reject { |owner, _side| owner.nil? }
|
|
6147
|
+
end
|
|
6148
|
+
|
|
6149
|
+
# The implicit-receiver / `self` arm of {#mixin_effects}: inside `class << self`, `include` / `prepend`
|
|
6150
|
+
# are singleton-side and `extend` reaches the singleton's own singleton, which nothing names.
|
|
6151
|
+
def own_mixin_effects(kind, side, current_class, in_singleton)
|
|
6152
|
+
return [] if in_singleton && kind == :extend
|
|
6153
|
+
|
|
6154
|
+
[[current_class, in_singleton ? :extend : side]]
|
|
6155
|
+
end
|
|
6156
|
+
|
|
6157
|
+
# A receiver that is not `self` reshapes the class it names, or the singleton of `singleton_class`. Any
|
|
6158
|
+
# other receiver is an object of its own (`ids.prepend(1)`, `obj.extend(Decorator)`) and reshapes none of
|
|
6159
|
+
# the enclosing class's ancestry — except in a `self.included(base)`-style hook, where `base` IS the
|
|
6160
|
+
# class that includes the enclosing module and a call on it taints both sides of the enclosing class.
|
|
6161
|
+
def receiver_mixin_effects(node, side, qualified_prefix, current_class, accumulator)
|
|
6162
|
+
receiver = node.receiver
|
|
6163
|
+
return [[current_class, :extend]] if singleton_class_receiver?(receiver)
|
|
6164
|
+
|
|
6165
|
+
owner = prepend_call_receiver(node, qualified_prefix)
|
|
6166
|
+
return [[owner, side]] if owner
|
|
6167
|
+
return [] unless hook_receiver?(receiver, accumulator)
|
|
6168
|
+
|
|
6169
|
+
[[current_class, :include], [current_class, :extend]]
|
|
6170
|
+
end
|
|
6171
|
+
|
|
6172
|
+
HOOK_DEFS = %i[included extended prepended inherited].freeze
|
|
6173
|
+
MIXIN_EVAL_CALLS = %i[class_eval module_eval class_exec module_exec instance_eval instance_exec].freeze
|
|
6174
|
+
private_constant :HOOK_DEFS, :MIXIN_EVAL_CALLS
|
|
6175
|
+
|
|
6176
|
+
# The parameter names of a `def included(base)`-style hook (a hook is written `def self.included`, or
|
|
6177
|
+
# `def included` inside `class << self`), else nil.
|
|
6178
|
+
def hook_def_params(node)
|
|
6179
|
+
return nil unless node.is_a?(Prism::DefNode) && HOOK_DEFS.include?(node.name)
|
|
6180
|
+
|
|
6181
|
+
parameters = node.parameters
|
|
6182
|
+
return [] unless parameters
|
|
6183
|
+
|
|
6184
|
+
(parameters.requireds + parameters.optionals).filter_map do |parameter|
|
|
6185
|
+
parameter.name if parameter.respond_to?(:name)
|
|
6186
|
+
end
|
|
6187
|
+
end
|
|
6188
|
+
|
|
6189
|
+
# Whether `receiver` is a hook parameter, or `singleton_class` / a no-argument call chain off one.
|
|
6190
|
+
def hook_receiver?(receiver, accumulator)
|
|
6191
|
+
case receiver
|
|
6192
|
+
when Prism::LocalVariableReadNode then accumulator.hook_local?(receiver.name)
|
|
6193
|
+
when Prism::CallNode
|
|
6194
|
+
receiver.arguments.nil? && receiver.receiver && hook_receiver?(receiver.receiver, accumulator)
|
|
6195
|
+
else false
|
|
6196
|
+
end
|
|
6197
|
+
end
|
|
6198
|
+
|
|
6199
|
+
# `base.class_eval { include X }` in a hook, and `opaque.module_eval { include B }` anywhere: the block
|
|
6200
|
+
# opens a class this walk cannot name, so its mixin calls record nowhere, yet the enclosing class's
|
|
6201
|
+
# ancestry is what the reader was asked about. Taint both sides of the enclosing class. Outside a hook
|
|
6202
|
+
# the block is scanned for a mixin call first, so an ordinary `obj.instance_eval { … }` DSL taints nothing.
|
|
6203
|
+
def taint_opaque_eval(node, current_class, accumulator)
|
|
6204
|
+
return unless current_class && node.block.is_a?(Prism::BlockNode) && MIXIN_EVAL_CALLS.include?(node.name)
|
|
6205
|
+
|
|
6206
|
+
receiver = node.receiver
|
|
6207
|
+
return if receiver.nil? || receiver.is_a?(Prism::SelfNode)
|
|
6208
|
+
return if Source::ConstantPath.qualified_name_or_nil(receiver)
|
|
6209
|
+
return unless hook_receiver?(receiver, accumulator) || block_mixes_in?(node.block)
|
|
6210
|
+
|
|
6211
|
+
accumulator.taint(current_class, :include)
|
|
6212
|
+
accumulator.taint(current_class, :extend)
|
|
6213
|
+
end
|
|
6214
|
+
|
|
6215
|
+
def block_mixes_in?(node)
|
|
6216
|
+
return true if node.is_a?(Prism::CallNode) && (MIXIN_KINDS.include?(node.name) || mixin_call_view(node))
|
|
6217
|
+
|
|
6218
|
+
found = false
|
|
6219
|
+
node.rigor_each_child { |child| found ||= block_mixes_in?(child) }
|
|
6220
|
+
found
|
|
6221
|
+
end
|
|
6222
|
+
|
|
6223
|
+
def singleton_class_receiver?(receiver)
|
|
6224
|
+
receiver.is_a?(Prism::CallNode) && receiver.name == :singleton_class && receiver.arguments.nil? &&
|
|
6225
|
+
(receiver.receiver.nil? || receiver.receiver.is_a?(Prism::SelfNode))
|
|
5788
6226
|
end
|
|
5789
6227
|
|
|
5790
6228
|
# Issue #1123 — one class's contribution to the two tables. A prepended module lands in BOTH: the
|
|
@@ -5802,12 +6240,15 @@ module Rigor
|
|
|
5802
6240
|
# freeze time — ahead of every include, where Ruby puts them.
|
|
5803
6241
|
def write_mixin_targets(accumulator, owner, targets, prepend:)
|
|
5804
6242
|
bucket = accumulator[owner] ||= {}
|
|
5805
|
-
|
|
5806
|
-
|
|
5807
|
-
|
|
5808
|
-
|
|
5809
|
-
|
|
5810
|
-
|
|
6243
|
+
kind, other = prepend ? %i[prepend include] : %i[include prepend]
|
|
6244
|
+
list = (bucket[kind] ||= [])
|
|
6245
|
+
# A target the OTHER kind already carries (`include M; prepend M`) is one body the two tables cannot
|
|
6246
|
+
# represent (Ruby's `[M, C, M]`), so the instance side is named unpositioned.
|
|
6247
|
+
accumulator.taint(owner, :include) if targets.any? { |target| bucket[other]&.include?(target) }
|
|
6248
|
+
# A repeated `include` keeps its first position (Ruby skips it). A repeated `prepend` is KEPT: the
|
|
6249
|
+
# prepend table holds every statement, nearest first, so the readers that walk the raw list see what
|
|
6250
|
+
# they always saw, and the resolution chain dedupes per owner, first statement winning.
|
|
6251
|
+
list.unshift(*(prepend ? targets : targets.reject { |target| list.include?(target) }))
|
|
5811
6252
|
end
|
|
5812
6253
|
|
|
5813
6254
|
# Whether a mixin call contributes to the tables at all: a receiverless `include` / `prepend` needs an
|
|
@@ -5851,9 +6292,19 @@ module Rigor
|
|
|
5851
6292
|
# Issue #915 — plus `class << self; include M; end`, the same singleton ancestor spelled through the
|
|
5852
6293
|
# singleton-class body. It folds like an `extend` because Ruby makes it one.
|
|
5853
6294
|
def build_discovered_extends(root)
|
|
5854
|
-
|
|
6295
|
+
extend_tables(root).fetch(:extends)
|
|
6296
|
+
end
|
|
6297
|
+
|
|
6298
|
+
# {#build_discovered_extends} and the `extend` edges whose order is not a fact ({MixinAccumulator}), from
|
|
6299
|
+
# one descent.
|
|
6300
|
+
def extend_tables(root)
|
|
6301
|
+
accumulator = MixinAccumulator.new
|
|
6302
|
+
accumulator.direct_body(program_statements(root))
|
|
5855
6303
|
walk_class_extends(root, [], nil, accumulator)
|
|
5856
|
-
|
|
6304
|
+
{
|
|
6305
|
+
extends: accumulator.transform_values { |mods| mods.uniq.freeze }.freeze,
|
|
6306
|
+
unpositioned: accumulator.unpositioned
|
|
6307
|
+
}
|
|
5857
6308
|
end
|
|
5858
6309
|
|
|
5859
6310
|
def walk_class_extends(node, qualified_prefix, current_class, accumulator, in_singleton: false,
|
|
@@ -5880,18 +6331,40 @@ module Rigor
|
|
|
5880
6331
|
return if walk_extends_meta_new?(node, qualified_prefix, current_class, accumulator,
|
|
5881
6332
|
in_singleton, singleton_self, singleton_cref)
|
|
5882
6333
|
when Prism::CallNode
|
|
5883
|
-
record_extend_call(node, current_class, accumulator, in_singleton: in_singleton)
|
|
6334
|
+
record_extend_call(node, qualified_prefix, current_class, accumulator, in_singleton: in_singleton)
|
|
5884
6335
|
if receiver_eval_call?(node)
|
|
5885
6336
|
return walk_eval_extends_call(node, qualified_prefix, current_class, accumulator,
|
|
5886
6337
|
in_singleton: in_singleton, singleton_self: singleton_self,
|
|
5887
6338
|
singleton_cref: singleton_cref)
|
|
5888
6339
|
end
|
|
5889
|
-
if node.block.is_a?(Prism::BlockNode)
|
|
5890
|
-
|
|
5891
|
-
|
|
6340
|
+
if node.block.is_a?(Prism::BlockNode) &&
|
|
6341
|
+
extends_block_handled?(node, qualified_prefix, current_class, accumulator,
|
|
6342
|
+
in_singleton, singleton_self, singleton_cref)
|
|
6343
|
+
return
|
|
5892
6344
|
end
|
|
5893
6345
|
end
|
|
5894
6346
|
|
|
6347
|
+
walk_extends_children(node, qualified_prefix, current_class, accumulator, in_singleton, singleton_self,
|
|
6348
|
+
singleton_cref)
|
|
6349
|
+
end
|
|
6350
|
+
|
|
6351
|
+
# The generic descent of {#walk_class_extends}; a hook's body walks with its parameters in scope.
|
|
6352
|
+
def walk_extends_children(node, qualified_prefix, current_class, accumulator, in_singleton, singleton_self,
|
|
6353
|
+
singleton_cref)
|
|
6354
|
+
hook_params = hook_def_params(node)
|
|
6355
|
+
if hook_params
|
|
6356
|
+
return accumulator.with_hook_params(hook_params) do
|
|
6357
|
+
walk_extends_children_plain(node, qualified_prefix, current_class, accumulator, in_singleton,
|
|
6358
|
+
singleton_self, singleton_cref)
|
|
6359
|
+
end
|
|
6360
|
+
end
|
|
6361
|
+
|
|
6362
|
+
walk_extends_children_plain(node, qualified_prefix, current_class, accumulator, in_singleton, singleton_self,
|
|
6363
|
+
singleton_cref)
|
|
6364
|
+
end
|
|
6365
|
+
|
|
6366
|
+
def walk_extends_children_plain(node, qualified_prefix, current_class, accumulator, in_singleton,
|
|
6367
|
+
singleton_self, singleton_cref)
|
|
5895
6368
|
node.rigor_each_child do |child|
|
|
5896
6369
|
walk_class_extends(child, qualified_prefix, current_class, accumulator,
|
|
5897
6370
|
in_singleton: in_singleton, singleton_self: singleton_self,
|
|
@@ -5915,6 +6388,7 @@ module Rigor
|
|
|
5915
6388
|
# singleton body, `self` IS the singleton and `class << self` opens the
|
|
5916
6389
|
# singleton's own singleton (`#<Class:#<Class:C>>`), which nothing names.
|
|
5917
6390
|
opens_self = node.expression.is_a?(Prism::SelfNode) && !in_singleton
|
|
6391
|
+
accumulator.direct_body(node.body) if opens_self && accumulator.direct?(node)
|
|
5918
6392
|
walk_class_extends(node.body, qualified_prefix, opens_self ? current_class : nil,
|
|
5919
6393
|
accumulator, in_singleton: opens_self,
|
|
5920
6394
|
singleton_self: true, singleton_cref: true)
|
|
@@ -5931,6 +6405,8 @@ module Rigor
|
|
|
5931
6405
|
return unless child_prefix && node.body
|
|
5932
6406
|
|
|
5933
6407
|
child_cref = unnameable_decl?(node, self_decl, singleton_cref)
|
|
6408
|
+
accumulator.direct_body(node.body) if accumulator.direct?(node)
|
|
6409
|
+
accumulator.body_statements(node.body)
|
|
5934
6410
|
walk_class_extends(node.body, child_cref ? [] : child_prefix,
|
|
5935
6411
|
child_cref ? nil : child_prefix.join("::"), accumulator,
|
|
5936
6412
|
singleton_cref: child_cref)
|
|
@@ -5954,6 +6430,110 @@ module Rigor
|
|
|
5954
6430
|
in_singleton, singleton_self, singleton_cref)
|
|
5955
6431
|
end
|
|
5956
6432
|
|
|
6433
|
+
# What the extends walk does with a block-carrying call: `:rebound` (its own eval / `define_method` / meta-new
|
|
6434
|
+
# handling), `:skip` (the block runs on an object this walk cannot vouch for, as an instance's `synchronize`
|
|
6435
|
+
# or an `on_load` hook, or may never run: `Registry.each { … }`), or `:descend` (it provably runs once with the
|
|
6436
|
+
# body's `self`, #1592, and its direct statements are positioned when the call itself is).
|
|
6437
|
+
def extends_block_plan(node, accumulator)
|
|
6438
|
+
return :rebound if rebinding_extends_block?(node)
|
|
6439
|
+
return :skip unless runs_body_block_once?(node, accumulator)
|
|
6440
|
+
|
|
6441
|
+
accumulator.direct_body(node.block.body) if accumulator.direct?(node)
|
|
6442
|
+
:descend
|
|
6443
|
+
end
|
|
6444
|
+
|
|
6445
|
+
# True when {#extends_block_plan} settled the block (walked as rebound, or skipped); false leaves it to the
|
|
6446
|
+
# generic descent.
|
|
6447
|
+
def extends_block_handled?(node, qualified_prefix, current_class, accumulator, in_singleton, singleton_self,
|
|
6448
|
+
singleton_cref)
|
|
6449
|
+
case extends_block_plan(node, accumulator)
|
|
6450
|
+
when :skip then true
|
|
6451
|
+
when :rebound
|
|
6452
|
+
walk_extends_block_call(node, qualified_prefix, current_class, accumulator,
|
|
6453
|
+
in_singleton, singleton_self, singleton_cref)
|
|
6454
|
+
true
|
|
6455
|
+
else false
|
|
6456
|
+
end
|
|
6457
|
+
end
|
|
6458
|
+
|
|
6459
|
+
ARRAY_ITERATORS = %i[each each_with_index map].freeze
|
|
6460
|
+
HASH_ITERATORS = %i[each each_pair each_key each_value map].freeze
|
|
6461
|
+
private_constant :ARRAY_ITERATORS, :HASH_ITERATORS
|
|
6462
|
+
|
|
6463
|
+
# A block that provably runs at least once with the class body's `self`: the call is a statement of the
|
|
6464
|
+
# body itself (not nested in a `def` or another block) and either `tap` / `then` on a literal (they
|
|
6465
|
+
# always yield once, self preserved) or an iterator over a literal that is known non-empty (a
|
|
6466
|
+
# non-empty Array or Hash literal, a positive `Integer#times`, an Integer-literal Range that is not empty,
|
|
6467
|
+
# `upto` / `downto` in the right order). A constant or variable receiver (`Registry.each`, a lazy `map`) may
|
|
6468
|
+
# never yield, so it is not one. The walk then records the block's direct statements as positioned: the
|
|
6469
|
+
# block runs where it is written, and a repeated `extend` is idempotent (#1592).
|
|
6470
|
+
def runs_body_block_once?(node, accumulator)
|
|
6471
|
+
return false unless accumulator.body_block?(node)
|
|
6472
|
+
return literal_or_self?(node.receiver) if %i[tap then].include?(node.name)
|
|
6473
|
+
|
|
6474
|
+
non_empty_literal_iteration?(node.name, node.receiver, node.arguments&.arguments || [])
|
|
6475
|
+
end
|
|
6476
|
+
|
|
6477
|
+
TAP_RECEIVERS = [Prism::IntegerNode, Prism::FloatNode, Prism::StringNode, Prism::SymbolNode, Prism::ArrayNode,
|
|
6478
|
+
Prism::HashNode, Prism::RangeNode, Prism::NilNode, Prism::TrueNode, Prism::FalseNode].freeze
|
|
6479
|
+
private_constant :TAP_RECEIVERS
|
|
6480
|
+
|
|
6481
|
+
# `tap` / `then` yield once only on a receiver whose method nobody overrode: a literal. A constant or `self`
|
|
6482
|
+
# may define its own `tap` (`def self.tap`), a promise's `then` defers, and a receiverless call may reach a
|
|
6483
|
+
# class-level DSL method, so none of those is read as running.
|
|
6484
|
+
def literal_or_self?(receiver)
|
|
6485
|
+
TAP_RECEIVERS.any? { |kind| receiver.is_a?(kind) }
|
|
6486
|
+
end
|
|
6487
|
+
|
|
6488
|
+
def non_empty_literal_iteration?(name, receiver, arguments)
|
|
6489
|
+
case receiver
|
|
6490
|
+
when Prism::ArrayNode
|
|
6491
|
+
ARRAY_ITERATORS.include?(name) && !receiver.elements.empty? &&
|
|
6492
|
+
receiver.elements.none?(Prism::SplatNode)
|
|
6493
|
+
when Prism::HashNode
|
|
6494
|
+
HASH_ITERATORS.include?(name) && !receiver.elements.empty? &&
|
|
6495
|
+
receiver.elements.all?(Prism::AssocNode)
|
|
6496
|
+
when Prism::IntegerNode then integer_iteration_runs?(name, receiver.value, arguments)
|
|
6497
|
+
when Prism::ParenthesesNode then non_empty_range_iteration?(name, receiver, arguments)
|
|
6498
|
+
else false
|
|
6499
|
+
end
|
|
6500
|
+
end
|
|
6501
|
+
|
|
6502
|
+
def integer_iteration_runs?(name, value, arguments)
|
|
6503
|
+
limit = arguments.first
|
|
6504
|
+
case name
|
|
6505
|
+
when :times then arguments.empty? && value.positive?
|
|
6506
|
+
when :upto then arguments.size == 1 && limit.is_a?(Prism::IntegerNode) && value <= limit.value
|
|
6507
|
+
when :downto then arguments.size == 1 && limit.is_a?(Prism::IntegerNode) && value >= limit.value
|
|
6508
|
+
else false
|
|
6509
|
+
end
|
|
6510
|
+
end
|
|
6511
|
+
|
|
6512
|
+
def positive_integer_literal?(node) = node.is_a?(Prism::IntegerNode) && node.value.positive?
|
|
6513
|
+
|
|
6514
|
+
# `(1..3).each` and `(1..3).map`, and `(1..3).step(2)` with a positive literal step.
|
|
6515
|
+
def non_empty_range_iteration?(name, parens, arguments)
|
|
6516
|
+
range = parens.body.is_a?(Prism::StatementsNode) && parens.body.body.size == 1 ? parens.body.body.first : nil
|
|
6517
|
+
return false unless range.is_a?(Prism::RangeNode)
|
|
6518
|
+
return false unless range.left.is_a?(Prism::IntegerNode) && range.right.is_a?(Prism::IntegerNode)
|
|
6519
|
+
|
|
6520
|
+
low = range.left.value
|
|
6521
|
+
high = range.right.value
|
|
6522
|
+
return false unless range.exclude_end? ? low < high : low <= high
|
|
6523
|
+
|
|
6524
|
+
case name
|
|
6525
|
+
when :each, :map then arguments.empty?
|
|
6526
|
+
when :step then positive_integer_literal?(arguments.first) && arguments.size == 1
|
|
6527
|
+
else false
|
|
6528
|
+
end
|
|
6529
|
+
end
|
|
6530
|
+
|
|
6531
|
+
# Whether {#walk_extends_block_call} owns this block-carrying call. Any other block (`[1].each { extend M }`,
|
|
6532
|
+
# `included do extend M end`) keeps the enclosing `self`, so the generic descent walks it (#1592).
|
|
6533
|
+
def rebinding_extends_block?(node)
|
|
6534
|
+
INSTANCE_EVAL_CALLS.include?(node.name) || node.name == :define_method || meta_new_constant_rvalue?(node)
|
|
6535
|
+
end
|
|
6536
|
+
|
|
5957
6537
|
# A block whose `self` is an object this walk cannot name — a `define_method` body (its
|
|
5958
6538
|
# self is the receiver's instance at call time) or an unnamed `Class.new { … }`-family
|
|
5959
6539
|
# block — walks ownerless with an unnameable self, while the call's receiver and
|
|
@@ -6067,19 +6647,39 @@ module Rigor
|
|
|
6067
6647
|
# `class << self; extend M; end` puts M on the singleton's OWN singleton, one level further out than
|
|
6068
6648
|
# anything this table describes, and `module_function` in a singleton body is not the scope toggle
|
|
6069
6649
|
# {#build_discovered_extends} over-approximates. So the two arms are disjoint rather than additive.
|
|
6070
|
-
|
|
6071
|
-
|
|
6650
|
+
# Inside `class << self` the instance walk is ownerless, so this walk also keeps the instance-side taints
|
|
6651
|
+
# (`base.include X` in a hook written there).
|
|
6652
|
+
def record_extend_call(node, qualified_prefix, current_class, accumulator, in_singleton: false)
|
|
6653
|
+
return record_module_function(node, current_class, accumulator, in_singleton) if node.name == :module_function
|
|
6072
6654
|
|
|
6073
|
-
|
|
6074
|
-
record_extend_targets(node, current_class, accumulator) if MIXIN_CALL_NAMES.include?(node.name)
|
|
6075
|
-
return
|
|
6076
|
-
end
|
|
6655
|
+
taint_opaque_eval(node, current_class, accumulator)
|
|
6077
6656
|
|
|
6078
|
-
|
|
6079
|
-
|
|
6080
|
-
|
|
6081
|
-
|
|
6082
|
-
|
|
6657
|
+
kind, _arguments, via_send = mixin_call_view(node)
|
|
6658
|
+
return if kind.nil?
|
|
6659
|
+
|
|
6660
|
+
effects = mixin_effects(node, kind, qualified_prefix, current_class, accumulator, in_singleton: in_singleton)
|
|
6661
|
+
.select { |_owner, side| side == :extend || in_singleton }
|
|
6662
|
+
return if effects.empty?
|
|
6663
|
+
return if !via_send && singleton_mixin_recorded?(node, kind, current_class, accumulator, in_singleton)
|
|
6664
|
+
|
|
6665
|
+
effects.each { |owner, side| accumulator.taint(owner, side) }
|
|
6666
|
+
end
|
|
6667
|
+
|
|
6668
|
+
# The bare `module_function` toggle: the module extends itself. A direct statement only.
|
|
6669
|
+
def record_module_function(node, current_class, accumulator, in_singleton)
|
|
6670
|
+
return unless current_class && node.receiver.nil? && !in_singleton && ModuleFunctionState.extends_self?(node)
|
|
6671
|
+
|
|
6672
|
+
(accumulator[current_class] ||= []) << current_class
|
|
6673
|
+
accumulator.note(node, current_class, :extend, [current_class], complete: true)
|
|
6674
|
+
end
|
|
6675
|
+
|
|
6676
|
+
# Records the singleton-side edges of a receiverless `extend` (or, inside `class << self`, `include` /
|
|
6677
|
+
# `prepend`). False when it recorded nothing.
|
|
6678
|
+
def singleton_mixin_recorded?(node, kind, current_class, accumulator, in_singleton)
|
|
6679
|
+
return false unless current_class && node.receiver.nil?
|
|
6680
|
+
return false unless in_singleton ? MIXIN_CALL_NAMES.include?(kind) : kind == :extend
|
|
6681
|
+
|
|
6682
|
+
!record_extend_targets(node, current_class, accumulator).empty?
|
|
6083
6683
|
end
|
|
6084
6684
|
|
|
6085
6685
|
# The table stores search order, not call order: a later `extend` statement prepends its module
|
|
@@ -6088,11 +6688,22 @@ module Rigor
|
|
|
6088
6688
|
# and consumers iterating the list forward read the same order the singleton ancestry searches.
|
|
6089
6689
|
def record_extend_targets(node, current_class, accumulator)
|
|
6090
6690
|
targets = []
|
|
6091
|
-
node.arguments&.arguments
|
|
6691
|
+
arguments = node.arguments&.arguments || []
|
|
6692
|
+
arguments.each do |arg|
|
|
6092
6693
|
target = arg.is_a?(Prism::SelfNode) ? current_class : Source::ConstantPath.qualified_name(arg)
|
|
6093
6694
|
targets << target if target
|
|
6094
6695
|
end
|
|
6095
|
-
|
|
6696
|
+
return targets if targets.empty?
|
|
6697
|
+
|
|
6698
|
+
list = (accumulator[current_class] ||= [])
|
|
6699
|
+
# A repeated `extend` keeps the table position it always had (the later statement's: the freeze `uniq`s
|
|
6700
|
+
# the nearest-first list), which is not Ruby's (Ruby skips the repeat and keeps the first, #1573), and
|
|
6701
|
+
# the folded singleton tables read that order. The order is not a fact, so the singleton side is named
|
|
6702
|
+
# unpositioned and the chain settles to the readers' own answer.
|
|
6703
|
+
accumulator.taint(current_class, :extend) if targets.any? { |target| list.include?(target) }
|
|
6704
|
+
list.unshift(*targets)
|
|
6705
|
+
accumulator.note(node, current_class, :extend, targets, complete: targets.size == arguments.size)
|
|
6706
|
+
targets
|
|
6096
6707
|
end
|
|
6097
6708
|
|
|
6098
6709
|
# The materialization half of #526: for every `C extends M`, M's INSTANCE defs become C's
|
|
@@ -6100,36 +6711,133 @@ module Rigor
|
|
|
6100
6711
|
# call-site return inference runs with `self = Singleton[C]`, which is what Ruby binds). A name C
|
|
6101
6712
|
# already defines on its own singleton wins (`||=`); an extend target with no discovered defs
|
|
6102
6713
|
# contributes nothing (RBS-module extends stay on the dispatch tiers).
|
|
6103
|
-
|
|
6714
|
+
#
|
|
6715
|
+
# ADR-119 WD1 — the copy follows its siblings, which this writes (copy on write; a table the caller passed
|
|
6716
|
+
# is never mutated). `unpositioned` is the project's `unpositioned_mixins`; an edge it lists, or taints
|
|
6717
|
+
# with "*", is not a fact, so what it copies is `possible`:
|
|
6718
|
+
#
|
|
6719
|
+
# - a slot the copy WROTE is contested when the edge is listed or the source slot is contested (a slot some
|
|
6720
|
+
# other copy wrote was decided by that copy, which the `||=` lets win);
|
|
6721
|
+
# - the singleton name is `possible` when the edge is listed or the source name is possible on the instance
|
|
6722
|
+
# side, unless C's singleton already answers it certainly (never `:both` from here: the instance side is
|
|
6723
|
+
# not touched);
|
|
6724
|
+
# - a certain copy makes the singleton name certain, so `:singleton` leaves the name's possible kind.
|
|
6725
|
+
#
|
|
6726
|
+
# A contested source with a certain edge contests the slot and leaves the name certain: a value-only
|
|
6727
|
+
# contest does not make the name possible.
|
|
6728
|
+
def fold_extends_into_singleton_tables(extends, def_nodes, singleton_def_nodes, methods, siblings:,
|
|
6729
|
+
unpositioned:)
|
|
6730
|
+
owned = {}.compare_by_identity
|
|
6104
6731
|
extends.each do |class_name, mods|
|
|
6732
|
+
listed_mods = unpositioned.dig(class_name, :extend)
|
|
6105
6733
|
mods.each do |mod_name|
|
|
6106
|
-
source_defs = extend_source_defs(def_nodes, class_name, mod_name)
|
|
6734
|
+
resolved, source_defs = extend_source_defs(def_nodes, class_name, mod_name)
|
|
6107
6735
|
next if source_defs.nil?
|
|
6108
6736
|
|
|
6109
6737
|
# An inner table inherited unchanged from a frozen seed must be thawed before the fold writes.
|
|
6110
6738
|
target = singleton_def_nodes[class_name]
|
|
6111
6739
|
target = singleton_def_nodes[class_name] = (target ? target.dup : {}) if target.nil? || target.frozen?
|
|
6740
|
+
listed = extend_edge_listed?(listed_mods, mod_name)
|
|
6112
6741
|
source_defs.each do |method_name, def_node|
|
|
6742
|
+
wrote = target[method_name].nil?
|
|
6113
6743
|
target[method_name] ||= def_node
|
|
6744
|
+
certain_before = singleton_name_certain?(methods, siblings, class_name, method_name)
|
|
6114
6745
|
record_method_kind(methods, class_name, method_name, :singleton)
|
|
6746
|
+
follow_extend_siblings(siblings, owned, [class_name, resolved, method_name],
|
|
6747
|
+
wrote: wrote, listed: listed, certain_before: certain_before)
|
|
6115
6748
|
end
|
|
6116
6749
|
end
|
|
6117
6750
|
end
|
|
6118
6751
|
end
|
|
6119
6752
|
|
|
6753
|
+
# Whether `unpositioned_mixins` lists the extend edge to `mod_name` (or taints the whole side).
|
|
6754
|
+
def extend_edge_listed?(listed_mods, mod_name)
|
|
6755
|
+
!listed_mods.nil? && (listed_mods.include?(mod_name) || listed_mods.include?(MixinAccumulator::WILDCARD))
|
|
6756
|
+
end
|
|
6757
|
+
|
|
6758
|
+
# Whether `class_name`'s singleton already answers `method_name` through a certain fact: the existence table
|
|
6759
|
+
# covers the singleton side and the `possible` copy does not.
|
|
6760
|
+
def singleton_name_certain?(methods, siblings, class_name, method_name)
|
|
6761
|
+
return false unless covers_singleton?(methods.dig(class_name, method_name))
|
|
6762
|
+
|
|
6763
|
+
!covers_singleton?(siblings[:possible_discovered_methods].dig(class_name, method_name))
|
|
6764
|
+
end
|
|
6765
|
+
|
|
6766
|
+
SINGLETON_KINDS = [:singleton, Scope::DiscoveryIndex::METHOD_KIND_BOTH].freeze
|
|
6767
|
+
INSTANCE_KINDS = [:instance, Scope::DiscoveryIndex::METHOD_KIND_BOTH].freeze
|
|
6768
|
+
private_constant :SINGLETON_KINDS, :INSTANCE_KINDS
|
|
6769
|
+
|
|
6770
|
+
def covers_singleton?(kind)
|
|
6771
|
+
SINGLETON_KINDS.include?(kind)
|
|
6772
|
+
end
|
|
6773
|
+
|
|
6774
|
+
# The sibling half of one copied name; see {#fold_extends_into_singleton_tables}. `names` is
|
|
6775
|
+
# `[extender, resolved source module, method name]`.
|
|
6776
|
+
def follow_extend_siblings(siblings, owned, names, wrote:, listed:, certain_before:)
|
|
6777
|
+
class_name, resolved, method_name = names
|
|
6778
|
+
contested_source = !siblings[:contested_discovered_def_nodes].empty? &&
|
|
6779
|
+
siblings[:contested_discovered_def_nodes].include?([resolved, method_name])
|
|
6780
|
+
if wrote && (listed || contested_source)
|
|
6781
|
+
own_sibling_table(siblings, owned, :contested_discovered_singleton_def_nodes) << [class_name, method_name]
|
|
6782
|
+
end
|
|
6783
|
+
if listed || source_name_possible?(siblings, resolved, method_name)
|
|
6784
|
+
return if certain_before
|
|
6785
|
+
|
|
6786
|
+
row = (own_sibling_table(siblings, owned, :possible_discovered_methods)[class_name] ||= {})
|
|
6787
|
+
row[method_name] = merge_sibling_kind(row[method_name], :singleton)
|
|
6788
|
+
else
|
|
6789
|
+
strip_possible_singleton(siblings, owned, class_name, method_name)
|
|
6790
|
+
end
|
|
6791
|
+
end
|
|
6792
|
+
|
|
6793
|
+
def source_name_possible?(siblings, resolved, method_name)
|
|
6794
|
+
INSTANCE_KINDS.include?(siblings[:possible_discovered_methods].dig(resolved, method_name))
|
|
6795
|
+
end
|
|
6796
|
+
|
|
6797
|
+
def merge_sibling_kind(sitting, arriving)
|
|
6798
|
+
sitting.nil? || sitting == arriving ? arriving : Scope::DiscoveryIndex::METHOD_KIND_BOTH
|
|
6799
|
+
end
|
|
6800
|
+
|
|
6801
|
+
# A certain copy: `:singleton` leaves the `possible` kind (`:both` becomes `:instance`), and an emptied row goes.
|
|
6802
|
+
def strip_possible_singleton(siblings, owned, class_name, method_name)
|
|
6803
|
+
return unless covers_singleton?(siblings[:possible_discovered_methods].dig(class_name, method_name))
|
|
6804
|
+
|
|
6805
|
+
possible = own_sibling_table(siblings, owned, :possible_discovered_methods)
|
|
6806
|
+
row = possible[class_name]
|
|
6807
|
+
if row[method_name] == :singleton
|
|
6808
|
+
row.delete(method_name)
|
|
6809
|
+
possible.delete(class_name) if row.empty?
|
|
6810
|
+
else
|
|
6811
|
+
row[method_name] = :instance
|
|
6812
|
+
end
|
|
6813
|
+
end
|
|
6814
|
+
|
|
6815
|
+
# The sibling `key` as a table this fold owns: the first write copies it (a Set, or a Hash of Hashes) and
|
|
6816
|
+
# keeps the copy in `siblings`, so a table shared with a seed, a bundle or an empty constant is never written.
|
|
6817
|
+
def own_sibling_table(siblings, owned, key)
|
|
6818
|
+
table = siblings.fetch(key)
|
|
6819
|
+
return table if owned.key?(table)
|
|
6820
|
+
|
|
6821
|
+
copy = table.is_a?(Set) ? table.dup : table.transform_values(&:dup)
|
|
6822
|
+
owned[copy] = true
|
|
6823
|
+
siblings[key] = copy
|
|
6824
|
+
end
|
|
6825
|
+
|
|
6120
6826
|
# `extend CustomSig` inside `module Outer; class F` stores the as-written name, while
|
|
6121
6827
|
# `def_nodes` keys the module's defs as `Outer::CustomSig`. Walk the enclosing namespaces
|
|
6122
|
-
# innermost-first so the fold finds the same module Ruby constant-lookup would.
|
|
6828
|
+
# innermost-first so the fold finds the same module Ruby constant-lookup would. Returns
|
|
6829
|
+
# `[the key the defs were found under, the defs]`, or nil.
|
|
6123
6830
|
def extend_source_defs(def_nodes, class_name, mod_name)
|
|
6124
6831
|
found = def_nodes[mod_name]
|
|
6125
|
-
return found if found
|
|
6832
|
+
return [mod_name, found] if found
|
|
6126
6833
|
return nil if mod_name.nil? || mod_name.start_with?("::")
|
|
6127
6834
|
|
|
6128
6835
|
parts = class_name.to_s.split("::")
|
|
6129
6836
|
while parts.size > 1
|
|
6130
6837
|
parts.pop
|
|
6131
|
-
|
|
6132
|
-
|
|
6838
|
+
key = "#{parts.join('::')}::#{mod_name}"
|
|
6839
|
+
found = def_nodes[key]
|
|
6840
|
+
return [key, found] if found
|
|
6133
6841
|
end
|
|
6134
6842
|
nil
|
|
6135
6843
|
end
|
|
@@ -6155,9 +6863,83 @@ module Rigor
|
|
|
6155
6863
|
# Top-level (no surrounding class) defs do not contribute — Ruby's top-level visibility nuances (private at
|
|
6156
6864
|
# top-level marks the method on `Object`) are out of scope for v0.1.2.
|
|
6157
6865
|
def build_discovered_method_visibilities(root)
|
|
6158
|
-
|
|
6866
|
+
build_method_visibility_tables(root).first
|
|
6867
|
+
end
|
|
6868
|
+
|
|
6869
|
+
# ADR-119 WD3 — the visibility table and its `contested_discovered_method_visibilities` sibling, from ONE walk
|
|
6870
|
+
# over the file's {Certainty} classification `certainty`: `[table, contested key paths]`. The recorded values
|
|
6871
|
+
# are {#build_discovered_method_visibilities}'s; a slot is contested when its last writer is ({VisibilityTable}).
|
|
6872
|
+
def build_method_visibility_tables(root, certainty: Certainty.possible_nodes(root))
|
|
6873
|
+
accumulator = VisibilityTable.new.track(certainty)
|
|
6159
6874
|
walk_method_visibilities(root, [], false, :public, accumulator)
|
|
6160
|
-
accumulator.transform_values(&:freeze).freeze
|
|
6875
|
+
[accumulator.transform_values(&:freeze).freeze, accumulator.contested_paths]
|
|
6876
|
+
end
|
|
6877
|
+
|
|
6878
|
+
# The bare calls that set the running default visibility of the body they run in: the modifiers and
|
|
6879
|
+
# `module_function`, which {#walk_method_visibilities} does not track as a value (#1569) but which moves the
|
|
6880
|
+
# default all the same.
|
|
6881
|
+
VISIBILITY_TOGGLES = (VISIBILITY_MODIFIERS + %i[module_function]).freeze
|
|
6882
|
+
|
|
6883
|
+
# The `{class => {name => visibility}}` accumulator of {#walk_method_visibilities}, which also carries the
|
|
6884
|
+
# ADR-119 WD3 state of the slots it writes. A slot is contested when the write that holds it is possible: a
|
|
6885
|
+
# `def` or a named target (`private :x`) the {Certainty} classification reads as possible, or a `def` written
|
|
6886
|
+
# after an UNCERTAIN bare toggle of its body. A toggle is uncertain when it is possible itself or when the walk
|
|
6887
|
+
# does not carry it into the body's running default: one under any child of the body that is not its
|
|
6888
|
+
# statement list (`if c then private end`, `begin private end`, a block, `(private)`), since Ruby applies a
|
|
6889
|
+
# bare toggle there to the body's later `def`s while the walk keeps the default it had. A certain toggle
|
|
6890
|
+
# written directly in the body settles the default again. A named target does not read the running default,
|
|
6891
|
+
# so a toggle does not contest it. A certain write clears the slot's contest.
|
|
6892
|
+
class VisibilityTable < Hash
|
|
6893
|
+
# The bare toggles the walk has met so far; a body's statement loop compares it around each child.
|
|
6894
|
+
attr_accessor :toggles
|
|
6895
|
+
# Whether the running default of the body being walked rests on an uncertain toggle.
|
|
6896
|
+
attr_accessor :uncertain
|
|
6897
|
+
# The file's {Certainty} answer.
|
|
6898
|
+
attr_reader :certainty
|
|
6899
|
+
|
|
6900
|
+
def track(certainty)
|
|
6901
|
+
@certainty = certainty
|
|
6902
|
+
@contested = nil
|
|
6903
|
+
@toggles = 0
|
|
6904
|
+
@uncertain = false
|
|
6905
|
+
self
|
|
6906
|
+
end
|
|
6907
|
+
|
|
6908
|
+
# Records that `node` wrote the `[class_name, name]` slot; `toggled` when the write reads the running
|
|
6909
|
+
# default (a `def`).
|
|
6910
|
+
def wrote(class_name, name, node, toggled:)
|
|
6911
|
+
if Certainty.possible?(@certainty, node) || (toggled && @uncertain)
|
|
6912
|
+
(@contested ||= Set.new) << [class_name, name].freeze
|
|
6913
|
+
else
|
|
6914
|
+
@contested&.delete([class_name, name])
|
|
6915
|
+
end
|
|
6916
|
+
end
|
|
6917
|
+
|
|
6918
|
+
# Runs the block as a fresh body (a class or module, `class << self`, a meta-new or eval block), whose
|
|
6919
|
+
# default starts certain and whose toggles do not reach the enclosing body.
|
|
6920
|
+
def body
|
|
6921
|
+
saved_toggles = @toggles
|
|
6922
|
+
saved_uncertain = @uncertain
|
|
6923
|
+
@uncertain = false
|
|
6924
|
+
yield
|
|
6925
|
+
ensure
|
|
6926
|
+
@toggles = saved_toggles
|
|
6927
|
+
@uncertain = saved_uncertain
|
|
6928
|
+
end
|
|
6929
|
+
|
|
6930
|
+
def contested_paths
|
|
6931
|
+
contested = @contested
|
|
6932
|
+
return Scope::DiscoveryIndex::EMPTY_SIBLINGS[:contested_discovered_method_visibilities] if contested.nil?
|
|
6933
|
+
return Scope::DiscoveryIndex::EMPTY_SIBLINGS[:contested_discovered_method_visibilities] if contested.empty?
|
|
6934
|
+
|
|
6935
|
+
contested.freeze
|
|
6936
|
+
end
|
|
6937
|
+
end
|
|
6938
|
+
|
|
6939
|
+
# A bare toggle ({VISIBILITY_TOGGLES}): receiverless, with no arguments.
|
|
6940
|
+
def bare_visibility_toggle?(node)
|
|
6941
|
+
node.is_a?(Prism::CallNode) && node.receiver.nil? && VISIBILITY_TOGGLES.include?(node.name) &&
|
|
6942
|
+
(node.arguments.nil? || node.arguments.arguments.empty?) && node.block.nil?
|
|
6161
6943
|
end
|
|
6162
6944
|
|
|
6163
6945
|
# rubocop:disable-next Metrics/CyclomaticComplexity, Metrics/MethodLength, Metrics/PerceivedComplexity, Metrics/AbcSize
|
|
@@ -6179,8 +6961,10 @@ module Rigor
|
|
|
6179
6961
|
child_cref = unnameable_decl?(node, self_decl, singleton_cref)
|
|
6180
6962
|
body_prefix = child_cref ? [] : child_prefix
|
|
6181
6963
|
if node.body
|
|
6182
|
-
|
|
6183
|
-
|
|
6964
|
+
accumulator.body do
|
|
6965
|
+
walk_method_visibilities(node.body, body_prefix, false, :public,
|
|
6966
|
+
accumulator, nil, singleton_cref: child_cref)
|
|
6967
|
+
end
|
|
6184
6968
|
end
|
|
6185
6969
|
return current_visibility
|
|
6186
6970
|
end
|
|
@@ -6200,9 +6984,11 @@ module Rigor
|
|
|
6200
6984
|
defs_singleton: defs_singleton)
|
|
6201
6985
|
end
|
|
6202
6986
|
if body
|
|
6203
|
-
|
|
6204
|
-
|
|
6205
|
-
|
|
6987
|
+
accumulator.body do
|
|
6988
|
+
walk_method_visibilities(body, qualified_prefix, false, :public,
|
|
6989
|
+
accumulator, body_self,
|
|
6990
|
+
singleton_cref: singleton_cref)
|
|
6991
|
+
end
|
|
6206
6992
|
end
|
|
6207
6993
|
return current_visibility
|
|
6208
6994
|
end
|
|
@@ -6217,6 +7003,7 @@ module Rigor
|
|
|
6217
7003
|
defs_singleton: defs_singleton)
|
|
6218
7004
|
return current_visibility
|
|
6219
7005
|
end
|
|
7006
|
+
accumulator.toggles += 1 if bare_visibility_toggle?(node)
|
|
6220
7007
|
# The visibility table is instance-side only — `private :x` inside `class <<`
|
|
6221
7008
|
# (or a receiver-eval body that re-evaluates a singleton self) marks the
|
|
6222
7009
|
# SINGLETON method, which this table cannot express.
|
|
@@ -6230,13 +7017,23 @@ module Rigor
|
|
|
6230
7017
|
|
|
6231
7018
|
# Statement-position StatementsNode preserves left-to-right visibility flow; everything else recurses with the
|
|
6232
7019
|
# entry visibility unchanged.
|
|
7020
|
+
#
|
|
7021
|
+
# ADR-119 WD3 — the statement loop also settles whether the running default is uncertain
|
|
7022
|
+
# ({VisibilityTable}): a bare toggle written here sets it from its own certainty, and any other statement
|
|
7023
|
+
# under which the walk met a toggle makes it uncertain.
|
|
6233
7024
|
if node.is_a?(Prism::StatementsNode)
|
|
6234
7025
|
local_visibility = current_visibility
|
|
6235
7026
|
node.rigor_each_child do |child|
|
|
7027
|
+
toggles = accumulator.toggles
|
|
6236
7028
|
local_visibility = walk_method_visibilities(child, qualified_prefix, in_singleton_class,
|
|
6237
7029
|
local_visibility, accumulator, def_owner_prefix,
|
|
6238
7030
|
singleton_cref: singleton_cref,
|
|
6239
7031
|
defs_singleton: defs_singleton)
|
|
7032
|
+
if bare_visibility_toggle?(child)
|
|
7033
|
+
accumulator.uncertain = Certainty.possible?(accumulator.certainty, child)
|
|
7034
|
+
elsif accumulator.toggles != toggles
|
|
7035
|
+
accumulator.uncertain = true
|
|
7036
|
+
end
|
|
6240
7037
|
end
|
|
6241
7038
|
else
|
|
6242
7039
|
node.rigor_each_child do |child|
|
|
@@ -6263,8 +7060,10 @@ module Rigor
|
|
|
6263
7060
|
defs_singleton: defs_singleton)
|
|
6264
7061
|
return unless node.body
|
|
6265
7062
|
|
|
6266
|
-
|
|
6267
|
-
|
|
7063
|
+
accumulator.body do
|
|
7064
|
+
walk_method_visibilities(node.body, qualified_prefix, true, :public, accumulator,
|
|
7065
|
+
singleton_prefix, singleton_cref: true)
|
|
7066
|
+
end
|
|
6268
7067
|
end
|
|
6269
7068
|
|
|
6270
7069
|
# The eval-block arm of {#walk_method_visibilities}: the block is a fresh class body under
|
|
@@ -6295,10 +7094,12 @@ module Rigor
|
|
|
6295
7094
|
# receiver-as-module and stay instance-side (`in_singleton_class` off) for a class
|
|
6296
7095
|
# receiver.
|
|
6297
7096
|
call_singleton, defs_flag = eval_body_def_context(node, in_singleton_class)
|
|
6298
|
-
|
|
6299
|
-
|
|
6300
|
-
|
|
6301
|
-
|
|
7097
|
+
accumulator.body do
|
|
7098
|
+
node.block.rigor_each_child do |child|
|
|
7099
|
+
walk_method_visibilities(child, qualified_prefix, call_singleton, :public, accumulator,
|
|
7100
|
+
eval_prefix, singleton_cref: singleton_cref,
|
|
7101
|
+
defs_singleton: defs_flag)
|
|
7102
|
+
end
|
|
6302
7103
|
end
|
|
6303
7104
|
end
|
|
6304
7105
|
|
|
@@ -6309,6 +7110,7 @@ module Rigor
|
|
|
6309
7110
|
class_name = qualified_prefix.join("::")
|
|
6310
7111
|
accumulator[class_name] ||= {}
|
|
6311
7112
|
accumulator[class_name][def_node.name] = current_visibility
|
|
7113
|
+
accumulator.wrote(class_name, def_node.name, def_node, toggled: true)
|
|
6312
7114
|
end
|
|
6313
7115
|
|
|
6314
7116
|
# Recognises modifier calls on the implicit-self receiver inside a class body. Returns the (possibly updated)
|
|
@@ -6329,19 +7131,20 @@ module Rigor
|
|
|
6329
7131
|
if args.empty?
|
|
6330
7132
|
call_node.name
|
|
6331
7133
|
else
|
|
6332
|
-
apply_named_visibility(args, qualified_prefix, call_node
|
|
7134
|
+
apply_named_visibility(args, qualified_prefix, call_node, accumulator)
|
|
6333
7135
|
current_visibility
|
|
6334
7136
|
end
|
|
6335
7137
|
end
|
|
6336
7138
|
|
|
6337
|
-
def apply_named_visibility(args, qualified_prefix,
|
|
7139
|
+
def apply_named_visibility(args, qualified_prefix, call_node, accumulator)
|
|
6338
7140
|
class_name = qualified_prefix.join("::")
|
|
6339
7141
|
args.each do |arg|
|
|
6340
7142
|
name = visibility_target_name(arg)
|
|
6341
7143
|
next if name.nil?
|
|
6342
7144
|
|
|
6343
7145
|
accumulator[class_name] ||= {}
|
|
6344
|
-
accumulator[class_name][name] =
|
|
7146
|
+
accumulator[class_name][name] = call_node.name
|
|
7147
|
+
accumulator.wrote(class_name, name, call_node, toggled: false)
|
|
6345
7148
|
end
|
|
6346
7149
|
end
|
|
6347
7150
|
|
|
@@ -6385,6 +7188,7 @@ module Rigor
|
|
|
6385
7188
|
def record_method_envelope_opaque(tables, class_name, method_name)
|
|
6386
7189
|
%i[instance singleton].each do |kind|
|
|
6387
7190
|
record_envelope(tables.envelopes, class_name, [kind, method_name], Source::ParameterEnvelope::OPAQUE)
|
|
7191
|
+
contest_envelope(tables, class_name, [kind, method_name]) unless tables.certain
|
|
6388
7192
|
end
|
|
6389
7193
|
end
|
|
6390
7194
|
|
|
@@ -6505,8 +7309,11 @@ module Rigor
|
|
|
6505
7309
|
# Post-pass over the `def_nodes` accumulator: for every `alias` declaration inside a class body, if the original
|
|
6506
7310
|
# method name maps to a `Prism::DefNode`, register the new name pointing to the same node so inter-procedural
|
|
6507
7311
|
# return-type inference works for the aliased name.
|
|
6508
|
-
|
|
6509
|
-
|
|
7312
|
+
#
|
|
7313
|
+
# ADR-119 WD3 — the alias is the slot's writer: the new name's slot is contested when the alias is possible
|
|
7314
|
+
# or the slot it copies is contested, and cleared otherwise.
|
|
7315
|
+
def apply_alias_def_nodes(root, accumulator, tables = nil)
|
|
7316
|
+
alias_map = collect_class_alias_map(root, [], AliasMap.new.track(tables&.certainty))
|
|
6510
7317
|
alias_map.each do |class_name, aliases|
|
|
6511
7318
|
class_defs = accumulator[class_name]
|
|
6512
7319
|
next unless class_defs
|
|
@@ -6516,10 +7323,40 @@ module Rigor
|
|
|
6516
7323
|
next unless def_node.is_a?(Prism::DefNode)
|
|
6517
7324
|
|
|
6518
7325
|
(accumulator[class_name] ||= {})[new_name] = def_node
|
|
7326
|
+
next if tables.nil?
|
|
7327
|
+
|
|
7328
|
+
certain = !alias_map.possible?(class_name, new_name) &&
|
|
7329
|
+
!tables.contested_def_nodes&.include?([class_name, old_name])
|
|
7330
|
+
follow_slot_writer(tables, :contested_def_nodes, [class_name, new_name], certain)
|
|
6519
7331
|
end
|
|
6520
7332
|
end
|
|
6521
7333
|
end
|
|
6522
7334
|
|
|
7335
|
+
# The `{class => {new name => old name}}` map {#collect_class_alias_map} fills, which also remembers, per
|
|
7336
|
+
# entry, whether the alias that wrote it last is possible ({Certainty}).
|
|
7337
|
+
class AliasMap < Hash
|
|
7338
|
+
def track(certainty)
|
|
7339
|
+
@certainty = certainty || Certainty::EMPTY
|
|
7340
|
+
@possible = nil
|
|
7341
|
+
self
|
|
7342
|
+
end
|
|
7343
|
+
|
|
7344
|
+
# Records that `node` wrote the `[class_name, new_name]` entry.
|
|
7345
|
+
def wrote(class_name, new_name, node)
|
|
7346
|
+
return if @certainty.nil?
|
|
7347
|
+
|
|
7348
|
+
if Certainty.possible?(@certainty, node)
|
|
7349
|
+
(@possible ||= Set.new) << [class_name, new_name]
|
|
7350
|
+
else
|
|
7351
|
+
@possible&.delete([class_name, new_name])
|
|
7352
|
+
end
|
|
7353
|
+
end
|
|
7354
|
+
|
|
7355
|
+
def possible?(class_name, new_name)
|
|
7356
|
+
!@possible.nil? && @possible.include?([class_name, new_name])
|
|
7357
|
+
end
|
|
7358
|
+
end
|
|
7359
|
+
|
|
6523
7360
|
# Builds a map `{class_name => {new_name_sym => old_name_sym}}` by walking the tree for `AliasMethodNode` nodes
|
|
6524
7361
|
# inside class bodies. `leaf_owner` is the meta-new override: inside `K = Class.new { … }` the
|
|
6525
7362
|
# block's aliases bind on the class the write names — it overrides `qualified_prefix`, which
|
|
@@ -6579,7 +7416,7 @@ module Rigor
|
|
|
6579
7416
|
# class bodies (`Class.new do … end`), so the walk continues below it either way.
|
|
6580
7417
|
names = alias_method_call_names(node)
|
|
6581
7418
|
if names && !rec_prefix.empty? && !(singleton_cref && leaf_owner.nil?)
|
|
6582
|
-
(accumulator
|
|
7419
|
+
record_alias_call_entry(accumulator, rec_prefix.join("::"), names, node)
|
|
6583
7420
|
end
|
|
6584
7421
|
false
|
|
6585
7422
|
end
|
|
@@ -6688,6 +7525,11 @@ module Rigor
|
|
|
6688
7525
|
accumulator
|
|
6689
7526
|
end
|
|
6690
7527
|
|
|
7528
|
+
def record_alias_call_entry(accumulator, class_name, names, node)
|
|
7529
|
+
(accumulator[class_name] ||= {})[names.first] = names.last
|
|
7530
|
+
accumulator.wrote(class_name, names.first, node) if accumulator.is_a?(AliasMap)
|
|
7531
|
+
end
|
|
7532
|
+
|
|
6691
7533
|
def record_alias_map_entry(alias_node, qualified_prefix, accumulator)
|
|
6692
7534
|
return if qualified_prefix.empty?
|
|
6693
7535
|
return unless alias_node.new_name.is_a?(Prism::SymbolNode) && alias_node.old_name.is_a?(Prism::SymbolNode)
|
|
@@ -6696,6 +7538,7 @@ module Rigor
|
|
|
6696
7538
|
new_name = alias_node.new_name.unescaped.to_sym
|
|
6697
7539
|
old_name = alias_node.old_name.unescaped.to_sym
|
|
6698
7540
|
(accumulator[class_name] ||= {})[new_name] = old_name
|
|
7541
|
+
accumulator.wrote(class_name, new_name, alias_node) if accumulator.is_a?(AliasMap)
|
|
6699
7542
|
end
|
|
6700
7543
|
|
|
6701
7544
|
def record_define_method(call_node, qualified_prefix, in_singleton_class, accumulator)
|
|
@@ -6848,6 +7691,8 @@ module Rigor
|
|
|
6848
7691
|
physical = buffer ? buffer.resolve(path) : path
|
|
6849
7692
|
root = Prism.parse(File.read(physical), filepath: path).value
|
|
6850
7693
|
accumulate_project_index(acc, path, root)
|
|
7694
|
+
rescue DeclarationWalk::ContractError
|
|
7695
|
+
raise # a broken walk contract, not an unreadable file: skipping it would hide the failure
|
|
6851
7696
|
rescue StandardError
|
|
6852
7697
|
# Skip files that fail to parse or read; the per-file analyzer surfaces the parse error separately.
|
|
6853
7698
|
next
|
|
@@ -6880,6 +7725,8 @@ module Rigor
|
|
|
6880
7725
|
code_fingerprints[path] = code_fingerprint(source, parsed.comments)
|
|
6881
7726
|
declaration_signatures[path] = declaration_signature(file_index)
|
|
6882
7727
|
refinements[path] = file_index[:refinements] if file_index[:refinements]
|
|
7728
|
+
rescue DeclarationWalk::ContractError
|
|
7729
|
+
raise # a broken walk contract, not an unreadable file: skipping it would hide the failure
|
|
6883
7730
|
rescue StandardError
|
|
6884
7731
|
next
|
|
6885
7732
|
end
|
|
@@ -6910,9 +7757,29 @@ module Rigor
|
|
|
6910
7757
|
append_def_signatures(parts, file_index[:def_nodes], "#")
|
|
6911
7758
|
append_def_signatures(parts, file_index[:singleton_def_nodes], ".")
|
|
6912
7759
|
append_envelope_signature(parts, file_index[:parameter_envelopes] || {})
|
|
7760
|
+
append_certainty_signature(parts, file_index[:siblings] || {})
|
|
6913
7761
|
Digest::SHA256.hexdigest(parts.join("\x00"))
|
|
6914
7762
|
end
|
|
6915
7763
|
|
|
7764
|
+
# ADR-119 WD3 — which of the file's contributions are possible is a declaration surface too: a `… end if X`
|
|
7765
|
+
# modifier, or moving a `def` into a block, keeps every def line and envelope and moves only this, while a
|
|
7766
|
+
# dependent's candidate-set read now declines where it answered.
|
|
7767
|
+
def append_certainty_signature(parts, siblings)
|
|
7768
|
+
certainty = []
|
|
7769
|
+
siblings.each do |name, table|
|
|
7770
|
+
next if table.empty?
|
|
7771
|
+
|
|
7772
|
+
if name == :possible_discovered_methods
|
|
7773
|
+
table.each do |class_name, row|
|
|
7774
|
+
row.each { |method_name, kind| certainty << "P:#{class_name}##{method_name}=#{kind}" }
|
|
7775
|
+
end
|
|
7776
|
+
else
|
|
7777
|
+
table.each { |path| certainty << "C:#{name}:#{path.inspect}" }
|
|
7778
|
+
end
|
|
7779
|
+
end
|
|
7780
|
+
parts.concat(certainty.sort)
|
|
7781
|
+
end
|
|
7782
|
+
|
|
6916
7783
|
# Issue #992 — the joined envelope table is a declaration surface the per-def signatures above cannot
|
|
6917
7784
|
# reconstruct: they keep the LAST `def` of a name, and they see neither a second `def` with another
|
|
6918
7785
|
# shape nor a `memoize :f` beside it, either of which moves a dependent's `call.wrong-arity` verdict.
|
|
@@ -6950,13 +7817,26 @@ module Rigor
|
|
|
6950
7817
|
def append_ancestry_signature(parts, file_index)
|
|
6951
7818
|
parts.concat(file_index[:class_sources].keys.sort_by(&:to_s).map { |cn| "c:#{cn}" })
|
|
6952
7819
|
parts.concat(sorted_by_class(file_index[:superclasses]).map { |cn, sc| "s:#{cn}<#{sc}" })
|
|
6953
|
-
parts
|
|
6954
|
-
"i:#{cn}=#{Array(mods).map(&:to_s).sort.join(',')}"
|
|
6955
|
-
end)
|
|
7820
|
+
append_mixin_signature(parts, file_index)
|
|
6956
7821
|
parts.concat(sorted_by_class(file_index[:data_member_layouts]).map { |cn, l| "d:#{cn}=#{l.inspect}" })
|
|
6957
7822
|
parts.concat(sorted_by_class(file_index[:struct_member_layouts]).map { |cn, l| "t:#{cn}=#{l.inspect}" })
|
|
6958
7823
|
end
|
|
6959
7824
|
|
|
7825
|
+
# The mixin surface of {#append_ancestry_signature}. The lists keep SOURCE order: this is one file's own
|
|
7826
|
+
# index, so the order is deterministic, and it is a fact an ancestor-order reader consumes, so an edit
|
|
7827
|
+
# that only reorders an `include` must move the signature. The unpositioned table is there for the same
|
|
7828
|
+
# reason: guarding an `include` changes no name and no order, only whether the order is known.
|
|
7829
|
+
def append_mixin_signature(parts, file_index)
|
|
7830
|
+
{ "i" => :includes, "p" => :prepends, "x" => :extends }.each do |tag, key|
|
|
7831
|
+
sorted_by_class(file_index[key] || {}).each { |cn, mods| parts << "#{tag}:#{cn}=#{Array(mods).join(',')}" }
|
|
7832
|
+
end
|
|
7833
|
+
sorted_by_class(file_index[:unpositioned_mixins] || {}).each do |cn, sides|
|
|
7834
|
+
sides.sort_by { |side, _| side.to_s }.each do |side, names|
|
|
7835
|
+
parts << "u:#{cn}.#{side}=#{names.sort.join(',')}"
|
|
7836
|
+
end
|
|
7837
|
+
end
|
|
7838
|
+
end
|
|
7839
|
+
|
|
6960
7840
|
# A class-keyed table's pairs sorted by class name (stringified) for a deterministic signature.
|
|
6961
7841
|
def sorted_by_class(table)
|
|
6962
7842
|
table.sort_by { |cn, _| cn.to_s }
|
|
@@ -7079,6 +7959,8 @@ module Rigor
|
|
|
7079
7959
|
root = Prism.parse(File.read(physical), filepath: path).value
|
|
7080
7960
|
collect_class_decls(root, [], classes)
|
|
7081
7961
|
accumulate_project_index(acc, path, root)
|
|
7962
|
+
rescue DeclarationWalk::ContractError
|
|
7963
|
+
raise # a broken walk contract, not an unreadable file: skipping it would hide the failure
|
|
7082
7964
|
rescue StandardError
|
|
7083
7965
|
# Skip files that fail to parse or read; the per-file analyzer surfaces the parse error separately.
|
|
7084
7966
|
next
|
|
@@ -7148,6 +8030,8 @@ module Rigor
|
|
|
7148
8030
|
classes.merge!(file_classes)
|
|
7149
8031
|
fold_file_index(acc, file_index)
|
|
7150
8032
|
end
|
|
8033
|
+
rescue DeclarationWalk::ContractError
|
|
8034
|
+
raise # a broken walk contract, not an unreadable file: skipping it would hide the failure
|
|
7151
8035
|
rescue StandardError
|
|
7152
8036
|
# Skip files that fail to parse / read; the per-file analyzer surfaces the parse error separately.
|
|
7153
8037
|
next
|
|
@@ -7170,6 +8054,7 @@ module Rigor
|
|
|
7170
8054
|
# Polymorphic over the def-node value: a re-walked file's `file_index` carries live nodes; a cached
|
|
7171
8055
|
# bundle's carries {DefHandle}s. The merges never deref the value, so both fold identically.
|
|
7172
8056
|
def fold_file_index(acc, file_index)
|
|
8057
|
+
fold_siblings(acc, file_index)
|
|
7173
8058
|
fold_def_tables(acc, file_index)
|
|
7174
8059
|
# Issue #992 — a pre-23 seed bundle carries no envelopes; the SCHEMA bump makes such a blob a cold
|
|
7175
8060
|
# rebuild, and an absent table only ever withholds a check.
|
|
@@ -7190,6 +8075,168 @@ module Rigor
|
|
|
7190
8075
|
fold_constant_tables(acc, file_index)
|
|
7191
8076
|
end
|
|
7192
8077
|
|
|
8078
|
+
# ADR-119 WD1 — the `possible_*` / `contested_*` siblings of {Scope::DiscoveryIndex::SIBLINGS}, carried as ONE
|
|
8079
|
+
# `{sibling name => table}` Hash (`acc[:siblings]`) through the accumulator, the fold, the compact-header
|
|
8080
|
+
# rename, the finalize and the seed bundle, so no copy path names a sibling and none can drop one. A pre-36
|
|
8081
|
+
# bundle carries the empty siblings of the walk before WD3's producers, and the SCHEMA bump makes such a blob
|
|
8082
|
+
# a cold rebuild. Called BEFORE the file's members fold, because the `possible` rule reads the accumulator's
|
|
8083
|
+
# member as it stood ({#fold_file_siblings}).
|
|
8084
|
+
def fold_siblings(acc, file_index)
|
|
8085
|
+
fold_file_siblings(acc, file_index[:siblings] || {},
|
|
8086
|
+
methods: file_index[:methods], def_nodes: file_index[:def_nodes],
|
|
8087
|
+
singleton_def_nodes: file_index[:singleton_def_nodes],
|
|
8088
|
+
method_visibilities: file_index[:method_visibilities])
|
|
8089
|
+
end
|
|
8090
|
+
|
|
8091
|
+
# ADR-119 WD3 — folds one file's siblings into the accumulator under each member's own fold, so the result is
|
|
8092
|
+
# the same in any file order and a bundle-served file folds as its live walk would:
|
|
8093
|
+
#
|
|
8094
|
+
# - `possible_discovered_methods` by {#fold_possible_methods} against `acc[:methods]` before this file's
|
|
8095
|
+
# `methods` join it;
|
|
8096
|
+
# - the def-node and visibility slots follow their writer, as the members fold later-wins: the file's contests
|
|
8097
|
+
# replace the accumulator's on every key the file writes ({#fold_slot_contests});
|
|
8098
|
+
# - every other sibling (the joined envelopes' contests among them) by union.
|
|
8099
|
+
def fold_file_siblings(acc, siblings, methods:, def_nodes:, singleton_def_nodes:, method_visibilities: {})
|
|
8100
|
+
target = acc[:siblings]
|
|
8101
|
+
siblings.each do |name, table|
|
|
8102
|
+
target[name] =
|
|
8103
|
+
case name
|
|
8104
|
+
when :possible_discovered_methods
|
|
8105
|
+
fold_possible_methods(target.fetch(name), acc[:methods], table, methods)
|
|
8106
|
+
when :contested_discovered_def_nodes then fold_slot_contests(target.fetch(name), def_nodes, table)
|
|
8107
|
+
when :contested_discovered_singleton_def_nodes
|
|
8108
|
+
fold_slot_contests(target.fetch(name), singleton_def_nodes, table)
|
|
8109
|
+
when :contested_discovered_method_visibilities
|
|
8110
|
+
fold_slot_contests(target.fetch(name), method_visibilities, table)
|
|
8111
|
+
else union_sibling_value(target.fetch(name), table)
|
|
8112
|
+
end
|
|
8113
|
+
end
|
|
8114
|
+
end
|
|
8115
|
+
|
|
8116
|
+
# ADR-119 WD3 — the `possible_discovered_methods` fold, per name and side: `P' = (P − C_f) ∪ (P_f − C)`,
|
|
8117
|
+
# where `C = member − P` is what the accumulator (or seed) answers certainly and `C_f = member_f − P_f` what
|
|
8118
|
+
# the file does. A side some contribution supplies certainly is never possible, whatever the order the files
|
|
8119
|
+
# fold in. Returns `possible` itself when nothing changes; otherwise a copy (the rows are copied once).
|
|
8120
|
+
def fold_possible_methods(possible, members, file_possible, file_members)
|
|
8121
|
+
result = strip_file_certain(possible, file_possible, file_members)
|
|
8122
|
+
add_file_possible(result, possible, members, file_possible)
|
|
8123
|
+
end
|
|
8124
|
+
|
|
8125
|
+
# `P − C_f`: `possible` less the sides the file answers certainly; `possible` itself when nothing goes.
|
|
8126
|
+
def strip_file_certain(possible, file_possible, file_members)
|
|
8127
|
+
result = possible
|
|
8128
|
+
possible.each do |class_name, row|
|
|
8129
|
+
file_row = file_members[class_name]
|
|
8130
|
+
next if file_row.nil?
|
|
8131
|
+
|
|
8132
|
+
row.each do |method_name, kind|
|
|
8133
|
+
certain = kind_minus(file_row[method_name], file_possible.dig(class_name, method_name))
|
|
8134
|
+
remaining = kind_minus(kind, certain)
|
|
8135
|
+
next if remaining == kind
|
|
8136
|
+
|
|
8137
|
+
result = possible.transform_values(&:dup) if result.equal?(possible)
|
|
8138
|
+
remaining.nil? ? result[class_name].delete(method_name) : (result[class_name][method_name] = remaining)
|
|
8139
|
+
end
|
|
8140
|
+
result.delete(class_name) if result[class_name].empty?
|
|
8141
|
+
end
|
|
8142
|
+
result
|
|
8143
|
+
end
|
|
8144
|
+
|
|
8145
|
+
# `∪ (P_f − C)`: the file's possible sides the accumulator (`members` less `possible`) does not answer
|
|
8146
|
+
# certainly. Copies `result` first when it is still `possible`.
|
|
8147
|
+
def add_file_possible(result, possible, members, file_possible)
|
|
8148
|
+
file_possible.each do |class_name, row|
|
|
8149
|
+
row.each do |method_name, kind|
|
|
8150
|
+
added = kind_minus(kind, kind_minus(members.dig(class_name, method_name),
|
|
8151
|
+
possible.dig(class_name, method_name)))
|
|
8152
|
+
next if added.nil?
|
|
8153
|
+
|
|
8154
|
+
merged = merge_sibling_kind(result.dig(class_name, method_name), added)
|
|
8155
|
+
next if merged == result.dig(class_name, method_name)
|
|
8156
|
+
|
|
8157
|
+
result = possible.transform_values(&:dup) if result.equal?(possible)
|
|
8158
|
+
(result[class_name] ||= {})[method_name] = merged
|
|
8159
|
+
end
|
|
8160
|
+
end
|
|
8161
|
+
result
|
|
8162
|
+
end
|
|
8163
|
+
|
|
8164
|
+
# The sides of `kind` that `removed` does not cover (nil when none); either may be nil.
|
|
8165
|
+
def kind_minus(kind, removed)
|
|
8166
|
+
return kind if removed.nil? || kind.nil?
|
|
8167
|
+
return nil if removed == Scope::DiscoveryIndex::METHOD_KIND_BOTH || removed == kind
|
|
8168
|
+
|
|
8169
|
+
kind == Scope::DiscoveryIndex::METHOD_KIND_BOTH ? without_kind(kind, removed) : kind
|
|
8170
|
+
end
|
|
8171
|
+
|
|
8172
|
+
# ADR-119 WD3 — a single-valued slot's contests after a later-wins fold: the slots the file writes take the
|
|
8173
|
+
# file's contests (`file_contested`), every other key keeps the accumulator's (`sitting`). `file_slots` is the
|
|
8174
|
+
# file's `{class => {name => value}}` table. Returns `sitting` itself when nothing changes.
|
|
8175
|
+
def fold_slot_contests(sitting, file_slots, file_contested)
|
|
8176
|
+
stale = sitting.empty? ? nil : sitting.select { |class_name, name| file_slots[class_name]&.key?(name) }
|
|
8177
|
+
return sitting if (stale.nil? || stale.empty?) && file_contested.empty?
|
|
8178
|
+
|
|
8179
|
+
result = stale.nil? || stale.empty? ? sitting.dup : sitting - stale
|
|
8180
|
+
result.merge(file_contested)
|
|
8181
|
+
end
|
|
8182
|
+
|
|
8183
|
+
def union_paths(sitting, arriving)
|
|
8184
|
+
return sitting if arriving.empty?
|
|
8185
|
+
return arriving if sitting.empty?
|
|
8186
|
+
|
|
8187
|
+
sitting | arriving
|
|
8188
|
+
end
|
|
8189
|
+
|
|
8190
|
+
# The union of two sibling tables or entries. A Hash merges per key and a Set or Array unions; two scalars
|
|
8191
|
+
# that disagree are a `discovered_methods`-style kind and combine to `:both`, as {#merge_method_kinds} does.
|
|
8192
|
+
def union_sibling_value(sitting, arriving)
|
|
8193
|
+
return arriving if sitting.nil? || sitting.empty?
|
|
8194
|
+
return sitting if arriving.nil? || arriving.empty?
|
|
8195
|
+
|
|
8196
|
+
case sitting
|
|
8197
|
+
when Hash then sitting.merge(arriving) { |_key, left, right| union_sibling_value(left, right) }
|
|
8198
|
+
when Set, Array then sitting | arriving
|
|
8199
|
+
else sitting == arriving ? sitting : Scope::DiscoveryIndex::METHOD_KIND_BOTH
|
|
8200
|
+
end
|
|
8201
|
+
end
|
|
8202
|
+
|
|
8203
|
+
# The sibling tables keyed by file path rather than by class name, which a compact-header rename leaves alone
|
|
8204
|
+
# exactly as it leaves the `deferred_ranges` member alone.
|
|
8205
|
+
SIBLINGS_KEYED_BY_PATH = %i[possible_discovered_deferred_ranges].freeze
|
|
8206
|
+
private_constant :SIBLINGS_KEYED_BY_PATH
|
|
8207
|
+
|
|
8208
|
+
# {#apply_compact_header_renames!} for the siblings: a class-keyed Hash re-keys (colliding keys union), and a
|
|
8209
|
+
# Set of key paths renames each path's first element, the class.
|
|
8210
|
+
def rename_siblings(siblings, renames)
|
|
8211
|
+
siblings.to_h do |name, table|
|
|
8212
|
+
next [name, table] if SIBLINGS_KEYED_BY_PATH.include?(name) || table.empty?
|
|
8213
|
+
|
|
8214
|
+
renamed = case table
|
|
8215
|
+
when Set then table.to_set { |path| [rename_compact_name(renames, path.first), *path.drop(1)] }
|
|
8216
|
+
else rename_sibling_hash(table, renames)
|
|
8217
|
+
end
|
|
8218
|
+
[name, renamed]
|
|
8219
|
+
end
|
|
8220
|
+
end
|
|
8221
|
+
|
|
8222
|
+
def rename_sibling_hash(table, renames)
|
|
8223
|
+
table.each_with_object({}) do |(class_name, entry), out|
|
|
8224
|
+
renamed = rename_compact_name(renames, class_name)
|
|
8225
|
+
out[renamed] = out.key?(renamed) ? union_sibling_value(out[renamed], entry) : entry
|
|
8226
|
+
end
|
|
8227
|
+
end
|
|
8228
|
+
|
|
8229
|
+
def freeze_siblings(siblings)
|
|
8230
|
+
siblings.each_value { |table| freeze_sibling_table(table) }
|
|
8231
|
+
siblings.freeze
|
|
8232
|
+
end
|
|
8233
|
+
|
|
8234
|
+
def freeze_sibling_table(table)
|
|
8235
|
+
table.each_value { |entry| freeze_sibling_table(entry) } if table.is_a?(Hash)
|
|
8236
|
+
table.each { |entry| entry.freeze if entry.is_a?(Array) } if table.is_a?(Set)
|
|
8237
|
+
table.freeze
|
|
8238
|
+
end
|
|
8239
|
+
|
|
7193
8240
|
# Issue #1120 — a union, so the fold is order-independent and a bundle-served file folds exactly as its live
|
|
7194
8241
|
# walk would. The slot stays nil until some file refines something (see {#finalize_def_index}).
|
|
7195
8242
|
def fold_refinements(acc, file_refinements)
|
|
@@ -7264,6 +8311,7 @@ module Rigor
|
|
|
7264
8311
|
# resolves the class as it did before the ordering fix, which is this table's empty state.
|
|
7265
8312
|
accumulate_prepend_lists(acc[:prepends], file_index[:prepends] || {})
|
|
7266
8313
|
accumulate_extend_lists(acc[:extends], file_index[:extends] || {})
|
|
8314
|
+
accumulate_unpositioned(acc[:unpositioned_mixins], file_index[:unpositioned_mixins] || {})
|
|
7267
8315
|
end
|
|
7268
8316
|
|
|
7269
8317
|
# The `extends` half stores singleton-ancestor search order ({#record_extend_targets}), so a file
|
|
@@ -7288,14 +8336,14 @@ module Rigor
|
|
|
7288
8336
|
# class. Shared with {#accumulate_extend_lists}' shape deliberately — the two tables answer the
|
|
7289
8337
|
# `prepend` question on the two sides of the class object, and both fold nearest-first.
|
|
7290
8338
|
def accumulate_prepend_lists(target, additions)
|
|
7291
|
-
additions.each { |cn, mods| target[cn] = (mods + (target[cn] || []))
|
|
8339
|
+
additions.each { |cn, mods| target[cn] = (mods + (target[cn] || [])) }
|
|
7292
8340
|
end
|
|
7293
8341
|
|
|
7294
8342
|
# ADR-85 WD2 — converts a file's live single-file index + its class table into a Marshal-clean seed
|
|
7295
8343
|
# bundle: the plain-data tables verbatim, the def-node tables re-expressed as `[node_id, name,
|
|
7296
8344
|
# fingerprint]` triples (the path is the bundle key), the class-source names (path implicit), and the
|
|
7297
8345
|
# content digest that gates the bundle's reuse.
|
|
7298
|
-
def build_seed_bundle(file_index, file_classes, digest, code_fingerprint)
|
|
8346
|
+
def build_seed_bundle(file_index, file_classes, digest, code_fingerprint) # rubocop:disable Metrics/AbcSize -- one read per bundle slot
|
|
7299
8347
|
{
|
|
7300
8348
|
digest: digest,
|
|
7301
8349
|
# B1 — the comment-stripped code fingerprint, so a recheck can prove this file's edit was
|
|
@@ -7324,6 +8372,9 @@ module Rigor
|
|
|
7324
8372
|
# the bundle stays Marshal-clean and a warm incremental file orders its prepends the way a cold
|
|
7325
8373
|
# walk of it does.
|
|
7326
8374
|
prepends: file_index[:prepends],
|
|
8375
|
+
# The mixin edges whose order is not a fact — plain `{class name => Array[String]}` data, so the
|
|
8376
|
+
# bundle stays Marshal-clean and a warm file flags the edges its cold walk flags.
|
|
8377
|
+
unpositioned_mixins: file_index[:unpositioned_mixins],
|
|
7327
8378
|
method_visibilities: file_index[:method_visibilities],
|
|
7328
8379
|
methods: file_index[:methods],
|
|
7329
8380
|
# Issue #992 — plain `{class name => {[kind, name] => [min, max, required_keywords] | :opaque}}`
|
|
@@ -7345,14 +8396,16 @@ module Rigor
|
|
|
7345
8396
|
# nothing), so the bundle stays Marshal-clean.
|
|
7346
8397
|
refinements: file_index[:refinements],
|
|
7347
8398
|
# Issue #1367 — the file's {GlobalWriteCensus}, a Set of frozen Arrays, which Marshal round-trips.
|
|
7348
|
-
global_write_census: file_index[:global_write_census]
|
|
8399
|
+
global_write_census: file_index[:global_write_census],
|
|
8400
|
+
# ADR-119 WD1 — `{sibling name => table}`, plain Hashes, Sets and rows, so the bundle stays Marshal-clean.
|
|
8401
|
+
siblings: file_index[:siblings]
|
|
7349
8402
|
}
|
|
7350
8403
|
end
|
|
7351
8404
|
|
|
7352
8405
|
# ADR-85 WD2 — reconstitutes a cached bundle into a single-file index {#fold_file_index} folds: the
|
|
7353
8406
|
# def-node triples become {DefHandle}s bound to this file's `path`, and the class-source names become a
|
|
7354
8407
|
# `{name => Set[path]}` table (the shape `accumulate_project_index` produces).
|
|
7355
|
-
def bundle_to_file_index(bundle, path)
|
|
8408
|
+
def bundle_to_file_index(bundle, path) # rubocop:disable Metrics/CyclomaticComplexity,Metrics/PerceivedComplexity -- one read per bundle slot
|
|
7356
8409
|
{
|
|
7357
8410
|
def_nodes: bundle_defs_to_handles(bundle[:def_nodes], path),
|
|
7358
8411
|
singleton_def_nodes: bundle_defs_to_handles(bundle[:singleton_def_nodes], path),
|
|
@@ -7368,6 +8421,8 @@ module Rigor
|
|
|
7368
8421
|
prepends: bundle[:prepends] || {},
|
|
7369
8422
|
# #526 — pre-extends bundles lack the key; default `{}` keeps the fold total.
|
|
7370
8423
|
extends: bundle[:extends] || {},
|
|
8424
|
+
# A pre-32 bundle lacks the key; the SCHEMA bump rebuilds it cold, and the fold defaults it anyway.
|
|
8425
|
+
unpositioned_mixins: bundle[:unpositioned_mixins],
|
|
7371
8426
|
method_visibilities: bundle[:method_visibilities],
|
|
7372
8427
|
methods: bundle[:methods],
|
|
7373
8428
|
parameter_envelopes: bundle[:parameter_envelopes] || {},
|
|
@@ -7384,7 +8439,9 @@ module Rigor
|
|
|
7384
8439
|
# Issue #1120 — absent from a pre-28 bundle, which the SCHEMA bump rebuilds cold.
|
|
7385
8440
|
refinements: bundle[:refinements],
|
|
7386
8441
|
# Issue #1367 — absent from a pre-29 bundle, which the SCHEMA bump rebuilds cold.
|
|
7387
|
-
global_write_census: bundle[:global_write_census] || GlobalWriteCensus::EMPTY
|
|
8442
|
+
global_write_census: bundle[:global_write_census] || GlobalWriteCensus::EMPTY,
|
|
8443
|
+
# ADR-119 WD1 — absent from a pre-35 bundle, which the SCHEMA bump rebuilds cold.
|
|
8444
|
+
siblings: bundle[:siblings] || Scope::DiscoveryIndex::EMPTY_SIBLINGS
|
|
7388
8445
|
}
|
|
7389
8446
|
end
|
|
7390
8447
|
|
|
@@ -7415,7 +8472,8 @@ module Rigor
|
|
|
7415
8472
|
def new_def_index_accumulator
|
|
7416
8473
|
{ def_nodes: {}, def_nestings: {}.compare_by_identity,
|
|
7417
8474
|
singleton_def_nodes: {}, def_sources: {}, singleton_def_sources: {},
|
|
7418
|
-
superclasses: {}, header_nestings: {}, includes: {}, prepends: {}, extends: {},
|
|
8475
|
+
superclasses: {}, header_nestings: {}, includes: {}, prepends: {}, extends: {}, unpositioned_mixins: {},
|
|
8476
|
+
method_visibilities: {},
|
|
7419
8477
|
methods: {},
|
|
7420
8478
|
deferred_ranges: {},
|
|
7421
8479
|
parameter_envelopes: {}, class_sources: {},
|
|
@@ -7424,7 +8482,9 @@ module Rigor
|
|
|
7424
8482
|
constant_writes: {},
|
|
7425
8483
|
data_member_layouts: {}, struct_member_layouts: {},
|
|
7426
8484
|
# Issue #1367 — the project's {GlobalWriteCensus}.
|
|
7427
|
-
global_write_census: Set.new
|
|
8485
|
+
global_write_census: Set.new,
|
|
8486
|
+
# ADR-119 WD1 — the pair siblings, `{sibling name => table}` (see {#fold_siblings}).
|
|
8487
|
+
siblings: Scope::DiscoveryIndex::EMPTY_SIBLINGS.dup }
|
|
7428
8488
|
end
|
|
7429
8489
|
|
|
7430
8490
|
# Post-processes and freezes a fully-folded def-index accumulator.
|
|
@@ -7437,27 +8497,42 @@ module Rigor
|
|
|
7437
8497
|
# Issue #644 — resolve the cross-file constant-reassignment rule here, where the whole project's
|
|
7438
8498
|
# write census is known, and turn the surviving literals into their published `Type::Constant`.
|
|
7439
8499
|
acc[:constant_values], acc[:constant_sources] = finalize_constant_writes(acc[:constant_writes])
|
|
7440
|
-
|
|
8500
|
+
# ADR-119 WD1 — the two passes below rewrite `def_nodes`, `singleton_def_nodes` and `methods`, and each carries
|
|
8501
|
+
# its sibling along. `subtract_def_methods` drops `def`-declared names from `methods`, and
|
|
8502
|
+
# {#subtract_sibling_methods!} applies the same drop to `possible_discovered_methods`. The extends fold copies
|
|
8503
|
+
# an extended module's instance defs onto the extender's singleton, and follows the siblings the same way (a
|
|
8504
|
+
# copy is possible or contested when its edge is listed in `unpositioned_mixins` or its source was).
|
|
8505
|
+
# Until a producer fills a sibling and an edge is listed, there is nothing for either pass to follow.
|
|
8506
|
+
fold_project_extends(acc)
|
|
7441
8507
|
# Cross-file method suppression is for the project's OWN accessors (attr_* / define_method / alias) — NOT for
|
|
7442
8508
|
# plain `def`s. A cross-file `def` on a class is exactly the ADR-17 monkey-patch case the undefined-method rule
|
|
7443
8509
|
# deliberately surfaces (fire + def-site annotation, nudging `pre_eval:`), so dropping the `def`-declared names
|
|
7444
8510
|
# keeps that contract intact while still letting `attr_reader :x` in one file suppress a false undefined-method
|
|
7445
8511
|
# for `obj.x` in another.
|
|
7446
8512
|
acc[:methods] = subtract_def_methods(acc[:methods], acc[:def_nodes])
|
|
8513
|
+
subtract_sibling_methods!(acc)
|
|
7447
8514
|
finalize_call_surface_tables(acc)
|
|
7448
|
-
%i[def_nodes singleton_def_nodes def_sources singleton_def_sources includes prepends
|
|
7449
|
-
methods parameter_envelopes class_sources constant_sources deferred_ranges].each do |key|
|
|
8515
|
+
%i[def_nodes singleton_def_nodes def_sources singleton_def_sources includes prepends
|
|
8516
|
+
method_visibilities methods parameter_envelopes class_sources constant_sources deferred_ranges].each do |key|
|
|
7450
8517
|
acc[key].each_value(&:freeze)
|
|
7451
8518
|
end
|
|
7452
8519
|
acc.transform_values(&:freeze)
|
|
7453
8520
|
end
|
|
7454
8521
|
|
|
8522
|
+
# The project-wide extends fold over the accumulator's tables and siblings.
|
|
8523
|
+
def fold_project_extends(acc)
|
|
8524
|
+
fold_extends_into_singleton_tables(acc[:extends], acc[:def_nodes], acc[:singleton_def_nodes], acc[:methods],
|
|
8525
|
+
siblings: acc[:siblings], unpositioned: acc[:unpositioned_mixins])
|
|
8526
|
+
end
|
|
8527
|
+
|
|
7455
8528
|
# The two whole-project tables only call rules read: the issue #992 envelope table gains its project-wide
|
|
7456
8529
|
# key, and the issue #1120 refinement table, nil until some file refines something, settles to a frozen one.
|
|
7457
8530
|
def finalize_call_surface_tables(acc)
|
|
7458
8531
|
acc[:parameter_envelopes][Scope::DiscoveryIndex::ENVELOPE_PROJECT_WIDE] ||= {}
|
|
7459
8532
|
acc[:refinements] = freeze_refinements(acc[:refinements])
|
|
7460
8533
|
acc[:global_write_census] = acc[:global_write_census].freeze
|
|
8534
|
+
acc[:unpositioned_mixins].each_value { |sides| sides.each_value(&:freeze).freeze }
|
|
8535
|
+
freeze_siblings(acc[:siblings])
|
|
7461
8536
|
end
|
|
7462
8537
|
|
|
7463
8538
|
# Removes, per class, the method names that have a project `def` node, leaving only
|
|
@@ -7478,6 +8553,30 @@ module Rigor
|
|
|
7478
8553
|
end
|
|
7479
8554
|
end
|
|
7480
8555
|
|
|
8556
|
+
# ADR-119 WD1 — the `possible` copy of `methods` loses the INSTANCE half of a name that has a project `def`
|
|
8557
|
+
# (`:instance` goes, `:both` becomes `:singleton`), so `possible_discovered_methods` stays a subset of its
|
|
8558
|
+
# member after the rule above. A `:singleton` entry stays: the `def` is the instance side, and the member
|
|
8559
|
+
# keeps the name's singleton half. Only that sibling is rewritten.
|
|
8560
|
+
def subtract_sibling_methods!(acc)
|
|
8561
|
+
possible = acc[:siblings][:possible_discovered_methods]
|
|
8562
|
+
return if possible.nil?
|
|
8563
|
+
|
|
8564
|
+
acc[:siblings] =
|
|
8565
|
+
acc[:siblings].merge(possible_discovered_methods: subtract_sibling_instance_defs(possible, acc[:def_nodes]))
|
|
8566
|
+
end
|
|
8567
|
+
|
|
8568
|
+
def subtract_sibling_instance_defs(possible, def_nodes)
|
|
8569
|
+
possible.each_with_object({}) do |(class_name, table), out|
|
|
8570
|
+
defs = def_nodes[class_name] || {}
|
|
8571
|
+
kept = table.each_with_object({}) do |(method_name, kind), acc|
|
|
8572
|
+
next acc[method_name] = kind unless defs.key?(method_name)
|
|
8573
|
+
|
|
8574
|
+
acc[method_name] = :singleton unless kind == :instance
|
|
8575
|
+
end
|
|
8576
|
+
out[class_name] = kept unless kept.empty?
|
|
8577
|
+
end
|
|
8578
|
+
end
|
|
8579
|
+
|
|
7481
8580
|
# Folds one file's class-keyed indexes into the cross-file accumulator. `method_visibilities` (ADR-35) is
|
|
7482
8581
|
# collected here so the override-visibility-reduced rule can read an ancestor's visibility declared in a sibling
|
|
7483
8582
|
# file.
|
|
@@ -7485,26 +8584,43 @@ module Rigor
|
|
|
7485
8584
|
# One combined descent yields both the methods existence table and the def-node table; the latter is also
|
|
7486
8585
|
# consumed by `record_class_sources`, so a def-dense file is walked once here instead of three times (methods +
|
|
7487
8586
|
# def-nodes ×2). See {#build_methods_and_def_nodes}.
|
|
7488
|
-
file_methods, file_def_nodes, file_envelopes, file_refinements
|
|
8587
|
+
file_methods, file_def_nodes, file_envelopes, file_refinements, file_singleton_def_nodes, certainty =
|
|
8588
|
+
walk_def_contributions(acc, path, root)
|
|
7489
8589
|
merge_discovered_defs(acc[:def_nodes], acc[:def_sources], path, file_def_nodes)
|
|
7490
8590
|
fold_parameter_envelopes(acc, file_envelopes)
|
|
7491
8591
|
fold_refinements(acc, file_refinements)
|
|
7492
8592
|
# Issue #681 — node-identity keyed, so this is a flat union: no two files can contribute the same key.
|
|
7493
|
-
|
|
8593
|
+
walked = declaration_walk_tables(root, path)
|
|
8594
|
+
acc[:def_nestings].merge!(walked.fetch(:def_nestings))
|
|
7494
8595
|
# ADR-46 slice 4 (singleton) — record the singleton-side `"path:line"` sources alongside the nodes,
|
|
7495
8596
|
# the exact mirror of the instance-side `merge_discovered_defs`, so a class/singleton-method body edit
|
|
7496
8597
|
# produces a changed `"Class.method"` fingerprint pair (and its call sites a symbol edge) instead of
|
|
7497
8598
|
# silently degrading to the file's full ancestry closure.
|
|
7498
8599
|
merge_discovered_defs(acc[:singleton_def_nodes], acc[:singleton_def_sources], path,
|
|
7499
|
-
|
|
7500
|
-
superclasses
|
|
8600
|
+
file_singleton_def_nodes)
|
|
8601
|
+
superclasses = walked.fetch(:superclasses)
|
|
7501
8602
|
acc[:superclasses].merge!(superclasses)
|
|
7502
|
-
merge_header_nestings(acc[:header_nestings], header_nestings)
|
|
8603
|
+
merge_header_nestings(acc[:header_nestings], walked.fetch(:header_nestings))
|
|
7503
8604
|
ancestry_keys = fold_file_mixin_tables(acc, root)
|
|
7504
8605
|
record_file_positions(acc, path, root, superclasses, ancestry_keys, file_def_nodes)
|
|
7505
8606
|
merge_constant_literal_tables(acc, root, path)
|
|
7506
|
-
merge_class_keyed_index_tables(acc, root, file_methods)
|
|
7507
|
-
merge_member_layout_tables(acc,
|
|
8607
|
+
merge_class_keyed_index_tables(acc, root, file_methods, certainty)
|
|
8608
|
+
merge_member_layout_tables(acc, walked)
|
|
8609
|
+
end
|
|
8610
|
+
|
|
8611
|
+
# The two def-contribution walks of {#accumulate_project_index}, over ONE {Certainty} classification, with the
|
|
8612
|
+
# file's siblings folded into the accumulator BEFORE its members join it (ADR-119 WD3: the `possible` rule
|
|
8613
|
+
# reads the accumulator's member as it stood). Returns `[methods, def_nodes, envelopes, refinements,
|
|
8614
|
+
# singleton_def_nodes, certainty]`, the classification passed on for the visibility walk.
|
|
8615
|
+
def walk_def_contributions(acc, path, root)
|
|
8616
|
+
certainty = Certainty.possible_nodes(root)
|
|
8617
|
+
file_methods, file_def_nodes, file_envelopes, file_refinements, file_siblings =
|
|
8618
|
+
build_methods_and_def_nodes(root, path, certainty: certainty)
|
|
8619
|
+
file_singleton_def_nodes, file_singleton_contested = build_singleton_def_tables(root, certainty: certainty)
|
|
8620
|
+
fold_file_siblings(acc, file_siblings.merge(contested_discovered_singleton_def_nodes: file_singleton_contested),
|
|
8621
|
+
methods: file_methods, def_nodes: file_def_nodes,
|
|
8622
|
+
singleton_def_nodes: file_singleton_def_nodes)
|
|
8623
|
+
[file_methods, file_def_nodes, file_envelopes, file_refinements, file_singleton_def_nodes, certainty]
|
|
7508
8624
|
end
|
|
7509
8625
|
|
|
7510
8626
|
# Issue #1123 — this file's three instance- / singleton-side module lists, folded into the
|
|
@@ -7516,12 +8632,24 @@ module Rigor
|
|
|
7516
8632
|
# {#accumulate_project_index} to hold its ABC budget.
|
|
7517
8633
|
def fold_file_mixin_tables(acc, root)
|
|
7518
8634
|
mixin = mixin_tables(root)
|
|
8635
|
+
extend_side = extend_tables(root)
|
|
7519
8636
|
accumulate_include_lists(acc[:includes], mixin[:includes])
|
|
7520
8637
|
accumulate_prepend_lists(acc[:prepends], mixin[:prepends])
|
|
7521
|
-
accumulate_extend_lists(acc[:extends],
|
|
8638
|
+
accumulate_extend_lists(acc[:extends], extend_side[:extends])
|
|
8639
|
+
accumulate_unpositioned(acc[:unpositioned_mixins], mixin[:unpositioned])
|
|
8640
|
+
accumulate_unpositioned(acc[:unpositioned_mixins], extend_side[:unpositioned])
|
|
7522
8641
|
mixin[:includes].merge(mixin[:prepends]) { |_cn, included_mods, _prepends| included_mods }
|
|
7523
8642
|
end
|
|
7524
8643
|
|
|
8644
|
+
# {Scope::DiscoveryIndex#unpositioned_mixins} folds by union, so the fold is order-independent and a
|
|
8645
|
+
# bundle-served file folds exactly as its live walk would.
|
|
8646
|
+
def accumulate_unpositioned(target, additions)
|
|
8647
|
+
additions.each do |class_name, sides|
|
|
8648
|
+
sitting = (target[class_name] ||= {})
|
|
8649
|
+
sides.each { |side, names| sitting[side] = sitting.key?(side) ? (sitting[side] | names) : names.dup }
|
|
8650
|
+
end
|
|
8651
|
+
end
|
|
8652
|
+
|
|
7525
8653
|
# Issue #644 — folds one file's publication census into the cross-file accumulator, keyed by
|
|
7526
8654
|
# (name, path) (kept out of {#accumulate_project_index} to hold its ABC budget). The conflict rule that
|
|
7527
8655
|
# makes the fold order irrelevant runs at {#finalize_constant_writes}. Issue #1367 — the same walk carries the
|
|
@@ -7536,16 +8664,24 @@ module Rigor
|
|
|
7536
8664
|
|
|
7537
8665
|
# Folds one file's Data + Struct member-layout tables into the cross-file accumulator (kept out of
|
|
7538
8666
|
# {#accumulate_project_index} to hold its ABC budget).
|
|
7539
|
-
def merge_member_layout_tables(acc,
|
|
7540
|
-
acc[:data_member_layouts].merge!(
|
|
7541
|
-
acc[:struct_member_layouts].merge!(
|
|
8667
|
+
def merge_member_layout_tables(acc, walked)
|
|
8668
|
+
acc[:data_member_layouts].merge!(walked.fetch(:data_member_layouts))
|
|
8669
|
+
acc[:struct_member_layouts].merge!(walked.fetch(:struct_member_layouts))
|
|
7542
8670
|
end
|
|
7543
8671
|
|
|
7544
8672
|
# Folds the per-class method-visibility and method-existence tables of one file into the cross-file accumulator
|
|
7545
8673
|
# (kept out of {#accumulate_project_index} to hold its ABC budget). `file_methods` is the existence table from the
|
|
7546
8674
|
# combined methods/def-nodes descent.
|
|
7547
|
-
|
|
7548
|
-
|
|
8675
|
+
#
|
|
8676
|
+
# ADR-119 WD3 — the visibility slots fold later-wins, so their contests follow the file that wrote them
|
|
8677
|
+
# ({#fold_slot_contests}), as the def-node slots' do.
|
|
8678
|
+
def merge_class_keyed_index_tables(acc, root, file_methods, certainty)
|
|
8679
|
+
file_visibilities, file_contested = build_method_visibility_tables(root, certainty: certainty)
|
|
8680
|
+
siblings = acc[:siblings]
|
|
8681
|
+
siblings[:contested_discovered_method_visibilities] = fold_slot_contests(
|
|
8682
|
+
siblings[:contested_discovered_method_visibilities], file_visibilities, file_contested
|
|
8683
|
+
)
|
|
8684
|
+
file_visibilities.each do |class_name, table|
|
|
7549
8685
|
(acc[:method_visibilities][class_name] ||= {}).merge!(table)
|
|
7550
8686
|
end
|
|
7551
8687
|
file_methods.each do |class_name, table|
|
|
@@ -7657,8 +8793,7 @@ module Rigor
|
|
|
7657
8793
|
tables.write_census.visit(node, top_level: qualified_prefix.empty?)
|
|
7658
8794
|
end
|
|
7659
8795
|
|
|
7660
|
-
rebound = rebound_block_self(node, qualified_prefix, nil, meta_owner,
|
|
7661
|
-
rebound_self_base(self_owner))
|
|
8796
|
+
rebound = rebound_block_self(node, qualified_prefix, nil, meta_owner, self_owner)
|
|
7662
8797
|
# A `ConstantWriteNode`'s only child is its rvalue, so this reaches exactly the call whose block the
|
|
7663
8798
|
# constant names — and nil everywhere else, leaving every other descent as it was.
|
|
7664
8799
|
#
|
|
@@ -8181,6 +9316,16 @@ module Rigor
|
|
|
8181
9316
|
end
|
|
8182
9317
|
end
|
|
8183
9318
|
|
|
9319
|
+
# {#rekey_class_table} for `unpositioned_mixins`: two bodies landing on one key keep BOTH sides' names,
|
|
9320
|
+
# where the Hash arm of {#combine_rekeyed_entries} would replace one side's list with the other's.
|
|
9321
|
+
def rekey_unpositioned(table, renames)
|
|
9322
|
+
table.each_with_object({}) do |(name, sides), out|
|
|
9323
|
+
out.merge!(rename_compact_name(renames, name) => sides) do |_key, sitting, arriving|
|
|
9324
|
+
sitting.merge(arriving) { |_side, a, b| a | b }
|
|
9325
|
+
end
|
|
9326
|
+
end
|
|
9327
|
+
end
|
|
9328
|
+
|
|
8184
9329
|
def rekey_parameter_envelopes(table, renames)
|
|
8185
9330
|
table.each_with_object({}) do |(name, entries), out|
|
|
8186
9331
|
out.merge!(rename_compact_name(renames, name) => entries) do |_key, sitting, arriving|
|
|
@@ -8221,7 +9366,7 @@ module Rigor
|
|
|
8221
9366
|
# definition — the pre-#728 answer for a name no site recorded — so it keeps unioning. A KEYED entry is
|
|
8222
9367
|
# the cref of the site that wrote that exact name, and the two sides are two different sites, so
|
|
8223
9368
|
# unioning their chains would hand each site's ancestor the other's namespace: `Wrap::Mixin` sorts
|
|
8224
|
-
# ahead of `::Mixin`, and
|
|
9369
|
+
# ahead of `::Mixin`, and the ancestor-name resolution takes the first known class as the sole
|
|
8225
9370
|
# resolution, so the top-level site's `include Mixin` silently became the `Wrap` one. That is not just
|
|
8226
9371
|
# a wrong class, it is a FALSE POSITIVE source — the two modules' same-named methods can differ in
|
|
8227
9372
|
# arity, and `call.wrong-arity` then fires on a correct program.
|
|
@@ -8309,6 +9454,8 @@ module Rigor
|
|
|
8309
9454
|
struct_member_layouts constant_writes].each do |key|
|
|
8310
9455
|
acc[key] = rekey_class_table(acc[key], renames)
|
|
8311
9456
|
end
|
|
9457
|
+
acc[:unpositioned_mixins] = rekey_unpositioned(acc[:unpositioned_mixins], renames)
|
|
9458
|
+
acc[:siblings] = rename_siblings(acc[:siblings], renames)
|
|
8312
9459
|
# Issue #992 — the envelope table cannot take {#combine_rekeyed_entries}' later-wins Hash merge: two
|
|
8313
9460
|
# bodies of one class landing on the same key are exactly the reopening whose disagreement must
|
|
8314
9461
|
# make a name opaque.
|
|
@@ -8486,10 +9633,11 @@ module Rigor
|
|
|
8486
9633
|
Source::ConstantPath.declaration_prefix(qualified_prefix, node.constant_path)
|
|
8487
9634
|
return false unless child_prefix
|
|
8488
9635
|
|
|
8489
|
-
if singleton_cref && !decl_nameable_under_cref?(node) && !self_decl&.any?
|
|
9636
|
+
if self_decl&.empty? || (singleton_cref && !decl_nameable_under_cref?(node) && !self_decl&.any?)
|
|
8490
9637
|
# `class D` below `class <<` opens `#<singleton>::D` — a real class nothing can
|
|
8491
9638
|
# name — so nothing is registered; the body still walks so nameable headers
|
|
8492
|
-
# below it re-anchor at a real cref.
|
|
9639
|
+
# below it re-anchor at a real cref. The same holds for `class self::D` where
|
|
9640
|
+
# `self` is opaque (`REGISTRY.first.class_eval { … }`): `self_decl` is `[]`.
|
|
8493
9641
|
if node.body
|
|
8494
9642
|
record_declarations(node.body, [], identity_table, discovered, renames,
|
|
8495
9643
|
nil, singleton_cref: true)
|