Module: Ryac::AnalysisPhases

Included in:
Pipeline::Analyzer
Defined in:
lib/ryac/analysis/cvar/collection.rb,
lib/ryac/analysis/gvar/collection.rb,
lib/ryac/analysis/ivar/collection.rb,
lib/ryac/analysis/method_aliasing.rb,
lib/ryac/analysis/method/collection.rb,
lib/ryac/analysis/keyword/collection.rb,
lib/ryac/analysis/constant/collection.rb,
sig/ryac/analysis/cvar/collection.rbs,
sig/ryac/analysis/gvar/collection.rbs,
sig/ryac/analysis/ivar/collection.rbs,
sig/ryac/analysis/method_aliasing.rbs,
sig/ryac/analysis/method/collection.rbs,
sig/ryac/analysis/keyword/collection.rbs,
sig/ryac/analysis/constant/collection.rbs

Constant Summary collapse

DYNAMIC_CVAR_METHODS =

Returns:

  • (Array[Symbol])
%i[
  class_variable_get class_variable_set
  class_variable_defined? class_variables
  remove_class_variable
].freeze
CVAR_WRITE_NODES =

Returns:

  • (Array[Class])
[
  Prism::ClassVariableWriteNode,
  Prism::ClassVariableTargetNode,
  Prism::ClassVariableOperatorWriteNode,
  Prism::ClassVariableOrWriteNode,
  Prism::ClassVariableAndWriteNode
].freeze
GVAR_NODES =

Returns:

  • (Array[Class])
[
  Prism::GlobalVariableReadNode,
  Prism::GlobalVariableWriteNode,
  Prism::GlobalVariableTargetNode,
  Prism::GlobalVariableOperatorWriteNode,
  Prism::GlobalVariableOrWriteNode,
  Prism::GlobalVariableAndWriteNode
].freeze
DYNAMIC_IVAR_METHODS =

Returns:

  • (Array[Symbol])
%i[
  instance_variable_get instance_variable_set
  instance_variable_defined? remove_instance_variable
  instance_variables
].freeze
ATTR_DECLARATION_METHODS =

Returns:

  • (Array[Symbol])
%i[attr attr_reader attr_writer attr_accessor].freeze
IVAR_WRITE_NODES =

Returns:

  • (Array[Class])
[
  Prism::InstanceVariableWriteNode,
  Prism::InstanceVariableTargetNode,
  Prism::InstanceVariableOperatorWriteNode,
  Prism::InstanceVariableOrWriteNode,
  Prism::InstanceVariableAndWriteNode
].freeze
UNRESOLVED_CALL_NODES =

Returns:

  • (Array[Class])
[
  Prism::CallNode,
  Prism::CallOperatorWriteNode,
  Prism::CallOrWriteNode,
  Prism::CallAndWriteNode
].freeze
DYNAMIC_DISPATCH_METHODS =

send/public_send/send are NOT listed here because TypeProf resolves them as MethodCallBoxes on the target method. resolve_method_calls handles grouping automatically, and MethodRenamer patches the symbol arg.

Returns:

  • (Array[Symbol])
%i[
  method define_method respond_to? instance_method
].freeze
VISIBILITY_MODIFIERS =

Returns:

  • (Array[Symbol])
%i[private protected public module_function
private_class_method public_class_method].freeze

Instance Method Summary collapse

Instance Method Details

#apply_accessor_call_renames(path_a_info, path_b_info, path_b_method_mapping, rename_map) ⇒ void

This method returns an undefined value.

Parameters:

  • path_a_info (Array[attr_info])
  • path_b_info (Array[attr_info])
  • path_b_method_mapping (Hash[[Array[Symbol], Symbol], Symbol])
  • rename_map (Hash[location_key, String])


254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
# File 'lib/ryac/analysis/ivar/collection.rb', line 254

def apply_accessor_call_renames(path_a_info, path_b_info, path_b_method_mapping, rename_map)
  path_a_info.each do |info|
    next unless info[:accessor]
    @oracle.each_call_site_key(info[:cpath], info[:singleton], :"#{info[:mid]}=") do |key|
      rename_map[key] = "#{info[:getter_short]}="
    end
  end

  path_b_info.each do |info|
    method_short = path_b_method_mapping[info[:ivar_key]]
    next unless method_short

    # The mapping's group sites, not the oracle's: sites attached by the
    # unresolved-call pass exist only in the mapping, and missing one here
    # would leave it calling the old name.
    @method_rename_mapping.each_group_call_site(info[:method_key]) do |site, _scope_id|
      rename_map[AstUtils.location_key(site)] = method_short.to_s
    end

    next unless info[:accessor]
    @oracle.each_call_site_key(info[:cpath], info[:singleton], :"#{info[:mid]}=") do |key|
      rename_map[key] = "#{method_short}="
    end
  end
end

#apply_ivar_renames(ivar_nodes_by_key, combined_mapping, attr_ivar_entries) ⇒ void

This method returns an undefined value.

Ivar read/write sites take their attr's name; a subclass's sites follow the ancestor that declared the attr.

Parameters:

  • ivar_nodes_by_key (Hash[[Array[Symbol], Symbol], Array[Prism::Node]])
  • combined_mapping (Hash[[Array[Symbol], Symbol], String])
  • attr_ivar_entries (Hash[location_key, String])


239
240
241
242
243
244
245
246
247
248
249
250
251
252
# File 'lib/ryac/analysis/ivar/collection.rb', line 239

def apply_ivar_renames(ivar_nodes_by_key, combined_mapping, attr_ivar_entries)
  ivar_nodes_by_key.each do |(cpath, ivar_name), nodes_list|
    short = combined_mapping[[cpath, ivar_name]]
    unless short
      @oracle.each_ancestor_cpath(cpath, false) do |ancestor_cpath|
        next if ancestor_cpath == cpath
        short = combined_mapping[[ancestor_cpath, ivar_name]]
        break if short
      end
    end
    next unless short
    nodes_list.each { |n| attr_ivar_entries[AstUtils.location_key(n)] = short }
  end
end

#assign_ivar_driven_names(path_b_info, ivar_nodes_by_key, path_a_mapping, rename_map) ⇒ [Hash[[Array[Symbol], Symbol], String], Hash[[Array[Symbol], Symbol], Symbol]]

Path B: pick a fresh @name whose bare form is free as a method name too, then keep it only when renaming ivar sites plus accessor calls saves more than it costs.

Parameters:

  • path_b_info (Array[attr_info])
  • ivar_nodes_by_key (Hash[[Array[Symbol], Symbol], Array[Prism::Node]])
  • path_a_mapping (Hash[[Array[Symbol], Symbol], String])
  • rename_map (Hash[location_key, String])

Returns:

  • ([Hash[[Array[Symbol], Symbol], String], Hash[[Array[Symbol], Symbol], Symbol]])


172
173
174
175
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
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
# File 'lib/ryac/analysis/ivar/collection.rb', line 172

def assign_ivar_driven_names(path_b_info, ivar_nodes_by_key, path_a_mapping, rename_map)
  path_b_mapping = {} #: Hash[[Array[Symbol], Symbol], String]
  path_b_method_mapping = {} #: Hash[[Array[Symbol], Symbol], Symbol]
  return [path_b_mapping, path_b_method_mapping] if path_b_info.none?

  used_ivar_names = @ivar_rename_mapping.node_mapping.values.to_set
  used_ivar_names.merge(path_a_mapping.values)
  used_method_names = rename_map.values.to_set
  scope_vars = @scope_visible_names
  generator = NameGenerator.new(prefix: "@")

  path_b_info
    .sort_by do |info|
      ivar_name = info[:ivar_key][1]
      count = ivar_nodes_by_key[info[:ivar_key]].size
      -(ivar_name.to_s.size * count)
    end
    .each do |info|
      ivar_key = info[:ivar_key]
      ivar_name = ivar_key[1]
      ivar_count = ivar_nodes_by_key[ivar_key].size
      next if ivar_count == 0
      next if ivar_name.to_s.size <= '@'.size + 1 # sigil + one character is already minimal

      getter_calls = 0
      implicit_scope_ids = [] #: Array[scope_id]
      @method_rename_mapping.each_group_call_site(info[:method_key]) do |_site, scope_id|
        getter_calls += 1
        implicit_scope_ids << scope_id if scope_id
      end

      short_name, method_short = loop do #: [String, Symbol]
        candidate = generator.next_name
        method_candidate = candidate.delete_prefix("@").to_sym

        next if used_ivar_names.include?(candidate)
        next if used_method_names.include?(method_candidate.to_s)
        next if @oracle.method_defined?(info[:cpath], info[:singleton], method_candidate)
        # A bare renamed getter call would parse as the local if a visible
        # local already owns the candidate at an implicit-receiver site.
        next if implicit_scope_ids.any? { |sid| scope_vars[sid].include?(method_candidate.to_s) }

        break [candidate, method_candidate]
      end

      setter_calls = 0
      if info[:accessor]
        setter_calls = @oracle.method_call_count(info[:cpath], info[:singleton], :"#{info[:mid]}=")
      end

      ivar_savings = (ivar_name.to_s.size - short_name.size) * ivar_count
      method_savings = (info[:mid].to_s.size - method_short.to_s.size) * (getter_calls + setter_calls + 1)
      next unless ivar_savings + method_savings > 0

      used_ivar_names << short_name
      used_method_names << method_short.to_s

      path_b_mapping[ivar_key] = short_name
      path_b_method_mapping[ivar_key] = method_short
    end

  [path_b_mapping, path_b_method_mapping]
end

#attr_backed_ivar?(attr_backed, cpath, name) ⇒ Boolean

An attr declared anywhere up the chain backs the ivar in every descendant: a subclass's @palette = ... fills the slot the ancestor's attr_reader :palette reads. Those sites belong to the attr coordination (which follows the declaring ancestor), not to plain ivar renaming — which would give the subclass its own name for the slot and leave the reader looking at nothing.

Parameters:

  • attr_backed (Hash[Array[Symbol], Set[Symbol]])
  • cpath (Array[Symbol])
  • name (Symbol)

Returns:

  • (Boolean)


38
39
40
41
42
43
44
# File 'lib/ryac/analysis/ivar/collection.rb', line 38

def attr_backed_ivar?(attr_backed, cpath, name)
  backed = false
  @oracle.each_ancestor_cpath(cpath, false) do |ancestor_cpath|
    backed ||= attr_backed.fetch(ancestor_cpath, nil)&.include?(name) || false
  end
  backed || (attr_backed.fetch(cpath, nil)&.include?(name) || false)
end

#augment_constant_counts_via_oracle ⇒ void

This method returns an undefined value.

A closed-world walk sees each textual reference once, but type analysis can record more read sites than the text shows (reads reached through resolution rather than spelling). Take whichever count is higher, so a rename is never judged unprofitable on an undercount.



224
225
226
227
228
229
230
# File 'lib/ryac/analysis/constant/collection.rb', line 224

def augment_constant_counts_via_oracle
  @constant_mapping.each_user_defined_path do |cpath|
    oracle_count = @oracle.constant_read_count(cpath)
    current_count = @constant_mapping.usage_count_for_path(cpath)
    @constant_mapping.set_usage_count_by_path(cpath, oracle_count) if oracle_count > current_count
  end
end

#build_attr_rename_map(path_a_info, path_b_info, path_b_method_mapping) ⇒ Hash[location_key, Hash[Symbol, String]]

Parameters:

  • path_a_info (Array[attr_info])
  • path_b_info (Array[attr_info])
  • path_b_method_mapping (Hash[[Array[Symbol], Symbol], Symbol])

Returns:

  • (Hash[location_key, Hash[Symbol, String]])


280
281
282
283
284
285
286
287
288
289
290
291
292
293
# File 'lib/ryac/analysis/ivar/collection.rb', line 280

def build_attr_rename_map(path_a_info, path_b_info, path_b_method_mapping)
  # Values are the getter shorts: path A stores Strings, path B Symbols;
  # consumers interpolate, so both work.
  attr_rename_map = {} #: Hash[location_key, Hash[Symbol, untyped]]
  path_a_info.each do |info|
    (attr_rename_map[info[:loc_key]] ||= {})[info[:mid]] = info[:getter_short]
  end
  path_b_info.each do |info|
    method_short = path_b_method_mapping[info[:ivar_key]]
    next unless method_short
    (attr_rename_map[info[:loc_key]] ||= {})[info[:mid]] = method_short
  end
  attr_rename_map
end

#carries_keyword_arguments?(call_node) ⇒ Boolean

Parameters:

  • call_node (Prism::CallNode)

Returns:

  • (Boolean)


207
208
209
# File 'lib/ryac/analysis/keyword/collection.rb', line 207

def carries_keyword_arguments?(call_node)
  call_node.arguments&.arguments&.any? { |arg| arg.is_a?(Prism::KeywordHashNode) } || false
end

#class_definition_cpath(node, nesting) ⇒ Array[Symbol]?

The fully-qualified path a class/module definition creates, or nil when the written path is not static.

Parameters:

  • node (Prism::ClassNode, Prism::ModuleNode)
  • nesting (Array[Symbol])

Returns:

  • (Array[Symbol], nil)


48
49
50
# File 'lib/ryac/analysis/constant/collection.rb', line 48

def class_definition_cpath(node, nesting)
  qualified_write_cpath(node.constant_path, nesting)
end

#classify_attr_declarations(prism_root) ⇒ [Array[attr_info], Array[attr_info]]

Parameters:

  • prism_root (Prism::Node)

Returns:

  • ([Array[attr_info], Array[attr_info]])


129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
# File 'lib/ryac/analysis/ivar/collection.rb', line 129

def classify_attr_declarations(prism_root)
  path_a_info = [] #: Array[attr_info]
  path_b_info = [] #: Array[attr_info]
  each_attr_declaration(prism_root, %i[attr_reader attr_accessor]) do |node, cpath, singleton, sym|
    method_key = [cpath, singleton, sym].freeze
    getter_short = @method_rename_mapping.short_name_for_key(method_key)
    info = {
      cpath: cpath, singleton: singleton, mid: sym,
      accessor: node.name == :attr_accessor,
      loc_key: AstUtils.location_key(node),
      ivar_key: [cpath, :"@#{sym}"],
      method_key: method_key
    }
    if getter_short
      info[:getter_short] = getter_short
      path_a_info << info
    elsif @method_rename_mapping.has_method?(method_key) &&
          @method_rename_mapping.group_keys(method_key) == [method_key]
      # An excluded mid (dynamic dispatch, unresolved call, hidden attr
      # write, ...) was left alone for a reason — the ivar side must not
      # rename it either. A group spanning other keys means the mid's
      # sites are shared with defs this declaration does not cover.
      path_b_info << info
    end
  end
  [path_a_info, path_b_info]
end

#classify_unresolved_call(mid, prism_node) ⇒ Symbol

Parameters:

  • mid (Symbol)
  • node (Prism::Node)

Returns:

  • (Symbol)


263
264
265
266
267
268
269
270
271
272
273
274
# File 'lib/ryac/analysis/method/collection.rb', line 263

def classify_unresolved_call(mid, prism_node)
  # Callers only pass the UNRESOLVED_CALL_NODES shapes, all of which carry
  # a receiver.
  # @type var prism_node: Prism::CallNode | Prism::CallOperatorWriteNode | Prism::CallOrWriteNode | Prism::CallAndWriteNode
  return :mapped unless prism_node.receiver

  target_keys = @oracle.resolved_targets(prism_node, mid)
  return :exclude if target_keys.nil?

  all_mapped = target_keys.all? { |key| @method_rename_mapping.has_method?(key) }
  all_mapped ? :mapped : :unrelated
end

#collect_alias_undef_methods(prism_root) ⇒ void

This method returns an undefined value.

Parameters:

  • prism_root (Prism::Node)


379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
# File 'lib/ryac/analysis/method/collection.rb', line 379

def collect_alias_undef_methods(prism_root)
  excluded_mids = Set.new
  AstUtils.each_node(prism_root) do |node|
    case node
    when Prism::AliasMethodNode
      excluded_mids << node.new_name.unescaped.to_sym if node.new_name.is_a?(Prism::SymbolNode)
      excluded_mids << node.old_name.unescaped.to_sym if node.old_name.is_a?(Prism::SymbolNode)
    when Prism::UndefNode
      node.names.each do |name|
        excluded_mids << name.unescaped.to_sym if name.is_a?(Prism::SymbolNode)
      end
    end
  end
  @method_rename_mapping.exclude_methods_by_mid(excluded_mids) unless excluded_mids.empty?
end

#collect_attr_backed_ivars(prism_root) ⇒ Hash[Array[Symbol], Set[Symbol]]

The attr-backed ivar names per class, closed upward through the program's own classes: an attr declared in D backs the slot in every ancestor of D the program defines, since a write there (@label_text = ... in a base class) fills what D's reader reads. Object and the rest of the built-in chain write nothing and are not closed over — they would make every attr back every class's ivar of that name.

Parameters:

  • prism_root (Prism::Node)

Returns:

  • (Hash[Array[Symbol], Set[Symbol]])


52
53
54
55
56
57
58
59
60
61
62
63
64
# File 'lib/ryac/analysis/ivar/collection.rb', line 52

def collect_attr_backed_ivars(prism_root)
  defined = Set.new #: Set[Array[Symbol]]
  each_constant_event(prism_root) { |kind, _node, cpath, _singleton, _in_def| defined << cpath if kind == :class_def && cpath }

  result = Hash.new { |h, k| h[k] = Set.new } #: Hash[Array[Symbol], Set[Symbol]]
  each_attr_declaration(prism_root, ATTR_DECLARATION_METHODS, require_class_body: false) do |_node, cpath, _singleton, sym|
    result[cpath] << :"@#{sym}"
    @oracle.each_ancestor_cpath(cpath, false) do |ancestor_cpath|
      result[ancestor_cpath] << :"@#{sym}" if defined.include?(ancestor_cpath)
    end
  end
  result
end

#collect_attr_write_exclusions(prism_root) ⇒ void

This method returns an undefined value.

attr_accessor's setter def is derived from the declared symbol and never registered as a method of its own, so a setter call site type inference missed is invisible to every other exclusion pass — renaming the declaration would strand that site on the old name until the path runs. Find such sites directly and keep their symbols. attr_writer never renames its declaration at all, so a reader sharing its symbol must stay too: renaming the reader side would split the getter from the ivar the writer-defined setter still assigns.

Parameters:

  • prism_root (Prism::Node)


332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
# File 'lib/ryac/analysis/method/collection.rb', line 332

def collect_attr_write_exclusions(prism_root)
  accessor_owners = Hash.new { |h, k| h[k] = [] } #: Hash[Symbol, Array[MethodRenameMapping::class_key]]
  reader_syms = Set.new
  writer_syms = Set.new
  each_attr_declaration(prism_root, ATTR_DECLARATION_METHODS, require_class_body: false) do |node, cpath, singleton, sym|
    case node.name
    when :attr_accessor then accessor_owners[sym] << [cpath, singleton]
    when :attr_reader then reader_syms << sym
    when :attr_writer then writer_syms << sym
    end
  end

  excluded = writer_syms & (reader_syms | accessor_owners.keys)

  if accessor_owners.any?
    known_setter_sites = Set.new
    accessor_owners.each do |sym, owners|
      owners.each do |cpath, singleton|
        @oracle.each_call_site_key(cpath, singleton, :"#{sym}=") do |key|
          known_setter_sites << key
        end
      end
    end

    AstUtils.each_node(prism_root) do |node|
      write_name = case node
      when Prism::CallNode
        node.name
      when Prism::CallOperatorWriteNode, Prism::CallOrWriteNode, Prism::CallAndWriteNode
        node.write_name
      else
        next
      end

      next unless AstUtils.setter_def_name?(write_name)

      sym = write_name.to_s.chomp('=').to_sym
      next unless accessor_owners.key?(sym)
      next if known_setter_sites.include?(AstUtils.location_key(node))

      excluded << sym
    end
  end

  @method_rename_mapping.exclude_methods_by_mid(excluded) if excluded.any?
end

#collect_constants(prism_root) ⇒ void

This method returns an undefined value.

Parameters:

  • prism_root (Prism::Node)


150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
# File 'lib/ryac/analysis/constant/collection.rb', line 150

def collect_constants(prism_root)
  each_constant_event(prism_root) do |kind, node, cpath, singleton, in_def|
    case kind
    when :class_def
      type = node.is_a?(Prism::ClassNode) ? :class : :module
      @constant_mapping.add_definition_with_path(cpath, definition_type: type)
    when :write
      # Skip constants defined inside `class << self` — they live on the
      # metaclass and cannot be accessed as Foo::X, so alias declarations
      # would fail at runtime. (An assignment inside a def body there is a
      # plain assignment and still registers.)
      next if singleton && !in_def

      type = struct_definition_write?(node) ? :class : :value
      @constant_mapping.add_definition_with_path(cpath, definition_type: type)
    end
  end
end

#collect_cvar_definitions(prism_root) ⇒ void

This method returns an undefined value.

Parameters:

  • prism_root (Prism::Node)


19
20
21
22
23
24
25
26
27
# File 'lib/ryac/analysis/cvar/collection.rb', line 19

def collect_cvar_definitions(prism_root)
  Nesting.each(prism_root) do |node, cpath, _singleton|
    case node
    when Prism::ClassVariableReadNode, *CVAR_WRITE_NODES
      # @type var node: Prism::ClassVariableReadNode | cvar_write_node
      @cvar_rename_mapping.add_site(cpath, node.name, node)
    end
  end
end

#collect_dynamic_ivar_attr_exclusions(prism_root) ⇒ void

This method returns an undefined value.

Renaming an attr declaration renames its backing ivar with it. In a class that touches ivars dynamically (optcarrot's Config assigns every option via instance_variable_set), the dynamic side keeps the original spelling and a renamed reader silently returns nil. The ivar renamer already refuses such classes; the attr names there must survive for the same reason, under either policy.

Parameters:

  • prism_root (Prism::Node)


110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
# File 'lib/ryac/analysis/method/collection.rb', line 110

def collect_dynamic_ivar_attr_exclusions(prism_root)
  dynamic_cpaths = Set.new
  Nesting.each(prism_root) do |node, cpath, _singleton, _in_def|
    next unless node.is_a?(Prism::CallNode) && DYNAMIC_IVAR_METHODS.include?(node.name)

    recv = node.receiver
    dynamic_cpaths << cpath if recv.nil? || recv.is_a?(Prism::SelfNode)
  end
  return if dynamic_cpaths.empty?

  excluded = Set.new
  each_attr_declaration(prism_root, ATTR_DECLARATION_METHODS, require_class_body: false) do |_node, cpath, _singleton, sym|
    excluded << sym << :"#{sym}=" if dynamic_cpaths.include?(cpath)
  end
  @method_rename_mapping.exclude_methods_by_mid(excluded) unless excluded.empty?
end

#collect_external_references(prism_root) ⇒ void

This method returns an undefined value.

Parameters:

  • prism_root (Prism::Node)


266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
# File 'lib/ryac/analysis/constant/collection.rb', line 266

def collect_external_references(prism_root)
  counted_prefix_ids = Set.new
  prefix_counts = Hash.new(0)

  each_constant_event(prism_root) do |kind, node, _cpath, _singleton, _in_def|
    next unless kind == :read
    next if counted_prefix_ids.include?(node.object_id)
    # A region's reference resolves when the region runs — after its own
    # requires. A preamble alias for it would resolve at boot.
    next if LazyRegions.contains?(@lazy_regions, node)

    mark_chain_prefixes(node, counted_prefix_ids)
    prefix = external_prefix_for(node)
    prefix_counts[prefix] += 1 if prefix
  end

  prefix_counts.each do |prefix, count|
    @constant_mapping.add_external_prefix(prefix, usage_count: count)
  end
end

#collect_gvar_definitions(prism_root) ⇒ void

This method returns an undefined value.

Parameters:

  • prism_root (Prism::Node)


14
15
16
17
18
19
20
21
22
23
# File 'lib/ryac/analysis/gvar/collection.rb', line 14

def collect_gvar_definitions(prism_root)
  AstUtils.each_node(prism_root) do |node|
    case node
    when *GVAR_NODES
      # @type var node: gvar_node
      @gvar_rename_mapping.add_site(node.name, node,
                                    write: !node.is_a?(Prism::GlobalVariableReadNode))
    end
  end
end

#collect_inherited_attr_exclusions(prism_root) ⇒ void

This method returns an undefined value.

An attr declared below a class that touches its ivar — attr_reader :label_text in a subclass of the class assigning @label_text — cannot rename: the coordination that moves ivar sites along with a renamed attr walks from the declaring class down, never up to that ancestor, which would go on writing the original name. The pair keeps its name.

Parameters:

  • prism_root (Prism::Node)


82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
# File 'lib/ryac/analysis/method/collection.rb', line 82

def collect_inherited_attr_exclusions(prism_root)
  touching = Hash.new { |h, k| h[k] = Set.new } #: Hash[Symbol, Set[Array[Symbol]]]
  Nesting.each(prism_root) do |node, cpath, _singleton, _in_def|
    case node
    when Prism::InstanceVariableReadNode, *IVAR_WRITE_NODES
      # @type var node: Prism::InstanceVariableReadNode | ivar_write_node
      touching[node.name] << cpath
    end
  end

  excluded = Set.new
  each_attr_declaration(prism_root, ATTR_DECLARATION_METHODS, require_class_body: false) do |_node, cpath, _singleton, sym|
    classes = touching.fetch(:"@#{sym}", nil)
    next unless classes

    @oracle.each_ancestor_cpath(cpath, false) do |ancestor_cpath|
      excluded << sym << :"#{sym}=" if ancestor_cpath != cpath && classes.include?(ancestor_cpath)
    end
  end
  @method_rename_mapping.exclude_methods_by_mid(excluded) unless excluded.empty?
end

#collect_ivar_definitions(prism_root, attr_backed) ⇒ void

This method returns an undefined value.

Parameters:

  • prism_root (Prism::Node)
  • attr_backed (Hash[Array[Symbol], Set[Symbol]])


21
22
23
24
25
26
27
28
29
30
# File 'lib/ryac/analysis/ivar/collection.rb', line 21

def collect_ivar_definitions(prism_root, attr_backed)
  Nesting.each(prism_root) do |node, cpath, _singleton, _in_def|
    case node
    when Prism::InstanceVariableReadNode, *IVAR_WRITE_NODES
      # @type var node: Prism::InstanceVariableReadNode | ivar_write_node
      next if attr_backed_ivar?(attr_backed, cpath, node.name)
      @ivar_rename_mapping.add_site(cpath, node.name, node)
    end
  end
end

#collect_ivar_nodes_by_key(prism_root) ⇒ Hash[[Array[Symbol], Symbol], Array[Prism::Node]]

Parameters:

  • prism_root (Prism::Node)

Returns:

  • (Hash[[Array[Symbol], Symbol], Array[Prism::Node]])


157
158
159
160
161
162
163
164
165
166
167
# File 'lib/ryac/analysis/ivar/collection.rb', line 157

def collect_ivar_nodes_by_key(prism_root)
  ivar_nodes_by_key = Hash.new { |h, k| h[k] = [] } #: Hash[[Array[Symbol], Symbol], Array[Prism::Node]]
  Nesting.each(prism_root) do |node, cpath, _singleton, _in_def|
    case node
    when Prism::InstanceVariableReadNode, *IVAR_WRITE_NODES
      # @type var node: Prism::InstanceVariableReadNode | ivar_write_node
      ivar_nodes_by_key[[cpath, node.name]] << node
    end
  end
  ivar_nodes_by_key
end

#collect_keyword_call_sites(prism_root) ⇒ void

This method returns an undefined value.

Everything call sites decide about keyword renames, in order: literal keywords register with their method, super-forwarding merges child and parent groups (or excludes the child when the parent is outside our control), polymorphic call sites merge every method they can reach, methods no resolved call reaches are excluded, and unresolved keyword-writing calls disqualify their method name entirely.

Parameters:

  • prism_root (Prism::Node)


70
71
72
73
74
75
76
77
# File 'lib/ryac/analysis/keyword/collection.rb', line 70

def collect_keyword_call_sites(prism_root)
  sites = register_keyword_calls
  merge_super_forwarding(sites[:super_merges])
  merge_polymorphic_keyword_groups(sites[:call_node_to_keys])
  exclude_unreachable_keyword_methods(sites[:zero_call_keys], sites[:super_merges])
  exclude_unresolved_keyword_calls(prism_root, sites[:resolved_site_keys])
  exclude_method_valued_shorthand_calls(prism_root)
end

#collect_keyword_info(prism_root) ⇒ void

This method returns an undefined value.

Parameters:

  • prism_root (Prism::Node)


5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
# File 'lib/ryac/analysis/keyword/collection.rb', line 5

def collect_keyword_info(prism_root)
  @keyword_def_node_registry = {}
  @keyword_forwarding_super_keys = Set.new

  Nesting.each_method_definition(prism_root) do |node, method_key|
    keywords = def_keyword_names(node)
    next if keywords.empty?

    keywords.each { |sym| @keyword_rename_mapping.add_keyword_def(method_key, sym) }

    @keyword_def_node_registry[method_key] ||= []
    @keyword_def_node_registry[method_key] << node

    @keyword_rename_mapping.exclude_method(method_key) if def_keyword_rest?(node)

    @keyword_forwarding_super_keys << method_key if forwards_parameters_to_super?(node)
  end

  collect_keyword_call_sites(prism_root)
end

#collect_method_definitions(prism_root) ⇒ void

This method returns an undefined value.

Parameters:

  • prism_root (Prism::Node)


5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
# File 'lib/ryac/analysis/method/collection.rb', line 5

def collect_method_definitions(prism_root)
  setter_mids = Set.new
  Nesting.each_method_definition(prism_root) do |node, method_key|
    next if EXCLUDED_METHODS.include?(method_key[2])

    # An explicit `def foo=` keeps its name: short names are allocated
    # per group, so nothing promises the setter would land on
    # `<getter's name>=`, and the def patcher would strip the `=` from
    # any unpaired spelling. The base name is kept with it — a compound
    # write reads through one spelling and writes through the other, so
    # the pair must move in lockstep or not at all (the same doctrine
    # that keeps accessor pairs in compound writes unrenamed).
    # attr-declared setters are different — they rename textually
    # coupled to their getter.
    if AstUtils.setter_def_name?(method_key[2])
      setter_mids << method_key[2] << method_key[2].to_s.chomp('=').to_sym
    end

    @method_rename_mapping.add_method(method_key, node)
    link_module_function_variant(node, method_key)
  end
  @method_rename_mapping.exclude_methods_by_mid(setter_mids) unless setter_mids.empty?

  # attr_reader/attr_accessor define getters worth renaming; attr_writer's
  # setter is derived from the getter name and is never registered on its
  # own.
  each_attr_declaration(prism_root, %i[attr_reader attr_accessor]) do |node, cpath, singleton, mid|
    next if EXCLUDED_METHODS.include?(mid)

    @method_rename_mapping.add_method([cpath, singleton, mid].freeze, node)
  end
end

#collect_shorthand_pun_methods(prism_root) ⇒ void

This method returns an undefined value.

A hash/keyword shorthand can pun on a METHOD (f(unit_label:) calls unit_label for its value); the one identifier is both the key and the call, so renaming the method would rewrite the key with it. Those methods keep their names.

Parameters:

  • prism_root (Prism::Node)


300
301
302
303
304
305
306
307
308
# File 'lib/ryac/analysis/method/collection.rb', line 300

def collect_shorthand_pun_methods(prism_root)
  punned = Set.new
  AstUtils.each_node(prism_root) do |node|
    AstUtils.shorthand_pun_values(node).each do |inner|
      punned << inner.name if inner.is_a?(Prism::CallNode)
    end
  end
  @method_rename_mapping.exclude_methods_by_mid(punned) if punned.any?
end

#collect_string_literal_mentions(prism_root) ⇒ void

This method returns an undefined value.

eval and send-by-string dispatch from strings, not the syntax tree: a method whose name is spelled inside any string literal may be called from text the renamer cannot rewrite, so under the safe policy it keeps its name. Setters count as mentioned when their base word is.

Parameters:

  • prism_root (Prism::Node)


132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
# File 'lib/ryac/analysis/method/collection.rb', line 132

def collect_string_literal_mentions(prism_root)
  words = Set.new
  AstUtils.each_node(prism_root) do |node|
    next unless node.is_a?(Prism::StringNode)

    # Byte escapes (`"\x89PNG"`) leave the literal invalid in the source
    # encoding, and String#scan refuses invalid text; the identifier
    # pattern is pure ASCII, so scanning the bytes finds the same words.
    # The pattern has no capture groups, so scan only produces strings;
    # the is_a? narrows the union scan's signature declares.
    node.unescaped.b.scan(/[a-zA-Z_][a-zA-Z0-9_]*[?!]?/).each do |w|
      words << w.to_sym if w.is_a?(String)
    end
  end
  return if words.empty?

  mentioned = @method_rename_mapping.method_mids.select { |mid|
    words.include?(mid) || words.include?(mid.to_s.chomp('=').to_sym)
  }
  @method_rename_mapping.exclude_methods_by_mid(mentioned.to_set) unless mentioned.empty?
end

#collect_visibility_modifier_methods(prism_root) ⇒ void

This method returns an undefined value.

Parameters:

  • prism_root (Prism::Node)


313
314
315
316
317
318
319
320
321
322
# File 'lib/ryac/analysis/method/collection.rb', line 313

def collect_visibility_modifier_methods(prism_root)
  excluded_mids = Set.new
  AstUtils.each_node(prism_root) do |node|
    next unless node.is_a?(Prism::CallNode)
    next unless VISIBILITY_MODIFIERS.include?(node.name)

    excluded_mids.merge(AstUtils.symbol_arguments(node))
  end
  @method_rename_mapping.exclude_methods_by_mid(excluded_mids) unless excluded_mids.empty?
end

#complete_const_chain?(node) ⇒ Boolean

True when the qualified chain is rooted at a plain constant (or written absolute), so its syntactic segments are its complete spelling.

Parameters:

  • node (Prism::Node)

Returns:

  • (Boolean)


328
329
330
331
332
333
334
335
# File 'lib/ryac/analysis/constant/collection.rb', line 328

def complete_const_chain?(node)
  case node
  when Prism::ConstantPathNode, Prism::ConstantPathTargetNode
    !Nesting.path_segments(node).nil?
  else
    false
  end
end

#coordinate_attr_renames(prism_root, rename_map, attr_ivar_entries) ⇒ Hash[location_key, Hash[Symbol, String]]

attr declarations rename along two paths. Path A (method-driven): the getter already got a short name from method renaming, and the backing ivar follows it. Path B (ivar-driven): the getter wasn't renamed, so a name is chosen from the ivar side and the getter (and setter) follow. Phase 1 decides every name, phase 2 applies them — declaration, ivar sites, and accessor call sites — only once all decisions are in.

Parameters:

  • prism_root (Prism::Node)
  • rename_map (Hash[location_key, String])
  • attr_ivar_entries (Hash[location_key, String])

Returns:

  • (Hash[location_key, Hash[Symbol, String]])


88
89
90
91
92
93
94
95
96
97
98
99
100
# File 'lib/ryac/analysis/ivar/collection.rb', line 88

def coordinate_attr_renames(prism_root, rename_map, attr_ivar_entries)
  path_a_info, path_b_info = classify_attr_declarations(prism_root)
  return {} unless path_a_info.any? || path_b_info.any?

  path_a_mapping = path_a_info.to_h { |info| [info[:ivar_key], "@#{info[:getter_short]}"] }
  ivar_nodes_by_key = collect_ivar_nodes_by_key(prism_root)
  path_b_mapping, path_b_method_mapping =
    assign_ivar_driven_names(path_b_info, ivar_nodes_by_key, path_a_mapping, rename_map)

  apply_ivar_renames(ivar_nodes_by_key, path_a_mapping.merge(path_b_mapping), attr_ivar_entries)
  apply_accessor_call_renames(path_a_info, path_b_info, path_b_method_mapping, rename_map)
  build_attr_rename_map(path_a_info, path_b_info, path_b_method_mapping)
end

#count_constant_references(prism_root) ⇒ void

This method returns an undefined value.

Parameters:

  • prism_root (Prism::Node)


180
181
182
183
184
185
186
187
188
189
# File 'lib/ryac/analysis/constant/collection.rb', line 180

def count_constant_references(prism_root)
  each_constant_event(prism_root) do |kind, node, cpath, _singleton, _in_def|
    case kind
    when :class_def, :write
      @constant_mapping.increment_usage_by_path(cpath)
    when :read
      increment_constant_read_usage(node)
    end
  end
end

#def_keyword_names(def_node) ⇒ Array[Symbol]

Parameters:

  • def_node (Prism::DefNode)

Returns:

  • (Array[Symbol])


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

def def_keyword_names(def_node)
  params = def_node.parameters
  return [] unless params

  names = [] #: Array[Symbol]
  params.keywords&.each do |kw|
    names << kw.name if kw.respond_to?(:name)
  end
  names
end

#def_keyword_rest?(def_node) ⇒ Boolean

Parameters:

  • def_node (Prism::DefNode)

Returns:

  • (Boolean)


39
40
41
# File 'lib/ryac/analysis/keyword/collection.rb', line 39

def def_keyword_rest?(def_node)
  def_node.parameters&.keyword_rest.is_a?(Prism::KeywordRestParameterNode)
end

#dynamic_root?(node) ⇒ Boolean

A path whose outermost parent is neither a constant nor absent (::X) — self::X, self.class::X, klass::X.

Parameters:

  • node (Prism::ConstantPathNode)

Returns:

  • (Boolean)


144
145
146
147
148
# File 'lib/ryac/analysis/constant/collection.rb', line 144

def dynamic_root?(node)
  current = node.parent
  current = current.parent while current.is_a?(Prism::ConstantPathNode)
  !(current.nil? || current.is_a?(Prism::ConstantReadNode))
end

#each_attr_declaration(prism_root, methods, require_class_body: true) {|arg0, arg1, arg2, arg3| ... } ⇒ void

This method returns an undefined value.

attr declarations with meta semantics, one yield per symbol argument. With require_class_body: false, bare attr_* calls anywhere count — the attr-backed scan wants those too, since the methods they define still read the ivar.

Parameters:

  • prism_root (Prism::Node)
  • methods (Array[Symbol])
  • require_class_body: (Boolean) (defaults to: true)

Yields:

Yield Parameters:

  • arg0 (Prism::CallNode)
  • arg1 (Array[Symbol])
  • arg2 (Boolean)
  • arg3 (Symbol)

Yield Returns:

  • (void)


299
300
301
302
303
304
305
# File 'lib/ryac/analysis/ivar/collection.rb', line 299

def each_attr_declaration(prism_root, methods, require_class_body: true)
  Nesting.each_meta_call(prism_root, methods, loose: !require_class_body) do |node, cpath, singleton|
    AstUtils.symbol_arguments(node).each do |sym|
      yield node, cpath, singleton, sym
    end
  end
end

#each_constant_event(prism_root) {|arg0, arg1, arg2, arg3, arg4| ... } ⇒ void

This method returns an undefined value.

One classified walk of every constant fact in the tree, shared by definition collection, usage counting, external-reference collection and the analyzer's resolution precompute — so the node-shape case analysis lives in exactly one place. Yields (kind, node, cpath, singleton, in_def):

:class_def  class/module definition; cpath is the path it creates
:read       a constant read at the node's own position (cpath nil)
:write      an assignment defining cpath

A compound write (X ||= 1) and a qualified target in multiple assignment (Foo::A, b = ary) read as well as write, so they yield both a :read and a :write. Writes with no static path yield nothing.

Parameters:

  • prism_root (Prism::Node)

Yields:

Yield Parameters:

  • arg0 (Symbol)
  • arg1 (Prism::Node)
  • arg2 (Array[Symbol], nil)
  • arg3 (Boolean)
  • arg4 (Boolean)

Yield Returns:

  • (void)


17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
# File 'lib/ryac/analysis/constant/collection.rb', line 17

def each_constant_event(prism_root)
  Nesting.each(prism_root) do |node, nesting, singleton, in_def|
    case node
    when Prism::ClassNode, Prism::ModuleNode
      cpath = class_definition_cpath(node, nesting)
      yield :class_def, node, cpath, singleton, in_def if cpath
    when Prism::ConstantReadNode, Prism::ConstantPathNode
      yield :read, node, nil, singleton, in_def
    when Prism::ConstantWriteNode
      yield :write, node, nesting + [node.name], singleton, in_def
    when Prism::ConstantTargetNode
      yield :write, node, nesting + [node.name], singleton, in_def
    when Prism::ConstantOperatorWriteNode, Prism::ConstantOrWriteNode, Prism::ConstantAndWriteNode
      yield :read, node, nil, singleton, in_def
      yield :write, node, nesting + [node.name], singleton, in_def
    when Prism::ConstantPathTargetNode
      yield :read, node, nil, singleton, in_def
      cpath = qualified_write_cpath(node, nesting)
      yield :write, node, cpath, singleton, in_def if cpath
    when Prism::ConstantPathWriteNode
      cpath = qualified_write_cpath(node.target, nesting, strip_doubled_nesting: true)
      yield :write, node, cpath, singleton, in_def if cpath
    when Prism::ConstantPathOperatorWriteNode, Prism::ConstantPathOrWriteNode, Prism::ConstantPathAndWriteNode
      cpath = qualified_write_cpath(node.target, nesting)
      yield :write, node, cpath, singleton, in_def if cpath
    end
  end
end

#exclude_dynamic_root_reads(prism_root) ⇒ void

This method returns an undefined value.

expr::NAME reads a constant on a scope only the running program knows — self.class::OPTIONS in an included helper lands on whichever class included it — so no static path stands in for the reference and the name in the text is the name looked up. Every user constant called NAME keeps its name, so any of them can be the one meant.

Parameters:

  • prism_root (Prism::Node)


127
128
129
130
131
132
133
134
135
136
137
138
139
140
# File 'lib/ryac/analysis/constant/collection.rb', line 127

def exclude_dynamic_root_reads(prism_root)
  names = Set.new #: Set[Symbol]
  each_constant_event(prism_root) do |kind, node, _cpath, _singleton, _in_def|
    next unless kind == :read && node.is_a?(Prism::ConstantPathNode) && dynamic_root?(node)

    name = node.name
    names << name if name
  end
  return if names.empty?

  pinned = [] #: Array[Array[Symbol]]
  @constant_mapping.each_user_defined_path { |cpath| pinned << cpath if names.include?(cpath.last) }
  pinned.each { |cpath| @constant_mapping.exclude_path(cpath) }
end

#exclude_lazy_definitions(prism_root) ⇒ void

This method returns an undefined value.

A constant only a lazy region defines does not exist until the region runs, so no alias for it could execute at the end of the file: it keeps its name, and Optcarrot.const_get(:SDL2Video) finds it as written. A constant the flat code defines too is merely reopened by the region and renames as usual.

Parameters:

  • prism_root (Prism::Node)


109
110
111
112
113
114
115
116
117
118
119
120
# File 'lib/ryac/analysis/constant/collection.rb', line 109

def exclude_lazy_definitions(prism_root)
  return if @lazy_regions.empty?

  flat = Set.new #: Set[Array[Symbol]]
  lazy = Set.new #: Set[Array[Symbol]]
  each_constant_event(prism_root) do |kind, node, cpath, _singleton, _in_def|
    next unless cpath && (kind == :class_def || kind == :write)

    (LazyRegions.contains?(@lazy_regions, node) ? lazy : flat) << cpath
  end
  (lazy - flat).each { |cpath| @constant_mapping.exclude_path(cpath) }
end

#exclude_method_valued_shorthand_calls(prism_root) ⇒ void

This method returns an undefined value.

A keyword shorthand can pun on a METHOD, not a local: n(label:) calls label for its value. Renaming the keyword rewrites the pun and with it the method the value resolves to — methods called with such a pun keep their keywords.

Parameters:

  • prism_root (Prism::Node)


196
197
198
199
200
201
202
203
204
205
# File 'lib/ryac/analysis/keyword/collection.rb', line 196

def exclude_method_valued_shorthand_calls(prism_root)
  punned_mids = Set.new
  AstUtils.each_node(prism_root) do |node|
    next unless node.is_a?(Prism::CallNode)
    node.arguments&.arguments&.each do |arg|
      punned_mids << node.name if AstUtils.shorthand_pun_values(arg).any?
    end
  end
  @keyword_rename_mapping.exclude_methods_by_mid(punned_mids) if punned_mids.any?
end

#exclude_private_constants(prism_root) ⇒ void

This method returns an undefined value.

Parameters:

  • prism_root (Prism::Node)


169
170
171
172
173
174
175
176
177
178
# File 'lib/ryac/analysis/constant/collection.rb', line 169

def exclude_private_constants(prism_root)
  Nesting.each(prism_root) do |node, nesting, _singleton, _in_def|
    next unless node.is_a?(Prism::CallNode)
    next unless %i[private_constant public_constant].include?(node.name)

    AstUtils.symbol_arguments(node).each do |sym|
      @constant_mapping.exclude_path(nesting + [sym])
    end
  end
end

#exclude_unreachable_keyword_methods(zero_call_keys, super_merges) ⇒ void

This method returns an undefined value.

A keyword-taking method type analysis knows but connects no call to is reachable in ways we cannot see; renaming its keywords would strand the unseen callers. A method reached through super stays: the merge already ties it to callers we did see.

Parameters:

  • zero_call_keys (Array[method_key])
  • super_merges (Array[[method_key, method_key]])


159
160
161
162
163
164
165
# File 'lib/ryac/analysis/keyword/collection.rb', line 159

def exclude_unreachable_keyword_methods(zero_call_keys, super_merges)
  super_targets = super_merges.map(&:last).to_set
  zero_call_keys.each do |key|
    next if super_targets.include?(key)
    @keyword_rename_mapping.exclude_method(key)
  end
end

#exclude_unresolved_keyword_calls(prism_root, resolved_site_keys) ⇒ void

This method returns an undefined value.

A call to a keyword-taking method that type inference never connected to its target would keep its written keywords while the def's got renamed — so any such call disqualifies the whole method name. Only calls that actually write keywords (literals or a **splat) can go stale, though: a keyword-less call is untouched by the rename no matter which method it reaches, and unrelated methods sharing the name would otherwise poison each other through it.

Parameters:

  • prism_root (Prism::Node)
  • resolved_site_keys (Set[location_key])


174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
# File 'lib/ryac/analysis/keyword/collection.rb', line 174

def exclude_unresolved_keyword_calls(prism_root, resolved_site_keys)
  keyword_mids = Set.new
  @keyword_rename_mapping.each_method_key { |key| keyword_mids << key[2] }
  return if keyword_mids.empty?

  unresolved_mids = Set.new
  AstUtils.each_node(prism_root) do |node|
    next unless node.is_a?(Prism::CallNode)
    if keyword_mids.include?(node.name) &&
       carries_keyword_arguments?(node) &&
       !resolved_site_keys.include?(AstUtils.location_key(node))
      unresolved_mids << node.name
    end
  end

  @keyword_rename_mapping.exclude_methods_by_mid(unresolved_mids)
end

#external_prefix_for(node) ⇒ Array[Symbol]?

The external prefix this reference should be counted under, or nil when it has none: the reference must not touch the project's own namespace, and must either resolve through type analysis or — the fallback for gems with no RBS — be spelled in full from a root that one of the program's own top-level requires provides. The requires are re-emitted ahead of the preamble, so such a root provably exists when the alias declaration runs; without that anchor an alias would turn "NameError if this line is ever reached" into "NameError at boot", or capture the wrong constant for a reference that resolves through its nesting at runtime.

Parameters:

  • node (Prism::Node)

Returns:

  • (Array[Symbol], nil)


308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
# File 'lib/ryac/analysis/constant/collection.rb', line 308

def external_prefix_for(node)
  full_path = syntactic_const_segments(node)
  resolved_cpath = @oracle.resolve_constant_read(node)
  return nil if @constant_mapping.user_defined_path?(full_path)
  return nil if resolved_cpath && @constant_mapping.user_defined_path?(resolved_cpath)

  effective_path = resolved_cpath ||
                   (full_path if complete_const_chain?(node) && required_external_root?(full_path.first))
  return nil unless effective_path
  return nil if effective_path.size < 2
  return nil if @constant_mapping.has_user_defined_prefix?(full_path)

  prefix = effective_path[0...-1]
  return nil if @constant_mapping.user_defined_path?(prefix)

  prefix
end

#forwards_parameters_to_super?(def_node) ⇒ Boolean

A bare super forwards this method's parameters to the parent by name, so the keyword names have to keep matching the parent's signature. When the parent is a def we also collected, collect_keyword_call_sites merges the two groups and both sides are renamed together; this only reports the forwarding, the decision is made once we know whether that merge happened.

Parameters:

  • def_node (Prism::DefNode)

Returns:

  • (Boolean)


48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
# File 'lib/ryac/analysis/keyword/collection.rb', line 48

def forwards_parameters_to_super?(def_node)
  found = false
  walk = lambda do |n|
    next if found || !n.is_a?(Prism::Node)
    # A nested def has its own parameters; its `super` is not about ours.
    next if n.is_a?(Prism::DefNode)
    if n.is_a?(Prism::ForwardingSuperNode)
      found = true
      next
    end
    n.compact_child_nodes.each { |child| walk.call(child) }
  end
  walk.call(def_node.body)
  found
end

#increment_constant_read_usage(node) ⇒ void

This method returns an undefined value.

Parameters:

  • node (Prism::Node)


191
192
193
194
195
196
197
198
# File 'lib/ryac/analysis/constant/collection.rb', line 191

def increment_constant_read_usage(node)
  user_path = resolve_user_defined_cpath(node)
  if user_path
    @constant_mapping.increment_usage_by_path(user_path)
  else
    @constant_mapping.increment_usage(node.name) # steep:ignore NoMethod
  end
end

This method returns an undefined value.

module_function publishes a single def as both an instance and a singleton method, so the two must always be renamed together.

The pair is identified by both entities pointing at the same definition site — an explicit def self.foo alongside def foo is two sites and must stay independent. The "no defs but has call boxes" shape is also accepted: older TypeProf releases recorded no def at all for the singleton side of module_function.

Parameters:

  • def_node (Prism::DefNode)
  • method_key (method_key)


166
167
168
169
170
171
172
173
174
175
176
177
178
# File 'lib/ryac/analysis/method/collection.rb', line 166

def link_module_function_variant(def_node, method_key)
  cpath, singleton, mid = method_key
  return unless @oracle.method_known?(cpath, !singleton, mid)

  alt_def_keys = @oracle.method_definition_keys(cpath, !singleton, mid)
  shares_definition = alt_def_keys.include?(AstUtils.location_key(def_node))
  call_only = alt_def_keys.empty? && @oracle.method_call_count(cpath, !singleton, mid) > 0
  return unless shares_definition || call_only

  alt_key = [cpath, !singleton, mid].freeze
  @method_rename_mapping.add_method(alt_key, nil)
  @method_rename_mapping.merge_groups(method_key, alt_key)
end

#mark_chain_prefixes(node, counted_prefix_ids) ⇒ void

This method returns an undefined value.

Mark the reference's prefix chain so sub-paths are not counted again as references of their own.

Parameters:

  • node (Prism::Node)
  • counted_prefix_ids (Set[Integer])


289
290
291
292
293
294
295
296
297
# File 'lib/ryac/analysis/constant/collection.rb', line 289

def mark_chain_prefixes(node, counted_prefix_ids)
  current = case node
            when Prism::ConstantPathNode, Prism::ConstantPathTargetNode then node.parent
            end
  while current.is_a?(Prism::ConstantPathNode) || current.is_a?(Prism::ConstantReadNode)
    counted_prefix_ids << current.object_id
    current = current.is_a?(Prism::ConstantPathNode) ? current.parent : nil
  end
end

#merge_inherited_cvars ⇒ void

This method returns an undefined value.



33
34
35
# File 'lib/ryac/analysis/cvar/collection.rb', line 33

def merge_inherited_cvars
  merge_inherited_sites(@cvar_rename_mapping)
end

#merge_inherited_ivars ⇒ void

This method returns an undefined value.



70
71
72
# File 'lib/ryac/analysis/ivar/collection.rb', line 70

def merge_inherited_ivars
  merge_inherited_sites(@ivar_rename_mapping)
end

#merge_inherited_sites(mapping) ⇒ void

This method returns an undefined value.

Parameters:



118
119
120
121
122
123
124
125
126
127
# File 'lib/ryac/analysis/ivar/collection.rb', line 118

def merge_inherited_sites(mapping)
  cpaths = [] #: Array[Array[Symbol]]
  mapping.each_canonical_cpath { |c| cpaths << c }
  cpaths.each do |cpath|
    @oracle.each_ancestor_cpath(cpath, false) do |ancestor_cpath|
      next if ancestor_cpath == cpath
      mapping.merge_with_ancestor(cpath, ancestor_cpath)
    end
  end
end

#merge_polymorphic_groups(call_node_to_keys) ⇒ void

This method returns an undefined value.

One call site reaching the same method name on several classes is polymorphism: merge, so every receiver renames in lockstep. One call site reaching several different names is a computed dispatch — optcarrot's send(mode) resolves to imm/zpg/abs/… at once — and "merging" that would assign one short name to all of them, collapsing distinct methods into whichever def lands last. Those names must simply survive.

Parameters:

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


192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
# File 'lib/ryac/analysis/method/collection.rb', line 192

def merge_polymorphic_groups(call_node_to_keys)
  computed_dispatch_mids = Set.new
  call_node_to_keys.each_value do |keys|
    next if keys.size < 2

    mids = keys.map { |k| k[2] }.uniq
    if mids.size > 1
      computed_dispatch_mids.merge(mids)
      next
    end

    (1...keys.size).each { |i| @method_rename_mapping.merge_groups(keys[i - 1], keys[i]) }
  end
  @method_rename_mapping.exclude_methods_by_mid(computed_dispatch_mids) unless computed_dispatch_mids.empty?
end

#merge_polymorphic_keyword_groups(call_node_to_keys) ⇒ void

This method returns an undefined value.

Parameters:

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


148
149
150
151
152
153
# File 'lib/ryac/analysis/keyword/collection.rb', line 148

def merge_polymorphic_keyword_groups(call_node_to_keys)
  call_node_to_keys.each_value do |keys|
    next if keys.size < 2
    (1...keys.size).each { |i| @keyword_rename_mapping.merge_groups(keys[i - 1], keys[i]) }
  end
end

#merge_super_forwarding(super_merges) ⇒ void

This method returns an undefined value.

Supers are discovered from the parent's call boxes, so a super whose parent we never collected produces no merge at all. That parent's signature is outside our control — Data.define and Struct.new generate theirs from the member list — and renaming only the child raises "unknown keywords" at runtime, so leave those keywords alone.

Parameters:

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


136
137
138
139
140
141
142
143
144
145
146
# File 'lib/ryac/analysis/keyword/collection.rb', line 136

def merge_super_forwarding(super_merges)
  super_merges.each do |child_key, parent_key|
    @keyword_rename_mapping.merge_groups(child_key, parent_key)
  end

  merged_children = Set.new(super_merges.map { |child_key, _| child_key })
  @keyword_forwarding_super_keys.each do |key|
    next if merged_children.include?(key)
    @keyword_rename_mapping.exclude_method(key)
  end
end

#merge_super_groups(super_merges) ⇒ void

This method returns an undefined value.

Parameters:

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


180
181
182
183
184
# File 'lib/ryac/analysis/method/collection.rb', line 180

def merge_super_groups(super_merges)
  super_merges.each do |child_key, parent_key|
    @method_rename_mapping.merge_groups(child_key, parent_key) if @method_rename_mapping.has_method?(child_key)
  end
end

#merge_unresolved_calls(resolved_call_keys) ⇒ void

This method returns an undefined value.

Parameters:

  • resolved_call_keys (Set[location_key])


215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
# File 'lib/ryac/analysis/method/collection.rb', line 215

def merge_unresolved_calls(resolved_call_keys)
  method_mids = @method_rename_mapping.method_mids
  unresolved_by_mid = Hash.new { |h, k| h[k] = [] } #: Hash[Symbol, Array[Prism::Node]]

  AstUtils.each_node(@prism_root) do |node|
    mids = case node
    when Prism::CallNode
      [node.name]
    when Prism::CallOperatorWriteNode, Prism::CallOrWriteNode, Prism::CallAndWriteNode
      [node.read_name, node.write_name]
    else
      next
    end

    loc = AstUtils.location_key(node)
    next if resolved_call_keys.include?(loc)

    mids.each do |mid|
      unresolved_by_mid[mid] << node if method_mids.include?(mid)
    end
  end

  exclude_mids = Set.new
  unresolved_by_mid.each do |mid, call_nodes|
    mapped_calls = [] #: Array[Prism::Node]
    should_exclude = false

    call_nodes.each do |node|
      verdict = classify_unresolved_call(mid, node)
      case verdict
      when :mapped  then mapped_calls << node
      when :exclude then should_exclude = true; break
      # :unrelated — call targets a different class's method, skip
      end
    end

    # An unresolved call the aggressive policy folds into the group is
    # exactly the bet the safe policy refuses: the name goes untouched.
    if should_exclude || (@method_policy == :safe && mapped_calls.any?)
      exclude_mids << mid
    elsif mapped_calls.any?
      @method_rename_mapping.merge_all_by_mid(mid)
      @method_rename_mapping.add_unresolved_sites_for_mid(mid, mapped_calls)
    end
  end
  @method_rename_mapping.exclude_methods_by_mid(exclude_mids) unless exclude_mids.empty?
end

#precompute_constant_resolution ⇒ void

This method returns an undefined value.

Location-keyed maps the patchers consume: resolution and full path per read, write cpath per assignment, cpath and superclass per definition.



234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
# File 'lib/ryac/analysis/constant/collection.rb', line 234

def precompute_constant_resolution
  @const_resolution_map = {} #: Hash[location_key, Array[Symbol]?]
  @const_full_path_map = {} #: Hash[location_key, Array[Symbol]]
  @const_write_cpath_map = {} #: Hash[location_key, Array[Symbol]]
  @class_cpath_map = {} #: Hash[location_key, Array[Symbol]]
  @superclass_resolution_map = {} #: Hash[location_key, Array[Symbol]]

  each_constant_event(@prism_root) do |kind, node, cpath, _singleton, _in_def|
    case kind
    when :read
      record_constant_read(node)
    when :write
      @const_write_cpath_map[AstUtils.location_key(node)] = cpath
    when :class_def
      key = AstUtils.location_key(node)
      @class_cpath_map[key] = cpath
      if node.is_a?(Prism::ClassNode) && node.superclass
        # cpath is never nil for a :class_def event.
        resolved = resolve_superclass_path(node.superclass, cpath) # steep:ignore ArgumentTypeMismatch
        @superclass_resolution_map[key] = resolved if resolved
      end
    end
  end
end

#qualified_write_cpath(target, nesting, strip_doubled_nesting: false) ⇒ Array[Symbol]?

The path a qualified assignment target names, or nil when it is not static (self::X, expr::X). A plain path write that repeats the lexical nesting (Foo::Bar::X = 1 inside module Foo; module Bar) names the same constant the bare write would, so the doubled prefix is dropped — but only for that form: compound and multiple-target writes concatenate as written, mirroring the previous implementation.

Parameters:

  • target (Prism::Node)
  • nesting (Array[Symbol])
  • strip_doubled_nesting: (Boolean) (defaults to: false)

Returns:

  • (Array[Symbol], nil)


58
59
60
61
62
63
64
65
# File 'lib/ryac/analysis/constant/collection.rb', line 58

def qualified_write_cpath(target, nesting, strip_doubled_nesting: false)
  segments, absolute = Nesting.path_segments(target)
  return nil unless segments
  return segments if absolute
  return segments if strip_doubled_nesting && segments.take(nesting.size) == nesting

  nesting + segments
end

#record_constant_read(node) ⇒ void

This method returns an undefined value.

Parameters:

  • node (Prism::Node)


259
260
261
262
263
264
# File 'lib/ryac/analysis/constant/collection.rb', line 259

def record_constant_read(node)
  key = AstUtils.location_key(node)
  resolved = @oracle.resolve_constant_read(node)
  @const_resolution_map[key] = resolved
  @const_full_path_map[key] = resolved || syntactic_const_segments(node)
end

#register_keyword_calls ⇒ Hash[Symbol, untyped]

Returns:

  • (Hash[Symbol, untyped])


79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
# File 'lib/ryac/analysis/keyword/collection.rb', line 79

def register_keyword_calls
  call_node_to_keys = Hash.new { |h, k| h[k] = [] } #: Hash[location_key, Array[method_key]]
  super_merges = [] #: Array[[method_key, method_key]]
  zero_call_keys = [] #: Array[method_key]
  resolved_site_keys = Set.new

  @keyword_rename_mapping.each_method_key do |key|
    call_count = 0
    splat_seen = false

    @oracle.each_caller(key[0], key[1], key[2]) do |info|
      resolved_site_keys << AstUtils.location_key(info.prism_node) if info.prism_node
      next if splat_seen

      if info.super
        # caller_cpath is always present on super records
        entry = [info.caller_cpath, key[1], key[2]].freeze #: method_key
        super_merges << [entry, key]
        next
      end

      if info.prism_node.nil?
        # An unrewritable dispatch with no source site: the method is
        # reached in a way we cannot see, which is exactly what the
        # zero-call rule below covers — it must not count as a seen call.
        next
      end

      call_count += 1

      next unless info.keyword_entries

      if info.keyword_splat
        @keyword_rename_mapping.exclude_method(key)
        splat_seen = true
        next
      end

      info.keyword_entries.each do |sym_node, val_node|
        @keyword_rename_mapping.add_keyword_call(key, sym_node.unescaped.to_sym, sym_node, val_node)
      end

      call_node_to_keys[AstUtils.location_key(info.prism_node)] << key
    end

    zero_call_keys << key if call_count == 0 && @oracle.method_known?(key[0], key[1], key[2])
  end

  { call_node_to_keys: call_node_to_keys, super_merges: super_merges,
    zero_call_keys: zero_call_keys, resolved_site_keys: resolved_site_keys }
end

#register_size_comparison(node, transform_map, suppressed_alias_keys) ⇒ void

This method returns an undefined value.

.size==0 (and !=0 / >0) against a proven receiver collapses to the literal comparison. Registered on the comparison node; the renamer patches from the data receiver through the comparison's end.

Parameters:

  • node (Prism::CallNode)
  • transform_map (Hash[location_key, String])
  • suppressed_alias_keys (Array[location_key])


43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
# File 'lib/ryac/analysis/method_aliasing.rb', line 43

def register_size_comparison(node, transform_map, suppressed_alias_keys)
  return unless SIZE_COMPARISON_OPS.key?(node.name)

  inner = node.receiver
  return unless inner.is_a?(Prism::CallNode)
  return unless inner.receiver && inner.call_operator_loc && !inner.safe_navigation?
  return unless inner.arguments.nil? && inner.block.nil?

  types = SIZE_QUERY_MIDS[inner.name]
  return unless types

  args = node.arguments&.arguments
  return unless args && args.size == 1 && args[0].is_a?(Prism::IntegerNode) && args[0].value == 0

  types.each do |type_name|
    next unless @oracle.receiver_within_type?(inner, type_name)
    transform_map[AstUtils.location_key(node)] = SIZE_COMPARISON_TRANSFORMS.fetch([node.name, type_name])
    suppressed_alias_keys << AstUtils.location_key(inner)
    break
  end
end

#required_external_root?(root) ⇒ Boolean

Parameters:

  • root (Symbol)

Returns:

  • (Boolean)


337
338
339
# File 'lib/ryac/analysis/constant/collection.rb', line 337

def required_external_root?(root)
  @boot_constant_roots&.include?(root) || false
end

#reserve_attr_ivar_names(prism_root) ⇒ void

This method returns an undefined value.

Parameters:

  • prism_root (Prism::Node)


74
75
76
77
78
79
80
# File 'lib/ryac/analysis/ivar/collection.rb', line 74

def reserve_attr_ivar_names(prism_root)
  each_attr_declaration(prism_root, %i[attr_reader attr_accessor]) do |_node, cpath, singleton, sym|
    getter_short = @method_rename_mapping.short_name_for_key([cpath, singleton, sym].freeze)
    next unless getter_short
    @ivar_rename_mapping.reserve_name(cpath, "@#{getter_short}")
  end
end

#resolve_method_aliases_and_transforms(prism_root) ⇒ [Hash[location_key, Symbol], Hash[location_key, String]]

Parameters:

  • prism_root (Prism::Node)

Returns:

  • ([Hash[location_key, Symbol], Hash[location_key, String]])


5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
# File 'lib/ryac/analysis/method_aliasing.rb', line 5

def resolve_method_aliases_and_transforms(prism_root)
  alias_map = {} #: Hash[location_key, Symbol]
  transform_map = {} #: Hash[location_key, String]
  suppressed_alias_keys = [] #: Array[location_key]
  AstUtils.each_node(prism_root) do |node|
    next unless node.is_a?(Prism::CallNode)

    if node.receiver
      shorter = METHOD_ALIASES[node.name]
      alias_map[AstUtils.location_key(node)] = shorter if shorter && @oracle.receiver_responds_to?(node, shorter)
    else
      shorter = KERNEL_ALIASES[node.name]
      alias_map[AstUtils.location_key(node)] = shorter if shorter
    end

    if node.receiver && (node.arguments.nil? || node.arguments.arguments.empty?)
      METHOD_TRANSFORMS.each do |(mid, type_name), replacement|
        next unless mid == node.name
        if @oracle.receiver_within_type?(node, type_name)
          transform_map[AstUtils.location_key(node)] = replacement
          break
        end
      end
    end

    register_size_comparison(node, transform_map, suppressed_alias_keys)
  end
  # A comparison transform swallows its inner size call whole — an
  # alias patch on that call would overlap it.
  suppressed_alias_keys.each { |k| alias_map.delete(k) }
  [alias_map, transform_map]
end

#resolve_method_calls ⇒ void

This method returns an undefined value.



38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
# File 'lib/ryac/analysis/method/collection.rb', line 38

def resolve_method_calls
  call_node_to_keys = Hash.new { |h, k| h[k] = [] } #: Hash[location_key, Array[method_key]]
  resolved_call_keys = Set.new
  super_merges = [] #: Array[[method_key, method_key]]
  sig_dispatch_mids = Set.new

  @method_rename_mapping.each_method_key do |key|
    @oracle.each_caller(key[0], key[1], key[2]) do |info|
      if info.super
        # caller_cpath is always present on super records
        entry = [info.caller_cpath, key[1], key[2]].freeze #: method_key
        super_merges << [entry, key]
        next
      end

      if info.prism_node.nil?
        # The dispatch happens inside an RBS declaration, so there is no
        # source site to rewrite; the name must survive on every class.
        sig_dispatch_mids << key[2]
        next
      end

      scope_id = info.receiver ? nil : @local_scopes.scope_id_of(info.prism_node)
      @method_rename_mapping.add_call_site(info.prism_node, key, has_receiver: info.receiver, scope_id: scope_id)
      loc = AstUtils.location_key(info.prism_node)
      call_node_to_keys[loc] << key
      resolved_call_keys << loc
    end
  end

  @method_rename_mapping.exclude_methods_by_mid(sig_dispatch_mids) unless sig_dispatch_mids.empty?
  merge_super_groups(super_merges)
  merge_polymorphic_groups(call_node_to_keys)
  merge_unresolved_calls(resolved_call_keys)
  # Folding blind defs into same-name sited groups is a probability
  # bet, not a proof — the aggressive policy's territory.
  @method_rename_mapping.merge_blind_def_groups if @method_policy == :aggressive
end

#resolve_superclass_path(superclass_node, class_cpath) ⇒ Array[Symbol]?

Where a superclass reference lands, for the alias patcher. A qualified path is taken as written; a bare name is meaningful only when it names a user-defined constant in the class's enclosing scope.

Parameters:

  • superclass_node (Prism::Node)
  • class_cpath (Array[Symbol])

Returns:

  • (Array[Symbol], nil)


210
211
212
213
214
215
216
217
218
# File 'lib/ryac/analysis/constant/collection.rb', line 210

def resolve_superclass_path(superclass_node, class_cpath)
  case superclass_node
  when Prism::ConstantPathNode
    syntactic_const_segments(superclass_node)
  when Prism::ConstantReadNode
    full_path = class_cpath[0...-1] + [superclass_node.name] # steep:ignore NoMethod
    full_path if @constant_mapping&.user_defined_path?(full_path)
  end
end

#resolve_user_defined_cpath(node) ⇒ Array[Symbol]?

Parameters:

  • node (Prism::Node)

Returns:

  • (Array[Symbol], nil)


200
201
202
203
204
205
# File 'lib/ryac/analysis/constant/collection.rb', line 200

def resolve_user_defined_cpath(node)
  [@oracle.resolve_constant_read(node), syntactic_const_segments(node)].each do |cpath|
    return cpath if cpath && @constant_mapping.user_defined_path?(cpath)
  end
  nil
end

#scan_alias_globals(prism_root) ⇒ void

This method returns an undefined value.

alias $new $old aliases the variable, so the two names refer to one storage cell; renaming either side independently would sever them.

Parameters:

  • prism_root (Prism::Node)


27
28
29
30
31
32
33
34
# File 'lib/ryac/analysis/gvar/collection.rb', line 27

def scan_alias_globals(prism_root)
  AstUtils.each_node(prism_root) do |node|
    next unless node.is_a?(Prism::AliasGlobalVariableNode)

    @gvar_rename_mapping.exclude_name(node.new_name.slice.to_sym)
    @gvar_rename_mapping.exclude_name(node.old_name.slice.to_sym)
  end
end

#scan_dynamic_cvar_access(prism_root) ⇒ void

This method returns an undefined value.

Parameters:

  • prism_root (Prism::Node)


29
30
31
# File 'lib/ryac/analysis/cvar/collection.rb', line 29

def scan_dynamic_cvar_access(prism_root)
  scan_dynamic_sigil_access(prism_root, DYNAMIC_CVAR_METHODS, @cvar_rename_mapping)
end

#scan_dynamic_ivar_access(prism_root) ⇒ void

This method returns an undefined value.

Parameters:

  • prism_root (Prism::Node)


66
67
68
# File 'lib/ryac/analysis/ivar/collection.rb', line 66

def scan_dynamic_ivar_access(prism_root)
  scan_dynamic_sigil_access(prism_root, DYNAMIC_IVAR_METHODS, @ivar_rename_mapping)
end

#scan_dynamic_method_references(prism_root) ⇒ void

This method returns an undefined value.

Parameters:

  • prism_root (Prism::Node)


278
279
280
281
282
283
284
285
286
287
# File 'lib/ryac/analysis/method/collection.rb', line 278

def scan_dynamic_method_references(prism_root)
  dynamic_mids = Set.new
  AstUtils.each_node(prism_root) do |node|
    next unless node.is_a?(Prism::CallNode)
    next unless DYNAMIC_DISPATCH_METHODS.include?(node.name)

    @oracle.first_argument_symbols(node).each { |sym| dynamic_mids << sym }
  end
  @method_rename_mapping.exclude_methods_by_mid(dynamic_mids) unless dynamic_mids.empty?
end

#scan_dynamic_sigil_access(prism_root, methods, mapping) ⇒ void

This method returns an undefined value.

A receiverless (or explicit-self) call to a dynamic sigil-variable API can reach any name in its class — nothing in that cpath is safe to rename. Shared by the ivar and cvar scans, which differ only in table and mapping.

Parameters:



108
109
110
111
112
113
114
115
116
# File 'lib/ryac/analysis/ivar/collection.rb', line 108

def scan_dynamic_sigil_access(prism_root, methods, mapping)
  Nesting.each(prism_root) do |node, cpath, _singleton, _in_def|
    next unless node.is_a?(Prism::CallNode)
    next unless methods.include?(node.name)

    recv = node.receiver
    mapping.exclude_cpath(cpath) if recv.nil? || recv.is_a?(Prism::SelfNode)
  end
end

#struct_definition_write?(node) ⇒ Boolean

X = Struct.new(...) / X = Data.define(...) assigns a class, so the constant registers as a class definition: it renames exactly when class renaming is on, and reopenings resolve through the same machinery.

Parameters:

  • node (Prism::Node)

Returns:

  • (Boolean)


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

def struct_definition_write?(node)
  return false unless node.is_a?(Prism::ConstantWriteNode) || node.is_a?(Prism::ConstantPathWriteNode)

  value = node.value
  return false unless value.is_a?(Prism::CallNode)

  receiver = value.receiver
  return false unless receiver.is_a?(Prism::ConstantReadNode)

  (value.name == :new && receiver.name == :Struct) ||
    (value.name == :define && receiver.name == :Data)
end

#syntactic_const_segments(node) ⇒ Array[Symbol]

The constant path as written, without any resolution — partial when the chain hangs off a dynamic root (expr::CONST gives just [:CONST]).

Parameters:

  • node (Prism::Node)

Returns:

  • (Array[Symbol])


69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
# File 'lib/ryac/analysis/constant/collection.rb', line 69

def syntactic_const_segments(node)
  case node
  when Prism::ConstantPathNode, Prism::ConstantPathTargetNode
    segments = [node.name]
    current = node.parent
    while current.is_a?(Prism::ConstantPathNode)
      segments.unshift(current.name)
      current = current.parent
    end
    segments.unshift(current.name) if current.is_a?(Prism::ConstantReadNode)
    segments
  else
    # Reached only for bare named nodes (ConstantRead and the compound
    # constant writes), which all carry #name; the parameter stays
    # Prism::Node for the shared walk.
    [node.name] # steep:ignore NoMethod
  end
end