rigortype 0.4.2 → 0.4.4
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/core_overlay/ostruct.rbs +40 -0
- data/docs/handbook/11-sig-gen.md +28 -5
- data/docs/manual/02-cli-reference.md +23 -4
- data/docs/manual/03-configuration.md +21 -8
- data/docs/manual/04-diagnostics.md +44 -2
- data/docs/manual/05-inspecting-types.md +50 -0
- data/docs/manual/07-plugins.md +1 -1
- data/docs/manual/10-mcp-server.md +32 -3
- 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 +26 -9
- data/docs/manual/plugins/rigor-rails-routes.md +2 -2
- data/docs/manual/plugins/rigor-shoulda-matchers.md +4 -1
- data/exe/rigor +12 -17
- data/lib/rigor/analysis/check_rules/rule_ids.rb +64 -0
- data/lib/rigor/analysis/check_rules.rb +68 -57
- data/lib/rigor/analysis/effects_cache_probe.rb +1 -1
- data/lib/rigor/analysis/incremental.rb +12 -0
- data/lib/rigor/analysis/incremental_run_slot.rb +3 -3
- data/lib/rigor/analysis/incremental_session.rb +207 -40
- 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 +139 -15
- data/lib/rigor/analysis/reachability/scan.rb +374 -173
- data/lib/rigor/analysis/reachability/scan_cache.rb +4 -2
- data/lib/rigor/analysis/rule_catalog.rb +6 -1
- data/lib/rigor/analysis/run_cache_key.rb +31 -7
- data/lib/rigor/analysis/run_cache_probe.rb +6 -4
- data/lib/rigor/analysis/run_stats.rb +28 -6
- data/lib/rigor/analysis/runner/diagnostic_aggregator.rb +74 -13
- data/lib/rigor/analysis/runner/pool_coordinator.rb +133 -19
- data/lib/rigor/analysis/runner/project_pre_passes.rb +46 -1
- data/lib/rigor/analysis/runner.rb +62 -18
- data/lib/rigor/analysis/severity_stamp.rb +34 -1
- data/lib/rigor/analysis/worker_session.rb +58 -2
- data/lib/rigor/cache/descriptor.rb +40 -6
- data/lib/rigor/cache/engine_source.rb +21 -7
- data/lib/rigor/cache/incremental_snapshot.rb +56 -12
- data/lib/rigor/cli/check_command.rb +138 -39
- 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/mcp_command.rb +1 -1
- data/lib/rigor/cli/skill_command.rb +3 -3
- data/lib/rigor/cli/skill_describe.rb +1 -1
- data/lib/rigor/cli/type_of_command.rb +2 -1
- data/lib/rigor/cli/unused_command.rb +157 -24
- data/lib/rigor/cli.rb +30 -4
- data/lib/rigor/config_audit.rb +111 -15
- data/lib/rigor/configuration/dependencies.rb +19 -11
- data/lib/rigor/configuration.rb +15 -0
- data/lib/rigor/environment/bundle_sig_discovery.rb +11 -7
- 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_loader.rb +79 -10
- data/lib/rigor/environment/required_features.rb +4 -2
- data/lib/rigor/environment.rb +12 -5
- data/lib/rigor/inference/block_call_timing.rb +5 -0
- data/lib/rigor/inference/builtins/cruby_definers.rb +22 -5
- data/lib/rigor/inference/captured_locals.rb +69 -6
- data/lib/rigor/inference/definer_resolution.rb +16 -7
- data/lib/rigor/inference/expression_typer.rb +117 -29
- data/lib/rigor/inference/guard_rebinding.rb +7 -2
- data/lib/rigor/inference/in_effect_refinements/scope_reads.rb +21 -17
- data/lib/rigor/inference/last_line/self_evidence.rb +1 -1
- data/lib/rigor/inference/match_rebinding.rb +1 -1
- data/lib/rigor/inference/method_dispatcher/argument_nodes.rb +25 -0
- data/lib/rigor/inference/method_dispatcher/kernel_dispatch.rb +3 -2
- 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/optional_count.rb +63 -0
- data/lib/rigor/inference/method_dispatcher/rbs_dispatch.rb +4 -16
- 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 +52 -3
- data/lib/rigor/inference/module_function_state.rb +80 -13
- data/lib/rigor/inference/narrowing.rb +184 -68
- data/lib/rigor/inference/project_patched_definedness_guard.rb +86 -0
- data/lib/rigor/inference/project_patched_methods.rb +2 -2
- data/lib/rigor/inference/project_patched_scanner.rb +38 -24
- data/lib/rigor/inference/refined_dispatch/class_object.rb +350 -0
- data/lib/rigor/inference/refined_dispatch/own_method.rb +112 -40
- data/lib/rigor/inference/refined_dispatch.rb +62 -14
- data/lib/rigor/inference/reflective_ivar_write.rb +116 -0
- data/lib/rigor/inference/repeated_or_writes.rb +416 -1
- data/lib/rigor/inference/scope_indexer.rb +565 -61
- data/lib/rigor/inference/self_call_ivar_writes.rb +341 -0
- data/lib/rigor/inference/statement_evaluator.rb +307 -60
- data/lib/rigor/inference/untyped_rejoin.rb +75 -0
- data/lib/rigor/language_server/project_context.rb +56 -5
- data/lib/rigor/lens/anchor.rb +115 -0
- data/lib/rigor/lens/contract_reader.rb +133 -0
- data/lib/rigor/lens/document.rb +113 -0
- data/lib/rigor/lens/file_reader.rb +802 -0
- data/lib/rigor/lens/members.rb +203 -0
- data/lib/rigor/lens/parameters.rb +91 -0
- data/lib/rigor/lens/plugins.rb +40 -0
- data/lib/rigor/lens/run.rb +322 -0
- data/lib/rigor/lens/slot.rb +79 -0
- data/lib/rigor/lens.rb +41 -0
- data/lib/rigor/mcp/server.rb +61 -20
- data/lib/rigor/plugin/box_probe.rb +154 -7
- data/lib/rigor/project_environment.rb +23 -1
- data/lib/rigor/protection/mutation_cache.rb +2 -1
- data/lib/rigor/scope.rb +33 -17
- data/lib/rigor/sig_gen/generator.rb +45 -5
- data/lib/rigor/sig_gen/layout_index.rb +36 -0
- data/lib/rigor/sig_gen/method_candidate.rb +8 -0
- data/lib/rigor/sig_gen/path_mapper.rb +18 -2
- data/lib/rigor/sig_gen/writer.rb +47 -34
- data/lib/rigor/version.rb +1 -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-dry-schema/lib/rigor/plugin/dry_schema.rb +5 -1
- data/plugins/rigor-dry-validation/lib/rigor/plugin/dry_validation.rb +5 -1
- data/plugins/rigor-ffi/lib/rigor/plugin/ffi/discoverer.rb +28 -6
- data/plugins/rigor-ffi/lib/rigor/plugin/ffi.rb +1 -1
- data/plugins/rigor-graphql/lib/rigor/plugin/graphql.rb +5 -1
- data/plugins/rigor-rspec/lib/rigor/plugin/rspec/matcher_analyzer.rb +155 -12
- data/plugins/rigor-rspec/lib/rigor/plugin/rspec.rb +10 -8
- data/sig/rigor/scope.rbs +3 -0
- data/sig/rigor.rbs +6 -3
- data/skills/rigor-project-init/references/02-configure.md +2 -2
- metadata +21 -1
|
@@ -51,10 +51,22 @@ module Rigor
|
|
|
51
51
|
# - `:leading` — `"V#{version}"` is the START of a name, not a namespace, so it can construct `V1_0`
|
|
52
52
|
# and `V2_2` as well as anything beneath them: a string-prefix match (#1734).
|
|
53
53
|
#
|
|
54
|
-
# `within` is set for `Foo.const_get("Bar::#{k}")
|
|
55
|
-
#
|
|
56
|
-
|
|
57
|
-
|
|
54
|
+
# `within` is set for a `const_get` on a known receiver — `Foo.const_get("Bar::#{k}")`, or an implicit
|
|
55
|
+
# receiver, `self` or `self.class`, which is the enclosing declaration written rooted: `prefix` is then
|
|
56
|
+
# relative to the receiver, a {Reference} that {Graph} resolves as it resolves any other before
|
|
57
|
+
# matching. A literal `name` keeps `within` too, and {Graph} looks it up from there (#1761). `inherit`
|
|
58
|
+
# is false when the call passes `inherit = false`, which confines the lookup to the receiver itself;
|
|
59
|
+
# otherwise the receiver's ancestors are searched as well. `role` is the referring file's (WD8).
|
|
60
|
+
#
|
|
61
|
+
# `receiver_open` is true when `within` is the enclosing declaration standing for a runtime `self` that
|
|
62
|
+
# may be one of its descendants (#1878): a `def self.x = const_get(:X)` runs on whichever subclass it
|
|
63
|
+
# is called on, and a module's method on whichever class includes, prepends or extends it. {Graph}
|
|
64
|
+
# then looks the literal name up in those descendants as well.
|
|
65
|
+
DynamicUse = Data.define(:name, :prefix, :reason, :site, :path, :line, :scope, :within, :inherit, :role,
|
|
66
|
+
:receiver_open) do
|
|
67
|
+
def initialize(scope: :namespace, within: nil, inherit: true, role: :production, receiver_open: false, **)
|
|
68
|
+
super
|
|
69
|
+
end
|
|
58
70
|
|
|
59
71
|
# Whether this site's evidence reaches `fqn`.
|
|
60
72
|
def taints?(fqn)
|
|
@@ -65,18 +77,28 @@ module Rigor
|
|
|
65
77
|
fqn == prefix || fqn.start_with?("#{prefix}::")
|
|
66
78
|
end
|
|
67
79
|
|
|
68
|
-
#
|
|
80
|
+
# The namespaces an interpolated site's `within` names, resolved by the block. A resolution of the whole
|
|
69
81
|
# written path is the anchor. {Graph#resolve} peels an unknown `Foo::Bar` to `Foo`, though, and
|
|
70
|
-
# anchoring there alone looked for `Foo::V*` and missed `Foo::Bar::V1`; the written name is then
|
|
71
|
-
# anchor as well as the peeled one, since an undeclared receiver may be an alias of either. A
|
|
72
|
-
#
|
|
73
|
-
|
|
74
|
-
|
|
82
|
+
# anchoring there alone looked for `Foo::V*` and missed `Foo::Bar::V1`; the written name is then an
|
|
83
|
+
# anchor as well as the peeled one, since an undeclared receiver may be an alias of either. A receiver
|
|
84
|
+
# that does not resolve at all keeps its written name. Over-reaching is safe for a taint only; a
|
|
85
|
+
# literal anchors at {#receiver} alone.
|
|
86
|
+
def anchors
|
|
87
|
+
resolved = yield(within)
|
|
88
|
+
whole?(resolved) ? [resolved] : [within.as_written, resolved].compact
|
|
89
|
+
end
|
|
75
90
|
|
|
76
|
-
|
|
91
|
+
# The declaration `within` names when it resolves as WHOLE, or nil. A receiver resolved only by peeling
|
|
92
|
+
# its last segment is unknown: `const_get` never searches the receiver's lexical parent, so anchoring a
|
|
93
|
+
# literal there could resolve it to a dead constant and miss the live one.
|
|
94
|
+
def receiver
|
|
77
95
|
resolved = yield(within)
|
|
78
|
-
|
|
79
|
-
|
|
96
|
+
whole?(resolved) ? resolved : nil
|
|
97
|
+
end
|
|
98
|
+
|
|
99
|
+
def whole?(resolved)
|
|
100
|
+
written = within.as_written
|
|
101
|
+
resolved == written || resolved&.end_with?("::#{written}")
|
|
80
102
|
end
|
|
81
103
|
end
|
|
82
104
|
|
|
@@ -128,7 +150,8 @@ module Rigor
|
|
|
128
150
|
# @param root — the project root `path` is made relative to when its role is decided.
|
|
129
151
|
# @return nil when the file does not parse (a parse error is the analyzer's business, not
|
|
130
152
|
# this scan's — it simply contributes nothing rather than half a file).
|
|
131
|
-
|
|
153
|
+
# `role`, when given, is the referring file's role, decided by the caller instead of from `path` (#1760).
|
|
154
|
+
def self.call(path:, source:, target_ruby: nil, root: nil, role: nil)
|
|
132
155
|
parsed = if target_ruby
|
|
133
156
|
Prism.parse(source, filepath: path,
|
|
134
157
|
version: target_ruby)
|
|
@@ -137,7 +160,7 @@ module Rigor
|
|
|
137
160
|
end
|
|
138
161
|
return nil unless parsed.success?
|
|
139
162
|
|
|
140
|
-
walker = Walker.new(path: path, role: role_for(path, root: root))
|
|
163
|
+
walker = Walker.new(path: path, role: role || role_for(path, root: root))
|
|
141
164
|
walker.walk(parsed.value, [])
|
|
142
165
|
Result.new(declarations: walker.declarations.freeze, references: walker.references.freeze,
|
|
143
166
|
dynamic_uses: walker.dynamic_uses.freeze)
|
|
@@ -197,8 +220,280 @@ module Rigor
|
|
|
197
220
|
end
|
|
198
221
|
end
|
|
199
222
|
|
|
223
|
+
# `Const = Class.new` / `Module.new` / `Data.define(...)` / `Struct.new(...)` declare a class under a
|
|
224
|
+
# constant, and `ScopeIndexer#record_class_new_constant_decl` already treats them as class declarations
|
|
225
|
+
# for cross-file resolution. The report must agree, or every such constant reads as an unreferenced
|
|
226
|
+
# value rather than a class.
|
|
227
|
+
META_NEW = { "Class" => :new, "Module" => :new, "Data" => :define, "Struct" => :new }.freeze
|
|
228
|
+
private_constant :META_NEW
|
|
229
|
+
|
|
230
|
+
# ADR-102 WD4 — the {Walker}'s reading of dynamic constant resolution: `constantize` and `const_get`
|
|
231
|
+
# sites, the receivers a `const_get` looks its name up from, and the values its argument can hold.
|
|
232
|
+
module DynamicSites
|
|
233
|
+
private
|
|
234
|
+
|
|
235
|
+
# Names that turn a String into a constant. `constantize` / `safe_constantize` are ActiveSupport;
|
|
236
|
+
# `const_get` is core and may carry an explicit receiver (`Object.const_get`, `self.class.const_get`).
|
|
237
|
+
DYNAMIC_RESOLVERS = %i[constantize safe_constantize const_get].freeze
|
|
238
|
+
private_constant :DYNAMIC_RESOLVERS
|
|
239
|
+
|
|
240
|
+
# The anchor of a lookup that starts at the top level.
|
|
241
|
+
TOP_LEVEL = ""
|
|
242
|
+
private_constant :TOP_LEVEL
|
|
243
|
+
|
|
244
|
+
SUBJECT_SHAPES = {
|
|
245
|
+
Prism::StringNode => "a literal string",
|
|
246
|
+
Prism::SymbolNode => "a literal symbol",
|
|
247
|
+
Prism::InterpolatedStringNode => "an interpolated string",
|
|
248
|
+
Prism::InterpolatedSymbolNode => "an interpolated symbol"
|
|
249
|
+
}.freeze
|
|
250
|
+
private_constant :SUBJECT_SHAPES
|
|
251
|
+
|
|
252
|
+
# Calls that hand a string or symbol through unchanged, as far as the name it spells: `"V#{v}".freeze`.
|
|
253
|
+
PASS_THROUGH = %i[freeze to_s to_str to_sym dup -@ +@].freeze
|
|
254
|
+
private_constant :PASS_THROUGH
|
|
255
|
+
|
|
256
|
+
# Blocks whose `self` is not the enclosing declaration: a concern's `included` / `class_methods` body
|
|
257
|
+
# runs on the including class, and an eval block on its receiver.
|
|
258
|
+
SELF_REBINDING = %i[included prepended extended class_methods class_eval module_eval class_exec
|
|
259
|
+
module_exec instance_eval instance_exec].freeze
|
|
260
|
+
private_constant :SELF_REBINDING
|
|
261
|
+
|
|
262
|
+
# The `*_eval` / `*_exec` family: on a constant receiver, the block's `self` IS that constant (#1878).
|
|
263
|
+
EVAL_FAMILY = %i[class_eval module_eval class_exec module_exec instance_eval instance_exec].freeze
|
|
264
|
+
private_constant :EVAL_FAMILY
|
|
265
|
+
|
|
266
|
+
# A call is a dynamic-resolution site, and its block may run with another `self` ({#rebinds_self?}).
|
|
267
|
+
def record_call(node, nesting)
|
|
268
|
+
record_dynamic_use(node, nesting)
|
|
269
|
+
return unless node.block.is_a?(Prism::BlockNode) && rebinds_self?(node)
|
|
270
|
+
|
|
271
|
+
@rebinding_blocks << node.block
|
|
272
|
+
@eval_receivers[node.block] = receiver_reference(node.receiver, nesting) if eval_on_constant?(node)
|
|
273
|
+
end
|
|
274
|
+
|
|
275
|
+
def eval_on_constant?(call)
|
|
276
|
+
EVAL_FAMILY.include?(call.name) && constant_receiver?(call.receiver)
|
|
277
|
+
end
|
|
278
|
+
|
|
279
|
+
# Rigor knows the argument's shape, which is the whole reason this can be tiered rather than treated
|
|
280
|
+
# as a blanket namespace poison: a literal argument names the exact constant and is as good as a
|
|
281
|
+
# written reference, while an interpolated one can only bound the names its literal head reaches.
|
|
282
|
+
#
|
|
283
|
+
# A local variable argument is read through its assignments in the same local scope, because the
|
|
284
|
+
# name is usually built one line up: GitLab's `Migration[2.2]` is `name = "V#{...}"` followed by
|
|
285
|
+
# `const_get(name, false)` (#1734).
|
|
286
|
+
def record_dynamic_use(node, nesting)
|
|
287
|
+
return unless DYNAMIC_RESOLVERS.include?(node.name)
|
|
288
|
+
|
|
289
|
+
subject = node.name == :const_get ? node.arguments&.arguments&.first : node.receiver
|
|
290
|
+
return if subject.nil?
|
|
291
|
+
|
|
292
|
+
anchors = node.name == :const_get ? const_get_anchors(node, nesting) : [TOP_LEVEL]
|
|
293
|
+
subjects(subject).each { |value| record_dynamic_subject(node, value, anchors) }
|
|
294
|
+
end
|
|
295
|
+
|
|
296
|
+
def record_dynamic_subject(node, subject, anchors)
|
|
297
|
+
name, head = dynamic_target(subject)
|
|
298
|
+
# A literal whose bytes are not valid UTF-8 cannot name a constant; dropping it is the only safe
|
|
299
|
+
# answer, and carrying it forward crashed the whole run downstream.
|
|
300
|
+
return if subject.is_a?(Prism::StringNode) && name.nil?
|
|
301
|
+
return if subject.is_a?(Prism::SymbolNode) && name.nil?
|
|
302
|
+
|
|
303
|
+
reason = "#{node.name} on #{SUBJECT_SHAPES.fetch(subject.class, 'a computed value')}"
|
|
304
|
+
bounds = if name then literal_bounds(node, name, anchors)
|
|
305
|
+
elsif head then anchors.filter_map { |anchor| bound(head, anchor) }.uniq
|
|
306
|
+
else []
|
|
307
|
+
end
|
|
308
|
+
bounds = [[nil, :namespace, nil]] if bounds.empty?
|
|
309
|
+
bounds.each do |prefix, scope, within|
|
|
310
|
+
@dynamic_uses << DynamicUse.new(name: name, prefix: prefix, scope: scope, within: within,
|
|
311
|
+
inherit: anchors.include?(TOP_LEVEL), role: @role,
|
|
312
|
+
receiver_open: !within.nil? && runtime_self?(node),
|
|
313
|
+
site: site(node), path: @path, line: node.location.start_line,
|
|
314
|
+
reason: reason)
|
|
315
|
+
end
|
|
316
|
+
end
|
|
317
|
+
|
|
318
|
+
# A literal name is looked up from each receiver the call can have, so it carries the receiver as
|
|
319
|
+
# `within` and {Graph} resolves it there first (#1761). A rooted name, or a call whose only anchor is
|
|
320
|
+
# the top level, is looked up from the top level as any reference is.
|
|
321
|
+
#
|
|
322
|
+
# On `self` the receiver is the enclosing declaration only where the walk can name it ({#self_lookup}).
|
|
323
|
+
# Where `self` is a `Class` or a singleton class, Ruby looks the name up at the top level. Where the
|
|
324
|
+
# walk cannot tell what `self` is, the name is read BOTH at the enclosing declaration and at the top
|
|
325
|
+
# level: either reading alone lists a live constant as a candidate when it is the wrong one, and a
|
|
326
|
+
# dead constant kept reachable is the cheaper mistake. An interpolated name keeps every anchor, since
|
|
327
|
+
# a taint is safe to over-reach.
|
|
328
|
+
def literal_bounds(node, name, anchors)
|
|
329
|
+
return [] if name.start_with?("::")
|
|
330
|
+
|
|
331
|
+
within = anchors.grep(Reference).map { |receiver| [nil, :namespace, receiver] }
|
|
332
|
+
return within unless self_receiver?(node.receiver)
|
|
333
|
+
|
|
334
|
+
case self_lookup(node.receiver)
|
|
335
|
+
when :declaration then within
|
|
336
|
+
when :top_level then []
|
|
337
|
+
else within + [[nil, :namespace, nil]]
|
|
338
|
+
end
|
|
339
|
+
end
|
|
340
|
+
|
|
341
|
+
# Whether a `const_get` on `self` can run on a descendant of the enclosing declaration (#1878): in a
|
|
342
|
+
# method, or a block that rebinds `self`, `self` is whichever subclass or includer the method was
|
|
343
|
+
# called on. Directly in a class body, or inside `Other.class_eval { }`, it is the declaration itself.
|
|
344
|
+
def runtime_self?(node)
|
|
345
|
+
node.name == :const_get && self_receiver?(node.receiver) && @eval_receiver.nil? &&
|
|
346
|
+
!(@self_kind == :declaration && !@in_block && !@self_rebound)
|
|
347
|
+
end
|
|
348
|
+
|
|
349
|
+
# Where a `const_get` on `self` (implicit, `self`, or `self.class`) looks its name up: `:declaration`
|
|
350
|
+
# when `self` is the enclosing class or module, `:top_level` when it is `Class` or a singleton class,
|
|
351
|
+
# `:unknown` otherwise. A block's `self` is unknown — `included do`, `class_eval` and an instance-
|
|
352
|
+
# evaluated DSL block (`configure { }`, an RSpec `describe` body) all rebind it, and the walk cannot
|
|
353
|
+
# tell those from an `each` block. So is the `self` of a `def Other.x` or of a `class << Other` body.
|
|
354
|
+
def self_lookup(receiver)
|
|
355
|
+
return :declaration if @eval_receiver && !receiver.is_a?(Prism::CallNode)
|
|
356
|
+
return :unknown if @in_block || @self_rebound
|
|
357
|
+
|
|
358
|
+
class_of_self = receiver.is_a?(Prism::CallNode)
|
|
359
|
+
case @self_kind
|
|
360
|
+
when :declaration, :singleton_method then class_of_self ? :top_level : :declaration
|
|
361
|
+
when :instance_method then class_of_self ? :declaration : :unknown
|
|
362
|
+
when :singleton_class then :top_level
|
|
363
|
+
else :unknown
|
|
364
|
+
end
|
|
365
|
+
end
|
|
366
|
+
|
|
367
|
+
# Where a `const_get` looks a name up, as the namespaces its interpolated argument is bounded by. The
|
|
368
|
+
# receiver's namespace comes first: an implicit receiver, `self` or `self.class` is the enclosing
|
|
369
|
+
# declaration, and a constant receiver is resolved by {Graph} as any reference is (`within`). With
|
|
370
|
+
# the default `inherit = true` the lookup also falls through to the top level, which is all the
|
|
371
|
+
# reading had before, so that stays tainted too; an explicit `false` confines it to the receiver. An
|
|
372
|
+
# unknown receiver bounds nothing beyond the top level.
|
|
373
|
+
def const_get_anchors(node, nesting)
|
|
374
|
+
receiver = node.receiver
|
|
375
|
+
namespaces =
|
|
376
|
+
if self_receiver?(receiver)
|
|
377
|
+
eval_self_references(receiver) || self_references(node, nesting)
|
|
378
|
+
elsif constant_receiver?(receiver)
|
|
379
|
+
[receiver_reference(receiver, nesting)]
|
|
380
|
+
else
|
|
381
|
+
[]
|
|
382
|
+
end
|
|
383
|
+
return [TOP_LEVEL] if namespaces.empty?
|
|
384
|
+
|
|
385
|
+
inherit = node.arguments.arguments[1]
|
|
386
|
+
inherit.is_a?(Prism::FalseNode) ? namespaces : namespaces + [TOP_LEVEL]
|
|
387
|
+
end
|
|
388
|
+
|
|
389
|
+
# Inside `Other.class_eval { }` an implicit or `self` receiver is `Other` (#1878).
|
|
390
|
+
def eval_self_references(receiver)
|
|
391
|
+
[@eval_receiver] if @eval_receiver && !receiver.is_a?(Prism::CallNode)
|
|
392
|
+
end
|
|
393
|
+
|
|
394
|
+
# The enclosing declaration as a rooted {Reference}, so {Graph} anchors it as it anchors a constant
|
|
395
|
+
# receiver and can search its ancestors. Inside a meta-new rvalue `self` is the declared class in its
|
|
396
|
+
# block but the outer scope in its arguments; the walk does not tell the two apart, so both are taken.
|
|
397
|
+
def self_references(node, nesting)
|
|
398
|
+
([@owner, nesting.join("::")].compact.uniq - [TOP_LEVEL]).map do |fqn|
|
|
399
|
+
Reference.new(as_written: fqn, nesting: [].freeze, from: nil, role: @role, path: @path,
|
|
400
|
+
line: node.location.start_line, rooted: true)
|
|
401
|
+
end
|
|
402
|
+
end
|
|
403
|
+
|
|
404
|
+
# Whether `call`'s block runs with a `self` other than the enclosing declaration's. A meta-new block
|
|
405
|
+
# assigned to a constant is that constant ({#walk_constant_write} credits it as `@owner`); an
|
|
406
|
+
# anonymous one is not the enclosing declaration either.
|
|
407
|
+
def rebinds_self?(call)
|
|
408
|
+
return true if SELF_REBINDING.include?(call.name)
|
|
409
|
+
|
|
410
|
+
recv = call.receiver
|
|
411
|
+
name = recv && Source::ConstantPath.qualified_name_or_nil(recv)&.delete_prefix("::")
|
|
412
|
+
@owner.nil? && !name.nil? && META_NEW[name] == call.name
|
|
413
|
+
end
|
|
414
|
+
|
|
415
|
+
def self_receiver?(receiver)
|
|
416
|
+
receiver.nil? || receiver.is_a?(Prism::SelfNode) ||
|
|
417
|
+
(receiver.is_a?(Prism::CallNode) && receiver.name == :class && receiver.receiver.is_a?(Prism::SelfNode))
|
|
418
|
+
end
|
|
419
|
+
|
|
420
|
+
# `Object.const_get` is the top level, which every lookup already reaches.
|
|
421
|
+
def constant_receiver?(receiver)
|
|
422
|
+
return false unless receiver.is_a?(Prism::ConstantReadNode) || receiver.is_a?(Prism::ConstantPathNode)
|
|
423
|
+
|
|
424
|
+
as_written = Source::ConstantPath.qualified_name_or_nil(receiver)
|
|
425
|
+
!as_written.nil? && as_written.delete_prefix("::") != "Object"
|
|
426
|
+
end
|
|
427
|
+
|
|
428
|
+
def receiver_reference(receiver, nesting)
|
|
429
|
+
Reference.new(as_written: Source::ConstantPath.qualified_name_or_nil(receiver),
|
|
430
|
+
nesting: nesting.dup.freeze, from: nil, role: @role, path: @path,
|
|
431
|
+
line: receiver.location.start_line, rooted: Source::ConstantPath.rooted?(receiver))
|
|
432
|
+
end
|
|
433
|
+
|
|
434
|
+
# `[prefix, scope, within]` for a literal head under one anchor. A head ending in `::` names a
|
|
435
|
+
# namespace and reaches everything beneath it; any other head is the start of a name, so
|
|
436
|
+
# `"V#{version}"` reaches `V1_0` and `V2_2` alike, and only a string-prefix match says that. A rooted
|
|
437
|
+
# head ignores the receiver. `within` carries a constant receiver for {Graph} to resolve.
|
|
438
|
+
def bound(head, anchor)
|
|
439
|
+
anchor = TOP_LEVEL if head.start_with?("::")
|
|
440
|
+
head = head.delete_prefix("::")
|
|
441
|
+
scope = head.end_with?("::") ? :namespace : :leading
|
|
442
|
+
head = head.delete_suffix("::")
|
|
443
|
+
return nil if head.empty?
|
|
444
|
+
|
|
445
|
+
anchor == TOP_LEVEL ? [head, scope, nil] : [head, scope, anchor]
|
|
446
|
+
end
|
|
447
|
+
|
|
448
|
+
# The values a dynamic-resolution subject can hold: the subject itself, or for a local variable every
|
|
449
|
+
# value assigned to it in the enclosing local scope. A local with an assignment whose value cannot be
|
|
450
|
+
# read off the node (`+=`, a multiple assignment, `rescue => name`) stays a computed value.
|
|
451
|
+
#
|
|
452
|
+
# A name an enclosing block binds as a parameter or block-local (`|name|`, `|x; name|`) is that
|
|
453
|
+
# block's value, not the method's: reading it through the method's assignments named a literal the
|
|
454
|
+
# block never sees, and a constant only that literal named was hidden from the report.
|
|
455
|
+
def subjects(subject)
|
|
456
|
+
subject = pass_through(subject)
|
|
457
|
+
return [subject] unless subject.is_a?(Prism::LocalVariableReadNode) && @local_scope
|
|
458
|
+
return [subject] if @block_params.include?(subject.name)
|
|
459
|
+
|
|
460
|
+
(LocalValues.values(@local_scope, subject.name) || [subject]).map { |value| pass_through(value) }
|
|
461
|
+
end
|
|
462
|
+
|
|
463
|
+
# The string or symbol under `"V#{v}".freeze` / `.to_sym`; any other node unchanged.
|
|
464
|
+
def pass_through(node)
|
|
465
|
+
while node.is_a?(Prism::CallNode) && PASS_THROUGH.include?(node.name) && node.arguments.nil? &&
|
|
466
|
+
node.block.nil? && SUBJECT_SHAPES.key?(pass_through(node.receiver).class)
|
|
467
|
+
node = node.receiver
|
|
468
|
+
end
|
|
469
|
+
node
|
|
470
|
+
end
|
|
471
|
+
|
|
472
|
+
# `[exact name, literal head]` for a dynamic-resolution subject. A literal names its constant exactly;
|
|
473
|
+
# an interpolation can only bound the names its literal head starts; anything else bounds nothing.
|
|
474
|
+
def dynamic_target(subject)
|
|
475
|
+
case subject
|
|
476
|
+
when Prism::StringNode, Prism::SymbolNode then [Scan.usable_name(subject.unescaped), nil]
|
|
477
|
+
when Prism::InterpolatedStringNode, Prism::InterpolatedSymbolNode then [nil, literal_head(subject)]
|
|
478
|
+
else [nil, nil]
|
|
479
|
+
end
|
|
480
|
+
end
|
|
481
|
+
|
|
482
|
+
# The literal head of an interpolated name, as written: `"Foo::Bar::#{k}"` gives `Foo::Bar::`. Returns
|
|
483
|
+
# nil when the interpolation starts the string, which bounds nothing.
|
|
484
|
+
def literal_head(node)
|
|
485
|
+
head = node.parts.first
|
|
486
|
+
return nil unless head.is_a?(Prism::StringNode)
|
|
487
|
+
|
|
488
|
+
literal = Scan.usable_name(head.unescaped)
|
|
489
|
+
literal.nil? || literal.empty? ? nil : literal
|
|
490
|
+
end
|
|
491
|
+
end
|
|
492
|
+
|
|
200
493
|
# Single-pass walker. Tracks `nesting` as the stack of enclosing declaration names.
|
|
201
494
|
class Walker
|
|
495
|
+
include DynamicSites
|
|
496
|
+
|
|
202
497
|
attr_reader :declarations, :references, :dynamic_uses
|
|
203
498
|
|
|
204
499
|
def initialize(path:, role:)
|
|
@@ -213,6 +508,23 @@ module Rigor
|
|
|
213
508
|
@local_scope = nil
|
|
214
509
|
# Names an enclosing block's parameters or block-locals shadow within @local_scope.
|
|
215
510
|
@block_params = Set.new.freeze
|
|
511
|
+
# Whether `self` here is something other than the enclosing declaration — inside an `included do`,
|
|
512
|
+
# `class_eval` or anonymous `Class.new` block. See {#rebinds_self?}.
|
|
513
|
+
@self_rebound = false
|
|
514
|
+
@rebinding_blocks = Set.new.compare_by_identity
|
|
515
|
+
# The constant receiver of each `*_eval` block, and the one the walk is inside (nil when `self` is not
|
|
516
|
+
# such a constant). See {#eval_on_constant?}.
|
|
517
|
+
@eval_receivers = {}.compare_by_identity
|
|
518
|
+
@eval_receiver = nil
|
|
519
|
+
# What `self` is here, for {#self_lookup}: `:declaration` in a class or module body (and at the top
|
|
520
|
+
# level), `:singleton_method` in a `def self.x` or a def in a `class << self` body,
|
|
521
|
+
# `:instance_method` in any other def, `:singleton_class` in a `class << self` body outside its
|
|
522
|
+
# methods, and `:unknown` where the walk cannot name it.
|
|
523
|
+
@self_kind = :declaration
|
|
524
|
+
# Whether the walk is inside a block, whose `self` it cannot name. See {#self_lookup}.
|
|
525
|
+
@in_block = false
|
|
526
|
+
# Whether the innermost declaration is a module, whose instance methods' `self.class` is not it.
|
|
527
|
+
@module_body = false
|
|
216
528
|
end
|
|
217
529
|
|
|
218
530
|
def walk(node, nesting)
|
|
@@ -220,7 +532,7 @@ module Rigor
|
|
|
220
532
|
|
|
221
533
|
case node
|
|
222
534
|
when Prism::ClassNode then return walk_declaration(node, nesting, superclass: node.superclass)
|
|
223
|
-
when Prism::ModuleNode then return walk_declaration(node, nesting, superclass: nil)
|
|
535
|
+
when Prism::ModuleNode then return walk_declaration(node, nesting, superclass: nil, module_body: true)
|
|
224
536
|
when Prism::ConstantReadNode, Prism::ConstantPathNode
|
|
225
537
|
record_reference(node, nesting)
|
|
226
538
|
# A constant path's segments are not separate references — `A::B::C` is one reference to the leaf,
|
|
@@ -236,7 +548,7 @@ module Rigor
|
|
|
236
548
|
walk_constant_write(node, nesting)
|
|
237
549
|
return
|
|
238
550
|
when Prism::CallNode
|
|
239
|
-
|
|
551
|
+
record_call(node, nesting)
|
|
240
552
|
when Prism::ProgramNode, Prism::DefNode, Prism::SingletonClassNode
|
|
241
553
|
return in_local_scope(node) { node.rigor_each_child { |child| walk(child, nesting) } }
|
|
242
554
|
when Prism::BlockNode, Prism::LambdaNode
|
|
@@ -248,7 +560,7 @@ module Rigor
|
|
|
248
560
|
|
|
249
561
|
private
|
|
250
562
|
|
|
251
|
-
def walk_declaration(node, nesting, superclass:)
|
|
563
|
+
def walk_declaration(node, nesting, superclass:, module_body: false)
|
|
252
564
|
name = Source::ConstantPath.qualified_name(node.constant_path)
|
|
253
565
|
return node.rigor_each_child { |child| walk(child, nesting) } if name.nil?
|
|
254
566
|
|
|
@@ -272,18 +584,15 @@ module Rigor
|
|
|
272
584
|
# The body's nesting names the declaration itself, so a credit inherited from a meta-new rvalue
|
|
273
585
|
# this declaration sits in must not leak into it.
|
|
274
586
|
owner = @owner
|
|
587
|
+
saved = [@self_rebound, @in_block, @module_body]
|
|
275
588
|
@owner = nil
|
|
276
|
-
|
|
589
|
+
@self_rebound = @in_block = false
|
|
590
|
+
@module_body = module_body
|
|
591
|
+
in_local_scope(node.body, :declaration) { walk(node.body, nesting + [name]) }
|
|
277
592
|
@owner = owner
|
|
593
|
+
@self_rebound, @in_block, @module_body = saved
|
|
278
594
|
end
|
|
279
595
|
|
|
280
|
-
# `Const = Class.new` / `Module.new` / `Data.define(...)` / `Struct.new(...)` declare a class under a
|
|
281
|
-
# constant, and `ScopeIndexer#record_class_new_constant_decl` already treats them as class declarations
|
|
282
|
-
# for cross-file resolution. The report must agree, or every such constant reads as an unreferenced
|
|
283
|
-
# value rather than a class.
|
|
284
|
-
META_NEW = { "Class" => :new, "Module" => :new, "Data" => :define, "Struct" => :new }.freeze
|
|
285
|
-
private_constant :META_NEW
|
|
286
|
-
|
|
287
596
|
# A meta-new rvalue — `Class.new(Base) { ... }`, `Struct.new(:a) do ... end` — is the declaration's
|
|
288
597
|
# header and body, so the references in it are credited to the declared constant, exactly as a
|
|
289
598
|
# `class Sub < Base` superclass is (#1720). Its lexical nesting is unchanged: a block opens no cref.
|
|
@@ -330,173 +639,65 @@ module Rigor
|
|
|
330
639
|
end
|
|
331
640
|
end
|
|
332
641
|
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
SUBJECT_SHAPES = {
|
|
343
|
-
Prism::StringNode => "a literal string",
|
|
344
|
-
Prism::SymbolNode => "a literal symbol",
|
|
345
|
-
Prism::InterpolatedStringNode => "an interpolated string"
|
|
346
|
-
}.freeze
|
|
347
|
-
private_constant :SUBJECT_SHAPES
|
|
348
|
-
|
|
349
|
-
# Rigor knows the argument's shape, which is the whole reason this can be tiered rather than treated
|
|
350
|
-
# as a blanket namespace poison: a literal argument names the exact constant and is as good as a
|
|
351
|
-
# written reference, while an interpolated one can only bound the names its literal head reaches.
|
|
352
|
-
#
|
|
353
|
-
# A local variable argument is read through its assignments in the same local scope, because the
|
|
354
|
-
# name is usually built one line up: GitLab's `Migration[2.2]` is `name = "V#{...}"` followed by
|
|
355
|
-
# `const_get(name, false)` (#1734).
|
|
356
|
-
def record_dynamic_use(node, nesting)
|
|
357
|
-
return unless DYNAMIC_RESOLVERS.include?(node.name)
|
|
358
|
-
|
|
359
|
-
subject = node.name == :const_get ? node.arguments&.arguments&.first : node.receiver
|
|
360
|
-
return if subject.nil?
|
|
361
|
-
|
|
362
|
-
anchors = node.name == :const_get ? const_get_anchors(node, nesting) : [TOP_LEVEL]
|
|
363
|
-
subjects(subject).each { |value| record_dynamic_subject(node, value, anchors) }
|
|
642
|
+
def in_local_scope(scope, self_kind = self_kind_in(scope))
|
|
643
|
+
saved = [@local_scope, @block_params, @self_kind, @eval_receiver]
|
|
644
|
+
@eval_receiver = nil
|
|
645
|
+
@local_scope = scope
|
|
646
|
+
@block_params = Set.new.freeze
|
|
647
|
+
@self_kind = self_kind
|
|
648
|
+
yield
|
|
649
|
+
ensure
|
|
650
|
+
@local_scope, @block_params, @self_kind, @eval_receiver = saved
|
|
364
651
|
end
|
|
365
652
|
|
|
366
|
-
def
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
bounds = [[nil, :namespace, nil]] if bounds.empty?
|
|
376
|
-
bounds.each do |prefix, scope, within|
|
|
377
|
-
@dynamic_uses << DynamicUse.new(name: name, prefix: prefix, scope: scope, within: within,
|
|
378
|
-
site: site(node), path: @path, line: node.location.start_line,
|
|
379
|
-
reason: reason)
|
|
653
|
+
# The {#self_lookup} kind inside `scope`, from the kind around it. Only a receiver-less def or a `def
|
|
654
|
+
# self.x` directly in a declaration body, and a `class << self` there, keep `self` nameable.
|
|
655
|
+
def self_kind_in(scope)
|
|
656
|
+
case scope
|
|
657
|
+
when Prism::ProgramNode then :declaration
|
|
658
|
+
when Prism::SingletonClassNode
|
|
659
|
+
scope.expression.is_a?(Prism::SelfNode) && @self_kind == :declaration ? :singleton_class : :unknown
|
|
660
|
+
when Prism::DefNode then def_self_kind(scope)
|
|
661
|
+
else :unknown
|
|
380
662
|
end
|
|
381
663
|
end
|
|
382
664
|
|
|
383
|
-
#
|
|
384
|
-
#
|
|
385
|
-
#
|
|
386
|
-
#
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
# Inside a meta-new rvalue `self` is the declared class in its block but the outer scope in its
|
|
394
|
-
# arguments; the walk does not tell the two apart, so both are taken.
|
|
395
|
-
[@owner, nesting.join("::")].compact.uniq - [TOP_LEVEL]
|
|
396
|
-
elsif constant_receiver?(receiver)
|
|
397
|
-
[receiver_reference(receiver, nesting)]
|
|
398
|
-
else
|
|
399
|
-
[]
|
|
665
|
+
# A receiver-less def names its `self` only directly in a class body or a `class << self` body. In a
|
|
666
|
+
# module body `self.class` is the includer's class, or `Module` under `module_function` / `extend
|
|
667
|
+
# self`; a def nested in another def lands on whatever that method's cref is, which the walk cannot
|
|
668
|
+
# tell apart.
|
|
669
|
+
def def_self_kind(node)
|
|
670
|
+
if node.receiver.nil?
|
|
671
|
+
case @self_kind
|
|
672
|
+
when :singleton_class then :singleton_method
|
|
673
|
+
when :declaration then @module_body ? :unknown : :instance_method
|
|
674
|
+
else :unknown
|
|
400
675
|
end
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
end
|
|
406
|
-
|
|
407
|
-
def self_receiver?(receiver)
|
|
408
|
-
receiver.nil? || receiver.is_a?(Prism::SelfNode) ||
|
|
409
|
-
(receiver.is_a?(Prism::CallNode) && receiver.name == :class && receiver.receiver.is_a?(Prism::SelfNode))
|
|
410
|
-
end
|
|
411
|
-
|
|
412
|
-
# `Object.const_get` is the top level, which every lookup already reaches.
|
|
413
|
-
def constant_receiver?(receiver)
|
|
414
|
-
return false unless receiver.is_a?(Prism::ConstantReadNode) || receiver.is_a?(Prism::ConstantPathNode)
|
|
415
|
-
|
|
416
|
-
as_written = Source::ConstantPath.qualified_name_or_nil(receiver)
|
|
417
|
-
!as_written.nil? && as_written.delete_prefix("::") != "Object"
|
|
418
|
-
end
|
|
419
|
-
|
|
420
|
-
def receiver_reference(receiver, nesting)
|
|
421
|
-
Reference.new(as_written: Source::ConstantPath.qualified_name_or_nil(receiver),
|
|
422
|
-
nesting: nesting.dup.freeze, from: nil, role: @role, path: @path,
|
|
423
|
-
line: receiver.location.start_line, rooted: Source::ConstantPath.rooted?(receiver))
|
|
424
|
-
end
|
|
425
|
-
|
|
426
|
-
# `[prefix, scope, within]` for a literal head under one anchor. A head ending in `::` names a
|
|
427
|
-
# namespace and reaches everything beneath it; any other head is the start of a name, so
|
|
428
|
-
# `"V#{version}"` reaches `V1_0` and `V2_2` alike, and only a string-prefix match says that. A rooted
|
|
429
|
-
# head ignores the receiver. `within` carries a constant receiver for {Graph} to resolve.
|
|
430
|
-
def bound(head, anchor)
|
|
431
|
-
anchor = TOP_LEVEL if head.start_with?("::")
|
|
432
|
-
head = head.delete_prefix("::")
|
|
433
|
-
scope = head.end_with?("::") ? :namespace : :leading
|
|
434
|
-
head = head.delete_suffix("::")
|
|
435
|
-
return nil if head.empty?
|
|
436
|
-
|
|
437
|
-
case anchor
|
|
438
|
-
when Reference then [head, scope, anchor]
|
|
439
|
-
when TOP_LEVEL then [head, scope, nil]
|
|
440
|
-
else ["#{anchor}::#{head}", scope, nil]
|
|
676
|
+
elsif node.receiver.is_a?(Prism::SelfNode) && @self_kind == :declaration
|
|
677
|
+
:singleton_method
|
|
678
|
+
else
|
|
679
|
+
:unknown
|
|
441
680
|
end
|
|
442
681
|
end
|
|
443
682
|
|
|
444
|
-
# The values a dynamic-resolution subject can hold: the subject itself, or for a local variable every
|
|
445
|
-
# value assigned to it in the enclosing local scope. A local with an assignment whose value cannot be
|
|
446
|
-
# read off the node (`+=`, a multiple assignment, `rescue => name`) stays a computed value.
|
|
447
|
-
#
|
|
448
|
-
# A name an enclosing block binds as a parameter or block-local (`|name|`, `|x; name|`) is that
|
|
449
|
-
# block's value, not the method's: reading it through the method's assignments named a literal the
|
|
450
|
-
# block never sees, and a constant only that literal named was hidden from the report.
|
|
451
|
-
def subjects(subject)
|
|
452
|
-
return [subject] unless subject.is_a?(Prism::LocalVariableReadNode) && @local_scope
|
|
453
|
-
return [subject] if @block_params.include?(subject.name)
|
|
454
|
-
|
|
455
|
-
LocalValues.values(@local_scope, subject.name) || [subject]
|
|
456
|
-
end
|
|
457
|
-
|
|
458
|
-
def in_local_scope(scope)
|
|
459
|
-
saved = [@local_scope, @block_params]
|
|
460
|
-
@local_scope = scope
|
|
461
|
-
@block_params = Set.new.freeze
|
|
462
|
-
yield
|
|
463
|
-
ensure
|
|
464
|
-
@local_scope, @block_params = saved
|
|
465
|
-
end
|
|
466
|
-
|
|
467
683
|
def in_block(block)
|
|
468
|
-
saved = @block_params
|
|
684
|
+
saved = [@block_params, @self_rebound, @in_block, @eval_receiver]
|
|
469
685
|
names = block.parameters ? LocalValues.bindings(block.parameters) : []
|
|
470
686
|
@block_params = (@block_params | names).freeze unless names.empty?
|
|
687
|
+
if @rebinding_blocks.include?(block)
|
|
688
|
+
@self_rebound = true
|
|
689
|
+
@eval_receiver = @eval_receivers[block]
|
|
690
|
+
end
|
|
691
|
+
@in_block = true
|
|
471
692
|
yield
|
|
472
693
|
ensure
|
|
473
|
-
@block_params = saved
|
|
474
|
-
end
|
|
475
|
-
|
|
476
|
-
# `[exact name, literal head]` for a dynamic-resolution subject. A literal names its constant exactly;
|
|
477
|
-
# an interpolation can only bound the names its literal head starts; anything else bounds nothing.
|
|
478
|
-
def dynamic_target(subject)
|
|
479
|
-
case subject
|
|
480
|
-
when Prism::StringNode, Prism::SymbolNode then [Scan.usable_name(subject.unescaped), nil]
|
|
481
|
-
when Prism::InterpolatedStringNode then [nil, literal_head(subject)]
|
|
482
|
-
else [nil, nil]
|
|
483
|
-
end
|
|
694
|
+
@block_params, @self_rebound, @in_block, @eval_receiver = saved
|
|
484
695
|
end
|
|
485
696
|
|
|
486
697
|
def site(node)
|
|
487
698
|
"#{@path}:#{node.location.start_line}"
|
|
488
699
|
end
|
|
489
700
|
|
|
490
|
-
# The literal head of an interpolated name, as written: `"Foo::Bar::#{k}"` gives `Foo::Bar::`. Returns
|
|
491
|
-
# nil when the interpolation starts the string, which bounds nothing.
|
|
492
|
-
def literal_head(node)
|
|
493
|
-
head = node.parts.first
|
|
494
|
-
return nil unless head.is_a?(Prism::StringNode)
|
|
495
|
-
|
|
496
|
-
literal = Scan.usable_name(head.unescaped)
|
|
497
|
-
literal.nil? || literal.empty? ? nil : literal
|
|
498
|
-
end
|
|
499
|
-
|
|
500
701
|
# `from` defaults to the innermost enclosing declaration; a declaration's own header (its superclass,
|
|
501
702
|
# its meta-new rvalue) is credited to that declaration instead.
|
|
502
703
|
def record_reference(node, nesting, from: @owner)
|