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