Class: Ryac::MethodRenameMapping
- Inherits:
-
Object
- Object
- Ryac::MethodRenameMapping
- 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.
2
Instance Method Summary collapse
-
#add_call_site(call_node, method_key, has_receiver:, scope_id: nil) ⇒ void
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
awould parse as the local, not the call. - #add_method(method_key, def_node) ⇒ void
- #add_unresolved_sites_for_mid(mid, call_nodes) ⇒ void
- #assign_short_name(keys, short_name) ⇒ void
-
#assign_short_names(scope_vars, oracle = nil) ⇒ void
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.
- #build_existing_method_names(oracle) ⇒ [Hash[class_key, Set[String]], hierarchy]
- #build_group_entries(groups) ⇒ Array[MethodGroupEntry]
- #each_cpath_for_mid(mid) {|arg0, arg1| ... } ⇒ void
-
#each_group_call_site(method_key) {|arg0, arg1| ... } ⇒ void
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.
- #each_method_key {|arg0| ... } ⇒ void
- #exclude_methods_by_mid(mids) ⇒ void
-
#exclude_uncalled_methods ⇒ void
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.
- #find_shortest_name(keys, scope_vars, existing_methods, hierarchy) ⇒ String
- #group_keys(method_key) ⇒ Array[method_key]
- #groups_by_root ⇒ Hash[method_key, Array[method_key]]
- #has_method?(method_key) ⇒ Boolean
-
#initialize ⇒ MethodRenameMapping
constructor
A new instance of MethodRenameMapping.
- #merge_all_by_mid(mid) ⇒ void
-
#merge_blind_def_groups ⇒ void
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.
- #method_mids ⇒ Set[Symbol]
- #node_mapping ⇒ Hash[[Integer, Integer], String]
- #propagate_short_name(keys, short_name, existing_methods, hierarchy) ⇒ void
- #short_name_for(node_location_key) ⇒ String?
- #short_name_for_key(method_key) ⇒ String?
-
#verify_no_shadowing!(hierarchy = {}) ⇒ void
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.
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.
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.
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.
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.
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.
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]
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]
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.
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.
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.
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.
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
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]
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]]
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
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.
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]
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]
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.
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?
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?
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.
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 |