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,298 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Ryac
|
|
4
|
+
module AnalysisPhases
|
|
5
|
+
def collect_method_definitions(prism_root)
|
|
6
|
+
Nesting.each_method_definition(prism_root) do |node, method_key|
|
|
7
|
+
next if EXCLUDED_METHODS.include?(method_key[2])
|
|
8
|
+
|
|
9
|
+
@method_rename_mapping.add_method(method_key, node)
|
|
10
|
+
link_module_function_variant(node, method_key)
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
# attr_reader/attr_accessor define getters worth renaming; attr_writer's
|
|
14
|
+
# setter is derived from the getter name and is never registered on its
|
|
15
|
+
# own.
|
|
16
|
+
each_attr_declaration(prism_root, %i[attr_reader attr_accessor]) do |node, cpath, singleton, mid|
|
|
17
|
+
next if EXCLUDED_METHODS.include?(mid)
|
|
18
|
+
|
|
19
|
+
@method_rename_mapping.add_method([cpath, singleton, mid].freeze, node)
|
|
20
|
+
end
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
def resolve_method_calls
|
|
24
|
+
call_node_to_keys = Hash.new { |h, k| h[k] = [] } #: Hash[location_key, Array[method_key]]
|
|
25
|
+
resolved_call_keys = Set.new
|
|
26
|
+
super_merges = [] #: Array[[method_key, method_key]]
|
|
27
|
+
sig_dispatch_mids = Set.new
|
|
28
|
+
|
|
29
|
+
@method_rename_mapping.each_method_key do |key|
|
|
30
|
+
@oracle.each_caller(key[0], key[1], key[2]) do |info|
|
|
31
|
+
if info.super
|
|
32
|
+
# caller_cpath is always present on super records
|
|
33
|
+
entry = [info.caller_cpath, key[1], key[2]].freeze #: method_key
|
|
34
|
+
super_merges << [entry, key]
|
|
35
|
+
next
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
if info.prism_node.nil?
|
|
39
|
+
# The dispatch happens inside an RBS declaration, so there is no
|
|
40
|
+
# source site to rewrite; the name must survive on every class.
|
|
41
|
+
sig_dispatch_mids << key[2]
|
|
42
|
+
next
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
scope_id = info.receiver ? nil : @local_scopes.scope_id_of(info.prism_node)
|
|
46
|
+
@method_rename_mapping.add_call_site(info.prism_node, key, has_receiver: info.receiver, scope_id: scope_id)
|
|
47
|
+
loc = AstUtils.location_key(info.prism_node)
|
|
48
|
+
call_node_to_keys[loc] << key
|
|
49
|
+
resolved_call_keys << loc
|
|
50
|
+
end
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
@method_rename_mapping.exclude_methods_by_mid(sig_dispatch_mids) unless sig_dispatch_mids.empty?
|
|
54
|
+
merge_super_groups(super_merges)
|
|
55
|
+
merge_polymorphic_groups(call_node_to_keys)
|
|
56
|
+
merge_unresolved_calls(resolved_call_keys)
|
|
57
|
+
@method_rename_mapping.merge_blind_def_groups
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
# Visibility resets at every reopen of this module, so each collection
|
|
61
|
+
# file's `private` covers that file alone.
|
|
62
|
+
private
|
|
63
|
+
|
|
64
|
+
# module_function publishes a single `def` as both an instance and a
|
|
65
|
+
# singleton method, so the two must always be renamed together.
|
|
66
|
+
#
|
|
67
|
+
# The pair is identified by both entities pointing at the same definition
|
|
68
|
+
# site — an explicit `def self.foo` alongside `def foo` is two sites and
|
|
69
|
+
# must stay independent. The "no defs but has call boxes" shape is also
|
|
70
|
+
# accepted: older TypeProf releases recorded no def at all for the
|
|
71
|
+
# singleton side of module_function.
|
|
72
|
+
def link_module_function_variant(def_node, method_key)
|
|
73
|
+
cpath, singleton, mid = method_key
|
|
74
|
+
return unless @oracle.method_known?(cpath, !singleton, mid)
|
|
75
|
+
|
|
76
|
+
alt_def_keys = @oracle.method_definition_keys(cpath, !singleton, mid)
|
|
77
|
+
shares_definition = alt_def_keys.include?(AstUtils.location_key(def_node))
|
|
78
|
+
call_only = alt_def_keys.empty? && @oracle.method_call_count(cpath, !singleton, mid) > 0
|
|
79
|
+
return unless shares_definition || call_only
|
|
80
|
+
|
|
81
|
+
alt_key = [cpath, !singleton, mid].freeze
|
|
82
|
+
@method_rename_mapping.add_method(alt_key, nil)
|
|
83
|
+
@method_rename_mapping.merge_groups(method_key, alt_key)
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
def merge_super_groups(super_merges)
|
|
87
|
+
super_merges.each do |child_key, parent_key|
|
|
88
|
+
@method_rename_mapping.merge_groups(child_key, parent_key) if @method_rename_mapping.has_method?(child_key)
|
|
89
|
+
end
|
|
90
|
+
end
|
|
91
|
+
|
|
92
|
+
# One call site reaching the same method name on several classes is
|
|
93
|
+
# polymorphism: merge, so every receiver renames in lockstep. One call site
|
|
94
|
+
# reaching several *different* names is a computed dispatch — optcarrot's
|
|
95
|
+
# `send(mode)` resolves to imm/zpg/abs/… at once — and "merging" that would
|
|
96
|
+
# assign one short name to all of them, collapsing distinct methods into
|
|
97
|
+
# whichever def lands last. Those names must simply survive.
|
|
98
|
+
def merge_polymorphic_groups(call_node_to_keys)
|
|
99
|
+
computed_dispatch_mids = Set.new
|
|
100
|
+
call_node_to_keys.each_value do |keys|
|
|
101
|
+
next if keys.size < 2
|
|
102
|
+
|
|
103
|
+
mids = keys.map { |k| k[2] }.uniq
|
|
104
|
+
if mids.size > 1
|
|
105
|
+
computed_dispatch_mids.merge(mids)
|
|
106
|
+
next
|
|
107
|
+
end
|
|
108
|
+
|
|
109
|
+
(1...keys.size).each { |i| @method_rename_mapping.merge_groups(keys[i - 1], keys[i]) }
|
|
110
|
+
end
|
|
111
|
+
@method_rename_mapping.exclude_methods_by_mid(computed_dispatch_mids) unless computed_dispatch_mids.empty?
|
|
112
|
+
end
|
|
113
|
+
|
|
114
|
+
UNRESOLVED_CALL_NODES = [
|
|
115
|
+
Prism::CallNode,
|
|
116
|
+
Prism::CallOperatorWriteNode,
|
|
117
|
+
Prism::CallOrWriteNode,
|
|
118
|
+
Prism::CallAndWriteNode
|
|
119
|
+
].freeze
|
|
120
|
+
|
|
121
|
+
def merge_unresolved_calls(resolved_call_keys)
|
|
122
|
+
method_mids = @method_rename_mapping.method_mids
|
|
123
|
+
unresolved_by_mid = Hash.new { |h, k| h[k] = [] } #: Hash[Symbol, Array[Prism::Node]]
|
|
124
|
+
|
|
125
|
+
AstUtils.each_node(@prism_root) do |node|
|
|
126
|
+
mids = case node
|
|
127
|
+
when Prism::CallNode
|
|
128
|
+
[node.name]
|
|
129
|
+
when Prism::CallOperatorWriteNode, Prism::CallOrWriteNode, Prism::CallAndWriteNode
|
|
130
|
+
[node.read_name, node.write_name]
|
|
131
|
+
else
|
|
132
|
+
next
|
|
133
|
+
end
|
|
134
|
+
|
|
135
|
+
loc = AstUtils.location_key(node)
|
|
136
|
+
next if resolved_call_keys.include?(loc)
|
|
137
|
+
|
|
138
|
+
mids.each do |mid|
|
|
139
|
+
unresolved_by_mid[mid] << node if method_mids.include?(mid)
|
|
140
|
+
end
|
|
141
|
+
end
|
|
142
|
+
|
|
143
|
+
exclude_mids = Set.new
|
|
144
|
+
unresolved_by_mid.each do |mid, call_nodes|
|
|
145
|
+
mapped_calls = [] #: Array[Prism::Node]
|
|
146
|
+
should_exclude = false
|
|
147
|
+
|
|
148
|
+
call_nodes.each do |node|
|
|
149
|
+
verdict = classify_unresolved_call(mid, node)
|
|
150
|
+
case verdict
|
|
151
|
+
when :mapped then mapped_calls << node
|
|
152
|
+
when :exclude then should_exclude = true; break
|
|
153
|
+
# :unrelated — call targets a different class's method, skip
|
|
154
|
+
end
|
|
155
|
+
end
|
|
156
|
+
|
|
157
|
+
if should_exclude
|
|
158
|
+
exclude_mids << mid
|
|
159
|
+
elsif mapped_calls.any?
|
|
160
|
+
@method_rename_mapping.merge_all_by_mid(mid)
|
|
161
|
+
@method_rename_mapping.add_unresolved_sites_for_mid(mid, mapped_calls)
|
|
162
|
+
end
|
|
163
|
+
end
|
|
164
|
+
@method_rename_mapping.exclude_methods_by_mid(exclude_mids) unless exclude_mids.empty?
|
|
165
|
+
end
|
|
166
|
+
|
|
167
|
+
def classify_unresolved_call(mid, prism_node)
|
|
168
|
+
# Callers only pass the UNRESOLVED_CALL_NODES shapes, all of which carry
|
|
169
|
+
# a receiver.
|
|
170
|
+
# @type var prism_node: Prism::CallNode | Prism::CallOperatorWriteNode | Prism::CallOrWriteNode | Prism::CallAndWriteNode
|
|
171
|
+
return :mapped unless prism_node.receiver
|
|
172
|
+
|
|
173
|
+
target_keys = @oracle.resolved_targets(prism_node, mid)
|
|
174
|
+
return :exclude if target_keys.nil?
|
|
175
|
+
|
|
176
|
+
all_mapped = target_keys.all? { |key| @method_rename_mapping.has_method?(key) }
|
|
177
|
+
all_mapped ? :mapped : :unrelated
|
|
178
|
+
end
|
|
179
|
+
|
|
180
|
+
public
|
|
181
|
+
|
|
182
|
+
def scan_dynamic_method_references(prism_root)
|
|
183
|
+
dynamic_mids = Set.new
|
|
184
|
+
AstUtils.each_node(prism_root) do |node|
|
|
185
|
+
next unless node.is_a?(Prism::CallNode)
|
|
186
|
+
next unless DYNAMIC_DISPATCH_METHODS.include?(node.name)
|
|
187
|
+
|
|
188
|
+
@oracle.first_argument_symbols(node).each { |sym| dynamic_mids << sym }
|
|
189
|
+
end
|
|
190
|
+
@method_rename_mapping.exclude_methods_by_mid(dynamic_mids) unless dynamic_mids.empty?
|
|
191
|
+
end
|
|
192
|
+
|
|
193
|
+
# send/public_send/__send__ are NOT listed here because TypeProf resolves
|
|
194
|
+
# them as MethodCallBoxes on the target method. resolve_method_calls
|
|
195
|
+
# handles grouping automatically, and MethodRenamer patches the symbol arg.
|
|
196
|
+
DYNAMIC_DISPATCH_METHODS = %i[
|
|
197
|
+
method define_method respond_to? instance_method
|
|
198
|
+
].freeze
|
|
199
|
+
|
|
200
|
+
# A hash/keyword shorthand can pun on a METHOD (`f(unit_label:)` calls
|
|
201
|
+
# unit_label for its value); the one identifier is both the key and the
|
|
202
|
+
# call, so renaming the method would rewrite the key with it. Those
|
|
203
|
+
# methods keep their names.
|
|
204
|
+
def collect_shorthand_pun_methods(prism_root)
|
|
205
|
+
punned = Set.new
|
|
206
|
+
AstUtils.each_node(prism_root) do |node|
|
|
207
|
+
AstUtils.shorthand_pun_values(node).each do |inner|
|
|
208
|
+
punned << inner.name if inner.is_a?(Prism::CallNode)
|
|
209
|
+
end
|
|
210
|
+
end
|
|
211
|
+
@method_rename_mapping.exclude_methods_by_mid(punned) if punned.any?
|
|
212
|
+
end
|
|
213
|
+
|
|
214
|
+
VISIBILITY_MODIFIERS = %i[private protected public module_function].freeze
|
|
215
|
+
|
|
216
|
+
def collect_visibility_modifier_methods(prism_root)
|
|
217
|
+
excluded_mids = Set.new
|
|
218
|
+
AstUtils.each_node(prism_root) do |node|
|
|
219
|
+
next unless node.is_a?(Prism::CallNode)
|
|
220
|
+
next unless VISIBILITY_MODIFIERS.include?(node.name)
|
|
221
|
+
|
|
222
|
+
excluded_mids.merge(AstUtils.symbol_arguments(node))
|
|
223
|
+
end
|
|
224
|
+
@method_rename_mapping.exclude_methods_by_mid(excluded_mids) unless excluded_mids.empty?
|
|
225
|
+
end
|
|
226
|
+
|
|
227
|
+
# attr_accessor's setter def is derived from the declared symbol and never
|
|
228
|
+
# registered as a method of its own, so a setter call site type inference
|
|
229
|
+
# missed is invisible to every other exclusion pass — renaming the
|
|
230
|
+
# declaration would strand that site on the old name until the path runs.
|
|
231
|
+
# Find such sites directly and keep their symbols. attr_writer never
|
|
232
|
+
# renames its declaration at all, so a reader sharing its symbol must stay
|
|
233
|
+
# too: renaming the reader side would split the getter from the ivar the
|
|
234
|
+
# writer-defined setter still assigns.
|
|
235
|
+
def collect_attr_write_exclusions(prism_root)
|
|
236
|
+
accessor_owners = Hash.new { |h, k| h[k] = [] } #: Hash[Symbol, Array[MethodRenameMapping::class_key]]
|
|
237
|
+
reader_syms = Set.new
|
|
238
|
+
writer_syms = Set.new
|
|
239
|
+
each_attr_declaration(prism_root, ATTR_DECLARATION_METHODS, require_class_body: false) do |node, cpath, singleton, sym|
|
|
240
|
+
case node.name
|
|
241
|
+
when :attr_accessor then accessor_owners[sym] << [cpath, singleton]
|
|
242
|
+
when :attr_reader then reader_syms << sym
|
|
243
|
+
when :attr_writer then writer_syms << sym
|
|
244
|
+
end
|
|
245
|
+
end
|
|
246
|
+
|
|
247
|
+
excluded = writer_syms & (reader_syms | accessor_owners.keys)
|
|
248
|
+
|
|
249
|
+
if accessor_owners.any?
|
|
250
|
+
known_setter_sites = Set.new
|
|
251
|
+
accessor_owners.each do |sym, owners|
|
|
252
|
+
owners.each do |cpath, singleton|
|
|
253
|
+
@oracle.each_call_site_key(cpath, singleton, :"#{sym}=") do |key|
|
|
254
|
+
known_setter_sites << key
|
|
255
|
+
end
|
|
256
|
+
end
|
|
257
|
+
end
|
|
258
|
+
|
|
259
|
+
AstUtils.each_node(prism_root) do |node|
|
|
260
|
+
write_name = case node
|
|
261
|
+
when Prism::CallNode
|
|
262
|
+
node.name
|
|
263
|
+
when Prism::CallOperatorWriteNode, Prism::CallOrWriteNode, Prism::CallAndWriteNode
|
|
264
|
+
node.write_name
|
|
265
|
+
else
|
|
266
|
+
next
|
|
267
|
+
end
|
|
268
|
+
|
|
269
|
+
next unless AstUtils.setter_def_name?(write_name)
|
|
270
|
+
|
|
271
|
+
sym = write_name.to_s.chomp('=').to_sym
|
|
272
|
+
next unless accessor_owners.key?(sym)
|
|
273
|
+
next if known_setter_sites.include?(AstUtils.location_key(node))
|
|
274
|
+
|
|
275
|
+
excluded << sym
|
|
276
|
+
end
|
|
277
|
+
end
|
|
278
|
+
|
|
279
|
+
@method_rename_mapping.exclude_methods_by_mid(excluded) if excluded.any?
|
|
280
|
+
end
|
|
281
|
+
|
|
282
|
+
def collect_alias_undef_methods(prism_root)
|
|
283
|
+
excluded_mids = Set.new
|
|
284
|
+
AstUtils.each_node(prism_root) do |node|
|
|
285
|
+
case node
|
|
286
|
+
when Prism::AliasMethodNode
|
|
287
|
+
excluded_mids << node.new_name.unescaped.to_sym if node.new_name.is_a?(Prism::SymbolNode)
|
|
288
|
+
excluded_mids << node.old_name.unescaped.to_sym if node.old_name.is_a?(Prism::SymbolNode)
|
|
289
|
+
when Prism::UndefNode
|
|
290
|
+
node.names.each do |name|
|
|
291
|
+
excluded_mids << name.unescaped.to_sym if name.is_a?(Prism::SymbolNode)
|
|
292
|
+
end
|
|
293
|
+
end
|
|
294
|
+
end
|
|
295
|
+
@method_rename_mapping.exclude_methods_by_mid(excluded_mids) unless excluded_mids.empty?
|
|
296
|
+
end
|
|
297
|
+
end
|
|
298
|
+
end
|
|
@@ -0,0 +1,337 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Ryac
|
|
4
|
+
EXCLUDED_METHODS = %i[
|
|
5
|
+
initialize initialize_copy initialize_clone initialize_dup
|
|
6
|
+
method_missing respond_to_missing? const_missing
|
|
7
|
+
inherited included extended prepended
|
|
8
|
+
marshal_dump marshal_load encode_with init_with
|
|
9
|
+
call to_s to_str to_i to_int to_f to_r to_c
|
|
10
|
+
to_a to_ary to_h to_hash to_io to_proc
|
|
11
|
+
inspect hash eql? equal? frozen? nil?
|
|
12
|
+
class is_a? kind_of? instance_of?
|
|
13
|
+
coerce respond_to? dup clone freeze
|
|
14
|
+
each __send__ __id__
|
|
15
|
+
<=> == === != < > <= >= + - * / % ** & | ^ << >> =~ !~
|
|
16
|
+
[] []= ! ~ +@ -@
|
|
17
|
+
].to_set.freeze
|
|
18
|
+
|
|
19
|
+
class MethodRenameMapping
|
|
20
|
+
include UnionFind
|
|
21
|
+
|
|
22
|
+
# Below this many saved characters a rename is churn, not compression.
|
|
23
|
+
# KeywordRenameMapping applies the same bar.
|
|
24
|
+
MIN_GROUP_SAVINGS = 2
|
|
25
|
+
|
|
26
|
+
MethodGroupEntry = Struct.new(:keys, :original_name, :total_occurrences)
|
|
27
|
+
|
|
28
|
+
def initialize
|
|
29
|
+
uf_init
|
|
30
|
+
@methods = {} # method_key => { def_nodes: [], call_sites: [] }
|
|
31
|
+
@node_to_key = {} # node_object_id => method_key
|
|
32
|
+
@node_short_names = {} # node_object_id => short_name (after freeze)
|
|
33
|
+
@key_short_names = {} # method_key => short_name (after freeze)
|
|
34
|
+
@implicit_receiver_sites = {} # node_object_id => scope_id (for collision check)
|
|
35
|
+
@frozen = false
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
def add_method(method_key, def_node)
|
|
39
|
+
@methods[method_key] ||= { def_nodes: [], call_sites: [] }
|
|
40
|
+
if def_node
|
|
41
|
+
@methods[method_key][:def_nodes] << def_node
|
|
42
|
+
@node_to_key[def_node.object_id] = method_key
|
|
43
|
+
end
|
|
44
|
+
uf_add(method_key)
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
def has_method?(method_key)
|
|
48
|
+
@methods.key?(method_key)
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
# scope_id names the scope containing an implicit-receiver call site, so
|
|
52
|
+
# the method can avoid short names a visible local already took there — a
|
|
53
|
+
# bare `a` would parse as the local, not the call. The caller resolves it
|
|
54
|
+
# rather than this class holding a resolver: this code minifies itself at
|
|
55
|
+
# L5, and a call through a stored collaborator is exactly the dynamic
|
|
56
|
+
# dispatch TypeProf cannot pin down, leaving the resolver's method
|
|
57
|
+
# renamed on one side and not the other.
|
|
58
|
+
def add_call_site(call_node, method_key, has_receiver:, scope_id: nil)
|
|
59
|
+
@methods[method_key] ||= { def_nodes: [], call_sites: [] }
|
|
60
|
+
@methods[method_key][:call_sites] << call_node
|
|
61
|
+
@node_to_key[call_node.object_id] = method_key
|
|
62
|
+
|
|
63
|
+
@implicit_receiver_sites[call_node.object_id] = scope_id if !has_receiver && scope_id
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
def exclude_methods_by_mid(mids)
|
|
67
|
+
keys_to_remove = @methods.keys.select { |key| mids.include?(key[2]) }
|
|
68
|
+
keys_to_remove.each do |key|
|
|
69
|
+
data = @methods.delete(key) #: method_data
|
|
70
|
+
data[:def_nodes].each { |n| @node_to_key.delete(n.object_id) }
|
|
71
|
+
data[:call_sites].each do |n|
|
|
72
|
+
@node_to_key.delete(n.object_id)
|
|
73
|
+
@implicit_receiver_sites.delete(n.object_id)
|
|
74
|
+
end
|
|
75
|
+
uf_remove(key)
|
|
76
|
+
end
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
def merge_all_by_mid(mid)
|
|
80
|
+
keys = @methods.keys.select { |k| k[2] == mid }
|
|
81
|
+
return if keys.size <= 1
|
|
82
|
+
# keys.size > 1 is guaranteed above, so [1..] cannot be nil
|
|
83
|
+
keys[1..].each { |k| merge_groups(keys[0], k) } # steep:ignore NoMethod
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
# Inference can silently miss a caller, leaving a def in a group with no
|
|
87
|
+
# call sites while its mid is called — and therefore renamed — through
|
|
88
|
+
# another group. Runtime dispatch does not share the blind spot: a call
|
|
89
|
+
# site patched to the short name can still land on the def that kept its
|
|
90
|
+
# long one. Any mid renamed anywhere must cover every def of that mid, so
|
|
91
|
+
# the blind groups merge in and rename in lockstep.
|
|
92
|
+
def merge_blind_def_groups
|
|
93
|
+
sited_mids = Set.new
|
|
94
|
+
blind_mids = Set.new
|
|
95
|
+
groups_by_root.each_value do |keys|
|
|
96
|
+
sites = keys.sum { |key| @methods[key][:call_sites].size }
|
|
97
|
+
target = sites > 0 ? sited_mids : blind_mids
|
|
98
|
+
keys.each { |key| target << key[2] }
|
|
99
|
+
end
|
|
100
|
+
|
|
101
|
+
(blind_mids & sited_mids).each { |mid| merge_all_by_mid(mid) }
|
|
102
|
+
end
|
|
103
|
+
|
|
104
|
+
def add_unresolved_sites_for_mid(mid, call_nodes)
|
|
105
|
+
target_key = @methods.keys.find { |k| k[2] == mid }
|
|
106
|
+
return unless target_key
|
|
107
|
+
call_nodes.each { |node| add_call_site(node, target_key, has_receiver: true) }
|
|
108
|
+
end
|
|
109
|
+
|
|
110
|
+
def group_keys(method_key)
|
|
111
|
+
groups_by_root[uf_root(method_key)]
|
|
112
|
+
end
|
|
113
|
+
|
|
114
|
+
# Every call site registered for the key's whole rename group — including
|
|
115
|
+
# sites attached by the unresolved-call pass, which type inference alone
|
|
116
|
+
# does not report. Yields the scope id for implicit-receiver sites, nil
|
|
117
|
+
# otherwise.
|
|
118
|
+
def each_group_call_site(method_key)
|
|
119
|
+
group_keys(method_key).each do |key|
|
|
120
|
+
@methods[key][:call_sites].each do |node|
|
|
121
|
+
yield node, @implicit_receiver_sites[node.object_id]
|
|
122
|
+
end
|
|
123
|
+
end
|
|
124
|
+
end
|
|
125
|
+
|
|
126
|
+
def assign_short_names(scope_mappings, oracle = nil)
|
|
127
|
+
group_entries = build_group_entries(groups_by_root)
|
|
128
|
+
group_entries.sort_by! { |entry| -(entry.original_name.size * entry.total_occurrences) }
|
|
129
|
+
|
|
130
|
+
scope_vars = self.class.build_scope_vars(scope_mappings)
|
|
131
|
+
existing_methods, hierarchy = oracle ? build_existing_method_names(oracle) : [{}, {}] #: [Hash[class_key, Set[String]], hierarchy]
|
|
132
|
+
|
|
133
|
+
group_entries.each do |entry|
|
|
134
|
+
short_name = find_shortest_name(entry.keys, scope_vars, existing_methods, hierarchy)
|
|
135
|
+
|
|
136
|
+
savings_per_use = entry.original_name.size - short_name.size
|
|
137
|
+
next unless savings_per_use > 0
|
|
138
|
+
|
|
139
|
+
total_savings = savings_per_use * entry.total_occurrences
|
|
140
|
+
next unless total_savings > MIN_GROUP_SAVINGS
|
|
141
|
+
|
|
142
|
+
assign_short_name(entry.keys, short_name)
|
|
143
|
+
propagate_short_name(entry.keys, short_name, existing_methods, hierarchy)
|
|
144
|
+
end
|
|
145
|
+
|
|
146
|
+
verify_no_shadowing!(hierarchy)
|
|
147
|
+
@frozen = true
|
|
148
|
+
end
|
|
149
|
+
|
|
150
|
+
# A rename group never spans mids, so after assignment each (cpath,
|
|
151
|
+
# singleton) namespace must map its mids onto distinct final names — a
|
|
152
|
+
# renamed method landing on another's name, renamed or kept, would shadow
|
|
153
|
+
# it and the output would still parse. The module_function regression
|
|
154
|
+
# (instance and singleton halves allocated independently) was exactly this
|
|
155
|
+
# shape; this turns any recurrence into a failed run.
|
|
156
|
+
#
|
|
157
|
+
# The namespace a dispatch actually resolves against is the class plus
|
|
158
|
+
# everything it inherits and includes, so with the hierarchy known the
|
|
159
|
+
# same rule is enforced per inheritance-effective namespace: two mids on
|
|
160
|
+
# one final anywhere along a class's ancestor chain shadow each other.
|
|
161
|
+
def verify_no_shadowing!(hierarchy = {})
|
|
162
|
+
namespaces = Hash.new { |h, k| h[k] = Hash.new { |h2, k2| h2[k2] = Set.new } } #: Hash[class_key, Hash[String, Set[Symbol]]]
|
|
163
|
+
@methods.each_key do |key|
|
|
164
|
+
cpath, singleton, mid = key
|
|
165
|
+
final = @key_short_names[key] || mid.to_s
|
|
166
|
+
namespaces[[cpath, singleton]][final] << mid
|
|
167
|
+
end
|
|
168
|
+
|
|
169
|
+
namespaces.each do |(cpath, singleton), finals|
|
|
170
|
+
collisions = finals.filter_map { |name, mids| [name, mids.to_a] if mids.size > 1 } #: Array[[String, Array[Symbol]]]
|
|
171
|
+
next if collisions.empty?
|
|
172
|
+
|
|
173
|
+
label = "#{cpath.join('::')}#{singleton ? '.' : '#'}"
|
|
174
|
+
raise Pipeline::RenameCollisionError.new(label, collisions)
|
|
175
|
+
end
|
|
176
|
+
|
|
177
|
+
(hierarchy[:ancestors] || {}).each do |class_key, ancestor_keys|
|
|
178
|
+
next if ancestor_keys.none?
|
|
179
|
+
|
|
180
|
+
merged = Hash.new { |h, k| h[k] = Set.new } #: Hash[String, Set[Symbol]]
|
|
181
|
+
([class_key] + ancestor_keys).each do |ck|
|
|
182
|
+
next unless namespaces.key?(ck)
|
|
183
|
+
namespaces[ck].each { |final, mids| merged[final].merge(mids) }
|
|
184
|
+
end
|
|
185
|
+
|
|
186
|
+
collisions = merged.filter_map { |name, mids| [name, mids.to_a] if mids.size > 1 } #: Array[[String, Array[Symbol]]]
|
|
187
|
+
next if collisions.empty?
|
|
188
|
+
|
|
189
|
+
label = "#{class_key[0].join('::')}#{class_key[1] ? '.' : '#'} (with ancestors)"
|
|
190
|
+
raise Pipeline::RenameCollisionError.new(label, collisions)
|
|
191
|
+
end
|
|
192
|
+
end
|
|
193
|
+
|
|
194
|
+
def short_name_for(node_location_key)
|
|
195
|
+
@node_short_names[node_location_key]
|
|
196
|
+
end
|
|
197
|
+
|
|
198
|
+
def short_name_for_key(method_key)
|
|
199
|
+
@key_short_names[method_key]
|
|
200
|
+
end
|
|
201
|
+
|
|
202
|
+
def node_mapping
|
|
203
|
+
@node_short_names.dup
|
|
204
|
+
end
|
|
205
|
+
|
|
206
|
+
def each_method_key(&block)
|
|
207
|
+
@methods.each_key(&block)
|
|
208
|
+
end
|
|
209
|
+
|
|
210
|
+
def method_mids
|
|
211
|
+
result = Set.new
|
|
212
|
+
@methods.each_key { |key| result << key[2] }
|
|
213
|
+
result
|
|
214
|
+
end
|
|
215
|
+
|
|
216
|
+
def each_cpath_for_mid(mid)
|
|
217
|
+
@methods.each_key do |key|
|
|
218
|
+
yield key[0], key[1] if key[2] == mid
|
|
219
|
+
end
|
|
220
|
+
end
|
|
221
|
+
|
|
222
|
+
private
|
|
223
|
+
|
|
224
|
+
def build_group_entries(groups)
|
|
225
|
+
result = [] #: Array[MethodGroupEntry]
|
|
226
|
+
groups.each_value do |keys|
|
|
227
|
+
mid = keys.first[2]
|
|
228
|
+
next if EXCLUDED_METHODS.include?(mid)
|
|
229
|
+
next if mid.to_s.size <= NameGenerator::KEPT_NAME_MAX
|
|
230
|
+
|
|
231
|
+
total_call_sites = keys.sum { |key| @methods[key][:call_sites].size }
|
|
232
|
+
next if total_call_sites == 0
|
|
233
|
+
|
|
234
|
+
total_occurrences = keys.sum do |key|
|
|
235
|
+
data = @methods[key]
|
|
236
|
+
data[:def_nodes].size + data[:call_sites].size
|
|
237
|
+
end
|
|
238
|
+
|
|
239
|
+
result << MethodGroupEntry.new(keys, mid.to_s, total_occurrences)
|
|
240
|
+
end
|
|
241
|
+
result
|
|
242
|
+
end
|
|
243
|
+
|
|
244
|
+
# Class method: the attr coordination inverts scope_mappings the same
|
|
245
|
+
# way for its own collision check, so there is exactly one inversion.
|
|
246
|
+
def self.build_scope_vars(scope_mappings)
|
|
247
|
+
scope_vars = Hash.new { |h, k| h[k] = Set.new } #: Hash[scope_id, Set[String]]
|
|
248
|
+
scope_mappings.each do |cref_id, mapping|
|
|
249
|
+
mapping.each_value { |mangled| scope_vars[cref_id] << mangled }
|
|
250
|
+
end
|
|
251
|
+
scope_vars
|
|
252
|
+
end
|
|
253
|
+
|
|
254
|
+
def groups_by_root
|
|
255
|
+
groups = Hash.new { |h, k| h[k] = [] } #: Hash[method_key, Array[method_key]]
|
|
256
|
+
@methods.each_key { |key| groups[uf_root(key)] << key }
|
|
257
|
+
groups
|
|
258
|
+
end
|
|
259
|
+
|
|
260
|
+
def build_existing_method_names(oracle)
|
|
261
|
+
result = {} #: Hash[class_key, Set[String]]
|
|
262
|
+
includers = Hash.new { |h, k| h[k] = Set.new } #: Hash[class_key, Set[class_key]]
|
|
263
|
+
ancestors_map = {} #: Hash[class_key, Array[class_key]]
|
|
264
|
+
|
|
265
|
+
@methods.each_key do |key|
|
|
266
|
+
cache_key = [key[0], key[1]] #: class_key
|
|
267
|
+
next if result.key?(cache_key)
|
|
268
|
+
names = Set.new
|
|
269
|
+
ancestor_keys = [] #: Array[class_key]
|
|
270
|
+
oracle.each_ancestor_methods(key[0], key[1]) do |ancestor_cpath, s, mids|
|
|
271
|
+
mids.each { |mid| names << mid }
|
|
272
|
+
ancestor_key = [ancestor_cpath, s] #: class_key
|
|
273
|
+
if ancestor_key != cache_key
|
|
274
|
+
includers[ancestor_key] << cache_key
|
|
275
|
+
ancestor_keys << ancestor_key
|
|
276
|
+
end
|
|
277
|
+
end
|
|
278
|
+
next if names.empty? && ancestor_keys.empty?
|
|
279
|
+
result[cache_key] = names
|
|
280
|
+
ancestors_map[cache_key] = ancestor_keys
|
|
281
|
+
end
|
|
282
|
+
|
|
283
|
+
hierarchy = { includers: includers, ancestors: ancestors_map } #: hierarchy
|
|
284
|
+
[result, hierarchy]
|
|
285
|
+
end
|
|
286
|
+
|
|
287
|
+
def find_shortest_name(keys, scope_vars, existing_methods, hierarchy)
|
|
288
|
+
includers = hierarchy[:includers] || {}
|
|
289
|
+
generator = NameGenerator.new
|
|
290
|
+
loop do
|
|
291
|
+
candidate = generator.next_name
|
|
292
|
+
collides = keys.any? do |key|
|
|
293
|
+
var_collision = @methods[key][:call_sites].any? do |node|
|
|
294
|
+
cref_id = @implicit_receiver_sites[node.object_id]
|
|
295
|
+
cref_id && scope_vars[cref_id].include?(candidate)
|
|
296
|
+
end
|
|
297
|
+
next true if var_collision
|
|
298
|
+
class_key = [key[0], key[1]] #: class_key
|
|
299
|
+
next true if existing_methods[class_key]&.include?(candidate)
|
|
300
|
+
# The name is also visible in every class that inherits or includes
|
|
301
|
+
# this one, where a same-named private helper would shadow it for
|
|
302
|
+
# dispatch through the base — check those namespaces too.
|
|
303
|
+
(includers[class_key] || []).any? { |ck| existing_methods[ck]&.include?(candidate) }
|
|
304
|
+
end
|
|
305
|
+
return candidate unless collides
|
|
306
|
+
end
|
|
307
|
+
end
|
|
308
|
+
|
|
309
|
+
def propagate_short_name(keys, short_name, existing_methods, hierarchy)
|
|
310
|
+
includers = hierarchy[:includers] || {}
|
|
311
|
+
ancestors = hierarchy[:ancestors] || {}
|
|
312
|
+
keys.each do |key|
|
|
313
|
+
class_key = [key[0], key[1]] #: class_key
|
|
314
|
+
existing_methods[class_key] ||= Set.new
|
|
315
|
+
existing_methods[class_key] << short_name
|
|
316
|
+
(includers[class_key] || []).each do |ck|
|
|
317
|
+
existing_methods[ck] ||= Set.new
|
|
318
|
+
existing_methods[ck] << short_name
|
|
319
|
+
end
|
|
320
|
+
(ancestors[class_key] || []).each do |ck|
|
|
321
|
+
existing_methods[ck] ||= Set.new
|
|
322
|
+
existing_methods[ck] << short_name
|
|
323
|
+
end
|
|
324
|
+
end
|
|
325
|
+
end
|
|
326
|
+
|
|
327
|
+
def assign_short_name(keys, short_name)
|
|
328
|
+
keys.each do |key|
|
|
329
|
+
@key_short_names[key] = short_name
|
|
330
|
+
data = @methods[key]
|
|
331
|
+
data[:def_nodes].each { |n| @node_short_names[AstUtils.location_key(n)] = short_name }
|
|
332
|
+
data[:call_sites].each { |n| @node_short_names[AstUtils.location_key(n)] = short_name }
|
|
333
|
+
end
|
|
334
|
+
end
|
|
335
|
+
|
|
336
|
+
end
|
|
337
|
+
end
|