Class: Ryac::MethodRenameMapping

Inherits:
Object
  • Object
show all
Includes:
UnionFind, UnionFind[method_key]
Defined in:
lib/ryac/analysis/method/rename_mapping.rb,
sig/ryac/analysis/method/rename_mapping.rbs

Defined Under Namespace

Classes: MethodGroupEntry

Constant Summary collapse

MIN_GROUP_SAVINGS =

Below this many saved characters a rename is churn, not compression. KeywordRenameMapping applies the same bar.

Returns:

  • (Integer)
2

Instance Method Summary collapse

Methods included from UnionFind

#merge_groups, #uf_add, #uf_init, #uf_remove, #uf_root

Constructor Details

#initialize ⇒ MethodRenameMapping

Returns a new instance of MethodRenameMapping.



28
29
30
31
32
33
34
35
36
# File 'lib/ryac/analysis/method/rename_mapping.rb', line 28

def initialize
  uf_init
  @methods = {}       # method_key => { def_nodes: [], call_sites: [] }
  @node_to_key = {}   # node_object_id => method_key
  @node_short_names = {} # node_object_id => short_name (after freeze)
  @key_short_names = {} # method_key => short_name (after freeze)
  @implicit_receiver_sites = {} # node_object_id => scope_id (for collision check)
  @frozen = false
end

Instance Method Details

#add_call_site(call_node, method_key, has_receiver:, scope_id: nil) ⇒ void

This method returns an undefined value.

scope_id names the scope containing an implicit-receiver call site, so the method can avoid short names a visible local already took there — a bare a would parse as the local, not the call. The caller resolves it rather than this class holding a resolver: this code minifies itself at L5, and a call through a stored collaborator is exactly the dynamic dispatch TypeProf cannot pin down, leaving the resolver's method renamed on one side and not the other.

Parameters:

  • call_node (Prism::Node)
  • method_key (method_key)
  • has_receiver: (Boolean)
  • scope_id: (scope_id, nil) (defaults to: nil)


58
59
60
61
62
63
64
# File 'lib/ryac/analysis/method/rename_mapping.rb', line 58

def add_call_site(call_node, method_key, has_receiver:, scope_id: nil)
  @methods[method_key] ||= { def_nodes: [], call_sites: [] }
  @methods[method_key][:call_sites] << call_node
  @node_to_key[call_node.object_id] = method_key

  @implicit_receiver_sites[call_node.object_id] = scope_id if !has_receiver && scope_id
end

#add_method(method_key, def_node) ⇒ void

This method returns an undefined value.

Parameters:

  • method_key (method_key)
  • def_node (Prism::Node, nil)


38
39
40
41
42
43
44
45
# File 'lib/ryac/analysis/method/rename_mapping.rb', line 38

def add_method(method_key, def_node)
  @methods[method_key] ||= { def_nodes: [], call_sites: [] }
  if def_node
    @methods[method_key][:def_nodes] << def_node
    @node_to_key[def_node.object_id] = method_key
  end
  uf_add(method_key)
end

#add_unresolved_sites_for_mid(mid, call_nodes) ⇒ void

This method returns an undefined value.

Parameters:

  • mid (Symbol)
  • call_nodes (Array[Prism::Node])


117
118
119
120
121
# File 'lib/ryac/analysis/method/rename_mapping.rb', line 117

def add_unresolved_sites_for_mid(mid, call_nodes)
  target_key = @methods.keys.find { |k| k[2] == mid }
  return unless target_key
  call_nodes.each { |node| add_call_site(node, target_key, has_receiver: true) }
end

#assign_short_name(keys, short_name) ⇒ void

This method returns an undefined value.

Parameters:

  • keys (Array[method_key])
  • short_name (String)


332
333
334
335
336
337
338
339
# File 'lib/ryac/analysis/method/rename_mapping.rb', line 332

def assign_short_name(keys, short_name)
  keys.each do |key|
    @key_short_names[key] = short_name
    data = @methods[key]
    data[:def_nodes].each { |n| @node_short_names[AstUtils.location_key(n)] = short_name }
    data[:call_sites].each { |n| @node_short_names[AstUtils.location_key(n)] = short_name }
  end
end

#assign_short_names(scope_vars, oracle = nil) ⇒ void

This method returns an undefined value.

scope_vars: LocalScopes#visible_local_names — every local name visible at a scope after renaming, so implicit-receiver sites can refuse a short name a bare call would resolve as a variable read.

Parameters:

  • scope_vars (Hash[scope_id, Set[String]])
  • oracle (TypeOracle, nil) (defaults to: nil)


142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
# File 'lib/ryac/analysis/method/rename_mapping.rb', line 142

def assign_short_names(scope_vars, oracle = nil)
  group_entries = build_group_entries(groups_by_root)
  group_entries.sort_by! { |entry| -(entry.original_name.size * entry.total_occurrences) }

  existing_methods, hierarchy = oracle ? build_existing_method_names(oracle) : [{}, {}] #: [Hash[class_key, Set[String]], hierarchy]

  group_entries.each do |entry|
    short_name = find_shortest_name(entry.keys, scope_vars, existing_methods, hierarchy)

    savings_per_use = entry.original_name.size - short_name.size
    next unless savings_per_use > 0

    total_savings = savings_per_use * entry.total_occurrences
    next unless total_savings > MIN_GROUP_SAVINGS

    assign_short_name(entry.keys, short_name)
    propagate_short_name(entry.keys, short_name, existing_methods, hierarchy)
  end

  verify_no_shadowing!(hierarchy)
  @frozen = true
end

#build_existing_method_names(oracle) ⇒ [Hash[class_key, Set[String]], hierarchy]

Parameters:

Returns:

  • ([Hash[class_key, Set[String]], hierarchy])


265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
# File 'lib/ryac/analysis/method/rename_mapping.rb', line 265

def build_existing_method_names(oracle)
  result = {} #: Hash[class_key, Set[String]]
  includers = Hash.new { |h, k| h[k] = Set.new } #: Hash[class_key, Set[class_key]]
  ancestors_map = {} #: Hash[class_key, Array[class_key]]

  @methods.each_key do |key|
    cache_key = [key[0], key[1]] #: class_key
    next if result.key?(cache_key)
    names = Set.new
    ancestor_keys = [] #: Array[class_key]
    oracle.each_ancestor_methods(key[0], key[1]) do |ancestor_cpath, s, mids|
      mids.each { |mid| names << mid }
      ancestor_key = [ancestor_cpath, s] #: class_key
      if ancestor_key != cache_key
        includers[ancestor_key] << cache_key
        ancestor_keys << ancestor_key
      end
    end
    next if names.empty? && ancestor_keys.empty?
    result[cache_key] = names
    ancestors_map[cache_key] = ancestor_keys
  end

  hierarchy = { includers: includers, ancestors: ancestors_map } #: hierarchy
  [result, hierarchy]
end

#build_group_entries(groups) ⇒ Array[MethodGroupEntry]

Parameters:

  • groups (Hash[method_key, Array[method_key]])

Returns:



239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
# File 'lib/ryac/analysis/method/rename_mapping.rb', line 239

def build_group_entries(groups)
  result = [] #: Array[MethodGroupEntry]
  groups.each_value do |keys|
    mid = keys.first[2]
    next if EXCLUDED_METHODS.include?(mid)
    next if mid.to_s.size <= NameGenerator::KEPT_NAME_MAX

    total_call_sites = keys.sum { |key| @methods[key][:call_sites].size }
    next if total_call_sites == 0

    total_occurrences = keys.sum do |key|
      data = @methods[key]
      data[:def_nodes].size + data[:call_sites].size
    end

    result << MethodGroupEntry.new(keys, mid.to_s, total_occurrences)
  end
  result
end

#each_cpath_for_mid(mid) {|arg0, arg1| ... } ⇒ void

This method returns an undefined value.

Parameters:

  • mid (Symbol)

Yields:

Yield Parameters:

  • arg0 (Array[Symbol])
  • arg1 (Boolean)

Yield Returns:

  • (void)


231
232
233
234
235
# File 'lib/ryac/analysis/method/rename_mapping.rb', line 231

def each_cpath_for_mid(mid)
  @methods.each_key do |key|
    yield key[0], key[1] if key[2] == mid
  end
end

#each_group_call_site(method_key) {|arg0, arg1| ... } ⇒ void

This method returns an undefined value.

Every call site registered for the key's whole rename group — including sites attached by the unresolved-call pass, which type inference alone does not report. Yields the scope id for implicit-receiver sites, nil otherwise.

Parameters:

  • method_key (method_key)

Yields:

Yield Parameters:

  • arg0 (Prism::Node)
  • arg1 (scope_id, nil)

Yield Returns:

  • (void)


131
132
133
134
135
136
137
# File 'lib/ryac/analysis/method/rename_mapping.rb', line 131

def each_group_call_site(method_key)
  group_keys(method_key).each do |key|
    @methods[key][:call_sites].each do |node|
      yield node, @implicit_receiver_sites[node.object_id]
    end
  end
end

#each_method_key {|arg0| ... } ⇒ void

This method returns an undefined value.

Yields:

Yield Parameters:

  • arg0 (method_key)

Yield Returns:

  • (void)


221
222
223
# File 'lib/ryac/analysis/method/rename_mapping.rb', line 221

def each_method_key(&block)
  @methods.each_key(&block)
end

#exclude_methods_by_mid(mids) ⇒ void

This method returns an undefined value.

Parameters:

  • mids (Set[Symbol])


66
67
68
69
70
71
72
73
74
75
76
77
# File 'lib/ryac/analysis/method/rename_mapping.rb', line 66

def exclude_methods_by_mid(mids)
  keys_to_remove = @methods.keys.select { |key| mids.include?(key[2]) }
  keys_to_remove.each do |key|
    data = @methods.delete(key) #: method_data
    data[:def_nodes].each { |n| @node_to_key.delete(n.object_id) }
    data[:call_sites].each do |n|
      @node_to_key.delete(n.object_id)
      @implicit_receiver_sites.delete(n.object_id)
    end
    uf_remove(key)
  end
end

#exclude_uncalled_methods ⇒ void

This method returns an undefined value.

A def no resolved call reaches is either dead or called from outside the program (a library's public surface, a runner script requiring the bundle) — renaming it is unsound both ways, so the safe policy drops those names before assignment.



108
109
110
111
112
113
114
115
# File 'lib/ryac/analysis/method/rename_mapping.rb', line 108

def exclude_uncalled_methods
  mids = Set.new
  groups_by_root.each_value do |keys|
    sites = keys.sum { |key| @methods[key][:call_sites].size }
    keys.each { |key| mids << key[2] } if sites.zero?
  end
  exclude_methods_by_mid(mids) unless mids.empty?
end

#find_shortest_name(keys, scope_vars, existing_methods, hierarchy) ⇒ String

Parameters:

  • keys (Array[method_key])
  • scope_vars (Hash[scope_id, Set[String]])
  • existing_methods (Hash[class_key, Set[String]])
  • hierarchy (hierarchy)

Returns:

  • (String)


292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
# File 'lib/ryac/analysis/method/rename_mapping.rb', line 292

def find_shortest_name(keys, scope_vars, existing_methods, hierarchy)
  includers = hierarchy[:includers] || {}
  generator = NameGenerator.new
  loop do
    candidate = generator.next_name
    collides = keys.any? do |key|
      var_collision = @methods[key][:call_sites].any? do |node|
        cref_id = @implicit_receiver_sites[node.object_id]
        cref_id && scope_vars[cref_id].include?(candidate)
      end
      next true if var_collision
      class_key = [key[0], key[1]] #: class_key
      next true if existing_methods[class_key]&.include?(candidate)
      # The name is also visible in every class that inherits or includes
      # this one, where a same-named private helper would shadow it for
      # dispatch through the base — check those namespaces too.
      (includers[class_key] || []).any? { |ck| existing_methods[ck]&.include?(candidate) }
    end
    return candidate unless collides
  end
end

#group_keys(method_key) ⇒ Array[method_key]

Parameters:

  • method_key (method_key)

Returns:

  • (Array[method_key])


123
124
125
# File 'lib/ryac/analysis/method/rename_mapping.rb', line 123

def group_keys(method_key)
  groups_by_root[uf_root(method_key)]
end

#groups_by_root ⇒ Hash[method_key, Array[method_key]]

Returns:

  • (Hash[method_key, Array[method_key]])


259
260
261
262
263
# File 'lib/ryac/analysis/method/rename_mapping.rb', line 259

def groups_by_root
  groups = Hash.new { |h, k| h[k] = [] } #: Hash[method_key, Array[method_key]]
  @methods.each_key { |key| groups[uf_root(key)] << key }
  groups
end

#has_method?(method_key) ⇒ Boolean

Parameters:

  • method_key (method_key)

Returns:

  • (Boolean)


47
48
49
# File 'lib/ryac/analysis/method/rename_mapping.rb', line 47

def has_method?(method_key)
  @methods.key?(method_key)
end

#merge_all_by_mid(mid) ⇒ void

This method returns an undefined value.

Parameters:

  • mid (Symbol)


79
80
81
82
83
84
# File 'lib/ryac/analysis/method/rename_mapping.rb', line 79

def merge_all_by_mid(mid)
  keys = @methods.keys.select { |k| k[2] == mid }
  return if keys.size <= 1
  # keys.size > 1 is guaranteed above, so [1..] cannot be nil
  keys[1..].each { |k| merge_groups(keys[0], k) } # steep:ignore NoMethod
end

#merge_blind_def_groups ⇒ void

This method returns an undefined value.

Inference can silently miss a caller, leaving a def in a group with no call sites while its mid is called — and therefore renamed — through another group. Runtime dispatch does not share the blind spot: a call site patched to the short name can still land on the def that kept its long one. Any mid renamed anywhere must cover every def of that mid, so the blind groups merge in and rename in lockstep.



92
93
94
95
96
97
98
99
100
101
102
# File 'lib/ryac/analysis/method/rename_mapping.rb', line 92

def merge_blind_def_groups
  sited_mids = Set.new
  blind_mids = Set.new
  groups_by_root.each_value do |keys|
    sites = keys.sum { |key| @methods[key][:call_sites].size }
    target = sites > 0 ? sited_mids : blind_mids
    keys.each { |key| target << key[2] }
  end

  (blind_mids & sited_mids).each { |mid| merge_all_by_mid(mid) }
end

#method_mids ⇒ Set[Symbol]

Returns:

  • (Set[Symbol])


225
226
227
228
229
# File 'lib/ryac/analysis/method/rename_mapping.rb', line 225

def method_mids
  result = Set.new
  @methods.each_key { |key| result << key[2] }
  result
end

#node_mapping ⇒ Hash[[Integer, Integer], String]

Returns:

  • (Hash[[Integer, Integer], String])


217
218
219
# File 'lib/ryac/analysis/method/rename_mapping.rb', line 217

def node_mapping
  @node_short_names.dup
end

#propagate_short_name(keys, short_name, existing_methods, hierarchy) ⇒ void

This method returns an undefined value.

Parameters:

  • keys (Array[method_key])
  • short_name (String)
  • existing_methods (Hash[class_key, Set[String]])
  • hierarchy (hierarchy)


314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
# File 'lib/ryac/analysis/method/rename_mapping.rb', line 314

def propagate_short_name(keys, short_name, existing_methods, hierarchy)
  includers = hierarchy[:includers] || {}
  ancestors = hierarchy[:ancestors] || {}
  keys.each do |key|
    class_key = [key[0], key[1]] #: class_key
    existing_methods[class_key] ||= Set.new
    existing_methods[class_key] << short_name
    (includers[class_key] || []).each do |ck|
      existing_methods[ck] ||= Set.new
      existing_methods[ck] << short_name
    end
    (ancestors[class_key] || []).each do |ck|
      existing_methods[ck] ||= Set.new
      existing_methods[ck] << short_name
    end
  end
end

#short_name_for(node_location_key) ⇒ String?

Parameters:

  • node_location_key ([Integer, Integer])

Returns:

  • (String, nil)


209
210
211
# File 'lib/ryac/analysis/method/rename_mapping.rb', line 209

def short_name_for(node_location_key)
  @node_short_names[node_location_key]
end

#short_name_for_key(method_key) ⇒ String?

Parameters:

  • method_key (method_key)

Returns:

  • (String, nil)


213
214
215
# File 'lib/ryac/analysis/method/rename_mapping.rb', line 213

def short_name_for_key(method_key)
  @key_short_names[method_key]
end

#verify_no_shadowing!(hierarchy = {}) ⇒ void

This method returns an undefined value.

A rename group never spans mids, so after assignment each (cpath, singleton) namespace must map its mids onto distinct final names — a renamed method landing on another's name, renamed or kept, would shadow it and the output would still parse. The module_function regression (instance and singleton halves allocated independently) was exactly this shape; this turns any recurrence into a failed run.

The namespace a dispatch actually resolves against is the class plus everything it inherits and includes, so with the hierarchy known the same rule is enforced per inheritance-effective namespace: two mids on one final anywhere along a class's ancestor chain shadow each other.

Parameters:

  • hierarchy (hierarchy) (defaults to: {})


176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
# File 'lib/ryac/analysis/method/rename_mapping.rb', line 176

def verify_no_shadowing!(hierarchy = {})
  namespaces = Hash.new { |h, k| h[k] = Hash.new { |h2, k2| h2[k2] = Set.new } } #: Hash[class_key, Hash[String, Set[Symbol]]]
  @methods.each_key do |key|
    cpath, singleton, mid = key
    final = @key_short_names[key] || mid.to_s
    namespaces[[cpath, singleton]][final] << mid
  end

  namespaces.each do |(cpath, singleton), finals|
    collisions = finals.filter_map { |name, mids| [name, mids.to_a] if mids.size > 1 } #: Array[[String, Array[Symbol]]]
    next if collisions.empty?

    label = "#{cpath.join('::')}#{singleton ? '.' : '#'}"
    raise Pipeline::RenameCollisionError.new(label, collisions)
  end

  (hierarchy[:ancestors] || {}).each do |class_key, ancestor_keys|
    next if ancestor_keys.none?

    merged = Hash.new { |h, k| h[k] = Set.new } #: Hash[String, Set[Symbol]]
    ([class_key] + ancestor_keys).each do |ck|
      next unless namespaces.key?(ck)
      namespaces[ck].each { |final, mids| merged[final].merge(mids) }
    end

    collisions = merged.filter_map { |name, mids| [name, mids.to_a] if mids.size > 1 } #: Array[[String, Array[Symbol]]]
    next if collisions.empty?

    label = "#{class_key[0].join('::')}#{class_key[1] ? '.' : '#'} (with ancestors)"
    raise Pipeline::RenameCollisionError.new(label, collisions)
  end
end