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.
Files changed (107) hide show
  1. checksums.yaml +7 -0
  2. data/LICENSE +21 -0
  3. data/README.md +145 -0
  4. data/Rakefile +45 -0
  5. data/Steepfile +9 -0
  6. data/bin/console +11 -0
  7. data/bin/ryac +134 -0
  8. data/bin/setup +8 -0
  9. data/lib/ryac/analysis/constant/collection.rb +332 -0
  10. data/lib/ryac/analysis/constant/rename_mapping.rb +346 -0
  11. data/lib/ryac/analysis/cvar/collection.rb +37 -0
  12. data/lib/ryac/analysis/gvar/collection.rb +36 -0
  13. data/lib/ryac/analysis/gvar/rename_mapping.rb +82 -0
  14. data/lib/ryac/analysis/ivar/collection.rb +281 -0
  15. data/lib/ryac/analysis/keyword/collection.rb +213 -0
  16. data/lib/ryac/analysis/keyword/rename_mapping.rb +167 -0
  17. data/lib/ryac/analysis/local_scopes.rb +582 -0
  18. data/lib/ryac/analysis/method/collection.rb +298 -0
  19. data/lib/ryac/analysis/method/rename_mapping.rb +337 -0
  20. data/lib/ryac/analysis/method_aliases.rb +83 -0
  21. data/lib/ryac/analysis/method_aliasing.rb +65 -0
  22. data/lib/ryac/analysis/nesting.rb +123 -0
  23. data/lib/ryac/analysis/scope_management.rb +25 -0
  24. data/lib/ryac/analysis/site_bucket_mapping.rb +127 -0
  25. data/lib/ryac/analysis/type_oracle.rb +386 -0
  26. data/lib/ryac/ast_utils.rb +321 -0
  27. data/lib/ryac/errors.rb +32 -0
  28. data/lib/ryac/gem_resolver.rb +55 -0
  29. data/lib/ryac/minifier.rb +169 -0
  30. data/lib/ryac/name_generator.rb +68 -0
  31. data/lib/ryac/pipeline/analyzer.rb +264 -0
  32. data/lib/ryac/pipeline/attr_decl_shorten.rb +61 -0
  33. data/lib/ryac/pipeline/boolean_shorten.rb +26 -0
  34. data/lib/ryac/pipeline/char_shorten.rb +20 -0
  35. data/lib/ryac/pipeline/compactor.rb +1202 -0
  36. data/lib/ryac/pipeline/concatenator.rb +248 -0
  37. data/lib/ryac/pipeline/constant_aliaser.rb +240 -0
  38. data/lib/ryac/pipeline/constant_fold.rb +93 -0
  39. data/lib/ryac/pipeline/control_flow_simplify.rb +352 -0
  40. data/lib/ryac/pipeline/data_types.rb +179 -0
  41. data/lib/ryac/pipeline/endless_method.rb +60 -0
  42. data/lib/ryac/pipeline/errors.rb +94 -0
  43. data/lib/ryac/pipeline/file_collector.rb +360 -0
  44. data/lib/ryac/pipeline/method_renamer.rb +151 -0
  45. data/lib/ryac/pipeline/paren_optimizer.rb +161 -0
  46. data/lib/ryac/pipeline/rename_patcher.rb +274 -0
  47. data/lib/ryac/pipeline/source_patcher.rb +54 -0
  48. data/lib/ryac/pipeline/spelling_shorten.rb +60 -0
  49. data/lib/ryac/pipeline/stage.rb +72 -0
  50. data/lib/ryac/pipeline/stage_runner.rb +84 -0
  51. data/lib/ryac/pipeline/variable_renamer.rb +125 -0
  52. data/lib/ryac/union_find.rb +42 -0
  53. data/lib/ryac/version.rb +5 -0
  54. data/lib/ryac.rb +29 -0
  55. data/rbs_collection.lock.yaml +224 -0
  56. data/rbs_collection.yaml +25 -0
  57. data/sig/ryac/analysis/constant/collection.rbs +30 -0
  58. data/sig/ryac/analysis/constant/rename_mapping.rbs +60 -0
  59. data/sig/ryac/analysis/cvar/collection.rbs +11 -0
  60. data/sig/ryac/analysis/gvar/collection.rbs +9 -0
  61. data/sig/ryac/analysis/gvar/rename_mapping.rbs +23 -0
  62. data/sig/ryac/analysis/ivar/collection.rbs +26 -0
  63. data/sig/ryac/analysis/keyword/collection.rbs +27 -0
  64. data/sig/ryac/analysis/keyword/rename_mapping.rbs +31 -0
  65. data/sig/ryac/analysis/local_scopes.rbs +77 -0
  66. data/sig/ryac/analysis/method/collection.rbs +25 -0
  67. data/sig/ryac/analysis/method/rename_mapping.rbs +82 -0
  68. data/sig/ryac/analysis/method_aliases.rbs +10 -0
  69. data/sig/ryac/analysis/method_aliasing.rbs +9 -0
  70. data/sig/ryac/analysis/nesting.rbs +9 -0
  71. data/sig/ryac/analysis/scope_management.rbs +5 -0
  72. data/sig/ryac/analysis/site_bucket_mapping.rbs +26 -0
  73. data/sig/ryac/analysis/type_oracle.rbs +61 -0
  74. data/sig/ryac/ast_utils.rbs +34 -0
  75. data/sig/ryac/errors.rbs +21 -0
  76. data/sig/ryac/gem_resolver.rbs +28 -0
  77. data/sig/ryac/minifier.rbs +33 -0
  78. data/sig/ryac/name_generator.rbs +14 -0
  79. data/sig/ryac/pipeline/analyzer.rbs +34 -0
  80. data/sig/ryac/pipeline/attr_decl_shorten.rbs +17 -0
  81. data/sig/ryac/pipeline/boolean_shorten.rbs +11 -0
  82. data/sig/ryac/pipeline/char_shorten.rbs +11 -0
  83. data/sig/ryac/pipeline/compactor.rbs +125 -0
  84. data/sig/ryac/pipeline/concatenator.rbs +28 -0
  85. data/sig/ryac/pipeline/constant_aliaser.rbs +35 -0
  86. data/sig/ryac/pipeline/constant_fold.rbs +20 -0
  87. data/sig/ryac/pipeline/control_flow_simplify.rbs +34 -0
  88. data/sig/ryac/pipeline/data_types.rbs +151 -0
  89. data/sig/ryac/pipeline/endless_method.rbs +14 -0
  90. data/sig/ryac/pipeline/errors.rbs +46 -0
  91. data/sig/ryac/pipeline/file_collector.rbs +34 -0
  92. data/sig/ryac/pipeline/method_renamer.rbs +25 -0
  93. data/sig/ryac/pipeline/paren_optimizer.rbs +16 -0
  94. data/sig/ryac/pipeline/rename_patcher.rbs +38 -0
  95. data/sig/ryac/pipeline/source_patcher.rbs +12 -0
  96. data/sig/ryac/pipeline/spelling_shorten.rbs +14 -0
  97. data/sig/ryac/pipeline/stage.rbs +28 -0
  98. data/sig/ryac/pipeline/stage_runner.rbs +18 -0
  99. data/sig/ryac/pipeline/variable_renamer.rbs +21 -0
  100. data/sig/ryac/union_find.rbs +17 -0
  101. data/sig/ryac.rbs +13 -0
  102. data/sig/shims/env.rbs +19 -0
  103. data/sig/shims/typeprof/core/ast.rbs +299 -0
  104. data/sig/shims/typeprof/core/type.rbs +22 -0
  105. data/sig/shims/typeprof/core.rbs +73 -0
  106. data/tools/regen_pins.rb +45 -0
  107. 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