ryac 0.2.0
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 +7 -0
- data/LICENSE +21 -0
- data/README.md +145 -0
- data/Rakefile +45 -0
- data/Steepfile +9 -0
- data/bin/console +11 -0
- data/bin/ryac +134 -0
- data/bin/setup +8 -0
- data/lib/ryac/analysis/constant/collection.rb +332 -0
- data/lib/ryac/analysis/constant/rename_mapping.rb +346 -0
- data/lib/ryac/analysis/cvar/collection.rb +37 -0
- data/lib/ryac/analysis/gvar/collection.rb +36 -0
- data/lib/ryac/analysis/gvar/rename_mapping.rb +82 -0
- data/lib/ryac/analysis/ivar/collection.rb +281 -0
- data/lib/ryac/analysis/keyword/collection.rb +213 -0
- data/lib/ryac/analysis/keyword/rename_mapping.rb +167 -0
- data/lib/ryac/analysis/local_scopes.rb +582 -0
- data/lib/ryac/analysis/method/collection.rb +298 -0
- data/lib/ryac/analysis/method/rename_mapping.rb +337 -0
- data/lib/ryac/analysis/method_aliases.rb +83 -0
- data/lib/ryac/analysis/method_aliasing.rb +65 -0
- data/lib/ryac/analysis/nesting.rb +123 -0
- data/lib/ryac/analysis/scope_management.rb +25 -0
- data/lib/ryac/analysis/site_bucket_mapping.rb +127 -0
- data/lib/ryac/analysis/type_oracle.rb +386 -0
- data/lib/ryac/ast_utils.rb +321 -0
- data/lib/ryac/errors.rb +32 -0
- data/lib/ryac/gem_resolver.rb +55 -0
- data/lib/ryac/minifier.rb +169 -0
- data/lib/ryac/name_generator.rb +68 -0
- data/lib/ryac/pipeline/analyzer.rb +264 -0
- data/lib/ryac/pipeline/attr_decl_shorten.rb +61 -0
- data/lib/ryac/pipeline/boolean_shorten.rb +26 -0
- data/lib/ryac/pipeline/char_shorten.rb +20 -0
- data/lib/ryac/pipeline/compactor.rb +1202 -0
- data/lib/ryac/pipeline/concatenator.rb +248 -0
- data/lib/ryac/pipeline/constant_aliaser.rb +240 -0
- data/lib/ryac/pipeline/constant_fold.rb +93 -0
- data/lib/ryac/pipeline/control_flow_simplify.rb +352 -0
- data/lib/ryac/pipeline/data_types.rb +179 -0
- data/lib/ryac/pipeline/endless_method.rb +60 -0
- data/lib/ryac/pipeline/errors.rb +94 -0
- data/lib/ryac/pipeline/file_collector.rb +360 -0
- data/lib/ryac/pipeline/method_renamer.rb +151 -0
- data/lib/ryac/pipeline/paren_optimizer.rb +161 -0
- data/lib/ryac/pipeline/rename_patcher.rb +274 -0
- data/lib/ryac/pipeline/source_patcher.rb +54 -0
- data/lib/ryac/pipeline/spelling_shorten.rb +60 -0
- data/lib/ryac/pipeline/stage.rb +72 -0
- data/lib/ryac/pipeline/stage_runner.rb +84 -0
- data/lib/ryac/pipeline/variable_renamer.rb +125 -0
- data/lib/ryac/union_find.rb +42 -0
- data/lib/ryac/version.rb +5 -0
- data/lib/ryac.rb +29 -0
- data/rbs_collection.lock.yaml +224 -0
- data/rbs_collection.yaml +25 -0
- data/sig/ryac/analysis/constant/collection.rbs +30 -0
- data/sig/ryac/analysis/constant/rename_mapping.rbs +60 -0
- data/sig/ryac/analysis/cvar/collection.rbs +11 -0
- data/sig/ryac/analysis/gvar/collection.rbs +9 -0
- data/sig/ryac/analysis/gvar/rename_mapping.rbs +23 -0
- data/sig/ryac/analysis/ivar/collection.rbs +26 -0
- data/sig/ryac/analysis/keyword/collection.rbs +27 -0
- data/sig/ryac/analysis/keyword/rename_mapping.rbs +31 -0
- data/sig/ryac/analysis/local_scopes.rbs +77 -0
- data/sig/ryac/analysis/method/collection.rbs +25 -0
- data/sig/ryac/analysis/method/rename_mapping.rbs +82 -0
- data/sig/ryac/analysis/method_aliases.rbs +10 -0
- data/sig/ryac/analysis/method_aliasing.rbs +9 -0
- data/sig/ryac/analysis/nesting.rbs +9 -0
- data/sig/ryac/analysis/scope_management.rbs +5 -0
- data/sig/ryac/analysis/site_bucket_mapping.rbs +26 -0
- data/sig/ryac/analysis/type_oracle.rbs +61 -0
- data/sig/ryac/ast_utils.rbs +34 -0
- data/sig/ryac/errors.rbs +21 -0
- data/sig/ryac/gem_resolver.rbs +28 -0
- data/sig/ryac/minifier.rbs +33 -0
- data/sig/ryac/name_generator.rbs +14 -0
- data/sig/ryac/pipeline/analyzer.rbs +34 -0
- data/sig/ryac/pipeline/attr_decl_shorten.rbs +17 -0
- data/sig/ryac/pipeline/boolean_shorten.rbs +11 -0
- data/sig/ryac/pipeline/char_shorten.rbs +11 -0
- data/sig/ryac/pipeline/compactor.rbs +125 -0
- data/sig/ryac/pipeline/concatenator.rbs +28 -0
- data/sig/ryac/pipeline/constant_aliaser.rbs +35 -0
- data/sig/ryac/pipeline/constant_fold.rbs +20 -0
- data/sig/ryac/pipeline/control_flow_simplify.rbs +34 -0
- data/sig/ryac/pipeline/data_types.rbs +151 -0
- data/sig/ryac/pipeline/endless_method.rbs +14 -0
- data/sig/ryac/pipeline/errors.rbs +46 -0
- data/sig/ryac/pipeline/file_collector.rbs +34 -0
- data/sig/ryac/pipeline/method_renamer.rbs +25 -0
- data/sig/ryac/pipeline/paren_optimizer.rbs +16 -0
- data/sig/ryac/pipeline/rename_patcher.rbs +38 -0
- data/sig/ryac/pipeline/source_patcher.rbs +12 -0
- data/sig/ryac/pipeline/spelling_shorten.rbs +14 -0
- data/sig/ryac/pipeline/stage.rbs +28 -0
- data/sig/ryac/pipeline/stage_runner.rbs +18 -0
- data/sig/ryac/pipeline/variable_renamer.rbs +21 -0
- data/sig/ryac/union_find.rbs +17 -0
- data/sig/ryac.rbs +13 -0
- data/sig/shims/env.rbs +19 -0
- data/sig/shims/typeprof/core/ast.rbs +299 -0
- data/sig/shims/typeprof/core/type.rbs +22 -0
- data/sig/shims/typeprof/core.rbs +73 -0
- data/tools/regen_pins.rb +45 -0
- metadata +201 -0
|
@@ -0,0 +1,582 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Ryac
|
|
4
|
+
# Lexical scope analysis for local variables, done entirely on the Prism
|
|
5
|
+
# tree.
|
|
6
|
+
#
|
|
7
|
+
# Locals are the one namespace Ruby resolves purely lexically, and Prism
|
|
8
|
+
# hands us that resolution ready-made: every scope node carries `locals` in
|
|
9
|
+
# declaration order, and every variable node carries `depth` — the exact
|
|
10
|
+
# number of scope hops to its declaring scope. TypeProf's view of the same
|
|
11
|
+
# information (lenv/cref chains, tbl) is reconstructed for type inference
|
|
12
|
+
# and had to be joined back to source positions; using it here meant the
|
|
13
|
+
# renamer's single largest data flow ran across that seam for information
|
|
14
|
+
# the syntax tree already had.
|
|
15
|
+
#
|
|
16
|
+
# Three phases, called in order:
|
|
17
|
+
# 1. build (initialize) — walk the tree once, record every scope
|
|
18
|
+
# 2. allocate — assign short names per scope
|
|
19
|
+
# 3. resolve — map every variable occurrence to its final name
|
|
20
|
+
#
|
|
21
|
+
# Phases are split because keyword-argument hints arrive between 1 and 2:
|
|
22
|
+
# they are keyed by the scope containing the call site, which requires the
|
|
23
|
+
# scope structure, and they influence which names a def hands out.
|
|
24
|
+
class LocalScopes
|
|
25
|
+
include RenameInvariants
|
|
26
|
+
|
|
27
|
+
DYNAMIC_VARIABLE_METHODS = %i[
|
|
28
|
+
eval instance_eval class_eval module_eval
|
|
29
|
+
binding local_variable_get local_variable_set
|
|
30
|
+
].freeze
|
|
31
|
+
|
|
32
|
+
NUMBERED_PARAM_RE = /\A_\d+\z/
|
|
33
|
+
|
|
34
|
+
Scope = Struct.new(:id, :kind, :node, :parent, :owner_call, :mapping, keyword_init: true) do
|
|
35
|
+
# declared on the Scope class itself in the RBS
|
|
36
|
+
def allocated? = !mapping.nil? # steep:ignore UndeclaredMethodDefinition, NoMethod
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
attr_reader :scopes
|
|
40
|
+
|
|
41
|
+
def initialize(program_node)
|
|
42
|
+
@program = program_node
|
|
43
|
+
@scopes = []
|
|
44
|
+
@scope_by_node = {}
|
|
45
|
+
build(program_node)
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
# Innermost scope containing the given byte offset. Used to attach
|
|
49
|
+
# information that arrives keyed by "the scope around this node" —
|
|
50
|
+
# keyword hints and implicit-receiver call sites.
|
|
51
|
+
def scope_id_at(offset)
|
|
52
|
+
best = nil #: Scope?
|
|
53
|
+
@scopes.each do |scope|
|
|
54
|
+
range = scope_range(scope)
|
|
55
|
+
next unless range.cover?(offset)
|
|
56
|
+
best = scope if best.nil? || range.begin >= scope_range(best).begin
|
|
57
|
+
end
|
|
58
|
+
best&.id
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
def scope_id_of(node)
|
|
62
|
+
# [0], not .first: the .first→[0] transform is oracle-gated, and a
|
|
63
|
+
# site the oracle resolves on one self-host pass but not the other
|
|
64
|
+
# drifts the fixed point by a byte. lib spells the target form.
|
|
65
|
+
scope_id_at(AstUtils.location_key(node)[0])
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
# Phase 2: assign short names. kw_def_map is keyed by def location_key,
|
|
69
|
+
# var_hints by scope id; both may be empty.
|
|
70
|
+
def allocate(kw_def_map: {}, var_hints: {})
|
|
71
|
+
@scopes.each do |scope|
|
|
72
|
+
case scope.kind
|
|
73
|
+
when :top then allocate_top(scope)
|
|
74
|
+
when :def then allocate_def(scope, kw_def_map, var_hints)
|
|
75
|
+
when :block then allocate_block(scope) if scope.owner_call
|
|
76
|
+
end
|
|
77
|
+
end
|
|
78
|
+
self
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
# {scope_id => {original => short}} for every scope that allocates names.
|
|
82
|
+
def scope_mappings
|
|
83
|
+
@scopes.each_with_object({}) do |scope, out| #$ scope_mapping_table
|
|
84
|
+
# allocated? guarantees mapping is non-nil here.
|
|
85
|
+
out[scope.id] = scope.mapping if scope.allocated? # steep:ignore ArgumentTypeMismatch
|
|
86
|
+
end
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
# Phase 3 outputs.
|
|
90
|
+
attr_reader :rename_entries, :def_param_names, :block_param_names, :for_index_names
|
|
91
|
+
|
|
92
|
+
def resolve
|
|
93
|
+
@rename_entries = {}
|
|
94
|
+
@def_param_names = {}
|
|
95
|
+
@block_param_names = {}
|
|
96
|
+
@for_index_names = {}
|
|
97
|
+
resolve_walk(@program, [])
|
|
98
|
+
self
|
|
99
|
+
end
|
|
100
|
+
|
|
101
|
+
private
|
|
102
|
+
|
|
103
|
+
def scope_range(scope)
|
|
104
|
+
loc = scope.node.location
|
|
105
|
+
loc.start_offset..loc.end_offset
|
|
106
|
+
end
|
|
107
|
+
|
|
108
|
+
# === Phase 1: structure ===
|
|
109
|
+
|
|
110
|
+
def add_scope(kind, node, parent, owner_call: nil)
|
|
111
|
+
scope = Scope.new(id: AstUtils.location_key(node), kind: kind, node: node,
|
|
112
|
+
parent: parent, owner_call: owner_call, mapping: nil)
|
|
113
|
+
@scopes << scope
|
|
114
|
+
@scope_by_node[node.object_id] = scope
|
|
115
|
+
scope
|
|
116
|
+
end
|
|
117
|
+
|
|
118
|
+
def build(node, current = nil, owner_call: nil)
|
|
119
|
+
return unless node.is_a?(Prism::Node)
|
|
120
|
+
|
|
121
|
+
case node
|
|
122
|
+
when Prism::ProgramNode
|
|
123
|
+
scope = add_scope(:top, node, current)
|
|
124
|
+
node.compact_child_nodes.each { |c| build(c, scope) }
|
|
125
|
+
when Prism::DefNode
|
|
126
|
+
# The receiver of `def x.foo` reads an outer local; parameters and
|
|
127
|
+
# body live in the fresh method scope.
|
|
128
|
+
build(node.receiver, current) if node.receiver
|
|
129
|
+
scope = add_scope(:def, node, current)
|
|
130
|
+
build(node.parameters, scope)
|
|
131
|
+
build(node.body, scope)
|
|
132
|
+
when Prism::ClassNode, Prism::ModuleNode, Prism::SingletonClassNode
|
|
133
|
+
# Their bodies can hold locals, but renaming never touches them —
|
|
134
|
+
# the scope exists so depth arithmetic stays exact.
|
|
135
|
+
node.compact_child_nodes.each do |c|
|
|
136
|
+
if c.equal?(node.body)
|
|
137
|
+
scope = add_scope(:class, node, current)
|
|
138
|
+
build(c, scope)
|
|
139
|
+
else
|
|
140
|
+
build(c, current)
|
|
141
|
+
end
|
|
142
|
+
end
|
|
143
|
+
when Prism::CallNode
|
|
144
|
+
node.compact_child_nodes.each do |c|
|
|
145
|
+
if c.equal?(node.block) && c.is_a?(Prism::BlockNode)
|
|
146
|
+
build(c, current, owner_call: node)
|
|
147
|
+
else
|
|
148
|
+
build(c, current)
|
|
149
|
+
end
|
|
150
|
+
end
|
|
151
|
+
when Prism::BlockNode
|
|
152
|
+
scope = add_scope(:block, node, current, owner_call: owner_call)
|
|
153
|
+
node.compact_child_nodes.each { |c| build(c, scope) }
|
|
154
|
+
when Prism::LambdaNode
|
|
155
|
+
# Parity with the previous implementation: lambda params and locals
|
|
156
|
+
# keep their names. The scope still participates in depth counting.
|
|
157
|
+
scope = add_scope(:lambda, node, current)
|
|
158
|
+
node.compact_child_nodes.each { |c| build(c, scope) }
|
|
159
|
+
else
|
|
160
|
+
node.compact_child_nodes.each { |c| build(c, current, owner_call: nil) }
|
|
161
|
+
end
|
|
162
|
+
end
|
|
163
|
+
|
|
164
|
+
# === Phase 2: allocation ===
|
|
165
|
+
|
|
166
|
+
def allocate_top(scope)
|
|
167
|
+
locals = scope.node.locals
|
|
168
|
+
return unless locals.any?
|
|
169
|
+
|
|
170
|
+
# A :top scope's node is always a ProgramNode, which has #statements.
|
|
171
|
+
unsafe, pinned = scope_constraints(scope.node.statements) # steep:ignore NoMethod
|
|
172
|
+
generator = NameGenerator.new(pinned.map(&:to_s))
|
|
173
|
+
mapping = {} #: Hash[Symbol, String]
|
|
174
|
+
locals.each do |var|
|
|
175
|
+
mapping[var] = allocated_name(var, unsafe, pinned, generator)
|
|
176
|
+
end
|
|
177
|
+
verify_injective!(mapping, 'top-level')
|
|
178
|
+
scope.mapping = mapping
|
|
179
|
+
end
|
|
180
|
+
|
|
181
|
+
def allocate_def(scope, kw_def_map, var_hints)
|
|
182
|
+
node = scope.node #: Prism::DefNode
|
|
183
|
+
return unless node.body
|
|
184
|
+
|
|
185
|
+
unsafe, pinned = scope_constraints(node.body)
|
|
186
|
+
keyword_params = keyword_param_names(node)
|
|
187
|
+
unused_rescue = unused_rescue_vars(node.body)
|
|
188
|
+
kw_mapping = kw_def_map[scope.id]
|
|
189
|
+
hints = var_hints[scope.id] || {}
|
|
190
|
+
# A hint aimed at a local that is itself a keyword parameter can never
|
|
191
|
+
# apply — the parameter branch below always wins — so reserving its
|
|
192
|
+
# value would only burn a short name. Worse, the waste is not a fixed
|
|
193
|
+
# point: re-minifying the output (where the hinting call already writes
|
|
194
|
+
# short keywords, so no hint arises) would allocate the burned names.
|
|
195
|
+
hints = hints.reject { |var, _| keyword_params.include?(var) }
|
|
196
|
+
|
|
197
|
+
reserved = hints.values.dup
|
|
198
|
+
reserved.concat(kw_mapping.values) if kw_mapping
|
|
199
|
+
reserved.concat(pinned.map(&:to_s))
|
|
200
|
+
keyword_names = {} #: Hash[Symbol, String]
|
|
201
|
+
keyword_params.each do |kw|
|
|
202
|
+
keyword_names[kw] = kw_mapping&.[](kw) || kw.to_s
|
|
203
|
+
reserved << keyword_names[kw]
|
|
204
|
+
end
|
|
205
|
+
|
|
206
|
+
generator = NameGenerator.new(reserved.uniq)
|
|
207
|
+
mapping = {} #: Hash[Symbol, String]
|
|
208
|
+
claimed = Set.new(keyword_names.values)
|
|
209
|
+
node.locals.each do |var|
|
|
210
|
+
next if unused_rescue.include?(var)
|
|
211
|
+
if pinned.include?(var)
|
|
212
|
+
mapping[var] = var.to_s
|
|
213
|
+
claimed << var.to_s
|
|
214
|
+
elsif keyword_names.key?(var)
|
|
215
|
+
mapping[var] = keyword_names[var]
|
|
216
|
+
elsif !unsafe && hints.key?(var) && !claimed.include?(hints[var])
|
|
217
|
+
mapping[var] = hints[var]
|
|
218
|
+
claimed << hints[var]
|
|
219
|
+
else
|
|
220
|
+
name = unsafe ? var.to_s : generator.next_name
|
|
221
|
+
mapping[var] = name
|
|
222
|
+
claimed << name
|
|
223
|
+
end
|
|
224
|
+
end
|
|
225
|
+
verify_injective!(mapping, "def #{node.name}")
|
|
226
|
+
scope.mapping = mapping
|
|
227
|
+
end
|
|
228
|
+
|
|
229
|
+
def allocate_block(scope)
|
|
230
|
+
node = scope.node #: Prism::BlockNode
|
|
231
|
+
return unless node.body
|
|
232
|
+
|
|
233
|
+
unsafe, pinned = scope_constraints(node.body)
|
|
234
|
+
f_args = block_formal_names(node)
|
|
235
|
+
parent_names = ancestor_mapping_values(scope)
|
|
236
|
+
reserved = parent_names + pinned.map(&:to_s)
|
|
237
|
+
|
|
238
|
+
if !unsafe && use_numbered_params?(node, f_args, parent_names)
|
|
239
|
+
mapping = {} #: Hash[Symbol, String]
|
|
240
|
+
# use_numbered_params? returns false when f_args contains nil, so every
|
|
241
|
+
# param here is a Symbol.
|
|
242
|
+
f_args.each_with_index { |param, idx| mapping[param] = "_#{idx + 1}" } # steep:ignore ArgumentTypeMismatch
|
|
243
|
+
else
|
|
244
|
+
generator = NameGenerator.new(reserved)
|
|
245
|
+
mapping = {} #: Hash[Symbol, String]
|
|
246
|
+
f_args.each do |param|
|
|
247
|
+
next unless param
|
|
248
|
+
next if param.to_s.match?(/\A_\d*\z/)
|
|
249
|
+
mapping[param] = allocated_name(param, unsafe, pinned, generator)
|
|
250
|
+
end
|
|
251
|
+
block_multi_targets(node).each do |mt|
|
|
252
|
+
collect_multi_target_names(mt).each do |name|
|
|
253
|
+
mapping[name] = allocated_name(name, unsafe, pinned, generator)
|
|
254
|
+
end
|
|
255
|
+
end
|
|
256
|
+
end
|
|
257
|
+
|
|
258
|
+
if (params = block_parameters(node))
|
|
259
|
+
generator ||= NameGenerator.new(reserved)
|
|
260
|
+
collect_extra_block_param_names(params).each do |name|
|
|
261
|
+
next if mapping.key?(name)
|
|
262
|
+
mapping[name] = allocated_name(name, unsafe, pinned, generator)
|
|
263
|
+
end
|
|
264
|
+
|
|
265
|
+
# Numbered parameters cannot coexist with optionals, rest, keywords
|
|
266
|
+
# or a block param — those force an explicit pipe list, so any _N
|
|
267
|
+
# picked above has to become an ordinary name.
|
|
268
|
+
if block_has_non_required_params?(params)
|
|
269
|
+
numbered = mapping.select { |_, v| v.match?(NUMBERED_PARAM_RE) }
|
|
270
|
+
if numbered.any?
|
|
271
|
+
gen = NameGenerator.new(mapping.values.reject { |v| v.match?(NUMBERED_PARAM_RE) })
|
|
272
|
+
numbered.each_key { |param| mapping[param] = gen.next_name }
|
|
273
|
+
end
|
|
274
|
+
end
|
|
275
|
+
end
|
|
276
|
+
|
|
277
|
+
# allocate only calls this when scope.owner_call is present.
|
|
278
|
+
owner_label = scope.owner_call.name # steep:ignore NoMethod
|
|
279
|
+
verify_injective!(mapping, "block for #{owner_label}")
|
|
280
|
+
scope.mapping = mapping
|
|
281
|
+
end
|
|
282
|
+
|
|
283
|
+
# Names a block avoids: every allocated ancestor's, all the way to the
|
|
284
|
+
# top. Visibility actually ends at the first def boundary, so anything
|
|
285
|
+
# beyond it is over-reservation — but the allocation order downstream is
|
|
286
|
+
# built around this exact set, and narrowing it reshuffles names without
|
|
287
|
+
# making any of them safer.
|
|
288
|
+
def ancestor_mapping_values(scope)
|
|
289
|
+
names = [] #: Array[String]
|
|
290
|
+
current = scope.parent
|
|
291
|
+
while current
|
|
292
|
+
# allocated? guarantees mapping is non-nil here.
|
|
293
|
+
names.concat(current.mapping.values) if current.allocated? # steep:ignore NoMethod
|
|
294
|
+
current = current.parent
|
|
295
|
+
end
|
|
296
|
+
names
|
|
297
|
+
end
|
|
298
|
+
|
|
299
|
+
# One walk answers both per-scope safety questions: whether the scope
|
|
300
|
+
# defeats renaming entirely (eval and friends can read any local by its
|
|
301
|
+
# original name) and which locals are name-pinned — a regex named
|
|
302
|
+
# capture writes the local named after the group, and a hash-pattern
|
|
303
|
+
# shorthand (`in { value: }`) binds the local named after the key it
|
|
304
|
+
# matches. Renaming a pinned local breaks the pairing the name encodes;
|
|
305
|
+
# the regex/pattern half keeps the original.
|
|
306
|
+
def scope_constraints(body)
|
|
307
|
+
unsafe = false
|
|
308
|
+
pinned = Set.new
|
|
309
|
+
walk_all(body) do |n|
|
|
310
|
+
case n
|
|
311
|
+
when Prism::CallNode
|
|
312
|
+
unsafe = true if DYNAMIC_VARIABLE_METHODS.include?(n.name)
|
|
313
|
+
when Prism::MatchWriteNode
|
|
314
|
+
n.targets.each { |t| pinned << t.name if t.is_a?(Prism::LocalVariableTargetNode) }
|
|
315
|
+
when Prism::HashPatternNode
|
|
316
|
+
n.elements.each do |e|
|
|
317
|
+
next unless e.is_a?(Prism::AssocNode) && e.value.is_a?(Prism::ImplicitNode)
|
|
318
|
+
target = e.value.value
|
|
319
|
+
pinned << target.name if target.is_a?(Prism::LocalVariableTargetNode)
|
|
320
|
+
end
|
|
321
|
+
end
|
|
322
|
+
end
|
|
323
|
+
[unsafe, pinned]
|
|
324
|
+
end
|
|
325
|
+
|
|
326
|
+
# The unsafe/pinned/generator decision every allocator makes per name.
|
|
327
|
+
def allocated_name(var, unsafe, pinned, generator)
|
|
328
|
+
unsafe || pinned.include?(var) ? var.to_s : generator.next_name
|
|
329
|
+
end
|
|
330
|
+
|
|
331
|
+
def unused_rescue_vars(body)
|
|
332
|
+
unused = Set.new
|
|
333
|
+
walk_all(body) do |n|
|
|
334
|
+
next unless n.is_a?(Prism::RescueNode)
|
|
335
|
+
ref = n.reference
|
|
336
|
+
next unless ref.is_a?(Prism::LocalVariableTargetNode)
|
|
337
|
+
|
|
338
|
+
used = false
|
|
339
|
+
walk_all(n.statements) do |child|
|
|
340
|
+
used = true if child.is_a?(Prism::LocalVariableReadNode) && child.name == ref.name
|
|
341
|
+
end
|
|
342
|
+
unused << ref.name unless used
|
|
343
|
+
end
|
|
344
|
+
unused
|
|
345
|
+
end
|
|
346
|
+
|
|
347
|
+
def keyword_param_names(def_node)
|
|
348
|
+
params = def_node.parameters
|
|
349
|
+
return [] unless params
|
|
350
|
+
|
|
351
|
+
names = [] #: Array[Symbol]
|
|
352
|
+
params.keywords&.each do |kw|
|
|
353
|
+
names << kw.name if kw.respond_to?(:name)
|
|
354
|
+
end
|
|
355
|
+
names
|
|
356
|
+
end
|
|
357
|
+
|
|
358
|
+
def block_parameters(block_node)
|
|
359
|
+
p = block_node.parameters
|
|
360
|
+
p.is_a?(Prism::BlockParametersNode) ? p.parameters : nil
|
|
361
|
+
end
|
|
362
|
+
|
|
363
|
+
# The "formal" params: requireds (nil for a destructuring target) plus
|
|
364
|
+
# optionals, in declaration order. Rest, posts, keywords and the block
|
|
365
|
+
# param are handled as extras — this split decides numbered-param
|
|
366
|
+
# eligibility and allocation order, so it must not drift.
|
|
367
|
+
def block_formal_names(block_node)
|
|
368
|
+
params = block_parameters(block_node)
|
|
369
|
+
return [] unless params
|
|
370
|
+
|
|
371
|
+
names = params.requireds.map { |r| r.is_a?(Prism::RequiredParameterNode) ? r.name : nil }
|
|
372
|
+
params.optionals.each { |o| names << o.name }
|
|
373
|
+
names
|
|
374
|
+
end
|
|
375
|
+
|
|
376
|
+
def block_multi_targets(block_node)
|
|
377
|
+
params = block_parameters(block_node)
|
|
378
|
+
return [] unless params
|
|
379
|
+
|
|
380
|
+
params.requireds.select { |r| r.is_a?(Prism::MultiTargetNode) } #: Array[Prism::MultiTargetNode]
|
|
381
|
+
end
|
|
382
|
+
|
|
383
|
+
def use_numbered_params?(block_node, f_args, parent_names)
|
|
384
|
+
return false unless f_args.any?
|
|
385
|
+
return false unless block_node.body
|
|
386
|
+
return false if f_args.include?(nil)
|
|
387
|
+
return false if f_args.any? { |p| p.to_s.match?(NUMBERED_PARAM_RE) }
|
|
388
|
+
return false if parent_names.any? { |n| n.match?(NUMBERED_PARAM_RE) }
|
|
389
|
+
|
|
390
|
+
param_set = f_args.to_set
|
|
391
|
+
ref_counts = Hash.new(0)
|
|
392
|
+
nested = false
|
|
393
|
+
written = false
|
|
394
|
+
walk_all(block_node.body) do |child|
|
|
395
|
+
case child
|
|
396
|
+
when Prism::BlockNode, Prism::LambdaNode
|
|
397
|
+
nested = true
|
|
398
|
+
when Prism::LocalVariableWriteNode, Prism::LocalVariableTargetNode,
|
|
399
|
+
Prism::LocalVariableOperatorWriteNode, Prism::LocalVariableOrWriteNode,
|
|
400
|
+
Prism::LocalVariableAndWriteNode
|
|
401
|
+
written = true if param_set.include?(child.name)
|
|
402
|
+
when Prism::LocalVariableReadNode
|
|
403
|
+
ref_counts[child.name] += 1 if param_set.include?(child.name)
|
|
404
|
+
end
|
|
405
|
+
end
|
|
406
|
+
return false if nested || written
|
|
407
|
+
|
|
408
|
+
highest_used = 0
|
|
409
|
+
f_args.each_with_index do |param, idx|
|
|
410
|
+
highest_used = idx + 1 if ref_counts[param] > 0
|
|
411
|
+
end
|
|
412
|
+
return false if highest_used < f_args.size
|
|
413
|
+
|
|
414
|
+
generator = NameGenerator.new(parent_names)
|
|
415
|
+
mangled = f_args.map { generator.next_name }
|
|
416
|
+
pipe_overhead = 2 + mangled.sum(&:length) + mangled.size - 1
|
|
417
|
+
reference_overhead = f_args.each_with_index.sum do |param, idx|
|
|
418
|
+
("_#{idx + 1}".size - mangled[idx].size) * ref_counts[param]
|
|
419
|
+
end
|
|
420
|
+
pipe_overhead - reference_overhead > 0
|
|
421
|
+
end
|
|
422
|
+
|
|
423
|
+
def collect_multi_target_names(multi_target_node)
|
|
424
|
+
names = [] #: Array[Symbol]
|
|
425
|
+
multi_target_node.lefts.each do |p|
|
|
426
|
+
if p.is_a?(Prism::RequiredParameterNode)
|
|
427
|
+
names << p.name
|
|
428
|
+
elsif p.is_a?(Prism::MultiTargetNode)
|
|
429
|
+
names.concat(collect_multi_target_names(p))
|
|
430
|
+
end
|
|
431
|
+
end
|
|
432
|
+
rest = multi_target_node.rest
|
|
433
|
+
if rest.is_a?(Prism::SplatNode) && rest.expression.is_a?(Prism::RequiredParameterNode)
|
|
434
|
+
names << rest.expression.name
|
|
435
|
+
end
|
|
436
|
+
names
|
|
437
|
+
end
|
|
438
|
+
|
|
439
|
+
def collect_extra_block_param_names(prism_params)
|
|
440
|
+
names = [] #: Array[Symbol]
|
|
441
|
+
prism_params.optionals&.each { |p| names << p.name }
|
|
442
|
+
names << prism_params.rest.name if prism_params.rest.is_a?(Prism::RestParameterNode) && prism_params.rest.name
|
|
443
|
+
prism_params.posts&.each { |p| names << p.name if p.is_a?(Prism::RequiredParameterNode) }
|
|
444
|
+
prism_params.keywords&.each { |p| names << p.name }
|
|
445
|
+
names << prism_params.keyword_rest.name if prism_params.keyword_rest.is_a?(Prism::KeywordRestParameterNode) && prism_params.keyword_rest.name
|
|
446
|
+
names << prism_params.block.name if prism_params.block.is_a?(Prism::BlockParameterNode) && prism_params.block.name
|
|
447
|
+
names
|
|
448
|
+
end
|
|
449
|
+
|
|
450
|
+
def block_has_non_required_params?(params)
|
|
451
|
+
params && (
|
|
452
|
+
params.optionals&.any? ||
|
|
453
|
+
params.rest ||
|
|
454
|
+
params.posts&.any? ||
|
|
455
|
+
params.keywords&.any? ||
|
|
456
|
+
params.keyword_rest ||
|
|
457
|
+
params.block
|
|
458
|
+
)
|
|
459
|
+
end
|
|
460
|
+
|
|
461
|
+
# Walks everything below a node, including nested scopes. Used for the
|
|
462
|
+
# per-scope safety checks, which deliberately look through nesting: an
|
|
463
|
+
# `eval` anywhere below a def taints the def.
|
|
464
|
+
def walk_all(node, &block)
|
|
465
|
+
return unless node.is_a?(Prism::Node)
|
|
466
|
+
yield node
|
|
467
|
+
node.compact_child_nodes.each { |c| walk_all(c, &block) }
|
|
468
|
+
end
|
|
469
|
+
|
|
470
|
+
# === Phase 3: resolution ===
|
|
471
|
+
|
|
472
|
+
VARIABLE_NODES = [
|
|
473
|
+
Prism::LocalVariableReadNode,
|
|
474
|
+
Prism::LocalVariableWriteNode,
|
|
475
|
+
Prism::LocalVariableTargetNode,
|
|
476
|
+
Prism::LocalVariableOperatorWriteNode,
|
|
477
|
+
Prism::LocalVariableOrWriteNode,
|
|
478
|
+
Prism::LocalVariableAndWriteNode
|
|
479
|
+
].freeze
|
|
480
|
+
|
|
481
|
+
def resolve_walk(node, stack)
|
|
482
|
+
return unless node.is_a?(Prism::Node)
|
|
483
|
+
|
|
484
|
+
if (scope = @scope_by_node[node.object_id])
|
|
485
|
+
enter_scope(scope, stack)
|
|
486
|
+
case node
|
|
487
|
+
when Prism::DefNode
|
|
488
|
+
# Receiver resolves outside the scope we just entered.
|
|
489
|
+
if node.receiver
|
|
490
|
+
stack.pop
|
|
491
|
+
resolve_walk(node.receiver, stack)
|
|
492
|
+
stack.push(scope)
|
|
493
|
+
end
|
|
494
|
+
record_def_params(node, scope)
|
|
495
|
+
resolve_walk(node.parameters, stack)
|
|
496
|
+
resolve_walk(node.body, stack)
|
|
497
|
+
when Prism::BlockNode
|
|
498
|
+
record_block_params(node, scope)
|
|
499
|
+
node.compact_child_nodes.each { |c| resolve_walk(c, stack) }
|
|
500
|
+
else
|
|
501
|
+
node.compact_child_nodes.each { |c| resolve_walk(c, stack) }
|
|
502
|
+
end
|
|
503
|
+
stack.pop
|
|
504
|
+
return
|
|
505
|
+
end
|
|
506
|
+
|
|
507
|
+
case node
|
|
508
|
+
when *VARIABLE_NODES
|
|
509
|
+
# @type var node: prism_variable_node
|
|
510
|
+
record_variable(node, stack)
|
|
511
|
+
when Prism::ForNode
|
|
512
|
+
record_for_index(node, stack)
|
|
513
|
+
end
|
|
514
|
+
|
|
515
|
+
node.compact_child_nodes.each { |c| resolve_walk(c, stack) }
|
|
516
|
+
end
|
|
517
|
+
|
|
518
|
+
def enter_scope(scope, stack)
|
|
519
|
+
stack.push(scope)
|
|
520
|
+
end
|
|
521
|
+
|
|
522
|
+
def record_variable(node, stack)
|
|
523
|
+
@rename_entries[AstUtils.location_key(node)] = resolved_name(node.name, node.depth, stack)
|
|
524
|
+
end
|
|
525
|
+
|
|
526
|
+
def resolved_name(name, depth, stack)
|
|
527
|
+
return name.to_s if name.to_s.match?(NUMBERED_PARAM_RE)
|
|
528
|
+
|
|
529
|
+
scope = stack[stack.size - 1 - depth]
|
|
530
|
+
scope&.mapping&.[](name) || name.to_s
|
|
531
|
+
end
|
|
532
|
+
|
|
533
|
+
def record_def_params(def_node, scope)
|
|
534
|
+
params = def_node.parameters
|
|
535
|
+
return unless params
|
|
536
|
+
|
|
537
|
+
mapping = scope.mapping || {}
|
|
538
|
+
names = {} #: Hash[Symbol, String]
|
|
539
|
+
each_def_param_name(params) { |sym| names[sym] = mapping[sym] || sym.to_s }
|
|
540
|
+
return unless names.any?
|
|
541
|
+
|
|
542
|
+
@def_param_names[AstUtils.line_col_key(def_node)] = names
|
|
543
|
+
end
|
|
544
|
+
|
|
545
|
+
def each_def_param_name(params, &block)
|
|
546
|
+
params.requireds.each { |p| yield p.name if p.is_a?(Prism::RequiredParameterNode) }
|
|
547
|
+
params.optionals.each { |p| yield p.name }
|
|
548
|
+
yield params.rest.name if params.rest.is_a?(Prism::RestParameterNode) && params.rest.name
|
|
549
|
+
params.posts.each { |p| yield p.name if p.is_a?(Prism::RequiredParameterNode) }
|
|
550
|
+
params.keywords.each { |p| yield p.name if p.respond_to?(:name) }
|
|
551
|
+
yield params.keyword_rest.name if params.keyword_rest.is_a?(Prism::KeywordRestParameterNode) && params.keyword_rest.name
|
|
552
|
+
yield params.block.name if params.block.is_a?(Prism::BlockParameterNode) && params.block.name
|
|
553
|
+
end
|
|
554
|
+
|
|
555
|
+
def record_block_params(block_node, scope)
|
|
556
|
+
call = scope.owner_call
|
|
557
|
+
return unless call
|
|
558
|
+
|
|
559
|
+
params = block_parameters(block_node)
|
|
560
|
+
f_args = block_formal_names(block_node)
|
|
561
|
+
extra = params ? collect_extra_block_param_names(params) : [] #: Array[Symbol]
|
|
562
|
+
return unless f_args.compact.any? || extra.any?
|
|
563
|
+
|
|
564
|
+
mapping = scope.mapping || {}
|
|
565
|
+
names = {} #: Hash[Symbol, String]
|
|
566
|
+
f_args.each { |p| names[p] = mapping[p] || p.to_s if p }
|
|
567
|
+
block_multi_targets(block_node).each do |mt|
|
|
568
|
+
collect_multi_target_names(mt).each { |n| names[n] = mapping[n] || n.to_s }
|
|
569
|
+
end
|
|
570
|
+
extra.each { |n| names[n] = mapping[n] || n.to_s unless names.key?(n) }
|
|
571
|
+
|
|
572
|
+
@block_param_names[AstUtils.location_key(call)] = names
|
|
573
|
+
end
|
|
574
|
+
|
|
575
|
+
def record_for_index(for_node, stack)
|
|
576
|
+
idx = for_node.index
|
|
577
|
+
return unless idx.is_a?(Prism::LocalVariableTargetNode)
|
|
578
|
+
|
|
579
|
+
@for_index_names[AstUtils.line_col_key(for_node)] = resolved_name(idx.name, idx.depth, stack)
|
|
580
|
+
end
|
|
581
|
+
end
|
|
582
|
+
end
|