rigortype 0.4.1 → 0.4.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/README.md +1 -1
- 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/11-sig-gen.md +6 -0
- data/docs/handbook/appendix-liskov.md +5 -0
- data/docs/manual/03-configuration.md +1 -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/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 +21 -2
- data/lib/rigor/analysis/check_rules.rb +289 -57
- data/lib/rigor/analysis/dependency_recorder.rb +15 -0
- 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 +30 -5
- data/lib/rigor/analysis/incremental_run_slot.rb +3 -1
- data/lib/rigor/analysis/incremental_session.rb +14 -1
- data/lib/rigor/analysis/reachability/graph.rb +123 -23
- data/lib/rigor/analysis/reachability/scan.rb +290 -36
- data/lib/rigor/analysis/rule_catalog.rb +43 -0
- data/lib/rigor/analysis/run_cache_key.rb +16 -1
- data/lib/rigor/analysis/runner/diagnostic_aggregator.rb +87 -5
- data/lib/rigor/analysis/runner/pool_coordinator.rb +50 -11
- data/lib/rigor/analysis/runner/run_snapshots.rb +6 -1
- data/lib/rigor/analysis/runner.rb +34 -3
- data/lib/rigor/analysis/worker_session.rb +7 -4
- data/lib/rigor/cache/incremental_snapshot.rb +22 -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 +2 -1
- 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 +13 -7
- data/lib/rigor/cli.rb +10 -2
- data/lib/rigor/configuration/severity_profile.rb +6 -0
- data/lib/rigor/configuration.rb +46 -2
- data/lib/rigor/environment/gated_signature_guard.rb +43 -0
- data/lib/rigor/environment/gated_signature_plan.rb +339 -0
- data/lib/rigor/environment/rbs_coverage_report.rb +1 -1
- data/lib/rigor/environment/rbs_loader.rb +283 -14
- data/lib/rigor/environment/required_features.rb +206 -0
- data/lib/rigor/environment.rb +21 -3
- data/lib/rigor/inference/acceptance.rb +28 -3
- data/lib/rigor/inference/builtins/cruby_definers.rb +61 -0
- data/lib/rigor/inference/builtins/method_catalog.rb +3 -0
- data/lib/rigor/inference/def_node_resolver.rb +23 -1
- data/lib/rigor/inference/expression_typer.rb +215 -16
- data/lib/rigor/inference/global_write_census.rb +7 -4
- data/lib/rigor/inference/guard_rebinding.rb +1 -1
- 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 +59 -12
- 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 +259 -28
- 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 +2 -1
- data/lib/rigor/inference/narrowing.rb +20 -6
- data/lib/rigor/inference/object_mixins.rb +180 -0
- 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/mixin_accumulator.rb +13 -0
- data/lib/rigor/inference/scope_indexer.rb +687 -60
- data/lib/rigor/inference/statement_evaluator.rb +71 -20
- 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/inflector.rb +1 -1
- data/lib/rigor/plugin/io_boundary.rb +42 -3
- data/lib/rigor/plugin/macro/block_as_method.rb +51 -8
- data/lib/rigor/project_environment.rb +29 -6
- data/lib/rigor/protection/mutation_cache.rb +1 -1
- data/lib/rigor/protection/mutator.rb +1 -1
- data/lib/rigor/reflection/constant_ancestors.rb +127 -11
- data/lib/rigor/reflection/constant_path.rb +13 -2
- data/lib/rigor/reflection.rb +40 -8
- data/lib/rigor/scope/discovery_index.rb +40 -1
- data/lib/rigor/scope/resolution_chain.rb +159 -11
- data/lib/rigor/scope.rb +189 -10
- data/lib/rigor/sig_gen/classification.rb +4 -1
- data/lib/rigor/sig_gen/generator.rb +55 -5
- 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 +34 -2
- 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 +7 -6
- 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/plugin/io_boundary.rbs +3 -0
- data/sig/rigor/scope.rbs +27 -4
- data/sig/rigor/source.rbs +4 -0
- data/sig/rigor.rbs +3 -1
- metadata +66 -1
|
@@ -0,0 +1,403 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "../builtins/cruby_definers"
|
|
4
|
+
|
|
5
|
+
module Rigor
|
|
6
|
+
module Inference
|
|
7
|
+
module RefinedDispatch
|
|
8
|
+
# Issue #1740 — does the receiver's own lookup answer a call ahead of every refinement in effect, so that
|
|
9
|
+
# `call.wrong-arity` and `call.argument-type-mismatch` may check it against the receiver's own method? Ruby
|
|
10
|
+
# finds a method the receiver's class (or a nearer ancestor) defines before it consults an ancestor's
|
|
11
|
+
# refinement: under `refine Object do def center(a, b, c)`, `"x".center(1, 2, 3)` still runs `String#center`
|
|
12
|
+
# and raises.
|
|
13
|
+
#
|
|
14
|
+
# It answers yes only from positive knowledge (the caller has already declined on an in-effect list that
|
|
15
|
+
# carries {InEffectRefinements::UNKNOWN} or an opaque module), and every condition it cannot prove is a no:
|
|
16
|
+
#
|
|
17
|
+
# - it reads the RAW refinement table, every name a `refine` argument's spelling can denote and the wildcard
|
|
18
|
+
# rows included, never {RefinedDispatch.resolved}'s typing subset: a decline needs only that a row may
|
|
19
|
+
# denote a class on the walk;
|
|
20
|
+
# - no in-effect row may name a class the walk cannot name, a project subclass of the receiver's class (a
|
|
21
|
+
# value typed as the class may be one), or a module the receiver's levels do not hold (`obj.extend(M)`,
|
|
22
|
+
# or a subclass's mixin);
|
|
23
|
+
# - walking {RefinedDispatch.levels} from the most derived, the first level or entry that is refined or
|
|
24
|
+
# defines the name must define it, with no `mixed` level (a project mixin RBS does not place) before it;
|
|
25
|
+
# - that definer must be proven ({.proven_definer?}): RBS alone is not proof.
|
|
26
|
+
#
|
|
27
|
+
# It shares the level walk with the typed arm's {RefinedDispatch.winner} and nothing else: the typer types
|
|
28
|
+
# only from rows the lexical lookup resolves, while a decline must count every row.
|
|
29
|
+
module OwnMethod # rubocop:disable Metrics/ModuleLength
|
|
30
|
+
# One candidate definer as `DefinerResolution`'s per-chain answer function reports it, read through the
|
|
31
|
+
# same four readers as the module's own hit (a call site may not name that one).
|
|
32
|
+
DefinerHit = Data.define(:answer, :owner, :index, :side)
|
|
33
|
+
EMPTY = [].freeze
|
|
34
|
+
private_constant :DefinerHit, :EMPTY
|
|
35
|
+
|
|
36
|
+
module_function
|
|
37
|
+
|
|
38
|
+
# Is `method_name` on an instance of `class_name` answered by the receiver's own method, ahead of every
|
|
39
|
+
# refinement the in-effect list `list` (no unknown marker, no opaque module) puts in effect?
|
|
40
|
+
def answers?(scope, class_name, method_name, list)
|
|
41
|
+
InEffectRefinements.record_refinement_names(method_name)
|
|
42
|
+
raw = raw_targets(scope.discovered_refinements, method_name, list)
|
|
43
|
+
return true if raw.nil?
|
|
44
|
+
|
|
45
|
+
add_ancestor_spellings(scope, raw)
|
|
46
|
+
|
|
47
|
+
record_targets(scope, raw)
|
|
48
|
+
return false if raw.each_key.any? { |refined| InEffectRefinements.class_wildcard_key?(scope, refined) }
|
|
49
|
+
|
|
50
|
+
levels = RefinedDispatch.levels(scope, class_name)
|
|
51
|
+
return false if levels.nil?
|
|
52
|
+
return false if refined_off_levels?(scope, class_name, levels, raw)
|
|
53
|
+
|
|
54
|
+
shadowing_definer?(scope, class_name, levels, method_name, raw)
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
# `{refined-class key => [its in-effect refining modules]}` for every key with a row for `method_name` or a
|
|
58
|
+
# names-wildcard row that `list` puts in effect, by every spelling the table records, or nil when there is
|
|
59
|
+
# none.
|
|
60
|
+
def raw_targets(refinements, method_name, list)
|
|
61
|
+
out = nil
|
|
62
|
+
refinements.each do |refined, methods|
|
|
63
|
+
modules = in_effect_modules(methods[method_name], list) |
|
|
64
|
+
in_effect_modules(methods[RefinedDispatch.wildcard], list)
|
|
65
|
+
(out ||= {})[refined] = modules unless modules.empty?
|
|
66
|
+
end
|
|
67
|
+
out
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
# Ruby looks a constant up through the refining module's ancestors before the top level, so `refine(Bar)` in
|
|
71
|
+
# a module that includes `Foo` refines `Foo::Bar` when `Foo` holds one; the table records the lexical
|
|
72
|
+
# spellings only (`R::Bar`, `Bar`). Each declared `<ancestor>::<last segment>` joins the raw targets for the
|
|
73
|
+
# same refining module: over-adding only declines.
|
|
74
|
+
def add_ancestor_spellings(scope, raw)
|
|
75
|
+
raw.to_a.each do |refined, modules|
|
|
76
|
+
segment = refined.split("::").last
|
|
77
|
+
modules.each do |module_name|
|
|
78
|
+
ancestor_modules(scope, module_name).each do |ancestor|
|
|
79
|
+
spelling = "#{ancestor}::#{segment}"
|
|
80
|
+
next unless declared_class?(scope, spelling)
|
|
81
|
+
|
|
82
|
+
raw[spelling] = (raw[spelling] || EMPTY) | [module_name]
|
|
83
|
+
end
|
|
84
|
+
end
|
|
85
|
+
end
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
# The modules `module_name` includes or prepends, transitively (`InEffectRefinements.activated_modules`, which
|
|
89
|
+
# records the chain's edges), itself left out.
|
|
90
|
+
def ancestor_modules(scope, module_name)
|
|
91
|
+
return EMPTY if module_name == RefinedDispatch.wildcard
|
|
92
|
+
|
|
93
|
+
ancestors = InEffectRefinements.activated_modules(scope, module_name)
|
|
94
|
+
ancestors.is_a?(Array) ? ancestors - [module_name] : EMPTY
|
|
95
|
+
end
|
|
96
|
+
|
|
97
|
+
def in_effect_modules(rows, list)
|
|
98
|
+
return EMPTY if rows.nil?
|
|
99
|
+
|
|
100
|
+
rows.select { |name| RefinedDispatch.in_effect?([name], list) }
|
|
101
|
+
end
|
|
102
|
+
|
|
103
|
+
# ADR-46 — a refined class appearing or vanishing (a new subclass declaration, a module turning up where a
|
|
104
|
+
# spelling named nothing), or an edit to a file declaring one (`class Kid` becoming `class Kid < Base`),
|
|
105
|
+
# moves the answer.
|
|
106
|
+
def record_targets(scope, raw)
|
|
107
|
+
return unless Analysis::DependencyRecorder.active?
|
|
108
|
+
|
|
109
|
+
raw.each_key do |refined|
|
|
110
|
+
next if refined == RefinedDispatch.wildcard
|
|
111
|
+
|
|
112
|
+
Analysis::DependencyRecorder.read_last_segment(:class, refined)
|
|
113
|
+
Scope::ResolutionChain.record_class(scope, refined)
|
|
114
|
+
end
|
|
115
|
+
end
|
|
116
|
+
|
|
117
|
+
# A refined class the receiver's levels do not show, but which a value typed as the receiver's class may
|
|
118
|
+
# still reach (critique F4): a project subclass of the class, or one whose own level mixes in a refined
|
|
119
|
+
# module (a module already on the class's levels is prepended again by `prepend`); a class RBS declares as
|
|
120
|
+
# a subclass; a module not on the levels, which `obj.extend` or a subclass's mixin can put ahead of the
|
|
121
|
+
# definer; and a module on the levels that some code prepends to an object's singleton class.
|
|
122
|
+
def refined_off_levels?(scope, class_name, levels, raw)
|
|
123
|
+
return true if unplaced_target?(scope, raw) || subclass_refined?(scope, class_name, raw)
|
|
124
|
+
|
|
125
|
+
on_levels = level_names(levels)
|
|
126
|
+
raw.each_key.any? do |refined|
|
|
127
|
+
next object_prepended?(scope, refined) if on_levels.include?(refined)
|
|
128
|
+
next true if module_name?(scope, refined)
|
|
129
|
+
|
|
130
|
+
declared_class?(scope, refined) && !placed_away?(scope, refined, class_name)
|
|
131
|
+
end
|
|
132
|
+
end
|
|
133
|
+
|
|
134
|
+
# Issue #1820 — can the class `key`, off the receiver's levels, be proven never to sit at or below
|
|
135
|
+
# `class_name` in an object's ancestry? Only when RBS does not declare it a subclass and its whole instance
|
|
136
|
+
# chain is read: no cut, no mixin the tables cannot name, no superclass the tables do not know (`class K <
|
|
137
|
+
# <expr>`, `K = Class.new(Parent)`, a superclass RBS does not declare), and `class_name` nowhere on it.
|
|
138
|
+
def placed_away?(scope, key, class_name)
|
|
139
|
+
return false if rbs_subclass?(scope, key, class_name)
|
|
140
|
+
return rbs_away?(scope, key, class_name) unless project_class?(scope, key)
|
|
141
|
+
|
|
142
|
+
chain = Scope::ResolutionChain.for(scope, key, :instance, :methods)
|
|
143
|
+
chain.record(scope) if Analysis::DependencyRecorder.active?
|
|
144
|
+
return false if chain.truncated? || chain.wildcard_mixin?
|
|
145
|
+
|
|
146
|
+
chain.level_count.times.all? { |index| level_away?(scope, chain, index, class_name) }
|
|
147
|
+
end
|
|
148
|
+
|
|
149
|
+
# A project level is away when it is not `class_name` and its superclass is known; an external superclass
|
|
150
|
+
# level when RBS declares it and its RBS ancestors do not hold `class_name`.
|
|
151
|
+
def level_away?(scope, chain, index, class_name)
|
|
152
|
+
level_class = chain.level_classes[index]
|
|
153
|
+
return level_class != class_name && !unknown_superclass?(scope, level_class) if level_class
|
|
154
|
+
|
|
155
|
+
external = chain.level_entries(index).find(&:external?)
|
|
156
|
+
name = external&.candidates&.find { |candidate| scope.environment&.class_known?(candidate) }
|
|
157
|
+
!name.nil? && rbs_away?(scope, name, class_name)
|
|
158
|
+
end
|
|
159
|
+
|
|
160
|
+
def rbs_away?(scope, name, class_name)
|
|
161
|
+
loader = scope.environment&.rbs_loader
|
|
162
|
+
ancestors = loader ? loader.ancestor_names_for(name) : EMPTY
|
|
163
|
+
!ancestors.empty? && !ancestors.include?(class_name)
|
|
164
|
+
end
|
|
165
|
+
|
|
166
|
+
def project_class?(scope, name)
|
|
167
|
+
scope.discovered_classes.key?(name) || !(scope.discovered_class_sources[name] || EMPTY).empty? ||
|
|
168
|
+
scope.known_user_class?(name)
|
|
169
|
+
end
|
|
170
|
+
|
|
171
|
+
# A project class whose superclass the tables do not know: `class K < <expr>` (the superclass table records
|
|
172
|
+
# `nil`) or `K = Class.new(Parent)` (`ENVELOPE_META_SUPERCLASS_MARK`).
|
|
173
|
+
def unknown_superclass?(scope, name)
|
|
174
|
+
superclasses = scope.discovered_superclasses
|
|
175
|
+
return true if superclasses.key?(name) && superclasses[name].nil?
|
|
176
|
+
|
|
177
|
+
scope.discovered_parameter_envelopes[name]&.key?(Scope::DiscoveryIndex::ENVELOPE_META_SUPERCLASS_MARK) ||
|
|
178
|
+
false
|
|
179
|
+
end
|
|
180
|
+
|
|
181
|
+
# A module some code `prepend`s to an object's singleton class (or `extend`s an object with in a method
|
|
182
|
+
# body), which puts it ahead of the object's class even where the class already includes it.
|
|
183
|
+
def object_prepended?(scope, name)
|
|
184
|
+
scope.discovered_parameter_envelopes[name]&.key?(Scope::DiscoveryIndex::ENVELOPE_OBJECT_EXTENDED_MARK) ||
|
|
185
|
+
false
|
|
186
|
+
end
|
|
187
|
+
|
|
188
|
+
# Is some in-effect row under a class no table declares by any spelling of it (the keys sharing its last
|
|
189
|
+
# segment that the same refining module refines)? Ruby resolved that `refine` argument to some class, through
|
|
190
|
+
# a lookup the table did not follow (the refining module's own ancestors: `module R; include Foo;
|
|
191
|
+
# refine(Bar)` refines `Foo::Bar`) or to a class outside the analysed paths (a vendored `VendorStr <
|
|
192
|
+
# String`), and Rigor cannot place it in the receiver's hierarchy.
|
|
193
|
+
def unplaced_target?(scope, raw)
|
|
194
|
+
raw.any? do |refined, modules|
|
|
195
|
+
segment = refined.split("::").last
|
|
196
|
+
modules.any? do |module_name|
|
|
197
|
+
raw.none? do |other, others|
|
|
198
|
+
others.include?(module_name) && other.split("::").last == segment && declared_class?(scope, other)
|
|
199
|
+
end
|
|
200
|
+
end
|
|
201
|
+
end
|
|
202
|
+
end
|
|
203
|
+
|
|
204
|
+
def declared_class?(scope, name)
|
|
205
|
+
scope.discovered_classes.key?(name) || !(scope.discovered_class_sources[name] || EMPTY).empty? ||
|
|
206
|
+
scope.known_user_class?(name) || scope.environment&.class_known?(name) || false
|
|
207
|
+
end
|
|
208
|
+
|
|
209
|
+
def level_names(levels)
|
|
210
|
+
names = Set.new
|
|
211
|
+
levels.each do |level_class, entries, _mixed, every|
|
|
212
|
+
names << level_class if level_class
|
|
213
|
+
(every || entries).each { |entry| entry.is_a?(Array) ? names.merge(entry) : names << entry }
|
|
214
|
+
end
|
|
215
|
+
names
|
|
216
|
+
end
|
|
217
|
+
|
|
218
|
+
def module_name?(scope, name)
|
|
219
|
+
bucket = scope.discovered_parameter_envelopes[name]
|
|
220
|
+
return true if bucket&.key?(Scope::DiscoveryIndex::ENVELOPE_MODULE_MARK)
|
|
221
|
+
|
|
222
|
+
scope.environment&.rbs_module?(name) || false
|
|
223
|
+
end
|
|
224
|
+
|
|
225
|
+
def rbs_subclass?(scope, refined, class_name)
|
|
226
|
+
environment = scope.environment
|
|
227
|
+
!environment.nil? && environment.class_known?(refined) &&
|
|
228
|
+
environment.class_ordering(refined, class_name) == :subclass
|
|
229
|
+
end
|
|
230
|
+
|
|
231
|
+
# Does a project subclass of `class_name` (the `discovered_superclasses` walk `SourceArity` makes, its
|
|
232
|
+
# parents resolved to any class the project or RBS knows, so a subclass of a core class counts) carry a
|
|
233
|
+
# refined entry on its own level, or a level the walk cannot read?
|
|
234
|
+
#
|
|
235
|
+
# A subclass level the walk cannot read (a truncated chain, or a mixin the tables cannot name) hides only
|
|
236
|
+
# modules, so it declines only when some in-effect row names a module.
|
|
237
|
+
def subclass_refined?(scope, class_name, raw)
|
|
238
|
+
each_subclass(scope, class_name) do |subclass|
|
|
239
|
+
chain = Scope::ResolutionChain.for(scope, subclass, :instance, :methods)
|
|
240
|
+
chain.record(scope) if Analysis::DependencyRecorder.active?
|
|
241
|
+
if chain.truncated? || chain.wildcard_mixin? || chain.level_count.zero?
|
|
242
|
+
return true if raw.each_key.any? { |refined| module_name?(scope, refined) }
|
|
243
|
+
|
|
244
|
+
next
|
|
245
|
+
end
|
|
246
|
+
return true if chain.level_entries(0).any? { |entry| refined_entry?(entry, raw) }
|
|
247
|
+
end
|
|
248
|
+
false
|
|
249
|
+
end
|
|
250
|
+
|
|
251
|
+
def refined_entry?(entry, raw)
|
|
252
|
+
return entry.candidates.any? { |candidate| raw.include?(candidate) } if entry.external?
|
|
253
|
+
|
|
254
|
+
raw.include?(entry.name)
|
|
255
|
+
end
|
|
256
|
+
|
|
257
|
+
# ADR-46 — every class whose subclasses are enumerated records `subclass_of:<last segment>`, which a class
|
|
258
|
+
# declaration with that superclass appearing or vanishing fires (`IncrementalSession#negative_affected`).
|
|
259
|
+
# A class whose superclass the tables do not know may be a subclass of any class, so it joins the walk too
|
|
260
|
+
# (`subclass_of:*`, which any class declaration appearing or vanishing fires).
|
|
261
|
+
def each_subclass(scope, class_name)
|
|
262
|
+
children = children_by_parent(scope)
|
|
263
|
+
record_subclasses_read(class_name)
|
|
264
|
+
queue = children.fetch(class_name.to_s, []) + unknown_superclass_classes(scope, class_name)
|
|
265
|
+
seen = {}
|
|
266
|
+
until queue.empty?
|
|
267
|
+
subclass = queue.shift
|
|
268
|
+
next if seen[subclass]
|
|
269
|
+
|
|
270
|
+
seen[subclass] = true
|
|
271
|
+
record_subclasses_read(subclass)
|
|
272
|
+
yield subclass
|
|
273
|
+
queue.concat(children.fetch(subclass, []))
|
|
274
|
+
end
|
|
275
|
+
end
|
|
276
|
+
|
|
277
|
+
def unknown_superclass_classes(scope, class_name)
|
|
278
|
+
Analysis::DependencyRecorder.read_name(:subclass_of, "*") if Analysis::DependencyRecorder.active?
|
|
279
|
+
mark = Scope::DiscoveryIndex::ENVELOPE_META_SUPERCLASS_MARK
|
|
280
|
+
computed = scope.discovered_superclasses.filter_map { |child, parent| child if parent.nil? }
|
|
281
|
+
meta = scope.discovered_parameter_envelopes.filter_map { |name, bucket| name if bucket.key?(mark) }
|
|
282
|
+
(computed + meta).uniq - [class_name.to_s]
|
|
283
|
+
end
|
|
284
|
+
|
|
285
|
+
def record_subclasses_read(class_name)
|
|
286
|
+
return unless Analysis::DependencyRecorder.active?
|
|
287
|
+
|
|
288
|
+
Analysis::DependencyRecorder.read_last_segment(:subclass_of, class_name.to_s)
|
|
289
|
+
end
|
|
290
|
+
|
|
291
|
+
def children_by_parent(scope)
|
|
292
|
+
scope.discovered_superclasses.each_with_object({}) do |(child, raw_parent), children|
|
|
293
|
+
parent = scope.ancestor_name_candidates(child, raw_parent).find { |name| known_class?(scope, name) }
|
|
294
|
+
(children[parent] ||= []) << child if parent
|
|
295
|
+
end
|
|
296
|
+
end
|
|
297
|
+
|
|
298
|
+
def known_class?(scope, name)
|
|
299
|
+
scope.known_user_class?(name) || scope.discovered_parameter_envelopes.key?(name) ||
|
|
300
|
+
scope.environment&.class_known?(name) || false
|
|
301
|
+
end
|
|
302
|
+
|
|
303
|
+
# Walks the levels from the most derived: true when the first level or entry that a raw target names or
|
|
304
|
+
# that defines `method_name` defines it, and that definer is proven. A refined level or entry (an entry no
|
|
305
|
+
# table names by any of its candidate names), a `mixed` level (a project mixin RBS does not place, which
|
|
306
|
+
# may be prepended or refined), an unproven definer, and the end of the walk all answer false. An entry no
|
|
307
|
+
# table names is passed over otherwise: it cannot be proven a definer, and a definer the walk misses there
|
|
308
|
+
# sits ahead of every refinement the walk then reaches.
|
|
309
|
+
def shadowing_definer?(scope, class_name, levels, method_name, raw)
|
|
310
|
+
levels.each do |level_class, entries, mixed, every|
|
|
311
|
+
return false if mixed || (level_class && raw.include?(level_class))
|
|
312
|
+
|
|
313
|
+
(every || entries).each do |entry|
|
|
314
|
+
case entry_answer(scope, entry, level_class, method_name, raw)
|
|
315
|
+
when :refined then return false
|
|
316
|
+
when :defines then return proven_definer?(scope, class_name, entry, method_name)
|
|
317
|
+
end
|
|
318
|
+
end
|
|
319
|
+
end
|
|
320
|
+
false
|
|
321
|
+
end
|
|
322
|
+
|
|
323
|
+
# `:refined`, `:defines`, or nil to move on. The level's own class was asked before its entries.
|
|
324
|
+
def entry_answer(scope, entry, level_class, method_name, raw)
|
|
325
|
+
return entry.any? { |candidate| raw.include?(candidate) } ? :refined : nil if entry.is_a?(Array)
|
|
326
|
+
return :refined if entry != level_class && raw.include?(entry)
|
|
327
|
+
|
|
328
|
+
:defines if RefinedDispatch.defines?(scope, entry, method_name)
|
|
329
|
+
end
|
|
330
|
+
|
|
331
|
+
# Is Ruby known to find `method_name` on `entry`, the first definer the walk reached for an instance of
|
|
332
|
+
# `class_name`? Neither proof stands on a class that rewrites its method table in a way no literal names
|
|
333
|
+
# (`ENVELOPE_DYNAMIC_MARK`), nor for a name the project removes from it with a literal `undef`,
|
|
334
|
+
# `remove_method` or `undef_method` anywhere (`ENVELOPE_REMOVED_KIND`), whatever defined it.
|
|
335
|
+
#
|
|
336
|
+
# - A project `def`, `attr_*` or `define_method` (ADR-119 A7): `DefinerResolution` must answer `Known` with
|
|
337
|
+
# `entry` as the owner, on the receiver's chain, so a definer that may not have run (a `def` under `if`,
|
|
338
|
+
# which Ruby skips, leaving the ancestor's refinement to answer) or that an unplaced mixin or fork may
|
|
339
|
+
# precede is not proof. A `def` whose envelope something made opaque (`remove_method`, `undef`, an
|
|
340
|
+
# alias, a wrapping macro) is not proof either.
|
|
341
|
+
# - A core method the offline CRuby catalogue lists on `entry` itself ({Builtins::CRubyDefiners}), which the
|
|
342
|
+
# project has not removed or rewritten there (A9: no opaque envelope for the name, which `remove_method`
|
|
343
|
+
# and `undef` record). Core RBS is not proof: it redeclares some inherited methods on a subclass
|
|
344
|
+
# (`Integer#quo`, `File#to_path`, owned by `Numeric` and `IO`) and declares some CRuby no longer defines
|
|
345
|
+
# (`Process::Status#&`).
|
|
346
|
+
#
|
|
347
|
+
# ADR-46 — the envelope bucket records the files declaring `entry`, and the class edge a new file reopening
|
|
348
|
+
# it (to `remove_method` the name, say) fires.
|
|
349
|
+
def proven_definer?(scope, class_name, entry, method_name)
|
|
350
|
+
Analysis::DependencyRecorder.read_last_segment(:class, entry) if Analysis::DependencyRecorder.active?
|
|
351
|
+
bucket = scope.parameter_envelopes_of(entry)
|
|
352
|
+
return false if bucket.key?(Scope::DiscoveryIndex::ENVELOPE_DYNAMIC_MARK)
|
|
353
|
+
return false if bucket.key?([Scope::DiscoveryIndex::ENVELOPE_REMOVED_KIND, method_name.to_sym])
|
|
354
|
+
|
|
355
|
+
opaque = Source::ParameterEnvelope.opaque?(bucket[[:instance, method_name.to_sym]])
|
|
356
|
+
return true if project_definer?(scope, class_name, entry, method_name, opaque)
|
|
357
|
+
|
|
358
|
+
!opaque && Builtins::CRubyDefiners.defines?(entry, method_name)
|
|
359
|
+
end
|
|
360
|
+
|
|
361
|
+
# A `def` counts while its envelope is its own (an alias or a wrapping macro makes it opaque); an `attr_*` or
|
|
362
|
+
# `define_method` always records an opaque one, so for those the existence table and the certainty read are
|
|
363
|
+
# the proof, beside the removal check above.
|
|
364
|
+
def project_definer?(scope, class_name, entry, method_name, opaque)
|
|
365
|
+
if scope.user_def_for(entry, method_name)
|
|
366
|
+
return false if opaque
|
|
367
|
+
elsif !scope.discovered_method?(entry, method_name, :instance)
|
|
368
|
+
return false
|
|
369
|
+
end
|
|
370
|
+
|
|
371
|
+
certain_owner?(scope, class_name, entry, method_name)
|
|
372
|
+
end
|
|
373
|
+
|
|
374
|
+
# ADR-119 WD2 — the candidate-set read over the receiver's chain, with every project `def`, `attr_*` and
|
|
375
|
+
# `define_method` as a candidate: true only for a `Known` answer whose owner is `entry`.
|
|
376
|
+
def certain_owner?(scope, class_name, entry, method_name)
|
|
377
|
+
case DefinerResolution.resolve(scope, class_name, method_name, :instance, question: :definer,
|
|
378
|
+
&definer_answer(scope, method_name))
|
|
379
|
+
in DefinerResolution::Known(answer: _answer, owner:) then owner == entry
|
|
380
|
+
in DefinerResolution::UNKNOWN then false
|
|
381
|
+
in DefinerResolution::ABSENT then false # rubocop:disable Lint/DuplicateBranch
|
|
382
|
+
end
|
|
383
|
+
end
|
|
384
|
+
|
|
385
|
+
# The first project entry at or after `from` that defines `method_name` on its instance side, by a `def` or
|
|
386
|
+
# by another definer the existence table records (`attr_*`, `define_method`).
|
|
387
|
+
def definer_answer(scope, method_name)
|
|
388
|
+
lambda do |chain, from|
|
|
389
|
+
index = from - 1
|
|
390
|
+
chain.search(scope, from) do |entry|
|
|
391
|
+
index += 1
|
|
392
|
+
next if entry.external? || entry.side != :instance
|
|
393
|
+
next unless scope.user_def_for(entry.name, method_name) ||
|
|
394
|
+
scope.discovered_method?(entry.name, method_name, :instance)
|
|
395
|
+
|
|
396
|
+
DefinerHit.new(entry.name, entry.name, index, entry.side)
|
|
397
|
+
end
|
|
398
|
+
end
|
|
399
|
+
end
|
|
400
|
+
end
|
|
401
|
+
end
|
|
402
|
+
end
|
|
403
|
+
end
|
|
@@ -0,0 +1,272 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Rigor
|
|
4
|
+
module Inference
|
|
5
|
+
# ADR-121 WD2 (issue #1664) — which in-effect refinement, if any, answers a call on an instance of a class. The
|
|
6
|
+
# typed refined arm (`ExpressionTyper#try_refined_dispatch`) asks it before every other answer, because those
|
|
7
|
+
# answer from the method a refinement replaces.
|
|
8
|
+
#
|
|
9
|
+
# Precedence follows `doc/syntax/refinements.rdoc` § Method Lookup: walk the receiver's classes from the most
|
|
10
|
+
# derived. At each class, the latest in-effect module refining the name for that class wins; otherwise its
|
|
11
|
+
# prepended modules, its own method and its included modules answer in that order, and the refinement
|
|
12
|
+
# declines; otherwise move on. A refinement of a module is decided where that module sits in the walk.
|
|
13
|
+
#
|
|
14
|
+
# The walk reads RBS ancestors for a class RBS knows (each ancestor a level of its own, so a core class's
|
|
15
|
+
# prepends are not told apart), and the project's `Scope::ResolutionChain` levels for one only the project
|
|
16
|
+
# declares, splicing in the RBS ancestors of the superclass it does not declare. "Defines the method" is an RBS
|
|
17
|
+
# declaration on that class itself, or a project `def`, `attr_*` or `define_method` of it. Core RBS sometimes
|
|
18
|
+
# redeclares an inherited method on a subclass, which stops the walk there: a known imprecision, in the
|
|
19
|
+
# declining direction.
|
|
20
|
+
#
|
|
21
|
+
# Issue #1740 — {OwnMethod} shares {.levels} with the walk below and asks the opposite question for the two
|
|
22
|
+
# argument checks: does the receiver's own method answer ahead of every refinement in effect?
|
|
23
|
+
module RefinedDispatch
|
|
24
|
+
# The refinement that answers: `module_name` refines `refined_class` (a class or module on the walk).
|
|
25
|
+
Winner = Data.define(:module_name, :refined_class)
|
|
26
|
+
|
|
27
|
+
# The answer when the in-effect list carries {InEffectRefinements::UNKNOWN} and some refinement defines the
|
|
28
|
+
# name on a class the walk reaches: any of them may answer.
|
|
29
|
+
UNKNOWN = :unknown
|
|
30
|
+
|
|
31
|
+
NO_TARGETS = {}.freeze
|
|
32
|
+
private_constant :NO_TARGETS
|
|
33
|
+
|
|
34
|
+
module_function
|
|
35
|
+
|
|
36
|
+
# `Scope::DiscoveryIndex::REFINEMENT_WILDCARD`, read when asked: this file loads before `Scope`.
|
|
37
|
+
def wildcard = Scope::DiscoveryIndex::REFINEMENT_WILDCARD
|
|
38
|
+
|
|
39
|
+
# A {Winner}, {UNKNOWN}, or nil when no in-effect refinement answers `method_name` on an instance of
|
|
40
|
+
# `class_name` (the class's own lookup does). `list` is the call site's in-effect refinements.
|
|
41
|
+
#
|
|
42
|
+
# ADR-121 WD7 — a row the walk could not read never yields a {Winner}: an in-effect module's row for the name
|
|
43
|
+
# whose class the walk could not name reaches every receiver, and a names-wildcard row of a level the walk
|
|
44
|
+
# reaches before a definer may define the name, so both answer {UNKNOWN}. So does a winner whose module shares
|
|
45
|
+
# its last segment with another listed module (A1): which of a `using`'s declared candidates Ruby finds can
|
|
46
|
+
# depend on load order.
|
|
47
|
+
def winner(scope, class_name, method_name, list)
|
|
48
|
+
refinements = scope.discovered_refinements
|
|
49
|
+
return UNKNOWN if class_unknown_row?(scope, refinements, method_name, list)
|
|
50
|
+
|
|
51
|
+
targets = resolved(scope, targets(refinements, method_name, list), method_name)
|
|
52
|
+
unread = targets(refinements, wildcard, list)
|
|
53
|
+
unknown = list.include?(InEffectRefinements::UNKNOWN)
|
|
54
|
+
return nil if targets.nil? && unread.nil? && !unknown
|
|
55
|
+
|
|
56
|
+
levels = levels(scope, class_name)
|
|
57
|
+
return nil if levels.nil?
|
|
58
|
+
return UNKNOWN if unknown && unknown_reaches?(scope, refinements, method_name, levels)
|
|
59
|
+
|
|
60
|
+
walk_targets(scope, levels, method_name, [targets, unread], list)
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
def walk_targets(scope, levels, method_name, (targets, unread), list)
|
|
64
|
+
return nil if targets.nil? && unread.nil?
|
|
65
|
+
|
|
66
|
+
answer = walk(scope, levels, method_name, targets || NO_TARGETS, unread || NO_TARGETS)
|
|
67
|
+
answer.is_a?(Winner) && rival_spelling?(answer.module_name, list) ? UNKNOWN : answer
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
# ADR-121 WD7 (A2) — does an in-effect module have a row for `method_name` (or a names-wildcard row) whose
|
|
71
|
+
# class the walk could not name ({InEffectRefinements.class_wildcard_key?})? It may refine the receiver's class.
|
|
72
|
+
def class_unknown_row?(scope, refinements, method_name, list)
|
|
73
|
+
refinements.any? do |refined, methods|
|
|
74
|
+
rows = [methods[method_name], methods[wildcard]].compact
|
|
75
|
+
!rows.empty? && rows.any? { |modules| in_effect?(modules, list) } &&
|
|
76
|
+
InEffectRefinements.class_wildcard_key?(scope, refined)
|
|
77
|
+
end
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
# Is one of `modules` (a row's refining modules) in `list`? The wildcard module, which the walk could not name,
|
|
81
|
+
# is any listed module.
|
|
82
|
+
def in_effect?(modules, list)
|
|
83
|
+
return list.any? { |name| name != InEffectRefinements::UNKNOWN } if modules.include?(wildcard)
|
|
84
|
+
|
|
85
|
+
modules.any? { |name| list.include?(name) }
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
def rival_spelling?(module_name, list)
|
|
89
|
+
segment = module_name.split("::").last
|
|
90
|
+
list.any? { |other| other.is_a?(String) && other != module_name && other.split("::").last == segment }
|
|
91
|
+
end
|
|
92
|
+
|
|
93
|
+
# `{refined class => the latest in-effect module refining method_name for it}`, or nil when none is in effect.
|
|
94
|
+
def targets(refinements, method_name, list)
|
|
95
|
+
out = nil
|
|
96
|
+
refinements.each do |refined, methods|
|
|
97
|
+
modules = methods[method_name]
|
|
98
|
+
next if modules.nil?
|
|
99
|
+
|
|
100
|
+
best = latest(modules, list)
|
|
101
|
+
(out ||= {})[refined] = best if best
|
|
102
|
+
end
|
|
103
|
+
out
|
|
104
|
+
end
|
|
105
|
+
|
|
106
|
+
# The refinement table records every name a `refine` argument's spelling can denote (`refine String` inside
|
|
107
|
+
# `module M` records `M::String` and `String`), which only withholds a check. To type, a key must be the class
|
|
108
|
+
# Ruby's lexical lookup resolves: one the project or RBS knows, with no longer spelling of the same last segment
|
|
109
|
+
# that the same module refines the name for and that is known too (that one is innermost, so it wins). Nil
|
|
110
|
+
# when no key survives.
|
|
111
|
+
def resolved(scope, targets, method_name)
|
|
112
|
+
return nil if targets.nil?
|
|
113
|
+
|
|
114
|
+
refinements = scope.discovered_refinements
|
|
115
|
+
kept = targets.select do |refined, module_name|
|
|
116
|
+
known_class?(scope, refined) && !shadowed?(scope, refinements, refined, module_name, method_name)
|
|
117
|
+
end
|
|
118
|
+
kept.empty? ? nil : kept
|
|
119
|
+
end
|
|
120
|
+
|
|
121
|
+
def shadowed?(scope, refinements, refined, module_name, method_name)
|
|
122
|
+
suffix = "::#{refined.split('::').last}"
|
|
123
|
+
depth = refined.count(":")
|
|
124
|
+
refinements.any? do |other, methods|
|
|
125
|
+
other.count(":") > depth && other.end_with?(suffix) && methods[method_name]&.include?(module_name) &&
|
|
126
|
+
known_class?(scope, other)
|
|
127
|
+
end
|
|
128
|
+
end
|
|
129
|
+
|
|
130
|
+
def known_class?(scope, name)
|
|
131
|
+
scope.discovered_classes.key?(name) || scope.environment&.class_known?(name) || false
|
|
132
|
+
end
|
|
133
|
+
|
|
134
|
+
def latest(modules, list)
|
|
135
|
+
best = nil
|
|
136
|
+
best_index = -1
|
|
137
|
+
modules.each do |module_name|
|
|
138
|
+
index = list.index(module_name)
|
|
139
|
+
next unless index && index > best_index
|
|
140
|
+
|
|
141
|
+
best = module_name
|
|
142
|
+
best_index = index
|
|
143
|
+
end
|
|
144
|
+
best
|
|
145
|
+
end
|
|
146
|
+
|
|
147
|
+
# ADR-121 WD7 — a names-wildcard row counts as a row for every name, and a row whose class the walk could not
|
|
148
|
+
# name reaches every level.
|
|
149
|
+
def unknown_reaches?(scope, refinements, method_name, levels)
|
|
150
|
+
unread_class = refinements.any? do |refined, methods|
|
|
151
|
+
(methods.key?(method_name) || methods.key?(wildcard)) &&
|
|
152
|
+
InEffectRefinements.class_wildcard_key?(scope, refined)
|
|
153
|
+
end
|
|
154
|
+
return true if unread_class
|
|
155
|
+
|
|
156
|
+
levels.any? do |_level_class, entries|
|
|
157
|
+
entries.any? do |entry|
|
|
158
|
+
methods = refinements[entry]
|
|
159
|
+
!methods.nil? && (methods.key?(method_name) || methods.key?(wildcard))
|
|
160
|
+
end
|
|
161
|
+
end
|
|
162
|
+
end
|
|
163
|
+
|
|
164
|
+
# A level the project mixes a module into whose position RBS does not record (`mixed`) is decided by its own
|
|
165
|
+
# refinement first, as any level is; past that, the mixin may define the name, so a refinement further up the
|
|
166
|
+
# walk may or may not win and the answer is {UNKNOWN}, never the replaced method's. ADR-121 WD7: a level an
|
|
167
|
+
# in-effect module has a names-wildcard row for (`unread`) may define the name too, so it answers {UNKNOWN}.
|
|
168
|
+
def walk(scope, levels, method_name, targets, unread)
|
|
169
|
+
levels.each_with_index do |(level_class, entries, mixed), index|
|
|
170
|
+
return UNKNOWN if level_class && unread.key?(level_class)
|
|
171
|
+
|
|
172
|
+
refining = level_class && targets[level_class]
|
|
173
|
+
return Winner.new(module_name: refining, refined_class: level_class) if refining
|
|
174
|
+
return targeted_after?(levels, index, targets, unread) ? UNKNOWN : nil if mixed
|
|
175
|
+
|
|
176
|
+
answer = walk_entries(scope, entries, level_class, method_name, targets, unread)
|
|
177
|
+
return answer unless answer == :continue
|
|
178
|
+
end
|
|
179
|
+
nil
|
|
180
|
+
end
|
|
181
|
+
|
|
182
|
+
# A level's prepended modules, its own method and its included modules, in that order: `:continue` when none
|
|
183
|
+
# answers, so the walk moves on.
|
|
184
|
+
def walk_entries(scope, entries, level_class, method_name, targets, unread)
|
|
185
|
+
entries.each do |entry|
|
|
186
|
+
unless entry == level_class
|
|
187
|
+
return UNKNOWN if unread.key?(entry)
|
|
188
|
+
|
|
189
|
+
refining = targets[entry]
|
|
190
|
+
return Winner.new(module_name: refining, refined_class: entry) if refining
|
|
191
|
+
end
|
|
192
|
+
return nil if defines?(scope, entry, method_name)
|
|
193
|
+
end
|
|
194
|
+
:continue
|
|
195
|
+
end
|
|
196
|
+
|
|
197
|
+
def targeted_after?(levels, index, targets, unread)
|
|
198
|
+
levels.drop(index).any? do |_level_class, entries|
|
|
199
|
+
entries.any? { |entry| targets.key?(entry) || unread.key?(entry) }
|
|
200
|
+
end
|
|
201
|
+
end
|
|
202
|
+
|
|
203
|
+
# `[[level class, [entries in lookup order], mixed, every entry], …]` for an instance of `class_name`, or nil
|
|
204
|
+
# when the walk cannot be trusted: a chain cut at its limit, or one that records a mixin the tables cannot
|
|
205
|
+
# name. `every entry` (project levels only) is the level's entries in lookup order with each one that
|
|
206
|
+
# neither the project's resolver nor RBS can name kept as its candidate names (an Array) where `entries`
|
|
207
|
+
# leaves it out: the typer never reads it, and {OwnMethod} declines where a refinement may name one.
|
|
208
|
+
def levels(scope, class_name)
|
|
209
|
+
environment = scope.environment
|
|
210
|
+
return rbs_levels(scope, class_name) if environment&.class_known?(class_name)
|
|
211
|
+
|
|
212
|
+
project_levels(scope, class_name)
|
|
213
|
+
end
|
|
214
|
+
|
|
215
|
+
# The RBS ancestors, each a level of its own, `mixed` where the project reopens one to mix a module in (`class
|
|
216
|
+
# String; include Loud; end`), whose position among them RBS does not record. Each ancestor's name is a
|
|
217
|
+
# dependency, so a file that adds such a reopening re-checks the consumer.
|
|
218
|
+
def rbs_levels(scope, class_name)
|
|
219
|
+
loader = scope.environment.rbs_loader
|
|
220
|
+
names = loader ? loader.ancestor_names_for(class_name) : []
|
|
221
|
+
names = [class_name] if names.empty?
|
|
222
|
+
names.map { |name| [name, [name], project_mixin?(scope, name)] }
|
|
223
|
+
end
|
|
224
|
+
|
|
225
|
+
def project_mixin?(scope, name)
|
|
226
|
+
Analysis::DependencyRecorder.read_last_segment(:class, name) if Analysis::DependencyRecorder.active?
|
|
227
|
+
!scope.includes_of(name).empty? || scope.discovery.discovered_prepends.key?(name) ||
|
|
228
|
+
(scope.discovered_classes.key?(name) &&
|
|
229
|
+
Scope::ResolutionChain.for(scope, name, :instance, :methods).wildcard_mixin?)
|
|
230
|
+
end
|
|
231
|
+
|
|
232
|
+
def project_levels(scope, class_name)
|
|
233
|
+
chain = Scope::ResolutionChain.for(scope, class_name, :instance, :methods)
|
|
234
|
+
chain.record(scope) if Analysis::DependencyRecorder.active?
|
|
235
|
+
return nil if chain.truncated? || chain.wildcard_mixin?
|
|
236
|
+
|
|
237
|
+
out = []
|
|
238
|
+
chain.level_count.times do |index|
|
|
239
|
+
level_class = chain.level_classes[index]
|
|
240
|
+
every = chain.level_entries(index).map { |entry| entry_name(scope, entry) || unnamed(entry) }
|
|
241
|
+
names = every.grep(String)
|
|
242
|
+
next out << [level_class, names, false, every] if level_class
|
|
243
|
+
|
|
244
|
+
names.each { |name| out.concat(rbs_levels(scope, name)) }
|
|
245
|
+
end
|
|
246
|
+
out
|
|
247
|
+
end
|
|
248
|
+
|
|
249
|
+
def unnamed(entry) = entry.candidates.empty? ? [entry.raw.to_s] : entry.candidates
|
|
250
|
+
|
|
251
|
+
def entry_name(scope, entry)
|
|
252
|
+
return entry.name unless entry.external?
|
|
253
|
+
|
|
254
|
+
environment = scope.environment
|
|
255
|
+
entry.candidates.find { |candidate| environment&.class_known?(candidate) }
|
|
256
|
+
end
|
|
257
|
+
|
|
258
|
+
# Does `name` define `method_name` on its instance side itself: a project `def` (its dependency edge recorded
|
|
259
|
+
# through `Scope#user_def_for`), another project definer (`attr_*`, `define_method`), or an RBS declaration
|
|
260
|
+
# written on `name` rather than on an ancestor?
|
|
261
|
+
def defines?(scope, name, method_name)
|
|
262
|
+
return true if scope.user_def_for(name, method_name)
|
|
263
|
+
return true if scope.discovered_method?(name, method_name, :instance)
|
|
264
|
+
|
|
265
|
+
definition = ExternalAncestorResolution.method_definition(name, method_name, :instance, scope: scope)
|
|
266
|
+
ExternalAncestorResolution.declared_on_class?(definition, name)
|
|
267
|
+
end
|
|
268
|
+
end
|
|
269
|
+
end
|
|
270
|
+
end
|
|
271
|
+
|
|
272
|
+
require_relative "refined_dispatch/own_method"
|