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,256 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "prism"
|
|
4
|
+
|
|
5
|
+
require_relative "../source/constant_path"
|
|
6
|
+
require_relative "../source/node_children"
|
|
7
|
+
require_relative "refine_census"
|
|
8
|
+
require_relative "refine_self"
|
|
9
|
+
require_relative "in_effect_refinements/proc_literals"
|
|
10
|
+
require_relative "in_effect_refinements/scope_reads"
|
|
11
|
+
require_relative "in_effect_refinements/refine_defs"
|
|
12
|
+
require_relative "in_effect_refinements/walk"
|
|
13
|
+
|
|
14
|
+
module Rigor
|
|
15
|
+
module Inference
|
|
16
|
+
# ADR-121 WD1 (issue #1673) — the in-effect refinements of one file: at each program point, the ORDERED list of
|
|
17
|
+
# refining modules whose refinements Ruby applies there (`CONTEXT.md` § in-effect refinements). A later entry
|
|
18
|
+
# wins over an earlier one. The check rules ({Analysis::CheckRules::LexicalMethodSites}) and the typer
|
|
19
|
+
# ({Scope#in_effect_refinements}) both read the one `Inference::ScopeIndexer` stamps for the file, so a call
|
|
20
|
+
# cannot be silenced as refined and typed as unrefined.
|
|
21
|
+
#
|
|
22
|
+
# The list is built from activations, each in effect over a span of the file and placed by its activation
|
|
23
|
+
# offset:
|
|
24
|
+
#
|
|
25
|
+
# - **Lexical `using M`**, from the end of the call to the end of the body that holds it: the file's top level
|
|
26
|
+
# or a `class` / `module` / `class << …` body, including everything nested in it. An outer body's `using`s
|
|
27
|
+
# precede an inner body's, and a body's own `using`s keep their textual order. A `using` inside a `def`
|
|
28
|
+
# raises in Ruby and activates nothing. `M` is resolved lexically: each name its spelling can denote sits at
|
|
29
|
+
# the `using`'s position, innermost LAST, so where several are declared the one Ruby's lookup finds wins.
|
|
30
|
+
# The caller's expansion block puts `M`'s included modules ahead of `M` and its prepended modules after it
|
|
31
|
+
# (issue #1671), as CRuby activates them.
|
|
32
|
+
# - **A `refine X do … end` block**, over the block: its own refining module, after the enclosing `using`s.
|
|
33
|
+
# The module is named where the block's `self` is: the innermost `module` / `class` body, the constant a
|
|
34
|
+
# `M = Module.new do … end` write names, or a module's own singleton method (`def self.x`, a `def` in its
|
|
35
|
+
# `class << self`, issue #1749), where `self` is the module once the method is called. Anywhere else (another
|
|
36
|
+
# block, an instance `def`, `class << …`, the top level) the module is not named, and the block contributes
|
|
37
|
+
# {UNKNOWN}. Where the `refine`'s `self` is known to be a class — directly in a `class` or `class << …` body,
|
|
38
|
+
# or in the block of a `Class.new` / `Struct.new` / `Data.define` call — it is not `Module#refine` (`Class`
|
|
39
|
+
# undefines it), so it activates nothing and its block is an ordinary one (issue #1689; {#class_body_refine?}).
|
|
40
|
+
# - **A `using` whose argument is not a constant** (`using Module.new { … }`) names no module. It contributes
|
|
41
|
+
# {UNKNOWN} throughout its file, which is broader than Ruby's scoping and the declining direction.
|
|
42
|
+
# - **Block sources** (ADR-121 WD1's third and fourth). A Proc literal that is directly the receiver of
|
|
43
|
+
# `Proc#refined` (#1666) — a lambda literal, or the literal block of a bare `proc` / `lambda` call or of
|
|
44
|
+
# `Proc.new` — puts each `.refined` argument in effect over the literal's body, after the literal's lexical
|
|
45
|
+
# list and in call order along a `.refined(A).refined(B)` chain (CRuby duplicates the block's cref and appends).
|
|
46
|
+
# Nested blocks and literals inherit it, being inside the span. An argument that is not a constant names no
|
|
47
|
+
# module and contributes {UNKNOWN} over that literal's body only. A plugin-declared refined block (#1667) is
|
|
48
|
+
# typing-time knowledge, so its declared modules arrive as the `declared` argument of {#at} and {#for_node} and
|
|
49
|
+
# are appended after the block site's lexical list. Both expand through includes as a `using` does.
|
|
50
|
+
#
|
|
51
|
+
# Activating a module that is already listed changes nothing: it keeps its first position
|
|
52
|
+
# (`rb_using_refinement` returns early, CRuby `eval.c`; `using A; using B; using A` answers B's method).
|
|
53
|
+
#
|
|
54
|
+
# Built lazily: the walk runs the first time a consumer asks, so a file nobody asks about pays one small object.
|
|
55
|
+
class InEffectRefinements
|
|
56
|
+
# The marker an activation contributes when it may put any refinement in effect: a non-constant `using`, a
|
|
57
|
+
# `using` whose include expansion is unknown, or a `refine` block whose module this walk cannot name. A list
|
|
58
|
+
# that carries it answers "any refinement may be in effect"; its position carries no meaning.
|
|
59
|
+
UNKNOWN = :unknown_refinement
|
|
60
|
+
EMPTY = [].freeze
|
|
61
|
+
# ADR-121 WD7 (A1) — what a caller's expansion block answers for a name nothing declares: such a candidate of a
|
|
62
|
+
# `using`'s spelling is not the module Ruby's lookup finds, so it leaves the list. Never itself a list entry.
|
|
63
|
+
UNDECLARED = :undeclared_module
|
|
64
|
+
|
|
65
|
+
# One activation: in effect over `[start, stop)`, ordered by `order`. `names` is the module names (several
|
|
66
|
+
# when a lexical spelling can denote several), or nil for {UNKNOWN}; `expand` says whether the caller's
|
|
67
|
+
# include expansion applies (a `using`'s, not a `refine` block's own module).
|
|
68
|
+
Activation = Data.define(:order, :start, :stop, :names, :expand, :refine_block) do
|
|
69
|
+
def covers?(offset) = offset >= start && offset < stop
|
|
70
|
+
end
|
|
71
|
+
private_constant :Activation
|
|
72
|
+
|
|
73
|
+
extend ScopeReads
|
|
74
|
+
include Walk
|
|
75
|
+
|
|
76
|
+
def initialize(root)
|
|
77
|
+
@root = root
|
|
78
|
+
@built = false
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
# The in-effect refinements at `offset` of this file, ordered so a later activation comes later, each module
|
|
82
|
+
# once at its first position, then `declared` (a block source's modules) on the same terms. The block, when
|
|
83
|
+
# given, answers the modules `using name` puts in effect, in CRuby's activation order, or nil when it cannot
|
|
84
|
+
# tell, which contributes {UNKNOWN}; without it a `using` contributes its own name.
|
|
85
|
+
# The caller vouches that `offset` is in this file; {#for_node} checks.
|
|
86
|
+
def at(offset, declared = EMPTY, &expand)
|
|
87
|
+
build
|
|
88
|
+
return EMPTY if @activations.empty? && declared.empty?
|
|
89
|
+
|
|
90
|
+
list = []
|
|
91
|
+
@activations.each do |activation|
|
|
92
|
+
append_activation(list, activation, expand) if activation.covers?(offset)
|
|
93
|
+
end
|
|
94
|
+
declared.each { |name| append_expanded(list, name, expand) }
|
|
95
|
+
list.empty? ? EMPTY : list.freeze
|
|
96
|
+
end
|
|
97
|
+
|
|
98
|
+
# {#at} for a node, or only `declared` when `node` is not this file's: a callee body another file wrote,
|
|
99
|
+
# typed under this file's scope, finds no lexical refinement here. Its own file's are not consulted.
|
|
100
|
+
def for_node(node, declared = EMPTY, &)
|
|
101
|
+
return at(EMPTY_OFFSET, declared, &) unless member?(node)
|
|
102
|
+
|
|
103
|
+
at(node.location.start_offset, declared, &)
|
|
104
|
+
end
|
|
105
|
+
|
|
106
|
+
# Issue #1120 — is a refinement from one of `modules` (refining-module names) in effect at `offset`, with
|
|
107
|
+
# `declared` (a plugin-declared refined block's modules, issue #1667) appended as {#at} appends them? The
|
|
108
|
+
# silencing answer is broader than the list in two places, both the declining direction: a {UNKNOWN} entry
|
|
109
|
+
# counts as every module, and inside a `refine` block every module counts, not only the block's own.
|
|
110
|
+
def refinement_active?(offset, modules, declared = EMPTY, &)
|
|
111
|
+
build
|
|
112
|
+
return true if @unresolved_using
|
|
113
|
+
return true if @activations.any? { |activation| activation.refine_block && activation.covers?(offset) }
|
|
114
|
+
|
|
115
|
+
at(offset, declared, &).any? { |name| name == UNKNOWN || modules.include?(name) }
|
|
116
|
+
end
|
|
117
|
+
|
|
118
|
+
# Does the file activate no refinement anywhere? A consumer asks first, so a file without one pays no list.
|
|
119
|
+
def empty?
|
|
120
|
+
build
|
|
121
|
+
@activations.empty?
|
|
122
|
+
end
|
|
123
|
+
|
|
124
|
+
# Issue #1664 — the `Prism::DefNode` a `refine class_name do … end` body in this file defines `method_name` with,
|
|
125
|
+
# for the refining module `module_name`, or nil. The last such `def` wins, as Ruby's method table keeps it. The
|
|
126
|
+
# refined class is matched by any name its spelling can denote, as the refinement table records it. A name whose
|
|
127
|
+
# last definer has no body answers `RefineDefs::BODILESS` (ADR-121 WD7).
|
|
128
|
+
def refinement_def(module_name, class_name, method_name)
|
|
129
|
+
build
|
|
130
|
+
@refine_defs&.lookup(module_name, class_name, method_name)
|
|
131
|
+
end
|
|
132
|
+
|
|
133
|
+
# `{refine-body DefNode => Module.nesting where it is written}` for the defs {#refinement_def} answers, which a
|
|
134
|
+
# body re-typed from another file's parse reads its constants by (`DefNodeResolver.refinement_query`).
|
|
135
|
+
def refine_def_nestings
|
|
136
|
+
build
|
|
137
|
+
@refine_defs ? @refine_defs.nestings : {}
|
|
138
|
+
end
|
|
139
|
+
|
|
140
|
+
# Issue #1689 — is `call_node` an implicit- or `self`-receiver `refine` call with a literal block
|
|
141
|
+
# ({ScopeIndexer.refine_call?}, whatever its argument) whose `self` this walk knows to be a class? `Class`
|
|
142
|
+
# undefines `refine`, so such a call is the class's own method (a DSL a base class defines), not
|
|
143
|
+
# `Module#refine`: its block is an ordinary block, and its `def`s refine nothing. Every walk that recognises a
|
|
144
|
+
# refine body asks this, so they agree on which calls are refine bodies. A node another file's parse made
|
|
145
|
+
# answers false.
|
|
146
|
+
def class_body_refine?(call_node)
|
|
147
|
+
build
|
|
148
|
+
offsets = @class_body_refines
|
|
149
|
+
!offsets.nil? && member?(call_node) && offsets.include?(call_node.block.location.start_offset)
|
|
150
|
+
end
|
|
151
|
+
|
|
152
|
+
# ADR-121 WD7 — the `RefineSelf::Context` of the `self` the `refine`-shaped `node` (a `refine` call, a `:refine`
|
|
153
|
+
# literal, a String naming `refine` passed to an eval, `send` or method-naming call) runs on, as this walk worked
|
|
154
|
+
# it out, or nil for a node it did not record (another file's, or no such node). The census charges what this
|
|
155
|
+
# answers, so the table and the activations agree.
|
|
156
|
+
def refine_context(node)
|
|
157
|
+
build
|
|
158
|
+
@refine_contexts[node.location.start_offset] if member?(node)
|
|
159
|
+
end
|
|
160
|
+
|
|
161
|
+
# Is this the query over `root`'s tree?
|
|
162
|
+
def over?(root) = @root.equal?(root)
|
|
163
|
+
|
|
164
|
+
# Issue #1367 — is any refinement in effect at `offset`?
|
|
165
|
+
def any_at?(offset) = !at(offset).empty?
|
|
166
|
+
|
|
167
|
+
# Is `def_node` one of the defs a `refine X do … end` body defines on X? Such a def redefines X's method by
|
|
168
|
+
# design, so X's declared signature for the name is not its contract (issue #1120, maintainer ruling a′).
|
|
169
|
+
def refinement_def?(def_node)
|
|
170
|
+
build
|
|
171
|
+
@refinement_defs.include?(def_node.location.start_offset)
|
|
172
|
+
end
|
|
173
|
+
|
|
174
|
+
private
|
|
175
|
+
|
|
176
|
+
EMPTY_OFFSET = -1
|
|
177
|
+
EMPTY_SET = Set.new.freeze
|
|
178
|
+
NO_CONTEXTS = {}.freeze
|
|
179
|
+
private_constant :EMPTY_OFFSET, :EMPTY_SET, :NO_CONTEXTS
|
|
180
|
+
|
|
181
|
+
# A spelling's candidates are alternatives, innermost first; Ruby's lexical lookup finds the innermost one
|
|
182
|
+
# that exists, so it goes last and wins where several are declared. ADR-121 WD7 (A1): every declared candidate
|
|
183
|
+
# stays, because which one Ruby finds can depend on load order; one nothing declares
|
|
184
|
+
# ({UNDECLARED}) leaves; and when none is declared, the spelling enters alone, a module no table declares,
|
|
185
|
+
# which the readers treat as opaque (`using` of a gem's module, issue #1796).
|
|
186
|
+
def append_activation(list, activation, expand)
|
|
187
|
+
names = activation.names
|
|
188
|
+
if names.nil?
|
|
189
|
+
list << UNKNOWN unless list.include?(UNKNOWN)
|
|
190
|
+
return
|
|
191
|
+
end
|
|
192
|
+
unless activation.expand && expand
|
|
193
|
+
names.reverse_each { |name| list << name unless list.include?(name) }
|
|
194
|
+
return
|
|
195
|
+
end
|
|
196
|
+
|
|
197
|
+
declared = false
|
|
198
|
+
names.reverse_each do |name|
|
|
199
|
+
expanded = expand.call(name)
|
|
200
|
+
next if expanded == UNDECLARED
|
|
201
|
+
|
|
202
|
+
declared = true
|
|
203
|
+
append_entries(list, expanded)
|
|
204
|
+
end
|
|
205
|
+
list << names.last unless declared || list.include?(names.last)
|
|
206
|
+
end
|
|
207
|
+
|
|
208
|
+
# A block source's declared module (#1667), listed as declared. One nothing declares refines nothing Rigor can
|
|
209
|
+
# see, so it matches no row; the readers never treat a declared module as opaque.
|
|
210
|
+
def append_expanded(list, name, expand)
|
|
211
|
+
expanded = expand ? expand.call(name) : [name]
|
|
212
|
+
append_entries(list, expanded == UNDECLARED ? [name] : expanded)
|
|
213
|
+
end
|
|
214
|
+
|
|
215
|
+
def append_entries(list, expanded)
|
|
216
|
+
if expanded.nil?
|
|
217
|
+
list << UNKNOWN unless list.include?(UNKNOWN)
|
|
218
|
+
else
|
|
219
|
+
expanded.each { |entry| list << entry unless list.include?(entry) }
|
|
220
|
+
end
|
|
221
|
+
end
|
|
222
|
+
|
|
223
|
+
# Is `node` one this file's parse holds? Prism gives every node of one parse the same `Prism::Source`, so a
|
|
224
|
+
# node another file's parse made answers false in O(1); a node synthesised over this parse's source counts.
|
|
225
|
+
def member?(node)
|
|
226
|
+
node.send(:source).equal?(@root.send(:source))
|
|
227
|
+
end
|
|
228
|
+
|
|
229
|
+
def build
|
|
230
|
+
return if @built
|
|
231
|
+
|
|
232
|
+
@built = true
|
|
233
|
+
@activations = EMPTY
|
|
234
|
+
@refinement_defs = EMPTY_SET
|
|
235
|
+
@refine_defs = nil
|
|
236
|
+
@chained_refined_calls = nil
|
|
237
|
+
@class_body_refines = nil
|
|
238
|
+
@refine_contexts = NO_CONTEXTS
|
|
239
|
+
@literal_bindings = nil
|
|
240
|
+
@refine_offsets = nil
|
|
241
|
+
@unresolved_using = false
|
|
242
|
+
return if @root.nil? || !mentions_refinements?
|
|
243
|
+
|
|
244
|
+
@activations = []
|
|
245
|
+
@refinement_defs = Set.new
|
|
246
|
+
@refine_defs = RefineDefs.new
|
|
247
|
+
@refine_contexts = {}
|
|
248
|
+
@nesting = EMPTY
|
|
249
|
+
location = @root.location
|
|
250
|
+
walk(@root, [], [location.start_offset, location.end_offset], false, RefineSelf.top)
|
|
251
|
+
sort_activations
|
|
252
|
+
@activations.freeze
|
|
253
|
+
end
|
|
254
|
+
end
|
|
255
|
+
end
|
|
256
|
+
end
|
|
@@ -0,0 +1,281 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "prism"
|
|
4
|
+
|
|
5
|
+
module Rigor
|
|
6
|
+
module Inference
|
|
7
|
+
module KeyPresenceGuard
|
|
8
|
+
# Issue #1703 — the syntax scans {KeyPresenceGuard} decides with: whether a guard may be relied on at all
|
|
9
|
+
# ({.eligible?}), whether the source between a guard and its read may have broken it ({.intervening?}), and
|
|
10
|
+
# whether one call or write breaks it ({.breaks?}, {.aliases_guard?}). Every answer errs towards breaking.
|
|
11
|
+
module Scan
|
|
12
|
+
# Blockless calls on a guarded receiver that cannot remove a key or rebind a slot, and do not answer the
|
|
13
|
+
# receiver itself (so `x = h.itself` or `x = h.to_h` is a hand-on, not a read). Any other call on it, and any
|
|
14
|
+
# call with a block, may break the guard (`h.delete(k)`, `h.send(:delete, k)`, `h.each { … h.clear }`).
|
|
15
|
+
READ_ONLY_RECEIVER_CALLS = %i[
|
|
16
|
+
[] key? has_key? include? member? fetch dig values_at fetch_values size length count empty? any? none?
|
|
17
|
+
keys values to_a dup clone frozen? hash == != eql? equal? inspect to_s is_a? kind_of? instance_of? nil?
|
|
18
|
+
respond_to?
|
|
19
|
+
].to_set.freeze
|
|
20
|
+
|
|
21
|
+
# The nodes whose `value` a write stores: storing the receiver or the key's root somewhere else aliases it.
|
|
22
|
+
WRITE_NODES = [
|
|
23
|
+
Prism::LocalVariableWriteNode, Prism::InstanceVariableWriteNode, Prism::ClassVariableWriteNode,
|
|
24
|
+
Prism::GlobalVariableWriteNode, Prism::ConstantWriteNode, Prism::ConstantPathWriteNode, Prism::MultiWriteNode,
|
|
25
|
+
Prism::LocalVariableOrWriteNode, Prism::LocalVariableAndWriteNode, Prism::LocalVariableOperatorWriteNode,
|
|
26
|
+
Prism::InstanceVariableOrWriteNode, Prism::InstanceVariableAndWriteNode,
|
|
27
|
+
Prism::InstanceVariableOperatorWriteNode, Prism::IndexOrWriteNode, Prism::IndexAndWriteNode,
|
|
28
|
+
Prism::IndexOperatorWriteNode, Prism::CallOrWriteNode, Prism::CallAndWriteNode, Prism::CallOperatorWriteNode
|
|
29
|
+
].to_set.freeze
|
|
30
|
+
|
|
31
|
+
# The nodes that bind a local by name (a write, or a target of a multiple assignment, `for`, `rescue => e`, a
|
|
32
|
+
# pattern or a named capture) and an instance variable by name.
|
|
33
|
+
LOCAL_BINDERS = [
|
|
34
|
+
Prism::LocalVariableWriteNode, Prism::LocalVariableOrWriteNode, Prism::LocalVariableAndWriteNode,
|
|
35
|
+
Prism::LocalVariableOperatorWriteNode, Prism::LocalVariableTargetNode
|
|
36
|
+
].to_set.freeze
|
|
37
|
+
|
|
38
|
+
IVAR_BINDERS = [
|
|
39
|
+
Prism::InstanceVariableWriteNode, Prism::InstanceVariableOrWriteNode, Prism::InstanceVariableAndWriteNode,
|
|
40
|
+
Prism::InstanceVariableOperatorWriteNode, Prism::InstanceVariableTargetNode
|
|
41
|
+
].to_set.freeze
|
|
42
|
+
|
|
43
|
+
CLOSURE_NODES = [Prism::BlockNode, Prism::LambdaNode].to_set.freeze
|
|
44
|
+
|
|
45
|
+
private_constant :LOCAL_BINDERS, :IVAR_BINDERS, :CLOSURE_NODES
|
|
46
|
+
|
|
47
|
+
module_function
|
|
48
|
+
|
|
49
|
+
# A guard is relied on only when nothing in reach can change what it proved:
|
|
50
|
+
# - a local receiver is never handed on in its method body — as an argument, a written value, inside a block, or
|
|
51
|
+
# as the receiver of anything but a blockless read ({.mentions?}) — before the guard or after it;
|
|
52
|
+
# - an instance-variable receiver is never handed on anywhere in the file;
|
|
53
|
+
# - a constant receiver is assigned once in the file, a frozen hash literal (`MAP = { … }.freeze`), and is never
|
|
54
|
+
# handed on in the file;
|
|
55
|
+
# - a local key root is bound nowhere in its method body after the guard, never stored elsewhere (`j = k`) and
|
|
56
|
+
# never read inside a block; an instance-variable key root is bound nowhere in the file.
|
|
57
|
+
def eligible?(walk, call_node, guard)
|
|
58
|
+
kind, name, key = guard
|
|
59
|
+
body = walk.enclosing_def(call_node) || walk.root
|
|
60
|
+
receiver_eligible?(walk, body, kind, name) && key_root_eligible?(walk, body, key, call_node)
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
# Memoised per receiver and, for a local, per method body: the answer depends on neither the guard nor its key.
|
|
64
|
+
def receiver_eligible?(walk, body, kind, name)
|
|
65
|
+
memo_key = [:receiver, kind, name, kind == :local ? body.location.start_offset : nil]
|
|
66
|
+
return walk.memo[memo_key] if walk.memo.key?(memo_key)
|
|
67
|
+
|
|
68
|
+
walk.memo[memo_key] = scan_receiver_eligible?(walk, body, kind, name)
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
def scan_receiver_eligible?(walk, body, kind, name)
|
|
72
|
+
case kind
|
|
73
|
+
when :local then !mentions?(body, [kind, name])
|
|
74
|
+
when :ivar then !mentions?(walk.root, [kind, name])
|
|
75
|
+
when :const then frozen_constant?(walk, name) && !mentions?(walk.root, [kind, name])
|
|
76
|
+
else false
|
|
77
|
+
end
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
def frozen_constant?(walk, name)
|
|
81
|
+
walk.frozen_constants ||= frozen_constant_names(walk.root)
|
|
82
|
+
walk.frozen_constants.include?(name)
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
# The constants the file assigns exactly once, to a frozen hash literal.
|
|
86
|
+
def frozen_constant_names(root)
|
|
87
|
+
writes = Hash.new { |table, key| table[key] = [] }
|
|
88
|
+
collect_constant_writes(root, writes)
|
|
89
|
+
writes.filter_map { |name, values| name if values.size == 1 && frozen_hash_literal?(values.first) }.to_set
|
|
90
|
+
end
|
|
91
|
+
|
|
92
|
+
def collect_constant_writes(node, writes)
|
|
93
|
+
writes[node.name] << node.value if node.is_a?(Prism::ConstantWriteNode)
|
|
94
|
+
writes[node.name] << nil if node.is_a?(Prism::ConstantOrWriteNode) || node.is_a?(Prism::ConstantTargetNode)
|
|
95
|
+
node.compact_child_nodes.each { |child| collect_constant_writes(child, writes) }
|
|
96
|
+
end
|
|
97
|
+
|
|
98
|
+
def frozen_hash_literal?(value)
|
|
99
|
+
value.is_a?(Prism::CallNode) && value.name == :freeze && value.arguments.nil? && value.block.nil? &&
|
|
100
|
+
value.receiver.is_a?(Prism::HashNode)
|
|
101
|
+
end
|
|
102
|
+
|
|
103
|
+
def key_root_eligible?(walk, body, key, guard_node)
|
|
104
|
+
root_ref = key.root
|
|
105
|
+
kind, name = root_ref
|
|
106
|
+
if kind == :ivar
|
|
107
|
+
memo_key = [:ivar_key_root, name]
|
|
108
|
+
return walk.memo[memo_key] if walk.memo.key?(memo_key)
|
|
109
|
+
|
|
110
|
+
walk.memo[memo_key] = !binds?(walk.root, IVAR_BINDERS, name, -1) && !closure_reads?(walk.root, root_ref)
|
|
111
|
+
else
|
|
112
|
+
!binds?(body, LOCAL_BINDERS, name, guard_node.location.start_offset) &&
|
|
113
|
+
!aliases?(body, key) && !closure_reads?(body, root_ref)
|
|
114
|
+
end
|
|
115
|
+
end
|
|
116
|
+
|
|
117
|
+
# Whether a binder of `name` starts after `offset` within `node`.
|
|
118
|
+
def binds?(node, binders, name, offset)
|
|
119
|
+
return true if binders.include?(node.class) && node.name == name && node.location.start_offset > offset
|
|
120
|
+
|
|
121
|
+
node.compact_child_nodes.any? { |child| binds?(child, binders, name, offset) }
|
|
122
|
+
end
|
|
123
|
+
|
|
124
|
+
# Whether a write stores the key's value or something a call on it answers (`j = k`, `s = k.itself`,
|
|
125
|
+
# `s = prop.column_type.to_s`): its value is the key's root read, or a receiver chain that starts with the
|
|
126
|
+
# whole key chain. Such a copy may be the key object itself, and mutating it changes the key.
|
|
127
|
+
def aliases?(node, key)
|
|
128
|
+
return true if WRITE_NODES.include?(node.class) && derived_from_key?(node.value, key)
|
|
129
|
+
|
|
130
|
+
node.compact_child_nodes.any? { |child| aliases?(child, key) }
|
|
131
|
+
end
|
|
132
|
+
|
|
133
|
+
def derived_from_key?(value, key)
|
|
134
|
+
path = receiver_path(value)
|
|
135
|
+
!path.nil? && path.first(key.path.size) == key.path
|
|
136
|
+
end
|
|
137
|
+
|
|
138
|
+
# `[[root_kind, root_name], method, …]` for a local / ivar read or a receiver chain rooted at one, or nil.
|
|
139
|
+
def receiver_path(node)
|
|
140
|
+
names = []
|
|
141
|
+
while node.is_a?(Prism::CallNode) && node.receiver
|
|
142
|
+
names.unshift(node.name)
|
|
143
|
+
node = node.receiver
|
|
144
|
+
end
|
|
145
|
+
root = chain_root(node)
|
|
146
|
+
root && [root, *names]
|
|
147
|
+
end
|
|
148
|
+
|
|
149
|
+
# Whether a block or lambda body reads `ref` at all.
|
|
150
|
+
def closure_reads?(node, ref)
|
|
151
|
+
return reads?(node, ref) if CLOSURE_NODES.include?(node.class)
|
|
152
|
+
|
|
153
|
+
node.compact_child_nodes.any? { |child| closure_reads?(child, ref) }
|
|
154
|
+
end
|
|
155
|
+
|
|
156
|
+
def reads?(node, ref)
|
|
157
|
+
receiver_ref(node) == ref || node.compact_child_nodes.any? { |child| reads?(child, ref) }
|
|
158
|
+
end
|
|
159
|
+
|
|
160
|
+
# Whether the source between the guard and the read holds anything that may break the guard, read in source
|
|
161
|
+
# order so a later operand of the guard's own condition counts (`h.key?(k) && purge(h, k)`). A read before its
|
|
162
|
+
# guard (a loop back-edge) is taken to intervene.
|
|
163
|
+
def intervening?(walk, guard, read_node)
|
|
164
|
+
start = walk.guard_ends[guard]
|
|
165
|
+
stop = read_node.location.start_offset
|
|
166
|
+
return true if start.nil? || start > stop
|
|
167
|
+
|
|
168
|
+
body = walk.enclosing_def(read_node) || walk.root
|
|
169
|
+
breaks_in_region?(body, guard, start, stop)
|
|
170
|
+
end
|
|
171
|
+
|
|
172
|
+
def breaks_in_region?(node, guard, start, stop)
|
|
173
|
+
location = node.location
|
|
174
|
+
return false if location.end_offset <= start || location.start_offset >= stop
|
|
175
|
+
|
|
176
|
+
return true if location.start_offset >= start && location.end_offset <= stop && node_breaks?(node, guard)
|
|
177
|
+
|
|
178
|
+
node.compact_child_nodes.any? { |child| breaks_in_region?(child, guard, start, stop) }
|
|
179
|
+
end
|
|
180
|
+
|
|
181
|
+
def node_breaks?(node, guard)
|
|
182
|
+
(node.is_a?(Prism::CallNode) && breaks?(node, guard)) ||
|
|
183
|
+
(WRITE_NODES.include?(node.class) && aliases_guard?(node, guard)) ||
|
|
184
|
+
rebinds_guard?(node, guard) ||
|
|
185
|
+
(CLOSURE_NODES.include?(node.class) && closure_breaks?(node, guard))
|
|
186
|
+
end
|
|
187
|
+
|
|
188
|
+
def aliases_guard?(node, guard)
|
|
189
|
+
kind, name, key = guard
|
|
190
|
+
mentions?(node.value, [kind, name]) || mentions?(node.value, key.root)
|
|
191
|
+
end
|
|
192
|
+
|
|
193
|
+
def rebinds_guard?(node, guard)
|
|
194
|
+
kind, name, key = guard
|
|
195
|
+
[[kind, name], key.root].any? do |ref_kind, ref_name|
|
|
196
|
+
binders = { local: LOCAL_BINDERS, ivar: IVAR_BINDERS }[ref_kind]
|
|
197
|
+
binders&.include?(node.class) && node.name == ref_name
|
|
198
|
+
end
|
|
199
|
+
end
|
|
200
|
+
|
|
201
|
+
def closure_breaks?(node, guard)
|
|
202
|
+
kind, name, key = guard
|
|
203
|
+
reads?(node, [kind, name]) || reads?(node, key.root)
|
|
204
|
+
end
|
|
205
|
+
|
|
206
|
+
# Whether `call_node` may break the guard `[receiver_kind, receiver_name, key]`:
|
|
207
|
+
# - it passes the receiver as an argument (or `self`, when an instance variable is involved), or its block
|
|
208
|
+
# mentions the receiver or the key's root;
|
|
209
|
+
# - it is a call on the receiver other than a blockless read ({READ_ONLY_RECEIVER_CALLS});
|
|
210
|
+
# - it is rooted at the key's variable and does not re-read the key chain (`prop.reload`, `prop.x = 1`);
|
|
211
|
+
# - it is a call on `self` and the receiver or the key is an instance variable.
|
|
212
|
+
# Passing the key's root as an argument (`overridden?(prop)`) does not break it, as it does not end a
|
|
213
|
+
# method-chain narrowing.
|
|
214
|
+
def breaks?(call_node, guard)
|
|
215
|
+
kind, name, key = guard
|
|
216
|
+
receiver_ref = [kind, name]
|
|
217
|
+
return true if escapes_through_operands?(call_node, receiver_ref, key.root)
|
|
218
|
+
|
|
219
|
+
receiver = call_node.receiver
|
|
220
|
+
return kind == :ivar || key.root.first == :ivar if receiver.nil? || receiver.is_a?(Prism::SelfNode)
|
|
221
|
+
return true if receiver_ref(receiver) == receiver_ref && !read_only_call?(call_node)
|
|
222
|
+
|
|
223
|
+
touches_key_root?(call_node, key)
|
|
224
|
+
end
|
|
225
|
+
|
|
226
|
+
def escapes_through_operands?(call_node, receiver_ref, key_root)
|
|
227
|
+
args = call_node.arguments
|
|
228
|
+
return true if args && mentions?(args, receiver_ref)
|
|
229
|
+
# `other.mutate_owner(self)` hands on every instance variable.
|
|
230
|
+
return true if args && (receiver_ref.first == :ivar || key_root.first == :ivar) && mentions_self?(args)
|
|
231
|
+
|
|
232
|
+
block = call_node.block
|
|
233
|
+
return false if block.nil?
|
|
234
|
+
|
|
235
|
+
mentions?(block, receiver_ref) || (block.is_a?(Prism::BlockNode) && reads?(block, key_root))
|
|
236
|
+
end
|
|
237
|
+
|
|
238
|
+
def read_only_call?(call_node) = call_node.block.nil? && READ_ONLY_RECEIVER_CALLS.include?(call_node.name)
|
|
239
|
+
|
|
240
|
+
def touches_key_root?(call_node, key)
|
|
241
|
+
return false unless chain_root(call_node.receiver) == key.root
|
|
242
|
+
|
|
243
|
+
own = key_expr(call_node)
|
|
244
|
+
own.nil? || !key.prefixed_by?(own)
|
|
245
|
+
end
|
|
246
|
+
|
|
247
|
+
# The `[kind, name]` of the local / ivar a receiver chain (`a.b.c`, `a[0].b`) starts from, or nil.
|
|
248
|
+
def chain_root(node)
|
|
249
|
+
node = node.receiver while node.is_a?(Prism::CallNode) && node.receiver
|
|
250
|
+
case node
|
|
251
|
+
when Prism::LocalVariableReadNode then [:local, node.name]
|
|
252
|
+
when Prism::InstanceVariableReadNode then [:ivar, node.name]
|
|
253
|
+
end
|
|
254
|
+
end
|
|
255
|
+
|
|
256
|
+
def mentions_self?(node)
|
|
257
|
+
node.is_a?(Prism::SelfNode) || node.compact_child_nodes.any? { |child| mentions_self?(child) }
|
|
258
|
+
end
|
|
259
|
+
|
|
260
|
+
# Whether `node` holds a read of the variable `ref` in a position that may hand it on: anywhere but as the
|
|
261
|
+
# receiver of a blockless read ({READ_ONLY_RECEIVER_CALLS}), so `h[k]` and `MAP.key?(x)` do not count while
|
|
262
|
+
# `zap(h)`, `[h]`, `g = h` and `h.delete(k)` do.
|
|
263
|
+
def mentions?(node, ref)
|
|
264
|
+
return false if node.nil?
|
|
265
|
+
|
|
266
|
+
own = receiver_ref(node)
|
|
267
|
+
return own == ref if own
|
|
268
|
+
|
|
269
|
+
if node.is_a?(Prism::CallNode) && receiver_ref(node.receiver) == ref && read_only_call?(node)
|
|
270
|
+
return [node.arguments, node.block].any? { |part| mentions?(part, ref) }
|
|
271
|
+
end
|
|
272
|
+
|
|
273
|
+
node.compact_child_nodes.any? { |child| mentions?(child, ref) }
|
|
274
|
+
end
|
|
275
|
+
|
|
276
|
+
def receiver_ref(node) = KeyPresenceGuard.receiver_ref(node)
|
|
277
|
+
def key_expr(node) = KeyPresenceGuard.key_expr(node)
|
|
278
|
+
end
|
|
279
|
+
end
|
|
280
|
+
end
|
|
281
|
+
end
|