rigortype 0.4.1 → 0.4.3
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 +18 -5
- data/docs/handbook/appendix-liskov.md +5 -0
- data/docs/manual/02-cli-reference.md +19 -1
- data/docs/manual/03-configuration.md +7 -5
- data/docs/manual/04-diagnostics.md +2 -1
- data/docs/manual/05-inspecting-types.md +50 -0
- data/docs/manual/06-baseline.md +2 -0
- data/docs/manual/07-plugins.md +1 -1
- data/docs/manual/12-caching.md +18 -1
- data/docs/manual/14-rails-quickstart.md +2 -2
- data/docs/manual/18-removing-dead-code.md +41 -4
- 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 +11 -0
- data/lib/rigor/analysis/check_rules/source_arity.rb +21 -2
- data/lib/rigor/analysis/check_rules.rb +292 -59
- 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/effects_cache_probe.rb +1 -1
- 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 +6 -4
- data/lib/rigor/analysis/incremental_session.rb +103 -23
- data/lib/rigor/analysis/path_expansion.rb +62 -6
- data/lib/rigor/analysis/plugin_fact_fingerprint.rb +124 -37
- data/lib/rigor/analysis/reachability/graph.rb +241 -37
- data/lib/rigor/analysis/reachability/scan.rb +490 -77
- data/lib/rigor/analysis/reachability/scan_cache.rb +4 -2
- data/lib/rigor/analysis/rule_catalog.rb +43 -0
- data/lib/rigor/analysis/run_cache_key.rb +16 -1
- data/lib/rigor/analysis/run_cache_probe.rb +1 -1
- data/lib/rigor/analysis/run_stats.rb +28 -6
- data/lib/rigor/analysis/runner/diagnostic_aggregator.rb +113 -6
- data/lib/rigor/analysis/runner/pool_coordinator.rb +77 -19
- data/lib/rigor/analysis/runner/project_pre_passes.rb +21 -1
- data/lib/rigor/analysis/runner/run_snapshots.rb +6 -1
- data/lib/rigor/analysis/runner.rb +62 -13
- data/lib/rigor/analysis/worker_session.rb +8 -5
- data/lib/rigor/cache/engine_source.rb +21 -7
- data/lib/rigor/cache/incremental_snapshot.rb +55 -5
- 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 +140 -40
- data/lib/rigor/cli/docs_command.rb +5 -4
- data/lib/rigor/cli/lens_command.rb +144 -0
- data/lib/rigor/cli/lsp_command.rb +5 -2
- data/lib/rigor/cli/skill_command.rb +3 -3
- data/lib/rigor/cli/skill_describe.rb +1 -1
- data/lib/rigor/cli/trace_command.rb +5 -0
- data/lib/rigor/cli/type_of_command.rb +6 -3
- data/lib/rigor/cli/unused_command.rb +166 -27
- data/lib/rigor/cli.rb +39 -5
- data/lib/rigor/configuration/severity_profile.rb +6 -0
- data/lib/rigor/configuration.rb +61 -2
- data/lib/rigor/environment/bundle_sig_discovery.rb +11 -7
- data/lib/rigor/environment/gated_signature_guard.rb +43 -0
- data/lib/rigor/environment/gated_signature_plan.rb +339 -0
- data/lib/rigor/environment/installed_gem_set.rb +38 -11
- data/lib/rigor/environment/lockfile_resolver.rb +37 -6
- data/lib/rigor/environment/missing_gem_constant_index.rb +22 -7
- data/lib/rigor/environment/rbs_coverage_report.rb +1 -1
- data/lib/rigor/environment/rbs_loader.rb +316 -14
- data/lib/rigor/environment/required_features.rb +208 -0
- data/lib/rigor/environment.rb +33 -8
- data/lib/rigor/inference/acceptance.rb +28 -3
- data/lib/rigor/inference/builtins/cruby_definers.rb +78 -0
- data/lib/rigor/inference/builtins/method_catalog.rb +3 -0
- data/lib/rigor/inference/captured_locals.rb +43 -3
- data/lib/rigor/inference/def_node_resolver.rb +23 -1
- data/lib/rigor/inference/expression_typer.rb +261 -29
- 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 +286 -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/match_rebinding.rb +1 -1
- data/lib/rigor/inference/method_dispatcher/alias_strict_nominals.rb +36 -0
- data/lib/rigor/inference/method_dispatcher/argument_nodes.rb +25 -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/kernel_dispatch.rb +3 -2
- data/lib/rigor/inference/method_dispatcher/keyword_arguments.rb +267 -0
- data/lib/rigor/inference/method_dispatcher/match_data_folding.rb +2 -2
- data/lib/rigor/inference/method_dispatcher/member_shape_projection.rb +3 -3
- data/lib/rigor/inference/method_dispatcher/method_folding.rb +2 -1
- data/lib/rigor/inference/method_dispatcher/optional_count.rb +63 -0
- data/lib/rigor/inference/method_dispatcher/overload_selector.rb +122 -105
- data/lib/rigor/inference/method_dispatcher/rbs_dispatch.rb +247 -28
- data/lib/rigor/inference/method_dispatcher/splat_arity.rb +71 -0
- data/lib/rigor/inference/method_dispatcher/struct_folding.rb +4 -7
- data/lib/rigor/inference/method_dispatcher/struct_materialization.rb +8 -26
- data/lib/rigor/inference/method_dispatcher/unit_denominator_rational.rb +67 -0
- data/lib/rigor/inference/method_dispatcher.rb +96 -14
- 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_definedness_guard.rb +86 -0
- data/lib/rigor/inference/project_patched_methods.rb +12 -7
- data/lib/rigor/inference/project_patched_scanner.rb +108 -26
- data/lib/rigor/inference/refine_census.rb +239 -0
- data/lib/rigor/inference/refine_self.rb +317 -0
- data/lib/rigor/inference/refined_dispatch/class_object.rb +350 -0
- data/lib/rigor/inference/refined_dispatch/own_method.rb +475 -0
- data/lib/rigor/inference/refined_dispatch.rb +320 -0
- data/lib/rigor/inference/repeated_or_writes.rb +2 -0
- data/lib/rigor/inference/scope_indexer/mixin_accumulator.rb +13 -0
- data/lib/rigor/inference/scope_indexer.rb +827 -65
- data/lib/rigor/inference/statement_evaluator.rb +294 -51
- data/lib/rigor/inference/string_mutation.rb +9 -3
- data/lib/rigor/inference/synthetic_method_scanner.rb +2 -1
- data/lib/rigor/inference/untyped_rejoin.rb +75 -0
- data/lib/rigor/inference/version_guard.rb +19 -10
- data/lib/rigor/language_server/project_context.rb +87 -5
- data/lib/rigor/language_server/server.rb +18 -0
- data/lib/rigor/lens/anchor.rb +115 -0
- data/lib/rigor/lens/contract_reader.rb +118 -0
- data/lib/rigor/lens/document.rb +113 -0
- data/lib/rigor/lens/file_reader.rb +433 -0
- data/lib/rigor/lens/members.rb +153 -0
- data/lib/rigor/lens/parameters.rb +91 -0
- data/lib/rigor/lens/plugins.rb +40 -0
- data/lib/rigor/lens/run.rb +280 -0
- data/lib/rigor/lens/slot.rb +79 -0
- data/lib/rigor/lens.rb +40 -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 +48 -6
- data/lib/rigor/protection/mutation_cache.rb +3 -2
- 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 +211 -21
- data/lib/rigor/sig_gen/classification.rb +4 -1
- data/lib/rigor/sig_gen/generator.rb +100 -10
- 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-active-model-serializers/lib/rigor/plugin/active_model_serializers/serializer_discoverer.rb +29 -3
- data/plugins/rigor-active-model-serializers/lib/rigor/plugin/active_model_serializers.rb +5 -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 +30 -4
- data/sig/rigor/source.rbs +4 -0
- data/sig/rigor.rbs +9 -4
- data/skills/rigor-project-init/references/02-configure.md +2 -2
- metadata +83 -1
|
@@ -0,0 +1,317 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "prism"
|
|
4
|
+
|
|
5
|
+
require_relative "../source/constant_path"
|
|
6
|
+
require_relative "refine_census"
|
|
7
|
+
|
|
8
|
+
module Rigor
|
|
9
|
+
module Inference
|
|
10
|
+
# ADR-121 WD7 — the one function that answers what `self` a `refine` (or a `:refine` literal) at a program point
|
|
11
|
+
# runs on. The in-effect walk ({InEffectRefinements}) works a {Context} out with these functions for every body,
|
|
12
|
+
# method and block it enters and records it for each `refine`-shaped node; the census (`ScopeIndexer`'s module-body
|
|
13
|
+
# walk and its scan of method and refine bodies) reads that record back ({InEffectRefinements#refine_context}), so
|
|
14
|
+
# the table and the activations charge the same module.
|
|
15
|
+
module RefineSelf
|
|
16
|
+
# `here`, for a `refine` call directly at that point; `literal`, for a `:refine` literal there (which names
|
|
17
|
+
# `Module#refine` for an alias or a `send` whose method runs where a method defined there does: in a module's
|
|
18
|
+
# `class << self` body that is the module, though a `refine` call there runs on the singleton class); and
|
|
19
|
+
# `instance` / `singleton`, for a method defined there on the instance / singleton side. Each is a module name,
|
|
20
|
+
# the names a constant's spelling can denote (an Array, innermost first, for a module only another file may
|
|
21
|
+
# declare), the wildcard (a module the walk cannot name), {CLASS}, {MAIN}, {ANONYMOUS} or {REFINEMENT}.
|
|
22
|
+
# `deferred` is true inside a method body (a `def`, a `define_method` block), where a `refine` runs only once the
|
|
23
|
+
# method is called.
|
|
24
|
+
# `own_method` is true inside a singleton `def` of a module the walk names ({.def_body}), and in the blocks that
|
|
25
|
+
# keep its `self`: `here` is then that module itself, so a `refine X do … end` written there refines X for it
|
|
26
|
+
# once the method is called, and the census records its rows where it would otherwise charge a wildcard.
|
|
27
|
+
# `remote` is true in the body of an eval on a constant (`M.module_eval { … }`, and a `class << self` there),
|
|
28
|
+
# whose file need not declare the module it runs on: a singleton method defined there is not `own_method`.
|
|
29
|
+
Context = Data.define(:here, :literal, :instance, :singleton, :deferred, :own_method, :remote)
|
|
30
|
+
# A plain class, or an instance of one: `Class` undefines `refine`, so a `refine` there is the class's own
|
|
31
|
+
# method (#1689) and a `:refine` literal is data.
|
|
32
|
+
CLASS = :class
|
|
33
|
+
# The top level's `main`, which has no `refine`.
|
|
34
|
+
MAIN = :main
|
|
35
|
+
# A module a bare `Module.new { … }` creates, which the census names by its position.
|
|
36
|
+
ANONYMOUS = :anonymous
|
|
37
|
+
# The refinement a `refine` block creates, where a method defined on its singleton side (`def self.x`, a
|
|
38
|
+
# `define_singleton_method` block, a `class << self` body) runs. `using` rejects a refinement with `TypeError`,
|
|
39
|
+
# so a `refine` there reaches no `using` span and the census charges it to nothing.
|
|
40
|
+
REFINEMENT = :refinement
|
|
41
|
+
|
|
42
|
+
# Core iteration methods: the Enumerable protocol never rebinds `self` in their block.
|
|
43
|
+
CORE_ITERATORS = Set[
|
|
44
|
+
:each, :each_with_index, :each_with_object, :each_pair, :each_key, :each_value, :map, :flat_map, :times,
|
|
45
|
+
:select, :reject
|
|
46
|
+
].freeze
|
|
47
|
+
LITERALS = [Prism::ArrayNode, Prism::HashNode, Prism::RangeNode, Prism::IntegerNode, Prism::FloatNode,
|
|
48
|
+
Prism::StringNode, Prism::InterpolatedStringNode, Prism::SymbolNode].freeze
|
|
49
|
+
NAMED_RECEIVERS = [Prism::ConstantReadNode, Prism::ConstantPathNode, Prism::LocalVariableReadNode,
|
|
50
|
+
Prism::InstanceVariableReadNode].freeze
|
|
51
|
+
METHOD_BLOCK_CALLS = Set[:define_method, :define_singleton_method].freeze
|
|
52
|
+
INSTANCE_EVAL_CALLS = Set[:instance_eval, :instance_exec].freeze
|
|
53
|
+
# Bindings of a local or an instance variable that are not a plain write: each may hold any value.
|
|
54
|
+
VARIABLE_BINDINGS = [
|
|
55
|
+
Prism::LocalVariableOperatorWriteNode, Prism::LocalVariableOrWriteNode, Prism::LocalVariableAndWriteNode,
|
|
56
|
+
Prism::LocalVariableTargetNode, Prism::RequiredParameterNode, Prism::OptionalParameterNode,
|
|
57
|
+
Prism::RestParameterNode, Prism::KeywordRestParameterNode, Prism::RequiredKeywordParameterNode,
|
|
58
|
+
Prism::OptionalKeywordParameterNode, Prism::BlockParameterNode, Prism::BlockLocalVariableNode,
|
|
59
|
+
Prism::InstanceVariableOperatorWriteNode, Prism::InstanceVariableOrWriteNode,
|
|
60
|
+
Prism::InstanceVariableAndWriteNode, Prism::InstanceVariableTargetNode
|
|
61
|
+
].freeze
|
|
62
|
+
# Bindings of a constant other than a bare `K = …`, matched by the constant's last segment.
|
|
63
|
+
CONSTANT_BINDINGS = [
|
|
64
|
+
Prism::ConstantOrWriteNode, Prism::ConstantOperatorWriteNode, Prism::ConstantAndWriteNode,
|
|
65
|
+
Prism::ConstantTargetNode, Prism::ConstantPathTargetNode
|
|
66
|
+
].freeze
|
|
67
|
+
CONSTANT_PATH_BINDINGS = [
|
|
68
|
+
Prism::ConstantPathWriteNode, Prism::ConstantPathOrWriteNode, Prism::ConstantPathOperatorWriteNode,
|
|
69
|
+
Prism::ConstantPathAndWriteNode
|
|
70
|
+
].freeze
|
|
71
|
+
|
|
72
|
+
private_constant :CORE_ITERATORS, :LITERALS, :NAMED_RECEIVERS, :METHOD_BLOCK_CALLS, :INSTANCE_EVAL_CALLS,
|
|
73
|
+
:VARIABLE_BINDINGS, :CONSTANT_BINDINGS, :CONSTANT_PATH_BINDINGS
|
|
74
|
+
|
|
75
|
+
module_function
|
|
76
|
+
|
|
77
|
+
def wildcard = RefineCensus.wildcard
|
|
78
|
+
|
|
79
|
+
# The top level: `main`; a method defined there is `Object`'s, which any module may run.
|
|
80
|
+
def top = context(MAIN, wildcard, MAIN)
|
|
81
|
+
|
|
82
|
+
# A body whose `self` the walk cannot name.
|
|
83
|
+
def unknown = context(wildcard, wildcard, wildcard)
|
|
84
|
+
|
|
85
|
+
def context(here, instance, singleton, deferred: false, literal: here, own_method: false, remote: false)
|
|
86
|
+
Context.new(here: here, literal: literal, instance: instance, singleton: singleton, deferred: deferred,
|
|
87
|
+
own_method: own_method, remote: remote)
|
|
88
|
+
end
|
|
89
|
+
|
|
90
|
+
# A `module` body (or a `Module.new` block) of the module `name` (nil when the walk cannot name it, or
|
|
91
|
+
# {ANONYMOUS}): its instance methods run on whatever includes or extends it, its singleton methods on it.
|
|
92
|
+
def module_body(name)
|
|
93
|
+
return unknown if name.nil?
|
|
94
|
+
|
|
95
|
+
context(name, wildcard, name)
|
|
96
|
+
end
|
|
97
|
+
|
|
98
|
+
# A `class` body (or a `Class.new` / `Struct.new` / `Data.define` block): `self` is a class; a `Module`
|
|
99
|
+
# subclass's instances are modules the walk cannot name.
|
|
100
|
+
def class_body(module_subclass) = context(CLASS, module_subclass ? wildcard : CLASS, CLASS)
|
|
101
|
+
|
|
102
|
+
# A `class << expr` body: `self` is a singleton class, so a `refine` call there is its own method; for
|
|
103
|
+
# `class << self` its methods (and a `:refine` literal an alias there names) run on the enclosing `self`, for any
|
|
104
|
+
# other expression on an object the walk cannot name.
|
|
105
|
+
def singleton_class_body(parent, of_self)
|
|
106
|
+
runs_on = of_self ? parent.singleton : wildcard
|
|
107
|
+
context(CLASS, runs_on, CLASS, literal: runs_on, remote: of_self && parent.remote)
|
|
108
|
+
end
|
|
109
|
+
|
|
110
|
+
# A method body defined in `parent` on the instance or singleton side: `self` is what that side runs on, and a
|
|
111
|
+
# method defined inside it is defined on something the walk cannot name.
|
|
112
|
+
def method_body(parent, singleton)
|
|
113
|
+
context(singleton ? parent.singleton : parent.instance, wildcard, wildcard, deferred: true)
|
|
114
|
+
end
|
|
115
|
+
|
|
116
|
+
# A `def` body in `parent`: on no receiver an instance method, on `self` (or on the constant that is the last
|
|
117
|
+
# segment of the module `parent` names, `def M.x` inside `module M`) a singleton one, and on any other object a
|
|
118
|
+
# method defined on something the walk cannot name. A singleton method of a module the walk names is the
|
|
119
|
+
# module's own (`own_method`): `def self.x`, or a `def` written in that module's `class << self`
|
|
120
|
+
# (`parent.instance` is the module there). `self` is then the module itself, whichever way the method is called,
|
|
121
|
+
# so a `refine` in it refines for that module; for an instance `def`, whose `parent.instance` is the wildcard,
|
|
122
|
+
# Ruby runs it on whatever module extends the owner. `def M.x` is not marked, as the constant names the module by
|
|
123
|
+
# its last segment only, and neither is a method an eval on a constant defines (`remote`): its file need not
|
|
124
|
+
# declare the module, so the lookup of a refine body, which consults the files that do, would not see it.
|
|
125
|
+
def def_body(parent, receiver)
|
|
126
|
+
return mark_own_method(method_body(parent, false), parent) if receiver.nil?
|
|
127
|
+
return unknown.with(deferred: true) unless lexical_self_receiver?(receiver, parent)
|
|
128
|
+
|
|
129
|
+
context = method_body(parent, true)
|
|
130
|
+
receiver.is_a?(Prism::SelfNode) ? mark_own_method(context, parent) : context
|
|
131
|
+
end
|
|
132
|
+
|
|
133
|
+
def mark_own_method(context, parent)
|
|
134
|
+
named_module?(context.here) && !parent.remote ? context.with(own_method: true) : context
|
|
135
|
+
end
|
|
136
|
+
|
|
137
|
+
def lexical_self_receiver?(receiver, context)
|
|
138
|
+
return true if receiver.is_a?(Prism::SelfNode)
|
|
139
|
+
|
|
140
|
+
here = context.here
|
|
141
|
+
receiver.is_a?(Prism::ConstantReadNode) && here.is_a?(String) && here.split("::").last == receiver.name.to_s
|
|
142
|
+
end
|
|
143
|
+
|
|
144
|
+
# A module name the walk can charge a `refine` to: not the wildcard, {CLASS}, {MAIN} or {ANONYMOUS}, and not the
|
|
145
|
+
# several names a constant's spelling can denote.
|
|
146
|
+
def named_module?(here) = here.is_a?(String) && here != wildcard
|
|
147
|
+
|
|
148
|
+
# The context inside the block of `node` (a block-carrying call, or a lambda) written in `parent`. `eval_names`
|
|
149
|
+
# are the modules a `*_eval` / `*_exec` call's constant receiver may name ({.eval_receiver_names}), or nil; the
|
|
150
|
+
# given block answers whether a constant, local or instance variable receiver holds a literal ({.keeps_self?}).
|
|
151
|
+
# A block keeps `self` only where its call is known to: a core iteration method on a receiver whose value is a
|
|
152
|
+
# literal in this file, and a `refine` call (its body is the refinement, handled by the reader). A `*_eval` /
|
|
153
|
+
# `*_exec` block runs on its receiver, a `define_method` / `define_singleton_method` block is a method body, and
|
|
154
|
+
# a `Module.new` / `Class.new` block is a module / class body. Any other block (a DSL may `module_eval` it,
|
|
155
|
+
# `tap`, `loop`, a project method, a core method's name on any other receiver), a lambda, and every block at the
|
|
156
|
+
# top level may run under any `self`.
|
|
157
|
+
def block(parent, node, eval_names, &)
|
|
158
|
+
return unknown if node.is_a?(Prism::LambdaNode)
|
|
159
|
+
return refinement_body(parent) if node.name == :refine
|
|
160
|
+
return eval_body(parent, node, eval_names) if RefineCensus::SELF_EVAL_CALLS.include?(node.name)
|
|
161
|
+
return meta_new_body(node) if ScopeIndexer.meta_new_constant_rvalue?(node)
|
|
162
|
+
return unknown if parent.here == MAIN
|
|
163
|
+
return method_body(parent, node.name == :define_singleton_method) if method_block?(node)
|
|
164
|
+
return parent if keeps_self?(node, &)
|
|
165
|
+
|
|
166
|
+
unknown
|
|
167
|
+
end
|
|
168
|
+
|
|
169
|
+
def method_block?(node) = METHOD_BLOCK_CALLS.include?(node.name) && RefineCensus.self_call?(node)
|
|
170
|
+
|
|
171
|
+
# The block of a `refine` call: a `refine` written in it (a nested one) is charged where the call is, but `self`
|
|
172
|
+
# is the refinement, so a method defined in it is defined on something the walk cannot name: a singleton method
|
|
173
|
+
# on the refinement itself ({REFINEMENT}, whichever the method the `refine` call sits in, as long as the module it
|
|
174
|
+
# refines for is one), an instance method on an instance of the refined class (the wildcard), not on the module
|
|
175
|
+
# around.
|
|
176
|
+
def refinement_body(parent)
|
|
177
|
+
singleton = module_here?(parent.here) ? REFINEMENT : parent.singleton
|
|
178
|
+
parent.with(instance: refinement_side(parent.instance), singleton: singleton, own_method: false)
|
|
179
|
+
end
|
|
180
|
+
|
|
181
|
+
def refinement_side(side) = side.is_a?(String) || side.is_a?(Array) ? wildcard : side
|
|
182
|
+
|
|
183
|
+
# Is `here` a module a `refine` there refines for (so its block is a refinement's body), or the refinement itself?
|
|
184
|
+
def module_here?(here) = named_module?(here) || here.is_a?(Array) || here == ANONYMOUS || here == REFINEMENT
|
|
185
|
+
|
|
186
|
+
def meta_new_body(node)
|
|
187
|
+
ScopeIndexer.module_new_call?(node) ? module_body(ANONYMOUS) : class_body(false)
|
|
188
|
+
end
|
|
189
|
+
|
|
190
|
+
# A core iteration method on a receiver whose value is a literal in this file: a literal, or a constant, local
|
|
191
|
+
# or instance variable the block answers true for ({.literal_bindings}). Any other name, even on a literal, and
|
|
192
|
+
# any other receiver may be a project method that runs the block under another `self`: on Ruby 4.0.5
|
|
193
|
+
# `Config.each { refine(String) { … } }` with `def self.each(&blk) = Ext.module_eval(&blk)` refines for `Ext`,
|
|
194
|
+
# and so does `[1].configure { … }` with `class Array; def configure(&blk) = Ext.module_eval(&blk); end`.
|
|
195
|
+
def keeps_self?(node)
|
|
196
|
+
return false unless CORE_ITERATORS.include?(node.name)
|
|
197
|
+
|
|
198
|
+
receiver = node.receiver
|
|
199
|
+
return true if literal?(receiver)
|
|
200
|
+
|
|
201
|
+
NAMED_RECEIVERS.any? { |klass| receiver.is_a?(klass) } && block_given? && yield(receiver)
|
|
202
|
+
end
|
|
203
|
+
|
|
204
|
+
def literal?(node) = LITERALS.any? { |klass| node.is_a?(klass) }
|
|
205
|
+
|
|
206
|
+
# A literal, or a literal's `.freeze`.
|
|
207
|
+
def literal_value?(node)
|
|
208
|
+
return true if literal?(node)
|
|
209
|
+
|
|
210
|
+
node.is_a?(Prism::CallNode) && node.name == :freeze && node.arguments.nil? && node.block.nil? &&
|
|
211
|
+
literal?(node.receiver)
|
|
212
|
+
end
|
|
213
|
+
|
|
214
|
+
# The constants (by qualified name), locals and instance variables (by name) that `root` binds, every binding a
|
|
215
|
+
# plain write of a literal ({.literal_value?}). A name any other binding gives a value (a parameter, a
|
|
216
|
+
# multiple assignment, `||=`, `+=`, a `const_set`, a path write `A::K = …`) is not one, wherever in the file
|
|
217
|
+
# it sits.
|
|
218
|
+
def literal_bindings(root)
|
|
219
|
+
literal = Set.new
|
|
220
|
+
other = Set.new
|
|
221
|
+
spoiled = ScopeIndexer.loose_constant_segments(root).dup
|
|
222
|
+
collect_bindings(root, [], literal, other, spoiled)
|
|
223
|
+
(literal - other).reject { |key| key.is_a?(String) && spoiled.include?(key.split("::").last) }.to_set.freeze
|
|
224
|
+
end
|
|
225
|
+
|
|
226
|
+
def collect_bindings(node, prefix, literal, other, spoiled)
|
|
227
|
+
case node
|
|
228
|
+
when Prism::ClassNode, Prism::ModuleNode, Prism::SingletonClassNode
|
|
229
|
+
return collect_declaration_bindings(node, prefix, literal, other, spoiled)
|
|
230
|
+
when Prism::LocalVariableWriteNode, Prism::InstanceVariableWriteNode
|
|
231
|
+
bind(node.value, node.name, literal, other)
|
|
232
|
+
when *VARIABLE_BINDINGS then other << node.name if node.name
|
|
233
|
+
else collect_constant_binding(node, prefix, literal, other, spoiled)
|
|
234
|
+
end
|
|
235
|
+
node.rigor_each_child { |child| collect_bindings(child, prefix, literal, other, spoiled) }
|
|
236
|
+
end
|
|
237
|
+
|
|
238
|
+
def collect_constant_binding(node, prefix, literal, other, spoiled)
|
|
239
|
+
case node
|
|
240
|
+
when Prism::ConstantWriteNode then bind(node.value, (prefix + [node.name.to_s]).join("::"), literal, other)
|
|
241
|
+
when *CONSTANT_BINDINGS then spoiled << node.name.to_s
|
|
242
|
+
when *CONSTANT_PATH_BINDINGS then spoiled << node.target.name.to_s
|
|
243
|
+
end
|
|
244
|
+
end
|
|
245
|
+
|
|
246
|
+
# A `class` / `module` body's constants are its own; a `class << expr` body's are the singleton class's, which
|
|
247
|
+
# no read outside it names.
|
|
248
|
+
def collect_declaration_bindings(node, prefix, literal, other, spoiled)
|
|
249
|
+
if node.is_a?(Prism::SingletonClassNode)
|
|
250
|
+
collect_bindings(node.expression, prefix, literal, other, spoiled)
|
|
251
|
+
inner = prefix + ["<singleton>"]
|
|
252
|
+
else
|
|
253
|
+
inner = Source::ConstantPath.declaration_prefix(prefix, node.constant_path) || prefix
|
|
254
|
+
superclass = node.is_a?(Prism::ClassNode) && node.superclass
|
|
255
|
+
collect_bindings(superclass, prefix, literal, other, spoiled) if superclass
|
|
256
|
+
end
|
|
257
|
+
collect_bindings(node.body, inner, literal, other, spoiled) if node.body
|
|
258
|
+
end
|
|
259
|
+
|
|
260
|
+
def bind(value, key, literal, other) = (literal_value?(value) ? literal : other) << key
|
|
261
|
+
|
|
262
|
+
# The key {.literal_bindings} holds for a receiver read in `prefix`: a constant's innermost lexical candidate (the
|
|
263
|
+
# one Ruby's lookup finds once this file's write defines it), a local's or an instance variable's name.
|
|
264
|
+
def binding_key(receiver, prefix)
|
|
265
|
+
case receiver
|
|
266
|
+
when Prism::ConstantReadNode, Prism::ConstantPathNode
|
|
267
|
+
ScopeIndexer.constant_receiver_candidates(receiver, prefix).first
|
|
268
|
+
else receiver.name
|
|
269
|
+
end
|
|
270
|
+
end
|
|
271
|
+
|
|
272
|
+
# A `*_eval` / `*_exec` block: on `self` (or no receiver) it keeps `self`, on a constant it runs on the module the
|
|
273
|
+
# constant names (any name its spelling can denote), on anything else on a module the walk cannot name. A `def` in
|
|
274
|
+
# an `instance_eval` binds on the receiver's singleton, so its instance side is the receiver itself.
|
|
275
|
+
def eval_body(parent, node, eval_names)
|
|
276
|
+
here = RefineCensus.self_call?(node) ? parent.here : eval_names
|
|
277
|
+
here = here.first if here.is_a?(Array) && here.size == 1
|
|
278
|
+
return context(CLASS, CLASS, CLASS, deferred: parent.deferred) if here == CLASS
|
|
279
|
+
return unknown unless here.is_a?(String) || here.is_a?(Array) || here == ANONYMOUS || here == REFINEMENT
|
|
280
|
+
return unknown if here == wildcard
|
|
281
|
+
|
|
282
|
+
instance = INSTANCE_EVAL_CALLS.include?(node.name) ? here : wildcard
|
|
283
|
+
remote = parent.remote || !RefineCensus.self_call?(node)
|
|
284
|
+
context(here, instance, here, deferred: parent.deferred, remote: remote)
|
|
285
|
+
end
|
|
286
|
+
|
|
287
|
+
# The modules a `*_eval` / `*_exec` call `node` written in `context` at the lexical `prefix` may run its block
|
|
288
|
+
# on, when its receiver is a constant: every name the constant's spelling can denote, innermost first, as another
|
|
289
|
+
# file may declare any of them (`M.module_eval { … }` in `module L` runs on `L::M` where some file declares it);
|
|
290
|
+
# a `self::`-anchored one as the method walk resolves it (`ScopeIndexer.eval_receiver_prefix`). Else nil.
|
|
291
|
+
def eval_receiver_names(node, prefix, context)
|
|
292
|
+
return nil unless RefineCensus::SELF_EVAL_CALLS.include?(node.name) && !RefineCensus.self_call?(node)
|
|
293
|
+
|
|
294
|
+
receiver = node.receiver
|
|
295
|
+
if Source::ConstantPath.qualified_name_or_nil(receiver)
|
|
296
|
+
return ScopeIndexer.constant_receiver_candidates(receiver, prefix)
|
|
297
|
+
end
|
|
298
|
+
|
|
299
|
+
here = context.here
|
|
300
|
+
self_prefix = here.is_a?(String) && here != wildcard ? here.split("::") : []
|
|
301
|
+
resolved = ScopeIndexer.eval_receiver_prefix(node, self_prefix, prefix, unnameable_self: self_prefix.empty?)
|
|
302
|
+
[resolved.join("::")] unless resolved.nil? || resolved.empty?
|
|
303
|
+
end
|
|
304
|
+
|
|
305
|
+
# The modules a context's `refine` call (or, with `literal`, its `:refine` literal) may refine for, as names a
|
|
306
|
+
# table row can hold: the module, each name a constant's spelling can denote, else the wildcard.
|
|
307
|
+
def charged_modules(context, literal: false)
|
|
308
|
+
here = literal ? context.literal : context.here
|
|
309
|
+
case here
|
|
310
|
+
when String then [here]
|
|
311
|
+
when Array then here
|
|
312
|
+
else [wildcard]
|
|
313
|
+
end
|
|
314
|
+
end
|
|
315
|
+
end
|
|
316
|
+
end
|
|
317
|
+
end
|
|
@@ -0,0 +1,350 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Rigor
|
|
4
|
+
module Inference
|
|
5
|
+
module RefinedDispatch
|
|
6
|
+
# Issue #1752 — the lookup of a class object, which Ruby walks like any object's: a refinement of `Object`,
|
|
7
|
+
# `Module`, `Class`, `Kernel` or `BasicObject` answers `Integer.oo`, because a class object is an `Object`.
|
|
8
|
+
#
|
|
9
|
+
# The walk is `C`'s singleton class with the modules it extends, then each superclass's singleton class with its
|
|
10
|
+
# extends, then the instance levels of `Class` (a class only), `Module`, `Object`, `Kernel` and `BasicObject`.
|
|
11
|
+
# A singleton class is a level whose own entry is a {SingletonClass}: a refinement table key names an instance
|
|
12
|
+
# side, so it never matches one (`refine C.singleton_class` records a class-unknown row), and it defines the
|
|
13
|
+
# name through its own singleton methods. An extended module is an instance-side entry, so `refine(Ext)`
|
|
14
|
+
# answers `C.z` where `C` extends `Ext` (Ruby 4.0.5).
|
|
15
|
+
#
|
|
16
|
+
# Every place the walk cannot read makes the whole walk unreadable (nil), where each reader declines: a chain
|
|
17
|
+
# cut at its limit or holding a mixin the tables cannot name, a superclass the tables do not know (`class K <
|
|
18
|
+
# <expr>`, `K = Class.new(Parent)`, an external superclass RBS does not declare), a class some constant write
|
|
19
|
+
# may have made (`Point = Struct.new(:x)`, whose superclass the tables do not record). A singleton level whose
|
|
20
|
+
# class records an `extend` the chain did not place there is read as `mixed` ({.skipped_extend?}).
|
|
21
|
+
module ClassObject
|
|
22
|
+
EMPTY = [].freeze
|
|
23
|
+
# The classes on every class object's lookup: the instance sides that hold `Class#new`, `Module#name` and
|
|
24
|
+
# `Object#tap`. A class no table places among them may still be one a class object reaches only when it is a
|
|
25
|
+
# module or a class the walk cannot place (`ClassObject.reachable?`).
|
|
26
|
+
TAIL_CLASSES = %w[Class Module Object Kernel BasicObject].freeze
|
|
27
|
+
private_constant :EMPTY
|
|
28
|
+
|
|
29
|
+
module_function
|
|
30
|
+
|
|
31
|
+
# `[[level class, entries, mixed, every entry], …]` in {RefinedDispatch.levels}' shape for the class object
|
|
32
|
+
# `class_name`, or nil when the walk cannot be read. A singleton level's class is a {SingletonClass}.
|
|
33
|
+
def levels(scope, class_name)
|
|
34
|
+
environment = scope.environment
|
|
35
|
+
return nil if environment.nil?
|
|
36
|
+
return rbs_levels(scope, class_name) if environment.class_known?(class_name)
|
|
37
|
+
|
|
38
|
+
project_levels(scope, class_name)
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
# RBS's singleton ancestry: each singleton class a level of its own, `mixed` where the project extends it at
|
|
42
|
+
# a position RBS does not record, and each instance side (a declared `extend`, `Class`, `Module`, …) a level
|
|
43
|
+
# `mixed` where the project mixes into it. `from` drops the leading singleton classes a project level already
|
|
44
|
+
# holds.
|
|
45
|
+
def rbs_levels(scope, class_name, from: 0)
|
|
46
|
+
loader = scope.environment&.rbs_loader
|
|
47
|
+
pairs = loader ? loader.singleton_ancestors_for(class_name) : EMPTY
|
|
48
|
+
return nil if pairs.empty?
|
|
49
|
+
|
|
50
|
+
readable(pairs.drop(from).map do |kind, name|
|
|
51
|
+
kind == :instance ? instance_level(scope, name) : singleton_level(scope, name)
|
|
52
|
+
end)
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
# RBS's instance ancestry of `class_name` (`Module`'s, for a module object's tail), each a level.
|
|
56
|
+
def rbs_instance_levels(scope, class_name)
|
|
57
|
+
loader = scope.environment&.rbs_loader
|
|
58
|
+
names = loader ? loader.ancestor_names_for(class_name) : EMPTY
|
|
59
|
+
return nil if names.empty?
|
|
60
|
+
|
|
61
|
+
readable(names.map { |name| instance_level(scope, name) })
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
def readable(levels) = levels.include?(nil) ? nil : levels
|
|
65
|
+
|
|
66
|
+
# A `mixed` level carries the modules the project mixes in beside its own entry, so a refinement of one of them
|
|
67
|
+
# makes the walk past the level {UNKNOWN} (`RefinedDispatch.targeted_after?`) and `call.undefined-method`'s
|
|
68
|
+
# reach holds it. Nil when that mixin chain cannot be read.
|
|
69
|
+
def instance_level(scope, name)
|
|
70
|
+
return [name, [name], false] unless RefinedDispatch.project_mixin?(scope, name)
|
|
71
|
+
|
|
72
|
+
mixins = mixin_entries(scope, name, :instance)
|
|
73
|
+
mixins && [name, [name, *mixins], true]
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
def singleton_level(scope, name)
|
|
77
|
+
return nil if instance_unreadable?(scope, name)
|
|
78
|
+
|
|
79
|
+
own = SingletonClass.new(name)
|
|
80
|
+
return [own, [own], false] unless singleton_mixin?(scope, name)
|
|
81
|
+
|
|
82
|
+
mixins = mixin_entries(scope, name, :singleton)
|
|
83
|
+
mixins && [own, [own, *mixins], true]
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
# The names of the modules on `name`'s own level of its project chain on `side`, an unnamed entry's candidate
|
|
87
|
+
# names included, or nil when the chain is cut or holds a mixin the tables cannot name.
|
|
88
|
+
def mixin_entries(scope, name, side)
|
|
89
|
+
chain = Scope::ResolutionChain.for(scope, name, side, :methods)
|
|
90
|
+
chain.record(scope) if Analysis::DependencyRecorder.active?
|
|
91
|
+
return nil if unreadable?(chain)
|
|
92
|
+
return EMPTY if chain.level_count.zero?
|
|
93
|
+
|
|
94
|
+
names = level_names(chain, 0)
|
|
95
|
+
return nil if side == :singleton && skipped_extend?(scope, name, names)
|
|
96
|
+
|
|
97
|
+
names.reject { |entry| entry == name }
|
|
98
|
+
end
|
|
99
|
+
|
|
100
|
+
# The names on level `index` of `chain` (an unnamed entry's candidate names included), the level's own
|
|
101
|
+
# singleton entry left out.
|
|
102
|
+
def level_names(chain, index)
|
|
103
|
+
chain.level_entries(index).flat_map do |entry|
|
|
104
|
+
next entry.candidates if entry.external?
|
|
105
|
+
|
|
106
|
+
entry.side == :instance ? [entry.name] : EMPTY
|
|
107
|
+
end.uniq
|
|
108
|
+
end
|
|
109
|
+
|
|
110
|
+
# Does `name` record an `extend` that the chain did not place on `name`'s own singleton level (`entries`, its
|
|
111
|
+
# names or candidate lists)? The chain skips an `extend` of a module already on the lookup, as Ruby does, but
|
|
112
|
+
# the tables record `class << self; prepend M` (and `singleton_class.prepend(M)`) as an `extend` too, and
|
|
113
|
+
# Ruby's `prepend` puts the module ahead of the class's own singleton methods again (`class Base3 < Top;
|
|
114
|
+
# class << self; prepend Ext` with `Top` extending `Ext`). Such a level cannot place that module, so it reads as
|
|
115
|
+
# `mixed`.
|
|
116
|
+
def skipped_extend?(scope, name, entries)
|
|
117
|
+
raws = scope.discovered_extends[name]
|
|
118
|
+
return false if raws.nil? || raws.empty?
|
|
119
|
+
|
|
120
|
+
placed = entries.flat_map { |entry| entry.is_a?(Array) ? entry : [entry] }
|
|
121
|
+
raws.any? { |raw| scope.ancestor_name_candidates(name, raw).none? { |candidate| placed.include?(candidate) } }
|
|
122
|
+
end
|
|
123
|
+
|
|
124
|
+
# The project's singleton-side `Scope::ResolutionChain` (`C`'s singleton, its `extend`s' instance chains,
|
|
125
|
+
# then each project superclass's), spliced into RBS's singleton ancestry at an external superclass, or
|
|
126
|
+
# followed by the implicit tail after the last project class.
|
|
127
|
+
def project_levels(scope, class_name)
|
|
128
|
+
chain = Scope::ResolutionChain.for(scope, class_name, :singleton, :methods)
|
|
129
|
+
chain.record(scope) if Analysis::DependencyRecorder.active?
|
|
130
|
+
return nil if unreadable?(chain) || chain.level_count.zero?
|
|
131
|
+
|
|
132
|
+
out = []
|
|
133
|
+
chain.level_count.times do |index|
|
|
134
|
+
level_class = chain.level_classes[index]
|
|
135
|
+
return splice_external(scope, out, chain.level_entries(index)) if level_class.nil?
|
|
136
|
+
return nil if instance_unreadable?(scope, level_class)
|
|
137
|
+
|
|
138
|
+
own = SingletonClass.new(level_class)
|
|
139
|
+
every = chain.level_entries(index).map { |entry| level_entry(scope, entry, own) }
|
|
140
|
+
out << [own, every.grep_v(Array), skipped_extend?(scope, level_class, every), every]
|
|
141
|
+
end
|
|
142
|
+
tail = implicit_tail(scope, chain.level_classes[chain.level_count - 1])
|
|
143
|
+
tail && out.concat(tail)
|
|
144
|
+
end
|
|
145
|
+
|
|
146
|
+
# A chain cut at its limit, or holding a mixin the tables cannot name.
|
|
147
|
+
def unreadable?(chain) = chain.truncated? || chain.wildcard_mixin?
|
|
148
|
+
|
|
149
|
+
# Is `name`'s instance chain unreadable? A mixin the tables cannot name there includes the one an
|
|
150
|
+
# `included` hook makes (`base.extend(ClassMethods)`, `base.singleton_class.include(CM)`), which installs
|
|
151
|
+
# singleton methods on `name` that its singleton chain never records.
|
|
152
|
+
def instance_unreadable?(scope, name)
|
|
153
|
+
chain = Scope::ResolutionChain.for(scope, name, :instance, :methods)
|
|
154
|
+
chain.record(scope) if Analysis::DependencyRecorder.active?
|
|
155
|
+
unreadable?(chain)
|
|
156
|
+
end
|
|
157
|
+
|
|
158
|
+
def level_entry(scope, entry, own)
|
|
159
|
+
return own if !entry.external? && entry.side == :singleton && entry.name == own.of
|
|
160
|
+
return SingletonClass.new(entry.name) if !entry.external? && entry.side == :singleton
|
|
161
|
+
|
|
162
|
+
RefinedDispatch.entry_name(scope, entry) || RefinedDispatch.unnamed(entry)
|
|
163
|
+
end
|
|
164
|
+
|
|
165
|
+
# An external superclass stands for its whole singleton ancestry, which RBS gives when it declares the class.
|
|
166
|
+
#
|
|
167
|
+
# ADR-46 — a project superclass with no methods is not admitted by the `:methods` resolver, so it is external
|
|
168
|
+
# here and the walk is unreadable; each candidate's `class:` edge lets a new file that gives it a method (and so
|
|
169
|
+
# a project entry) re-check the consumer.
|
|
170
|
+
def splice_external(scope, out, entries)
|
|
171
|
+
external = entries.find(&:external?)
|
|
172
|
+
if Analysis::DependencyRecorder.active?
|
|
173
|
+
external&.candidates&.each { |candidate| Analysis::DependencyRecorder.read_last_segment(:class, candidate) }
|
|
174
|
+
end
|
|
175
|
+
name = external&.candidates&.find { |candidate| scope.environment&.class_known?(candidate) }
|
|
176
|
+
tail = name && rbs_levels(scope, name)
|
|
177
|
+
tail && out.concat(tail)
|
|
178
|
+
end
|
|
179
|
+
|
|
180
|
+
# What follows the last project class on the walk: `Module` onwards for a module; RBS's singleton ancestry
|
|
181
|
+
# past the class's own singleton for a class RBS declares (a reopened core class, a `sig/` class);
|
|
182
|
+
# `Object`'s for a class with no superclass written. Nil where the superclass is not known: one the tables
|
|
183
|
+
# record as unknown, and one a constant write may have given the class (a class only a constant write names
|
|
184
|
+
# carries no superclass entry, and a reopening keeps the superclass that write made).
|
|
185
|
+
def implicit_tail(scope, name)
|
|
186
|
+
return rbs_instance_levels(scope, "Module") if module?(scope, name)
|
|
187
|
+
return nil if unknown_superclass?(scope, name)
|
|
188
|
+
return rbs_levels(scope, name, from: 1) if scope.environment&.class_known?(name)
|
|
189
|
+
|
|
190
|
+
rbs_levels(scope, "Object")
|
|
191
|
+
end
|
|
192
|
+
|
|
193
|
+
def module?(scope, name)
|
|
194
|
+
scope.discovered_parameter_envelopes[name]&.key?(Scope::DiscoveryIndex::ENVELOPE_MODULE_MARK) ||
|
|
195
|
+
scope.environment&.rbs_module?(name) || false
|
|
196
|
+
end
|
|
197
|
+
|
|
198
|
+
def unknown_superclass?(scope, name)
|
|
199
|
+
superclasses = scope.discovered_superclasses
|
|
200
|
+
return true if superclasses.key?(name) && superclasses[name].nil?
|
|
201
|
+
return true if scope.discovered_parameter_envelopes[name]&.key?(
|
|
202
|
+
Scope::DiscoveryIndex::ENVELOPE_META_SUPERCLASS_MARK
|
|
203
|
+
)
|
|
204
|
+
|
|
205
|
+
Analysis::DependencyRecorder.read_last_segment(:constant, name) if Analysis::DependencyRecorder.active?
|
|
206
|
+
!scope.bound_constant_names(name).empty?
|
|
207
|
+
end
|
|
208
|
+
|
|
209
|
+
# Does the project mix a module into `name`'s singleton class, at a position RBS does not record?
|
|
210
|
+
def singleton_mixin?(scope, name)
|
|
211
|
+
Analysis::DependencyRecorder.read_last_segment(:class, name) if Analysis::DependencyRecorder.active?
|
|
212
|
+
return true if scope.discovered_extends.key?(name)
|
|
213
|
+
|
|
214
|
+
Scope::ResolutionChain.for(scope, name, :singleton, :methods).wildcard_mixin?
|
|
215
|
+
end
|
|
216
|
+
|
|
217
|
+
# Does the singleton class of `name` define `method_name` itself: a singleton method the project writes in
|
|
218
|
+
# `name`'s own body (`def self.x`, `class << self`, an `attr_*` there), or one RBS declares on `name`'s
|
|
219
|
+
# singleton? A method the extends fold copied onto the singleton from a module `name` extends is that
|
|
220
|
+
# module's, found where the module sits.
|
|
221
|
+
def defines?(scope, name, method_name)
|
|
222
|
+
return true if own_singleton_definer?(scope, name, method_name)
|
|
223
|
+
|
|
224
|
+
definition = ExternalAncestorResolution.method_definition(name, method_name, :singleton, scope: scope)
|
|
225
|
+
ExternalAncestorResolution.declared_on_class?(definition, name)
|
|
226
|
+
end
|
|
227
|
+
|
|
228
|
+
# A singleton `def` is the class's own unless it is the def a module the class extends holds, which the extends
|
|
229
|
+
# fold copied ({.fold_copy?}). A singleton name without a node of its own (`attr_reader` or `define_method` in
|
|
230
|
+
# `class << self`) counts when the existence table holds it and the class's envelope table records it, which
|
|
231
|
+
# the fold never writes. A `define_singleton_method` in the body records only the opaque envelope entry, so it
|
|
232
|
+
# is no definer here; the typed arm reads that entry as a rewritten singleton name ({.rewritten?}).
|
|
233
|
+
def own_singleton_definer?(scope, name, method_name)
|
|
234
|
+
node = scope.singleton_def_for(name, method_name)
|
|
235
|
+
return true if node && !fold_copy?(scope, name, method_name)
|
|
236
|
+
return false unless scope.discovered_method?(name, method_name, :singleton)
|
|
237
|
+
|
|
238
|
+
node.nil? || scope.parameter_envelopes_of(name).key?([:singleton, method_name.to_sym])
|
|
239
|
+
end
|
|
240
|
+
|
|
241
|
+
# Is `name`'s singleton `def` of the name the receiverless instance `def` of the module its `Module.nesting`
|
|
242
|
+
# starts with, which the extends fold copied? The node is that module's own
|
|
243
|
+
# (`SingletonHookDecline.copied_from?`), or, where the two tables came from two parses of the file, one with
|
|
244
|
+
# its location and text, and no site of a singleton `def` of the name is recorded for `name` (the fold copies
|
|
245
|
+
# nodes, not sites).
|
|
246
|
+
def fold_copy?(scope, name, method_name)
|
|
247
|
+
head = SingletonHookDecline.def_head(scope, name, method_name)
|
|
248
|
+
return false if head.nil? || head == name
|
|
249
|
+
return true if SingletonHookDecline.copied_from?(scope, head, method_name, name)
|
|
250
|
+
|
|
251
|
+
node = scope.singleton_def_for(name, method_name)
|
|
252
|
+
source = scope.user_def_for(head, method_name)
|
|
253
|
+
!node.nil? && node.receiver.nil? && !source.nil? && same_def?(node, source) &&
|
|
254
|
+
scope.discovery.discovered_singleton_def_sources.dig(name, method_name.to_sym).nil?
|
|
255
|
+
end
|
|
256
|
+
|
|
257
|
+
def same_def?(node, other)
|
|
258
|
+
node.location.start_offset == other.location.start_offset &&
|
|
259
|
+
node.location.end_offset == other.location.end_offset && node.slice == other.slice
|
|
260
|
+
end
|
|
261
|
+
|
|
262
|
+
# The typed arm's answer at a singleton level (`entries` is every entry, an unnamed one kept as its candidate
|
|
263
|
+
# names): {Winner}, {UNKNOWN}, nil (the class object's own lookup answers), `:continue`, or `:continue_unsure`
|
|
264
|
+
# when only an unnamed entry may answer, so a refinement the walk reaches later is {UNKNOWN}
|
|
265
|
+
# (`RefinedDispatch.walk`). The tables record `class << self; prepend M` (and `singleton_class.prepend(M)`) as
|
|
266
|
+
# an `extend`, so a module on the level may sit ahead of the singleton class or of another module: one entry
|
|
267
|
+
# that answers decides, several that answer decide only when none of them is refined.
|
|
268
|
+
def walk_level(scope, entries, method_name, targets, unread)
|
|
269
|
+
answers = entries.filter_map { |entry| entry_answer(scope, entry, method_name, targets, unread) }
|
|
270
|
+
return :continue if answers.empty?
|
|
271
|
+
|
|
272
|
+
known = answers.reject { |answer| answer == UNNAMED }
|
|
273
|
+
return :continue_unsure if known.empty?
|
|
274
|
+
return known.first.answer if answers.size == 1
|
|
275
|
+
|
|
276
|
+
known.all? { |answer| answer.answer.nil? } ? nil : UNKNOWN
|
|
277
|
+
end
|
|
278
|
+
|
|
279
|
+
Answer = Data.define(:answer)
|
|
280
|
+
# An entry no table names (an `extend` of a module from outside the analysed paths): it may define the name.
|
|
281
|
+
UNNAMED = Answer.new(:unnamed)
|
|
282
|
+
private_constant :Answer, :UNNAMED
|
|
283
|
+
|
|
284
|
+
def entry_answer(scope, entry, method_name, targets, unread)
|
|
285
|
+
if entry.is_a?(Array)
|
|
286
|
+
refined = entry.any? { |candidate| targets.key?(candidate) || unread.key?(candidate) }
|
|
287
|
+
return refined ? Answer.new(UNKNOWN) : UNNAMED
|
|
288
|
+
end
|
|
289
|
+
|
|
290
|
+
if entry.is_a?(SingletonClass)
|
|
291
|
+
return Answer.new(UNKNOWN) if rewritten?(scope, entry.of, method_name)
|
|
292
|
+
else
|
|
293
|
+
return Answer.new(UNKNOWN) if unread.key?(entry)
|
|
294
|
+
|
|
295
|
+
refining = targets[entry]
|
|
296
|
+
return Answer.new(Winner.new(module_name: refining, refined_class: entry)) if refining
|
|
297
|
+
end
|
|
298
|
+
Answer.new(nil) if RefinedDispatch.defines?(scope, entry, method_name)
|
|
299
|
+
end
|
|
300
|
+
|
|
301
|
+
# Does the project rewrite `name`'s singleton methods in a way the walk cannot read: the class's dynamic mark
|
|
302
|
+
# (a `class_eval`, a computed definer, a `send`), or an opaque envelope entry for the singleton name (a
|
|
303
|
+
# `define_singleton_method`, a `def Const.x` written outside `name`'s body, an alias or a wrapping macro)?
|
|
304
|
+
def rewritten?(scope, name, method_name)
|
|
305
|
+
bucket = scope.parameter_envelopes_of(name)
|
|
306
|
+
bucket.key?(Scope::DiscoveryIndex::ENVELOPE_DYNAMIC_MARK) ||
|
|
307
|
+
Source::ParameterEnvelope.opaque?(bucket[[:singleton, method_name.to_sym]])
|
|
308
|
+
end
|
|
309
|
+
|
|
310
|
+
# Every class and module whose refinements a call on the class object `class_name` may reach, for
|
|
311
|
+
# `call.undefined-method`'s decline: each name on the levels (an unnamed entry's candidates and a `mixed`
|
|
312
|
+
# level's project mixins included), and the RBS ancestry of a module an extend names that the project does
|
|
313
|
+
# not declare. Nil when the walk cannot be read.
|
|
314
|
+
def reach(scope, class_name)
|
|
315
|
+
levels = levels(scope, class_name)
|
|
316
|
+
return nil if levels.nil?
|
|
317
|
+
|
|
318
|
+
names = Set.new
|
|
319
|
+
levels.each do |_level_class, entries, _mixed, every|
|
|
320
|
+
(every || entries).each { |entry| add_reach(scope, names, entry) }
|
|
321
|
+
end
|
|
322
|
+
names
|
|
323
|
+
end
|
|
324
|
+
|
|
325
|
+
def add_reach(scope, names, entry)
|
|
326
|
+
return names.merge(entry) if entry.is_a?(Array)
|
|
327
|
+
return if entry.is_a?(SingletonClass)
|
|
328
|
+
|
|
329
|
+
names << entry
|
|
330
|
+
return if scope.discovered_classes.key?(entry)
|
|
331
|
+
|
|
332
|
+
loader = scope.environment&.rbs_loader
|
|
333
|
+
names.merge(loader.ancestor_names_for(entry)) if loader
|
|
334
|
+
end
|
|
335
|
+
|
|
336
|
+
# Can a refined-class key sit on some class object's lookup the walk could not read? Every class object
|
|
337
|
+
# reaches the {TAIL_CLASSES} and whatever modules it extends, so only a class known to be none of those is
|
|
338
|
+
# out of reach: a project class that is not a module, or one RBS declares as a class. A key nothing declares
|
|
339
|
+
# may be a module.
|
|
340
|
+
def reachable?(scope, key)
|
|
341
|
+
return true if TAIL_CLASSES.include?(key)
|
|
342
|
+
return false if scope.discovered_classes.key?(key) && !module?(scope, key)
|
|
343
|
+
|
|
344
|
+
environment = scope.environment
|
|
345
|
+
!(environment&.class_known?(key) && !environment.rbs_module?(key))
|
|
346
|
+
end
|
|
347
|
+
end
|
|
348
|
+
end
|
|
349
|
+
end
|
|
350
|
+
end
|