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,54 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Rigor
|
|
4
|
+
module Inference
|
|
5
|
+
class InEffectRefinements
|
|
6
|
+
# Issue #1664 — the `def`s one file's refine bodies define, keyed by refining module, refined class (each name
|
|
7
|
+
# the `refine` argument's spelling can denote) and method, with the `Module.nesting` each is written in. The
|
|
8
|
+
# last `def` of a name wins, as Ruby's method table keeps it.
|
|
9
|
+
class RefineDefs
|
|
10
|
+
# ADR-121 WD7 — what {#lookup} answers for a name whose last definer has no body (`define_method`, `attr_*`,
|
|
11
|
+
# `undef`): the search for a body ends there, so the typed arm answers `Dynamic[top]` rather than another
|
|
12
|
+
# file's `def`.
|
|
13
|
+
BODILESS = :bodiless
|
|
14
|
+
# Issue #1749 — what {#lookup} answers for a name whose refinement the walk cannot attribute: one from a
|
|
15
|
+
# `refine` that runs inside a method (it exists once the method has run, replacing what a refine body wrote for
|
|
16
|
+
# the name, or adding nothing if it never runs, and `using` sees it only if it runs first), or from an eval on
|
|
17
|
+
# a constant that several names denote (it runs on one of them, which another file decides, and replaces or is
|
|
18
|
+
# replaced by a `def` of the name in a refine body of the module, whichever runs first). A `def` of the name
|
|
19
|
+
# in a module-body refine, in this file or another, cannot say which one answers, so the name is never typed
|
|
20
|
+
# from any body: the lookup ends there across every file that declares the module, and unlike {BODILESS} a
|
|
21
|
+
# later record does not replace it.
|
|
22
|
+
UNDECIDED = :undecided
|
|
23
|
+
|
|
24
|
+
attr_reader :nestings
|
|
25
|
+
|
|
26
|
+
def initialize
|
|
27
|
+
@nodes = {}
|
|
28
|
+
@nestings = {}.compare_by_identity
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
# `owners` are the refining modules (several where an eval on a constant may run on any of its names), each
|
|
32
|
+
# filed under every class of `targets`. `name` is the method `def_node` answers for: its own name, or an
|
|
33
|
+
# alias's (ADR-121 WD7). A nil `def_node` is a later definer with no body (`define_method`, `attr_*`, `undef`),
|
|
34
|
+
# which replaces the earlier `def`. A name whose refinement the walk cannot attribute keeps no body and is
|
|
35
|
+
# {UNDECIDED}: several owners (Ruby runs the body on one of them only), or `created`, true for a `refine` that
|
|
36
|
+
# runs inside a method.
|
|
37
|
+
def record(owners, targets, name, def_node, nesting, created: false)
|
|
38
|
+
undecided = created || owners.size > 1
|
|
39
|
+
def_node = nil if undecided
|
|
40
|
+
owners.each do |owner|
|
|
41
|
+
by_class = (@nodes[owner] ||= {})
|
|
42
|
+
targets.each do |class_name|
|
|
43
|
+
by_name = (by_class[class_name] ||= {})
|
|
44
|
+
by_name[name] = undecided ? UNDECIDED : (def_node || BODILESS) unless by_name[name] == UNDECIDED
|
|
45
|
+
end
|
|
46
|
+
end
|
|
47
|
+
@nestings[def_node] = nesting if def_node
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
def lookup(module_name, class_name, method_name) = @nodes.dig(module_name, class_name, method_name)
|
|
51
|
+
end
|
|
52
|
+
end
|
|
53
|
+
end
|
|
54
|
+
end
|
|
@@ -0,0 +1,282 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Rigor
|
|
4
|
+
module Inference
|
|
5
|
+
class InEffectRefinements
|
|
6
|
+
# Issue #1673 — the answers that need the project as well as the file: a `using`'s include expansion, the
|
|
7
|
+
# refining modules of a name, and the typer's entry ({Scope#in_effect_refinements}). Each records the
|
|
8
|
+
# dependencies its answer reads (ADR-46), so a warm run answers as a cold one.
|
|
9
|
+
module ScopeReads
|
|
10
|
+
# The in-effect refinements at `node` as {Scope#in_effect_refinements} answers them: the file's list from
|
|
11
|
+
# the query `Inference::ScopeIndexer` stamps on the discovery index, each `using` expanded through
|
|
12
|
+
# {.activated_modules}, then `declared`. A scope no index stamped answers `declared` alone.
|
|
13
|
+
def for_node(scope, node, declared = EMPTY)
|
|
14
|
+
query = scope.discovery.in_effect_refinements
|
|
15
|
+
return EMPTY if declared.empty? && (query.nil? || query.empty?)
|
|
16
|
+
return query.for_node(node, declared) { |name| activated_modules(scope, name) } if query
|
|
17
|
+
|
|
18
|
+
declared.uniq.freeze
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
# ADR-121 WD7 — the in-effect list at `offset` of `query`'s file with no block source, each `using` expanded
|
|
22
|
+
# through {.activated_modules}: what the check rules read through `Analysis::CheckRules::LexicalMethodSites`,
|
|
23
|
+
# and what {.for_node} answers the typer for the same node with no declared module, so the two agree.
|
|
24
|
+
def lexical_list(scope, query, offset)
|
|
25
|
+
query.at(offset) { |name| activated_modules(scope, name) }
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
# Issue #1664 — the `def` a refine body gives `method_name` on `class_name` for the refining module
|
|
29
|
+
# `module_name`, or nil where none can be read: this file's own refine body first, else one in a file that
|
|
30
|
+
# declares the module (`Scope#discovered_class_sources`), parsed once a run. A gem refinement, a module a
|
|
31
|
+
# `Module.new` write names in another file, or an anonymous module answer nil, which the typed arm reads as
|
|
32
|
+
# an unreadable body (`Dynamic[top]`). Issue #1749 — so does a name {RefineDefs::UNDECIDED} in this file or in
|
|
33
|
+
# any file that declares the module (a `refine` that runs inside a method, or an eval on a constant that
|
|
34
|
+
# several names denote): which refinement answers depends on an order and a module the walk cannot decide, and
|
|
35
|
+
# it can replace a module-body `def` of the name wherever it is written, so no body of the name is read. The
|
|
36
|
+
# consumer's `refinement:<name>` edge covers an edit to that body (`Incremental.changed_refinement_names`).
|
|
37
|
+
def refinement_def(scope, module_name, class_name, method_name)
|
|
38
|
+
refinement_def_with_query(scope, module_name, class_name, method_name).first
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
# {.refinement_def} as `[def_node, query]`, where `query` is the other file's {InEffectRefinements} the body was
|
|
42
|
+
# found in, or nil for this file's own body (or none): the typed arm re-types a foreign body under that file's
|
|
43
|
+
# in-effect refinements.
|
|
44
|
+
def refinement_def_with_query(scope, module_name, class_name, method_name)
|
|
45
|
+
chosen = nil
|
|
46
|
+
each_declaring_query(scope, module_name) do |query, foreign|
|
|
47
|
+
found = query&.refinement_def(module_name, class_name, method_name)
|
|
48
|
+
return NO_DEF if found == RefineDefs::UNDECIDED
|
|
49
|
+
next if found.nil? || chosen
|
|
50
|
+
return NO_DEF if found == RefineDefs::BODILESS
|
|
51
|
+
|
|
52
|
+
chosen = [found, (query if foreign)]
|
|
53
|
+
end
|
|
54
|
+
chosen || NO_DEF
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
# This file's query, then one per file that declares `module_name`.
|
|
58
|
+
def each_declaring_query(scope, module_name)
|
|
59
|
+
yield scope.discovery.in_effect_refinements, false
|
|
60
|
+
(scope.discovered_class_sources[module_name] || EMPTY).each do |path|
|
|
61
|
+
yield DefNodeResolver.refinement_query(path), true
|
|
62
|
+
end
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
NO_DEF = [nil, nil].freeze
|
|
66
|
+
private_constant :NO_DEF
|
|
67
|
+
|
|
68
|
+
# Issue #1689 — {InEffectRefinements#class_body_refine?} on the query stamped for `scope`'s file; false where no
|
|
69
|
+
# index stamped one, which keeps the call's refine-body reading.
|
|
70
|
+
def class_body_refine?(scope, call_node)
|
|
71
|
+
query = scope.discovery.in_effect_refinements
|
|
72
|
+
!query.nil? && query.class_body_refine?(call_node)
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
# Issue #1671 — the modules whose refinements `using name` puts in effect, in activation order: the project
|
|
76
|
+
# modules on `name`'s instance-side `Scope::ResolutionChain` (what a module includes and prepends,
|
|
77
|
+
# transitively) in reverse ancestor order, each at its first position, so the module's own refinements
|
|
78
|
+
# follow its includes and a module it prepends follows it and wins. CRuby's `using_module_recursive` walks
|
|
79
|
+
# `RCLASS_SUPER` to its end before each link, and a prepended module sits above the module's origin. `name`
|
|
80
|
+
# itself goes last when the chain does not list it. Nil, read as "any module may be in effect", when the
|
|
81
|
+
# chain may hold a module it does not list: it was cut at its limit, or a module on it records a mixin the
|
|
82
|
+
# tables cannot name.
|
|
83
|
+
#
|
|
84
|
+
# ADR-121 WD7 — {UNDECLARED} when nothing declares `name` ({.declared_module?}), which drops it from a
|
|
85
|
+
# `using`'s candidates (A1). A chain entry the project does not declare is dropped when it is a core or
|
|
86
|
+
# stdlib module (the core and standard-library signatures the rbs gem ships declare no refinements); any
|
|
87
|
+
# other enters by its spelling, which {.opaque_module?} then rejects, so a module included from a gem or from
|
|
88
|
+
# a file outside the analysed paths may refine anything.
|
|
89
|
+
#
|
|
90
|
+
# ADR-46 — the answer reads include edges declared in other files, so it depends on every file that
|
|
91
|
+
# declares a module on the chain, and on the existence of each one's name: a new file reopening `name`, or
|
|
92
|
+
# declaring an included module the project did not declare before, re-checks the consumer through
|
|
93
|
+
# `class:<name>`.
|
|
94
|
+
def activated_modules(scope, name)
|
|
95
|
+
unless declared_module?(scope, name)
|
|
96
|
+
Analysis::DependencyRecorder.read_last_segment(:class, name) if Analysis::DependencyRecorder.active?
|
|
97
|
+
return UNDECLARED
|
|
98
|
+
end
|
|
99
|
+
|
|
100
|
+
chain = Scope::ResolutionChain.for(scope, name, :instance, :constants)
|
|
101
|
+
record_chain(scope, name, chain) if Analysis::DependencyRecorder.active?
|
|
102
|
+
return nil if chain.truncated? || chain.wildcard_mixin?
|
|
103
|
+
|
|
104
|
+
modules = chain.entries.reverse_each.filter_map { |entry| activated_entry(scope, entry) }.uniq
|
|
105
|
+
modules.include?(name) ? modules : modules << name
|
|
106
|
+
end
|
|
107
|
+
|
|
108
|
+
# ADR-121 WD7 (A4) — can Rigor not read every refinement of `name`, a module in an in-effect list? True when
|
|
109
|
+
# the project does not declare it (a gem's module, a module required from outside the analysed paths, or
|
|
110
|
+
# one only an `Object.const_set` makes), unless it is a core or stdlib module, which refines nothing; and
|
|
111
|
+
# true when a targets-wildcard row (`{"*" => {"*" => [...]}}`) lists it or the wildcard, which some unreadable
|
|
112
|
+
# `refine` wrote. Every reader that declines on an opaque module asks this, and nothing else. Gem modules
|
|
113
|
+
# stay opaque even when the gem source walker read them.
|
|
114
|
+
#
|
|
115
|
+
# ADR-46 — the answer moves when a file declares or stops declaring the module (`class:<last segment>`) or
|
|
116
|
+
# when a targets-wildcard row appears or vanishes (`refinement:*`).
|
|
117
|
+
def opaque_module?(scope, name)
|
|
118
|
+
if Analysis::DependencyRecorder.active?
|
|
119
|
+
Analysis::DependencyRecorder.read_last_segment(:class, name)
|
|
120
|
+
Analysis::DependencyRecorder.read_name(:refinement, WILDCARD)
|
|
121
|
+
end
|
|
122
|
+
unread = scope.discovered_refinements.dig(WILDCARD, WILDCARD)
|
|
123
|
+
return true if unread && (unread.include?(name) || unread.include?(WILDCARD))
|
|
124
|
+
return false if project_module?(scope, name)
|
|
125
|
+
|
|
126
|
+
!core_or_stdlib_module?(scope, name)
|
|
127
|
+
end
|
|
128
|
+
|
|
129
|
+
# Does `list` (an in-effect list, the unknown marker skipped) hold an {.opaque_module?}?
|
|
130
|
+
def opaque_in?(scope, list)
|
|
131
|
+
list.any? { |name| name != UNKNOWN && opaque_module?(scope, name) }
|
|
132
|
+
end
|
|
133
|
+
|
|
134
|
+
# ADR-121 WD7 (A1) — does the project, RBS or an opted-in gem declare `name`? A `using`'s lexical candidates
|
|
135
|
+
# that nothing declares are not the module Ruby's lookup finds, so they leave the list.
|
|
136
|
+
def declared_module?(scope, name)
|
|
137
|
+
return true if project_module?(scope, name)
|
|
138
|
+
return true if scope.environment&.class_known?(name)
|
|
139
|
+
|
|
140
|
+
gem_module?(scope, name)
|
|
141
|
+
end
|
|
142
|
+
|
|
143
|
+
# Issue #1120 — every module that refines `method_name` into `class_name` or one of its ancestors (a
|
|
144
|
+
# refinement of `Object` reaches a `String` receiver), or nil when none does. The table is empty on a
|
|
145
|
+
# project that refines nothing, which answers without a lookup.
|
|
146
|
+
#
|
|
147
|
+
# ADR-121 WD7 — a row the walk could not read counts too: a names-wildcard row of the class (the body may
|
|
148
|
+
# define any name), and a row whose class the walk could not name ({.class_wildcard_key?}), which reaches
|
|
149
|
+
# every receiver.
|
|
150
|
+
#
|
|
151
|
+
# ADR-46 — the answer is a function of every refinement of this name in the project, so the consumer
|
|
152
|
+
# depends on the name whichever way it answers; a refine body edited in another file must re-check it
|
|
153
|
+
# (`IncrementalSession#refinement_affected`). It also depends on the wildcard rows (`refinement:*`).
|
|
154
|
+
def refining_modules(scope, class_name, method_name)
|
|
155
|
+
record_refinement_names(method_name)
|
|
156
|
+
refinements = scope.discovered_refinements
|
|
157
|
+
return nil if refinements.empty?
|
|
158
|
+
|
|
159
|
+
modules = nil
|
|
160
|
+
refinements.each do |refined, methods|
|
|
161
|
+
names = methods[method_name]
|
|
162
|
+
unread = methods[WILDCARD]
|
|
163
|
+
next if names.nil? && unread.nil?
|
|
164
|
+
next unless refined_receiver_class?(scope, class_name, refined) || class_wildcard_key?(scope, refined)
|
|
165
|
+
|
|
166
|
+
modules = (modules || []).concat(names || EMPTY, unread || EMPTY)
|
|
167
|
+
end
|
|
168
|
+
modules
|
|
169
|
+
end
|
|
170
|
+
|
|
171
|
+
# ADR-121 WD7 — {.refining_modules} for a receiver the class rows do not reach (a class object): only the rows
|
|
172
|
+
# whose class the walk could not name ({.class_wildcard_key?}), for the name or as a names-wildcard.
|
|
173
|
+
def class_unknown_refining_modules(scope, method_name)
|
|
174
|
+
record_refinement_names(method_name)
|
|
175
|
+
modules = nil
|
|
176
|
+
scope.discovered_refinements.each do |refined, methods|
|
|
177
|
+
names = methods[method_name]
|
|
178
|
+
unread = methods[WILDCARD]
|
|
179
|
+
next if (names.nil? && unread.nil?) || !class_wildcard_key?(scope, refined)
|
|
180
|
+
|
|
181
|
+
modules = (modules || []).concat(names || EMPTY, unread || EMPTY)
|
|
182
|
+
end
|
|
183
|
+
modules
|
|
184
|
+
end
|
|
185
|
+
|
|
186
|
+
# ADR-121 WD7 (A5) — the name edges a consumer of the refinement table records: the method's, and the
|
|
187
|
+
# wildcard's, so a wildcard row appearing or vanishing in another file re-checks it.
|
|
188
|
+
def record_refinement_names(method_name)
|
|
189
|
+
return unless Analysis::DependencyRecorder.active?
|
|
190
|
+
|
|
191
|
+
Analysis::DependencyRecorder.read_name(:refinement, method_name)
|
|
192
|
+
Analysis::DependencyRecorder.read_name(:refinement, WILDCARD)
|
|
193
|
+
end
|
|
194
|
+
|
|
195
|
+
# ADR-121 WD7 — is the refined-class key `refined` a class the walk could not name: the wildcard, or (A6) a
|
|
196
|
+
# name no class declaration or RBS knows, of which some segment is a constant a write binds: a project
|
|
197
|
+
# constant write (`K = String; refine(K)`), by the last segment the project's census keys writes on, or one an
|
|
198
|
+
# opted-in gem's files make (`DependencySourceInference::Index#bound_constant?`, issue #1814), by the name the
|
|
199
|
+
# write binds, asked for each prefix of the key (`Shared::Target` for `module Shared; Target = String; end` in
|
|
200
|
+
# a gem; `Pag::Enc` for the gem's own `module Pag; Enc = ::Encoding; refine(Enc::Converter)`), so a gem
|
|
201
|
+
# constant that merely shares a core class's name (`Names::String`) moves no sibling key (`Ext::String`).
|
|
202
|
+
# Every segment counts, not only the last, so a prefix alias (`Enc = ::Encoding; refine(Enc::Converter)`,
|
|
203
|
+
# #1815) reads the same way. A key that is a core or declared class never moves, whatever a file writes under
|
|
204
|
+
# its name. A constant nothing binds (a gem's class with no RBS) keeps a normal row, which matches no receiver.
|
|
205
|
+
# Each segment is a `constant:` dependency, so a file that starts or stops binding any of them re-checks the
|
|
206
|
+
# consumer.
|
|
207
|
+
def class_wildcard_key?(scope, refined)
|
|
208
|
+
return true if refined == WILDCARD
|
|
209
|
+
return false if scope.discovered_classes.key?(refined) || scope.environment&.class_known?(refined)
|
|
210
|
+
|
|
211
|
+
segments = refined.split("::").reject(&:empty?)
|
|
212
|
+
if Analysis::DependencyRecorder.active?
|
|
213
|
+
Analysis::DependencyRecorder.read_last_segment(:class, refined)
|
|
214
|
+
segments.each { |segment| Analysis::DependencyRecorder.read_last_segment(:constant, segment) }
|
|
215
|
+
end
|
|
216
|
+
segments.any? { |segment| !scope.bound_constant_names(segment).empty? } || gem_bound_key?(scope, segments)
|
|
217
|
+
end
|
|
218
|
+
|
|
219
|
+
# The gem half of {.class_wildcard_key?}: does an opted-in gem bind some prefix of the key (issue #1814)? A
|
|
220
|
+
# leading `Object::` spells the top level (`Object::Shared::Target` is `Shared::Target`), so it is dropped.
|
|
221
|
+
def gem_bound_key?(scope, segments)
|
|
222
|
+
index = scope.environment&.dependency_source_index
|
|
223
|
+
return false if index.nil? || !index.binds_constants?
|
|
224
|
+
|
|
225
|
+
segments = segments.drop_while { |segment| segment == "Object" }
|
|
226
|
+
segments.each_index.any? { |i| index.bound_constant?(segments[0..i].join("::")) }
|
|
227
|
+
end
|
|
228
|
+
|
|
229
|
+
private
|
|
230
|
+
|
|
231
|
+
WILDCARD = Scope::DiscoveryIndex::REFINEMENT_WILDCARD
|
|
232
|
+
private_constant :WILDCARD
|
|
233
|
+
|
|
234
|
+
def activated_entry(scope, entry)
|
|
235
|
+
return entry.name unless entry.external?
|
|
236
|
+
|
|
237
|
+
candidates = entry.candidates
|
|
238
|
+
return nil if candidates.any? { |candidate| core_or_stdlib_module?(scope, candidate) }
|
|
239
|
+
|
|
240
|
+
candidates.first || entry.raw
|
|
241
|
+
end
|
|
242
|
+
|
|
243
|
+
def project_module?(scope, name)
|
|
244
|
+
scope.discovered_classes.key?(name) || !(scope.discovered_class_sources[name] || EMPTY).empty?
|
|
245
|
+
end
|
|
246
|
+
|
|
247
|
+
def core_or_stdlib_module?(scope, name)
|
|
248
|
+
scope.environment&.rbs_loader&.core_or_stdlib_class?(name) || false
|
|
249
|
+
end
|
|
250
|
+
|
|
251
|
+
# A module an opted-in gem's source declares: one its catalogue files a method under, or one its refinement
|
|
252
|
+
# rows name as a refining module.
|
|
253
|
+
def gem_module?(scope, name)
|
|
254
|
+
return true if scope.environment&.dependency_source_index&.gem_for(name)
|
|
255
|
+
|
|
256
|
+
scope.discovered_refinements.each_value.any? do |methods|
|
|
257
|
+
methods.each_value.any? { |modules| modules.include?(name) }
|
|
258
|
+
end
|
|
259
|
+
end
|
|
260
|
+
|
|
261
|
+
def refined_receiver_class?(scope, class_name, refined)
|
|
262
|
+
return true if class_name == refined
|
|
263
|
+
|
|
264
|
+
environment = scope.environment
|
|
265
|
+
!environment.nil? && environment.class_ordering(class_name, refined) == :subclass
|
|
266
|
+
end
|
|
267
|
+
|
|
268
|
+
def record_chain(scope, name, chain)
|
|
269
|
+
chain.record(scope)
|
|
270
|
+
Analysis::DependencyRecorder.read_last_segment(:class, name)
|
|
271
|
+
chain.entries.each do |entry|
|
|
272
|
+
if entry.external?
|
|
273
|
+
entry.candidates.each { |candidate| Analysis::DependencyRecorder.read_last_segment(:class, candidate) }
|
|
274
|
+
else
|
|
275
|
+
Analysis::DependencyRecorder.read_missing(:class, entry.last_segment)
|
|
276
|
+
end
|
|
277
|
+
end
|
|
278
|
+
end
|
|
279
|
+
end
|
|
280
|
+
end
|
|
281
|
+
end
|
|
282
|
+
end
|
|
@@ -0,0 +1,276 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "../../source/constant_path"
|
|
4
|
+
|
|
5
|
+
module Rigor
|
|
6
|
+
module Inference
|
|
7
|
+
class InEffectRefinements
|
|
8
|
+
# The one walk of a file that records its activations, its refine-body `def`s and the `def`s a refine body
|
|
9
|
+
# defines on its refined class ({InEffectRefinements} describes what each source contributes).
|
|
10
|
+
module Walk
|
|
11
|
+
private
|
|
12
|
+
|
|
13
|
+
# `Module.nesting` inside `node`'s body while the block runs, for the refine-body defs recorded there.
|
|
14
|
+
def within_nesting(node)
|
|
15
|
+
outer = @nesting
|
|
16
|
+
@nesting = Source::ConstantPath.pushed_nesting(outer, node.constant_path) || outer
|
|
17
|
+
yield
|
|
18
|
+
ensure
|
|
19
|
+
@nesting = outer
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
# A file whose text names neither `using` nor `refine` (`refined` among it) holds none of the activations the
|
|
23
|
+
# walk records, so the scan of the source string spares it the tree walk.
|
|
24
|
+
def mentions_refinements?
|
|
25
|
+
text = @root.send(:source).source
|
|
26
|
+
text.include?("using") || text.include?("refine")
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
# `body` is the `[start, end]` of the body a `using` here stays in effect to the end of; `context` is the
|
|
30
|
+
# `RefineSelf::Context` of the `self` a `refine` here runs on, worked out by `RefineCensus`'s functions for
|
|
31
|
+
# every body, method and block this walk enters. The census reads it back for each `refine`-shaped node
|
|
32
|
+
# ({InEffectRefinements#refine_context}), so the two walks charge the same module.
|
|
33
|
+
def walk(node, prefix, body, in_def, context)
|
|
34
|
+
return if walked_body?(node, prefix, body, in_def, context)
|
|
35
|
+
|
|
36
|
+
case node
|
|
37
|
+
when Prism::SymbolNode
|
|
38
|
+
record_refine_context(node, context) if RefineCensus.refine_symbol?(node)
|
|
39
|
+
when Prism::ConstantWriteNode, Prism::ConstantPathWriteNode, Prism::ConstantOrWriteNode,
|
|
40
|
+
Prism::ConstantPathOrWriteNode
|
|
41
|
+
return if walked_meta_new_write?(node, prefix, body, in_def, context)
|
|
42
|
+
when Prism::LambdaNode, Prism::BlockNode
|
|
43
|
+
# A call's block walks under {#walk_block_call}; a `super` / `yield`-side block or a lambda may run
|
|
44
|
+
# under any `self`.
|
|
45
|
+
return walk_children(node, prefix, body, in_def, RefineSelf.unknown)
|
|
46
|
+
when Prism::CallNode
|
|
47
|
+
record_call(node, prefix, body, in_def, context)
|
|
48
|
+
return walk_block_call(node, prefix, body, in_def, context) if node.block.is_a?(Prism::BlockNode)
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
walk_children(node, prefix, body, in_def, context)
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
# A `class` / `module` / `class << …` body or a `def` body, each with a `self` of its own.
|
|
55
|
+
def walked_body?(node, prefix, body, in_def, context)
|
|
56
|
+
case node
|
|
57
|
+
when Prism::ClassNode, Prism::ModuleNode
|
|
58
|
+
# A superclass expression runs where the `class` keyword is, a parameter's default in the method's body.
|
|
59
|
+
walk(node.superclass, prefix, body, in_def, context) if node.is_a?(Prism::ClassNode) && node.superclass
|
|
60
|
+
inner = Source::ConstantPath.declaration_prefix(prefix, node.constant_path) || prefix
|
|
61
|
+
within_nesting(node) do
|
|
62
|
+
walk_children(node.body, inner, span_of(node), false, declaration_context(node, inner))
|
|
63
|
+
end
|
|
64
|
+
when Prism::SingletonClassNode
|
|
65
|
+
walk(node.expression, prefix, body, in_def, context)
|
|
66
|
+
walk_children(node.body, prefix, span_of(node), false,
|
|
67
|
+
RefineSelf.singleton_class_body(context, node.expression.is_a?(Prism::SelfNode)))
|
|
68
|
+
when Prism::DefNode
|
|
69
|
+
method_context = RefineSelf.def_body(context, node.receiver)
|
|
70
|
+
walk_children(node.parameters, prefix, body, true, method_context)
|
|
71
|
+
walk_children(node.body, prefix, body, true, method_context)
|
|
72
|
+
else
|
|
73
|
+
return false
|
|
74
|
+
end
|
|
75
|
+
true
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
def declaration_context(node, inner)
|
|
79
|
+
return RefineSelf.module_body(inner.empty? ? nil : inner.join("::")) if node.is_a?(Prism::ModuleNode)
|
|
80
|
+
|
|
81
|
+
superclass = node.superclass
|
|
82
|
+
module_superclass = (superclass.is_a?(Prism::ConstantReadNode) ||
|
|
83
|
+
(superclass.is_a?(Prism::ConstantPathNode) && superclass.parent.nil?)) &&
|
|
84
|
+
superclass.name == :Module
|
|
85
|
+
RefineSelf.class_body(module_superclass)
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
# A block-carrying call: its receiver and arguments walk where the call is, its block under
|
|
89
|
+
# `RefineSelf.block` (a `define_method` block is a method body, where `using` raises).
|
|
90
|
+
def walk_block_call(node, prefix, body, in_def, context)
|
|
91
|
+
walk(node.receiver, prefix, body, in_def, context) if node.receiver
|
|
92
|
+
walk_children(node.arguments, prefix, body, in_def, context)
|
|
93
|
+
eval_names = RefineSelf.eval_receiver_names(node, prefix, context)
|
|
94
|
+
block_context = RefineSelf.block(context, node, eval_names) do |receiver|
|
|
95
|
+
literal_bound?(receiver, prefix, node.block)
|
|
96
|
+
end
|
|
97
|
+
walk_children(node.block, prefix, body, in_def || RefineSelf.method_block?(node), block_context)
|
|
98
|
+
end
|
|
99
|
+
|
|
100
|
+
# Does every binding of the constant, local or instance variable `receiver` in this file give it a literal
|
|
101
|
+
# (`RefineSelf.literal_bindings`, worked out once per file)? Asked only for a `block` whose text names `refine`
|
|
102
|
+
# or opens a heredoc: no other block can hold a `refine`-shaped node, so its context is never read, and the
|
|
103
|
+
# unnamed module it then runs on is the declining answer anyway.
|
|
104
|
+
def literal_bound?(receiver, prefix, block)
|
|
105
|
+
return false unless refine_text_in?(block)
|
|
106
|
+
|
|
107
|
+
key = RefineSelf.binding_key(receiver, prefix)
|
|
108
|
+
!key.nil? && (@literal_bindings ||= RefineSelf.literal_bindings(@root)).include?(key)
|
|
109
|
+
end
|
|
110
|
+
|
|
111
|
+
# The census's cost gate (`MethodTables#refine_in?`) over `node`'s text: a `refine` word or a heredoc opener.
|
|
112
|
+
def refine_text_in?(node)
|
|
113
|
+
location = node.location
|
|
114
|
+
(@refine_offsets ||= [ScopeIndexer.refine_word_offsets(@root), ScopeIndexer.heredoc_offsets(@root)])
|
|
115
|
+
.any? { |offsets| ScopeIndexer.offset_in?(offsets, location.start_offset, location.end_offset) }
|
|
116
|
+
end
|
|
117
|
+
|
|
118
|
+
# ADR-121 WD7 — what the census reads back for a `refine`-shaped node.
|
|
119
|
+
def record_refine_context(node, context)
|
|
120
|
+
@refine_contexts[node.location.start_offset] = context
|
|
121
|
+
end
|
|
122
|
+
|
|
123
|
+
def walk_children(node, prefix, body, in_def, context)
|
|
124
|
+
return if node.nil?
|
|
125
|
+
|
|
126
|
+
node.rigor_each_child { |child| walk(child, prefix, body, in_def, context) }
|
|
127
|
+
end
|
|
128
|
+
|
|
129
|
+
# `M = Module.new do … end`: the block's `self` is the module the write names, so a `refine` in it refines
|
|
130
|
+
# for `M`. The write's other parts walk as they would anyway.
|
|
131
|
+
def walked_meta_new_write?(node, prefix, body, in_def, context)
|
|
132
|
+
call = ScopeIndexer.meta_new_block_call(node)
|
|
133
|
+
return false if call.nil? || !ScopeIndexer.module_new_call?(call)
|
|
134
|
+
|
|
135
|
+
named = meta_new_owner(node, prefix)
|
|
136
|
+
walk(call.receiver, prefix, body, in_def, context) if call.receiver
|
|
137
|
+
walk_children(call.arguments, prefix, body, in_def, context)
|
|
138
|
+
record_call(call, prefix, body, in_def, context)
|
|
139
|
+
walk_children(call.block, prefix, body, in_def, RefineSelf.module_body(named))
|
|
140
|
+
true
|
|
141
|
+
end
|
|
142
|
+
|
|
143
|
+
def meta_new_owner(node, prefix)
|
|
144
|
+
case node
|
|
145
|
+
when Prism::ConstantWriteNode, Prism::ConstantOrWriteNode then (prefix + [node.name.to_s]).join("::")
|
|
146
|
+
else Source::ConstantPath.declaration_prefix(prefix, node.target)&.join("::")
|
|
147
|
+
end
|
|
148
|
+
end
|
|
149
|
+
|
|
150
|
+
def record_call(node, prefix, body, in_def, context)
|
|
151
|
+
record_refine_string_contexts(node, context)
|
|
152
|
+
record_refine_context(node, context) if node.name == :refine
|
|
153
|
+
owners = RefineSelf.charged_modules(context)
|
|
154
|
+
owners = nil if owners.include?(RefineCensus.wildcard)
|
|
155
|
+
if context.here == RefineSelf::CLASS && ScopeIndexer.refine_call?(node)
|
|
156
|
+
record_class_body_refine(node)
|
|
157
|
+
elsif (target = ScopeIndexer.refine_target(node))
|
|
158
|
+
record_refine_block(node, owners, ScopeIndexer.constant_receiver_candidates(target, prefix), context)
|
|
159
|
+
elsif ScopeIndexer.refine_call?(node)
|
|
160
|
+
# ADR-121 WD7 — a target the walk cannot name (`refine(k)`): the block is still a refine body, in effect
|
|
161
|
+
# inside itself, with no class to file its defs under.
|
|
162
|
+
record_refine_block(node, owners, EMPTY, context)
|
|
163
|
+
elsif using_call?(node) && !in_def
|
|
164
|
+
record_using(node, prefix, body)
|
|
165
|
+
elsif node.name == :refined
|
|
166
|
+
record_refined_chain(node, prefix)
|
|
167
|
+
end
|
|
168
|
+
end
|
|
169
|
+
|
|
170
|
+
# By `order`; activations a `.refined` chain recorded share the literal's offset and keep their recording (call)
|
|
171
|
+
# order, so only then does the sort carry the index.
|
|
172
|
+
def sort_activations
|
|
173
|
+
if @chained_refined_calls.nil?
|
|
174
|
+
@activations.sort_by!(&:order)
|
|
175
|
+
else
|
|
176
|
+
@activations = @activations.sort_by.with_index { |activation, index| [activation.order, index] }
|
|
177
|
+
end
|
|
178
|
+
end
|
|
179
|
+
|
|
180
|
+
# ADR-121 WD1's `Proc#refined` source (#1666). The walk meets a chain's outermost `.refined` first, so it
|
|
181
|
+
# records the whole chain from the literal outwards — the call order — and marks the inner calls done.
|
|
182
|
+
def record_refined_chain(node, prefix)
|
|
183
|
+
return if @chained_refined_calls&.include?(node)
|
|
184
|
+
|
|
185
|
+
@chained_refined_calls ||= Set.new.compare_by_identity
|
|
186
|
+
literal, chain = ProcLiterals.refined_chain(node)
|
|
187
|
+
chain.each { |call| @chained_refined_calls << call }
|
|
188
|
+
return if literal.nil?
|
|
189
|
+
|
|
190
|
+
chain.each do |call|
|
|
191
|
+
(call.arguments&.arguments || EMPTY).each do |argument|
|
|
192
|
+
record_block_activation(literal, refined_argument_candidates(argument, prefix))
|
|
193
|
+
end
|
|
194
|
+
end
|
|
195
|
+
end
|
|
196
|
+
|
|
197
|
+
# A constant argument's lexical candidates, or nil ({UNKNOWN}) for any other argument: a local, a splat, a call.
|
|
198
|
+
def refined_argument_candidates(argument, prefix)
|
|
199
|
+
return nil unless argument.is_a?(Prism::ConstantReadNode) || argument.is_a?(Prism::ConstantPathNode)
|
|
200
|
+
|
|
201
|
+
candidates = ScopeIndexer.constant_receiver_candidates(argument, prefix)
|
|
202
|
+
candidates.empty? ? nil : candidates
|
|
203
|
+
end
|
|
204
|
+
|
|
205
|
+
# An activation over a block's body (`block` a `Prism::BlockNode` or `Prism::LambdaNode`) that puts `names` in
|
|
206
|
+
# effect after the block site's lexical list, each expanded through its includes as a `using`'s is; nil names
|
|
207
|
+
# contribute {UNKNOWN}.
|
|
208
|
+
def record_block_activation(block, names)
|
|
209
|
+
start, stop = span_of(block)
|
|
210
|
+
@activations << Activation.new(order: start, start: start, stop: stop, names: names, expand: true,
|
|
211
|
+
refine_block: false)
|
|
212
|
+
end
|
|
213
|
+
|
|
214
|
+
# `owners` are the refining module's names (several where its spelling can denote several), or nil; `context`
|
|
215
|
+
# is the `RefineSelf::Context` the `refine` call runs in.
|
|
216
|
+
def record_refine_block(node, owners, targets, context)
|
|
217
|
+
start, stop = span_of(node.block)
|
|
218
|
+
@activations << Activation.new(order: start, start: start, stop: stop, names: owners, expand: false,
|
|
219
|
+
refine_block: true)
|
|
220
|
+
body = node.block.body
|
|
221
|
+
return if body.nil?
|
|
222
|
+
|
|
223
|
+
ScopeIndexer.each_refinement_def(body) { |def_node| @refinement_defs << def_node.location.start_offset }
|
|
224
|
+
return if owners.nil? || targets.empty?
|
|
225
|
+
|
|
226
|
+
# ADR-121 WD7 (issue #1799) — a `def`, and an alias of a `def` the body wrote earlier; a later definer with no
|
|
227
|
+
# body (`define_method`, `attr_*`, `undef`) clears the name, which then types as `Dynamic[top]`. A `refine`
|
|
228
|
+
# that runs inside a method (issue #1749) creates its refinement only when the method runs, after or before a
|
|
229
|
+
# `using` and over or under a module-body `def` of the name, an order no walk can decide: no body of its names
|
|
230
|
+
# is kept, and no later `def` of one replaces that ({RefineDefs#record}).
|
|
231
|
+
RefineCensus.read_body(body).defs.each do |name, def_node|
|
|
232
|
+
@refine_defs.record(owners, targets, name, def_node, @nesting, created: context.deferred)
|
|
233
|
+
end
|
|
234
|
+
end
|
|
235
|
+
|
|
236
|
+
def record_class_body_refine(node) = (@class_body_refines ||= Set.new) << node.block.location.start_offset
|
|
237
|
+
|
|
238
|
+
def record_refine_string_contexts(node, context)
|
|
239
|
+
return unless RefineCensus.string_literal_call?(node)
|
|
240
|
+
|
|
241
|
+
node.arguments&.arguments&.each do |argument|
|
|
242
|
+
record_refine_context(argument, context) if RefineCensus.refine_string?(argument)
|
|
243
|
+
end
|
|
244
|
+
end
|
|
245
|
+
|
|
246
|
+
def using_call?(node)
|
|
247
|
+
node.name == :using && (node.receiver.nil? || node.receiver.is_a?(Prism::SelfNode)) &&
|
|
248
|
+
node.arguments&.arguments&.size == 1
|
|
249
|
+
end
|
|
250
|
+
|
|
251
|
+
def record_using(node, prefix, body)
|
|
252
|
+
argument = node.arguments.arguments.first
|
|
253
|
+
order = node.location.end_offset
|
|
254
|
+
candidates =
|
|
255
|
+
if argument.is_a?(Prism::ConstantReadNode) || argument.is_a?(Prism::ConstantPathNode)
|
|
256
|
+
ScopeIndexer.constant_receiver_candidates(argument, prefix)
|
|
257
|
+
end
|
|
258
|
+
if candidates && !candidates.empty?
|
|
259
|
+
@activations << Activation.new(order: order, start: order, stop: body.last, names: candidates,
|
|
260
|
+
expand: true, refine_block: false)
|
|
261
|
+
else
|
|
262
|
+
@unresolved_using = true
|
|
263
|
+
location = @root.location
|
|
264
|
+
@activations << Activation.new(order: order, start: location.start_offset, stop: location.end_offset,
|
|
265
|
+
names: nil, expand: false, refine_block: false)
|
|
266
|
+
end
|
|
267
|
+
end
|
|
268
|
+
|
|
269
|
+
def span_of(node)
|
|
270
|
+
location = node.location
|
|
271
|
+
[location.start_offset, location.end_offset]
|
|
272
|
+
end
|
|
273
|
+
end
|
|
274
|
+
end
|
|
275
|
+
end
|
|
276
|
+
end
|