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,386 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'typeprof'
4
+
5
+ module Ryac
6
+ # The one place allowed to talk to TypeProf.
7
+ #
8
+ # Everything the renamer needs from type analysis is phrased as a handful
9
+ # of questions — which methods a call can reach, who calls a method, what
10
+ # a receiver is, what a constant resolves to, what a class inherits. The
11
+ # collections compute every lexical fact themselves on the Prism tree and
12
+ # come here only for those questions, so a change in TypeProf's internals
13
+ # breaks this file and nothing else.
14
+ #
15
+ # Answers refer to source positions (location keys) or plain values, never
16
+ # to TypeProf objects.
17
+ class TypeOracle
18
+ # Boots TypeProf over the program and its RBS. This owns the two
19
+ # internals the boot requires — the Service construction and the
20
+ # private @rb_text_nodes table behind update_rb_file — so an upstream
21
+ # change to either breaks here and nowhere else.
22
+ def self.boot(content, rbs_files)
23
+ path = '(minify_concat)'
24
+ service = TypeProf::Core::Service.new({})
25
+ rbs_files.each do |rbs_path, rbs_content|
26
+ service.update_rbs_file(rbs_path, rbs_content)
27
+ end
28
+ service.update_rb_file(path, content)
29
+ new(service.genv, service.instance_variable_get(:@rb_text_nodes)[path])
30
+ end
31
+
32
+ # TypeProf keeps the backing Prism node private because an editor has no
33
+ # reason to ask; the coordinate join (tp_key below) asks constantly,
34
+ # since Prism is where the syntax being rewritten lives.
35
+ def self.raw_prism_node(node)
36
+ node.instance_variable_get(:@raw_node)
37
+ end
38
+
39
+ def initialize(genv, tp_root)
40
+ @genv = genv
41
+ @tp_root = tp_root
42
+ end
43
+
44
+ # Yields every ancestor cpath of the class, including its own.
45
+ # Unresolvable classes yield nothing.
46
+ def each_ancestor_cpath(cpath, singleton, &block)
47
+ @ancestor_cpaths ||= {} #: Hash[MethodRenameMapping::class_key, Array[Array[Symbol]]]
48
+ cached = @ancestor_cpaths[[cpath, singleton]] ||= begin
49
+ list = [] #: Array[Array[Symbol]]
50
+ mod = @genv.resolve_cpath(cpath) rescue nil
51
+ if mod
52
+ @genv.each_superclass(mod, singleton) do |ancestor_mod, _singleton|
53
+ list << ancestor_mod.cpath
54
+ end
55
+ end
56
+ list
57
+ end
58
+ cached.each(&block)
59
+ end
60
+
61
+ # Yields [ancestor_cpath, singleton, method_names] along the ancestor
62
+ # chain, own class first — the namespace a new method name must not
63
+ # collide with.
64
+ def each_ancestor_methods(cpath, singleton)
65
+ mod = @genv.resolve_cpath(cpath) rescue nil
66
+ return unless mod
67
+
68
+ @genv.each_superclass(mod, singleton) do |ancestor_mod, s|
69
+ names = [] #: Array[String]
70
+ ancestor_mod.methods[s]&.each_key { |mid| names << mid.to_s }
71
+ yield ancestor_mod.cpath, s, names
72
+ end
73
+ end
74
+
75
+ # True when type analysis has any record of the method at all.
76
+ def method_known?(cpath, singleton, mid)
77
+ !resolve_method(cpath, singleton, mid).nil?
78
+ end
79
+
80
+ # True when the method has at least one definition in the analyzed source.
81
+ def method_defined?(cpath, singleton, mid)
82
+ entity = resolve_method(cpath, singleton, mid)
83
+ !entity.nil? && entity.defs.size > 0
84
+ end
85
+
86
+ def method_call_count(cpath, singleton, mid)
87
+ entity = resolve_method(cpath, singleton, mid)
88
+ entity ? entity.method_call_boxes.size : 0
89
+ end
90
+
91
+ # Location keys of every call site that dispatches to the method.
92
+ def each_call_site_key(cpath, singleton, mid)
93
+ entity = resolve_method(cpath, singleton, mid)
94
+ return unless entity
95
+
96
+ entity.method_call_boxes.each do |call_box|
97
+ yield tp_key(call_box.node)
98
+ end
99
+ end
100
+
101
+ # Location keys of the method's definition sites.
102
+ def method_definition_keys(cpath, singleton, mid)
103
+ entity = resolve_method(cpath, singleton, mid)
104
+ return [] unless entity
105
+
106
+ entity.defs.to_a.map { |d| tp_key(d.node) }
107
+ end
108
+
109
+ # One record per call site that dispatches to the method, as plain data:
110
+ #
111
+ # prism_node the call in the syntax tree (nil for a super with no
112
+ # syntactic call node of its own, and for a dispatch
113
+ # synthesized inside an RBS declaration)
114
+ # super true for `super` — dispatch from a subclass override
115
+ # caller_cpath the class nesting the call sits in
116
+ # receiver whether the call is written with an explicit receiver
117
+ # keyword_entries [[symbol_node, value_node], ...] for literal keyword
118
+ # arguments, nil when the call passes none
119
+ # keyword_splat true when keywords arrive via **splat
120
+ CallerInfo = Struct.new(:prism_node, :super, :caller_cpath, :receiver,
121
+ :keyword_entries, :keyword_splat, keyword_init: true)
122
+
123
+ def each_caller(cpath, singleton, mid)
124
+ entity = resolve_method(cpath, singleton, mid)
125
+ return unless entity
126
+
127
+ entity.method_call_boxes.each do |call_box|
128
+ node = call_box.node
129
+
130
+ if node.is_a?(TypeProf::Core::AST::SuperNode) ||
131
+ node.is_a?(TypeProf::Core::AST::ForwardingSuperNode)
132
+ yield CallerInfo.new(prism_node: TypeOracle.raw_prism_node(node), super: true,
133
+ caller_cpath: node.lenv.cref.cpath, receiver: false,
134
+ keyword_entries: nil, keyword_splat: false)
135
+ next
136
+ end
137
+
138
+ # A box whose node is not a syntactic call names no site in the
139
+ # program — e.g. one synthesized while TypeProf evaluated an RBS
140
+ # declaration: `[x].map(&:foo)` dispatches to foo from Array#map's
141
+ # declared signature. Nothing in the source can be rewritten to
142
+ # follow a rename, so the record carries only the fact that an
143
+ # unrewritable caller exists.
144
+ unless node.is_a?(TypeProf::Core::AST::CallBaseNode)
145
+ yield CallerInfo.new(prism_node: nil, super: false, caller_cpath: nil,
146
+ receiver: false, keyword_entries: nil, keyword_splat: false)
147
+ next
148
+ end
149
+
150
+ entries = nil
151
+ splat = false
152
+ kw = node.respond_to?(:keyword_args) ? node.keyword_args : nil
153
+ if kw.is_a?(TypeProf::Core::AST::HashNode)
154
+ splat = kw.keys.any?(&:nil?)
155
+ entries = kw.keys.zip(kw.vals).filter_map do |sym_node, val_node|
156
+ # @type var val_node: TypeProf::Core::AST::Node
157
+ next unless sym_node.is_a?(TypeProf::Core::AST::SymbolNode)
158
+ [TypeOracle.raw_prism_node(sym_node), TypeOracle.raw_prism_node(val_node)] #: [Prism::Node, Prism::Node]
159
+ end
160
+ end
161
+
162
+ yield CallerInfo.new(prism_node: TypeOracle.raw_prism_node(node), super: false,
163
+ caller_cpath: nil, receiver: !node.recv.nil?,
164
+ keyword_entries: entries, keyword_splat: splat)
165
+ end
166
+ end
167
+
168
+ # The method keys a call could dispatch to, per type inference. nil when
169
+ # inference has no answer at all — distinct from an empty list.
170
+ def resolved_targets(prism_call_node, mid)
171
+ tp_node = tp_call_for(prism_call_node, mid)
172
+ return nil unless tp_node
173
+
174
+ keys = [] #: Array[method_key]
175
+ tp_node.boxes(:mcall) do |box|
176
+ box.resolve(@genv, nil) do |entity, ty, _mid, _orig_ty|
177
+ next unless entity
178
+ singleton = ty.is_a?(TypeProf::Core::Type::Singleton)
179
+ keys << [ty.mod.cpath, singleton, mid]
180
+ end
181
+ end
182
+ keys.any? ? keys : nil
183
+ end
184
+
185
+ # Symbol values inference assigns to the call's first positional argument
186
+ # — how `method(:foo)`-style dynamic references name their target.
187
+ def first_argument_symbols(prism_call_node)
188
+ tp_node = tp_call_for(prism_call_node, prism_call_node.name)
189
+ return [] unless tp_node
190
+
191
+ sym_arg = tp_node.positional_args&.first #: TypeProf::Core::AST::Node
192
+ ret = sym_arg.ret rescue nil
193
+ return [] unless ret
194
+
195
+ ret.types.each_key.filter_map do |ty|
196
+ ty.sym if ty.is_a?(TypeProf::Core::Type::Symbol)
197
+ end
198
+ end
199
+
200
+ # True when every inferred type of the call's receiver responds to mid —
201
+ # the safety condition for rewriting the call to a stdlib alias.
202
+ def receiver_responds_to?(prism_call_node, mid)
203
+ every_receiver_base_type(prism_call_node) do |base|
204
+ singleton = base.is_a?(TypeProf::Core::Type::Singleton)
205
+ type_responds_to?(base.mod, singleton, mid)
206
+ end
207
+ end
208
+
209
+ # Resolves a constant reference to its fully-qualified path, using type
210
+ # analysis to see through lexical-scope lookup and value constants.
211
+ # nil when the reference is not statically resolvable. Memoized — the
212
+ # counting, external-reference, and precompute passes each ask about the
213
+ # same nodes.
214
+ def resolve_constant_read(prism_node)
215
+ @const_resolution_cache ||= {} #: Hash[location_key, Array[Symbol]?]
216
+ key = AstUtils.location_key(prism_node)
217
+ return @const_resolution_cache[key] if @const_resolution_cache.key?(key)
218
+
219
+ tp_node = tp_const_for(prism_node)
220
+ @const_resolution_cache[key] = tp_node ? resolve_tp_const(tp_node) : nil
221
+ end
222
+
223
+ # How many read sites type analysis records for the constant. Can exceed
224
+ # the syntactic count when reads reach it through resolution the text
225
+ # does not show.
226
+ def constant_read_count(cpath)
227
+ # @type var entity: untyped
228
+ entity = begin
229
+ @genv.resolve_const(cpath)
230
+ rescue StandardError
231
+ nil
232
+ end
233
+ entity.respond_to?(:read_boxes) ? entity.read_boxes.size : 0
234
+ end
235
+
236
+ # True when every inferred type of the call's receiver is type_name or a
237
+ # subclass — the safety condition for type-specific rewrites like
238
+ # `.empty?` → `=={}` on a Hash.
239
+ def receiver_within_type?(prism_call_node, type_name)
240
+ target_mod = @genv.resolve_cpath([type_name])
241
+ return false unless target_mod
242
+
243
+ every_receiver_base_type(prism_call_node) do |base|
244
+ mod_is_or_inherits?(base.mod, target_mod)
245
+ end
246
+ end
247
+
248
+ private
249
+
250
+ # Location key of a TypeProf node, via the Prism node it was built from.
251
+ # TypeProf models source it cannot point back at — a node may carry no
252
+ # @raw_node at all, or an RBS node instead of a Prism one on sig-built
253
+ # nodes. Either way a node we cannot locate is one we cannot rename, so
254
+ # this raises rather than inventing a key that could never match. The
255
+ # conversion stays inside the oracle: everything outside it holds Prism
256
+ # nodes, and AstUtils keys those.
257
+ def tp_key(node)
258
+ prism_node = TypeOracle.raw_prism_node(node)
259
+ raise ArgumentError, "no source location behind #{node.class}" unless prism_node.is_a?(Prism::Node)
260
+
261
+ AstUtils.location_key(prism_node)
262
+ end
263
+
264
+ def resolve_method(cpath, singleton, mid)
265
+ @method_cache ||= {} #: Hash[method_key, TypeProf::Core::MethodEntity?]
266
+ key = [cpath, singleton, mid] #: method_key
267
+ return @method_cache[key] if @method_cache.key?(key)
268
+
269
+ @method_cache[key] = begin
270
+ @genv.resolve_method(cpath, singleton, mid)
271
+ rescue StandardError
272
+ nil
273
+ end
274
+ end
275
+
276
+ # The shared scaffold of the receiver-safety questions: false unless the
277
+ # receiver has inferred types at all, then the block must hold for every
278
+ # type's base.
279
+ def every_receiver_base_type(prism_call_node)
280
+ recv = tp_call_for(prism_call_node, prism_call_node.name)&.recv #: untyped
281
+ return false unless recv.respond_to?(:ret) && recv.ret
282
+
283
+ types = recv.ret.types
284
+ return false if types.empty?
285
+
286
+ types.all? do |ty, _|
287
+ base = ty.base_type(@genv)
288
+ next false unless base.respond_to?(:mod)
289
+ yield base
290
+ end
291
+ end
292
+
293
+ def type_responds_to?(mod, singleton, mid)
294
+ @genv.each_superclass(mod, singleton) do |ancestor_mod, s|
295
+ entity = ancestor_mod.methods[s]&.[](mid)
296
+ return true if entity && (entity.exist? || entity.aliases.any?)
297
+ end
298
+ false
299
+ end
300
+
301
+ def mod_is_or_inherits?(mod, target)
302
+ return true if mod == target
303
+ @genv.each_superclass(mod, false) do |ancestor, _|
304
+ return true if ancestor == target
305
+ end
306
+ false
307
+ end
308
+
309
+ TP_CALL_NODES = [
310
+ TypeProf::Core::AST::CallNode,
311
+ TypeProf::Core::AST::CallReadNode,
312
+ TypeProf::Core::AST::CallWriteNode
313
+ ].freeze
314
+
315
+ # TypeProf's node for a call at a given source position. TypeProf answers
316
+ # type questions per node in its own tree; this index is how a question
317
+ # about a Prism node finds its counterpart.
318
+ #
319
+ # Indexed by position *and* method name: a compound write like
320
+ # `self.count += 1` is one position but two TypeProf nodes — a read of
321
+ # `count` and a write of `count=` — and a question about either name has
322
+ # to reach its own node.
323
+ def tp_call_for(prism_node, mid)
324
+ build_tp_indexes
325
+ @tp_calls_by_loc[AstUtils.location_key(prism_node)]&.[](mid)
326
+ end
327
+
328
+ # TypeProf's constant-read node at a given source position. Covers plain
329
+ # reads, every level of a qualified chain, and the read half of compound
330
+ # writes (`X ||= 1` reads at the whole expression's position).
331
+ def tp_const_for(prism_node)
332
+ build_tp_indexes
333
+ @tp_consts_by_loc[AstUtils.location_key(prism_node)]
334
+ end
335
+
336
+ # Both position indexes come from the same walk; a real run always needs
337
+ # both, so one traversal fills them together.
338
+ def build_tp_indexes
339
+ return if @tp_calls_by_loc
340
+
341
+ calls = {} #: Hash[location_key, Hash[Symbol, TypeProf::Core::AST::CallBaseNode]]
342
+ consts = {} #: Hash[location_key, TypeProf::Core::AST::ConstantReadNode]
343
+ @tp_root.traverse do |event, node|
344
+ next unless event == :enter
345
+ case node
346
+ when *TP_CALL_NODES
347
+ # @type var node: TypeProf::Core::AST::CallBaseNode
348
+ (calls[tp_key(node)] ||= {})[node.mid] = node
349
+ when TypeProf::Core::AST::ConstantReadNode
350
+ consts[tp_key(node)] = node
351
+ end
352
+ end
353
+ @tp_calls_by_loc = calls
354
+ @tp_consts_by_loc = consts
355
+ end
356
+
357
+ # A class/module reference resolves through its own analysis result; a
358
+ # value constant through its definition's location; a qualified chain
359
+ # (Foo::CONST) through its prefix, recursively.
360
+ def resolve_tp_const(node)
361
+ return nil unless node.is_a?(TypeProf::Core::AST::ConstantReadNode)
362
+
363
+ static_ret = begin
364
+ node.static_ret
365
+ rescue StandardError
366
+ nil
367
+ end
368
+ return nil unless static_ret.respond_to?(:cpath)
369
+ return static_ret.cpath if static_ret.cpath
370
+
371
+ cdef = static_ret.respond_to?(:cdef) ? static_ret.cdef : nil #: untyped
372
+ if cdef.respond_to?(:defs)
373
+ cdef.defs.each do |d|
374
+ return d.static_cpath if d.respond_to?(:static_cpath) && d.static_cpath
375
+ end
376
+ end
377
+
378
+ if node.cbase
379
+ base_cpath = resolve_tp_const(node.cbase)
380
+ return base_cpath + [node.cname] if base_cpath
381
+ end
382
+
383
+ nil
384
+ end
385
+ end
386
+ end
@@ -0,0 +1,321 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Ryac
4
+ module AstUtils
5
+ module_function
6
+
7
+ # The plain-symbol arguments of a call, as symbols — how attr_*,
8
+ # private_constant and friends name their subjects.
9
+ def symbol_arguments(call_node)
10
+ call_node.arguments&.arguments&.filter_map { |arg|
11
+ arg.unescaped.to_sym if arg.is_a?(Prism::SymbolNode)
12
+ } || []
13
+ end
14
+
15
+ def unwrap_statements(node)
16
+ return node unless node
17
+ node = node.body if node.is_a?(Prism::ParenthesesNode)
18
+ if node.is_a?(Prism::StatementsNode) && node.body.size == 1
19
+ node.body.first
20
+ else
21
+ node
22
+ end
23
+ end
24
+
25
+ MIDDLE_METHODS = %i[+ - * / ** % ^ > < <= >= <=> == === != & | << >> =~ !~].freeze
26
+
27
+ def middle_method?(method)
28
+ MIDDLE_METHODS.include?(method)
29
+ end
30
+
31
+ def logical_op?(node)
32
+ node.is_a?(Prism::OrNode) || node.is_a?(Prism::AndNode)
33
+ end
34
+
35
+ def has_block?(node)
36
+ node.block != nil
37
+ end
38
+
39
+ def single_statement_body?(body)
40
+ return false if body.nil?
41
+ return body.body.size == 1 if body.is_a?(Prism::StatementsNode)
42
+ true
43
+ end
44
+
45
+ def setter_def_name?(name)
46
+ s = name.to_s
47
+ s.end_with?('=') && !%w[== != <= >= ===].include?(s)
48
+ end
49
+
50
+ def simple_negatable?(node)
51
+ inner = unwrap_statements(node)
52
+ return false if logical_op?(inner)
53
+
54
+ inner.is_a?(Prism::CallNode) ? !middle_method?(inner.name) : true
55
+ end
56
+
57
+ def first_arg_starts_with_brace?(arg)
58
+ inner = unwrap_statements(arg)
59
+ return true if inner.is_a?(Prism::HashNode)
60
+ return first_arg_starts_with_brace?(inner.receiver) if inner.is_a?(Prism::CallNode) && inner.receiver
61
+
62
+ false
63
+ end
64
+
65
+ # Does this node's string representation end with a character
66
+ # that could form `name!` or `name?` if `!` or `?` is appended?
67
+ # Used for spacing before `!~` operator and ternary `?`/`:`.
68
+ # Recursively follows rightmost sub-expressions for compound nodes.
69
+ def ends_with_name_char?(node)
70
+ inner = unwrap_statements(node)
71
+ case inner
72
+ # Empty parens unwrap to nothing; the rendered `()` ends with ')'.
73
+ when nil
74
+ false
75
+ # nil renders as `()` — ends with ')', not a name char.
76
+ when Prism::NilNode
77
+ false
78
+ # Terminals: definitely safe (end with delimiter/sigil/literal)
79
+ when Prism::ParenthesesNode,
80
+ Prism::StringNode, Prism::InterpolatedStringNode,
81
+ Prism::InterpolatedSymbolNode,
82
+ Prism::RegularExpressionNode, Prism::InterpolatedRegularExpressionNode,
83
+ Prism::ArrayNode, Prism::HashNode, Prism::LambdaNode,
84
+ Prism::InstanceVariableReadNode, Prism::ClassVariableReadNode,
85
+ Prism::GlobalVariableReadNode,
86
+ Prism::IntegerNode, Prism::FloatNode, Prism::RationalNode, Prism::ImaginaryNode,
87
+ Prism::DefinedNode, Prism::RescueModifierNode
88
+ false
89
+ # Bare symbol :name ends with name char; quoted :"name" ends with delimiter
90
+ when Prism::SymbolNode
91
+ !inner.closing_loc
92
+ # Compound: follow rightmost sub-expression
93
+ when Prism::OrNode, Prism::AndNode
94
+ ends_with_name_char?(inner.right)
95
+ when Prism::RangeNode
96
+ inner.right ? ends_with_name_char?(inner.right) : false
97
+ when Prism::CallNode
98
+ return false if inner.block
99
+ if middle_method?(inner.name)
100
+ # Binary op: a+b → ends with right operand
101
+ arg = inner.arguments&.arguments&.first
102
+ arg ? ends_with_name_char?(arg) : true
103
+ elsif inner.name == :[]=
104
+ # Index assignment: recv[k]=val → ends with value
105
+ last_arg = inner.arguments&.arguments&.last
106
+ last_arg ? ends_with_name_char?(last_arg) : false
107
+ elsif inner.receiver && !inner.arguments && (inner.name == :! || inner.name == :~)
108
+ # Unary prefix operators: !x, ~x → output ends with operand
109
+ ends_with_name_char?(inner.receiver)
110
+ elsif inner.opening_loc
111
+ false # foo(args) → ends with )
112
+ elsif inner.name.to_s.end_with?('?', '!')
113
+ false
114
+ else
115
+ true # bare method: foo.bar → ends with ident char
116
+ end
117
+ when Prism::YieldNode, Prism::SuperNode
118
+ inner.arguments.nil?
119
+ # Assignment: var=value → ends with value
120
+ when Prism::LocalVariableWriteNode, Prism::InstanceVariableWriteNode,
121
+ Prism::ClassVariableWriteNode, Prism::GlobalVariableWriteNode,
122
+ Prism::ConstantWriteNode, Prism::ConstantPathWriteNode
123
+ ends_with_name_char?(inner.value)
124
+ when Prism::LocalVariableOrWriteNode, Prism::LocalVariableAndWriteNode,
125
+ Prism::InstanceVariableOrWriteNode, Prism::InstanceVariableAndWriteNode,
126
+ Prism::ClassVariableOrWriteNode, Prism::ClassVariableAndWriteNode,
127
+ Prism::GlobalVariableOrWriteNode, Prism::GlobalVariableAndWriteNode,
128
+ Prism::ConstantOrWriteNode, Prism::ConstantAndWriteNode
129
+ ends_with_name_char?(inner.value)
130
+ else
131
+ true # conservative: local var, const, nil, true, false, self, etc.
132
+ end
133
+ end
134
+
135
+ # Does this node's output end with ? or ! method suffix?
136
+ # Recursively follows rightmost sub-expressions.
137
+ def ends_with_method_suffix?(node)
138
+ inner = unwrap_statements(node)
139
+ case inner
140
+ when Prism::OrNode, Prism::AndNode
141
+ ends_with_method_suffix?(inner.right)
142
+ when Prism::CallNode
143
+ return false if inner.block
144
+ return false if inner.opening_loc && inner.name != :[]=
145
+ if middle_method?(inner.name)
146
+ arg = inner.arguments&.arguments&.first
147
+ arg ? ends_with_method_suffix?(arg) : false
148
+ elsif inner.name == :[]=
149
+ last_arg = inner.arguments&.arguments&.last
150
+ last_arg ? ends_with_method_suffix?(last_arg) : false
151
+ elsif inner.receiver && !inner.arguments && (inner.name == :! || inner.name == :~)
152
+ # Unary prefix operators (!x, ~x) — output ends with operand, not suffix
153
+ ends_with_method_suffix?(inner.receiver)
154
+ else
155
+ inner.name.to_s.end_with?('?', '!')
156
+ end
157
+ when Prism::SymbolNode
158
+ !inner.closing_loc && inner.value.to_s.end_with?('?', '!')
159
+ when Prism::LocalVariableWriteNode, Prism::InstanceVariableWriteNode,
160
+ Prism::ClassVariableWriteNode, Prism::GlobalVariableWriteNode,
161
+ Prism::ConstantWriteNode, Prism::ConstantPathWriteNode,
162
+ Prism::LocalVariableOrWriteNode, Prism::LocalVariableAndWriteNode,
163
+ Prism::InstanceVariableOrWriteNode, Prism::InstanceVariableAndWriteNode,
164
+ Prism::ClassVariableOrWriteNode, Prism::ClassVariableAndWriteNode,
165
+ Prism::GlobalVariableOrWriteNode, Prism::GlobalVariableAndWriteNode,
166
+ Prism::ConstantOrWriteNode, Prism::ConstantAndWriteNode
167
+ ends_with_method_suffix?(inner.value)
168
+ else
169
+ false
170
+ end
171
+ end
172
+
173
+ # Needs space before ternary `?`?
174
+ # True when condition ends with: name char, `?`, `!`, or digit.
175
+ def needs_ternary_q_space?(node)
176
+ ends_with_name_char?(node) || ends_with_method_suffix?(node)
177
+ end
178
+
179
+ def modifier_control_flow?(node)
180
+ modifier_conditional?(node) || modifier_loop?(node)
181
+ end
182
+
183
+ def modifier_conditional?(node)
184
+ case node
185
+ when Prism::IfNode then node.if_keyword_loc && !node.end_keyword_loc
186
+ when Prism::UnlessNode then node.keyword_loc && !node.end_keyword_loc
187
+ else false
188
+ end
189
+ end
190
+
191
+ def modifier_loop?(node)
192
+ case node
193
+ when Prism::WhileNode, Prism::UntilNode then !node.closing_loc
194
+ else false
195
+ end
196
+ end
197
+
198
+ # Bytes before which a ternary does NOT need wrapping parens
199
+ TERNARY_SAFE_PREV = [';'.ord, "\n".ord, "\r".ord, '('.ord, '['.ord, '{'.ord].freeze
200
+ COMPARISON_PREV = ['<'.ord, '>'.ord, '!'.ord, '='.ord].freeze
201
+ PIPE_BREAK_BYTES = [';'.ord, "\n".ord, '{'.ord].freeze
202
+
203
+ def ternary_needs_parens?(node, source)
204
+ start = node.location.start_offset
205
+ return false if start == 0
206
+ prev = source.getbyte(start - 1)
207
+ return false if TERNARY_SAFE_PREV.include?(prev)
208
+ if prev == '='.ord
209
+ return false if start < 2
210
+ prev2 = source.getbyte(start - 2)
211
+ return COMPARISON_PREV.include?(prev2)
212
+ end
213
+ if prev == '|'.ord
214
+ return true if start < 3
215
+ return true if source.getbyte(start - 2) == '|'.ord
216
+ pos = start - 2
217
+ while pos >= 0
218
+ byte = source.getbyte(pos)
219
+ if byte == '|'.ord
220
+ return !(pos > 0 && source.getbyte(pos - 1) == '{'.ord)
221
+ end
222
+ break if PIPE_BREAK_BYTES.include?(byte)
223
+ pos -= 1
224
+ end
225
+ return true
226
+ end
227
+ true
228
+ end
229
+
230
+ def can_omit_parens?(node, method_name = nil)
231
+ return false if has_block?(node)
232
+ # `.()` proc-call sugar has no message; its parens ARE the call.
233
+ return false if node.message_loc.nil?
234
+ raw_args = node.arguments&.arguments || []
235
+ positional_args = raw_args.reject { |a| a.is_a?(Prism::KeywordHashNode) }
236
+ return false if positional_args.empty?
237
+ return false if raw_args.any? { |a| a.is_a?(Prism::ForwardingArgumentsNode) }
238
+ return false if node.name == :[] || node.name == :[]=
239
+ name_to_check = method_name || node.name.to_s
240
+ return false if name_to_check.end_with?('?')
241
+ return false if first_arg_starts_with_brace?(positional_args.first)
242
+ return false if first_arg_is_regex?(positional_args.first)
243
+ return false if raw_args.any? { |a| contains_bare_block?(a) }
244
+
245
+ true
246
+ end
247
+
248
+ def first_arg_is_regex?(arg)
249
+ arg.is_a?(Prism::RegularExpressionNode) || arg.is_a?(Prism::InterpolatedRegularExpressionNode)
250
+ end
251
+
252
+ def contains_bare_block?(node)
253
+ case node
254
+ when Prism::CallNode
255
+ return true if node.block.is_a?(Prism::BlockNode)
256
+ return true if node.receiver && contains_bare_block?(node.receiver)
257
+ return false if node.opening_loc
258
+ node.arguments&.arguments&.any? { |a| contains_bare_block?(a) } || false
259
+ when Prism::ParenthesesNode, Prism::ArrayNode, Prism::HashNode, Prism::LambdaNode
260
+ false
261
+ when Prism::KeywordHashNode
262
+ node.elements.any? { |e| e.is_a?(Prism::AssocNode) && contains_bare_block?(e.value) }
263
+ when Prism::SplatNode
264
+ contains_bare_block?(node.expression)
265
+ else
266
+ false
267
+ end
268
+ end
269
+
270
+ # Depth-first over every node. The shared primitive for analyses that
271
+ # need the whole tree without caring about scope or nesting context.
272
+ def self.each_node(node, &block)
273
+ return unless node.is_a?(Prism::Node)
274
+ yield node
275
+ node.compact_child_nodes.each { |child| each_node(child, &block) }
276
+ end
277
+
278
+ # Expressions that rebind to the surrounding statement when their parens
279
+ # are stripped: `def f =expr in pat` parses as `(def f =expr) in pat`.
280
+ # Every "needs parens" list must include these.
281
+ MATCH_REBIND_NODES = [Prism::MatchPredicateNode, Prism::MatchRequiredNode].freeze
282
+
283
+ def self.match_rebind?(node)
284
+ MATCH_REBIND_NODES.any? { |t| node.is_a?(t) }
285
+ end
286
+
287
+ # The shorthand-pun values of a hash-ish node: implicit assoc values that
288
+ # are NOT plain local reads (`{ label: }` calling method label). The one
289
+ # definition of "pun" shared by the keyword and method exclusion passes.
290
+ def self.shorthand_pun_values(node)
291
+ return [] unless node.is_a?(Prism::KeywordHashNode) || node.is_a?(Prism::HashNode)
292
+
293
+ node.elements.filter_map do |el|
294
+ next unless el.is_a?(Prism::AssocNode) && el.value.is_a?(Prism::ImplicitNode)
295
+
296
+ inner = el.value.value
297
+ inner unless inner.is_a?(Prism::LocalVariableReadNode)
298
+ end
299
+ end
300
+
301
+ # The single place the analysis and the patchers agree on what "the same
302
+ # piece of source" is: every map between phases is keyed by this. Prism
303
+ # nodes only — TypeOracle converts its own tree's nodes before keying,
304
+ # so the seam between the two trees stays inside the oracle.
305
+ #
306
+ # Byte offsets, because that is the coordinate system patches are applied
307
+ # in — deriving a separate line/column space for the join only gave the two
308
+ # sides a way to disagree.
309
+ def self.location_key(node)
310
+ loc = node.location
311
+ [loc.start_offset, loc.end_offset]
312
+ end
313
+
314
+ # The line/column coordinate space (syntax_data and friends) — kept
315
+ # separate from byte-offset location_key on purpose.
316
+ def self.line_col_key(node)
317
+ loc = node.location
318
+ [loc.start_line, loc.start_column]
319
+ end
320
+ end
321
+ end