Class: Ryac::Pipeline::ConstantAliaser

Inherits:
Stage
  • Object
show all
Includes:
RenamePatcher
Defined in:
lib/ryac/pipeline/constant_aliaser.rb,
sig/ryac/pipeline/constant_aliaser.rbs

Overview

Constant aliasing via source patching.

Instance Method Summary collapse

Methods included from RenamePatcher

#patch_assoc, #patch_block_param_node, #patch_block_params, #patch_def_params, #patch_for_node, #patch_keyword_hash, #patch_keyword_params, #patch_keyword_rest, #patch_multi_target_params, #patch_named_param, #patch_optional_params, #patch_post_params, #patch_required_params, #patch_rest_param, #patch_variable, #patch_variable_name_only, #prism_location_key, #walk_prism

Methods inherited from Stage

#analysis, #analysis_options, #call, #fixpoint?, new

Methods included from SourcePatcher

#apply_patches, #mk, #src, #verify_parses

Constructor Details

#initialize(rename_classes: false) ⇒ ConstantAliaser

Returns a new instance of ConstantAliaser.

Parameters:

  • rename_classes: (Boolean) (defaults to: false)


13
14
15
# File 'lib/ryac/pipeline/constant_aliaser.rb', line 13

def initialize(rename_classes: false)
  @rename_classes = rename_classes
end

Instance Method Details

#build_renamed_via_user_prefix(node, analysis) ⇒ String?

Parameters:

Returns:

  • (String, nil)


192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
# File 'lib/ryac/pipeline/constant_aliaser.rb', line 192

def build_renamed_via_user_prefix(node, analysis)
  parent_node = node.parent
  return nil unless parent_node

  parent_key = prism_location_key(parent_node)
  parent_resolved = analysis.const_resolution_map[parent_key]
  return nil unless parent_resolved

  if @constant_mapping.user_defined_path?(parent_resolved)
    parent_short = if parent_node.is_a?(Prism::ConstantReadNode)
      @constant_mapping.short_name_for_path(parent_resolved) || parent_node.name.to_s
    else
      get_short_cpath(parent_resolved)
    end
    "#{parent_short}::#{node.name}"
  elsif parent_node.is_a?(Prism::ConstantPathNode)
    parent_renamed = build_renamed_via_user_prefix(parent_node, analysis)
    parent_renamed ? "#{parent_renamed}::#{node.name}" : nil
  end
end

#collect(ctx, patches) ⇒ void

This method returns an undefined value.

Parameters:



17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
# File 'lib/ryac/pipeline/constant_aliaser.rb', line 17

def collect(ctx, patches)
  analysis = analysis(ctx)
  @class_module_cpath_offsets = Set.new
  # Declared optional on AnalysisResult but always present by the time
  # constant patches are collected.
  @constant_mapping = analysis.constant_mapping #: ConstantRenameMapping
  # Short-name assignment is this stage's policy — it happens exactly
  # once, before any constant patch is emitted, and no other stage
  # reads the mapping.
  if @constant_mapping
    generator = NameGenerator.new([], upcase: true)
    @constant_mapping.assign_short_names(generator, skip_class_modules: !@rename_classes)
  end
  collect_patches(ctx.ast, patches, analysis)
end

#collect_patches(node, patches, analysis) ⇒ void

This method returns an undefined value.

Parameters:

  • node (Prism::Node)
  • patches (Array[patch_entry])
  • analysis (AnalysisResult)


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
# File 'lib/ryac/pipeline/constant_aliaser.rb', line 46

def collect_patches(node, patches, analysis)
  walk_prism(node) do |subnode|
    case subnode
    when Prism::ConstantReadNode
      patch_constant_ref(subnode, patches, analysis)
    when Prism::ConstantPathNode
      patch_constant_ref(subnode, patches, analysis)
      next :skip_children
    when Prism::ConstantWriteNode
      patch_constant_write(subnode, patches, analysis)
    when Prism::ConstantTargetNode
      # `A, B = ...` — Prism models each target as a ConstantTargetNode,
      # not a ConstantWriteNode. TypeProf still reports one constant per
      # target at the same location, so without this the references get
      # renamed while the definitions keep their original names.
      patch_constant_write(subnode, patches, analysis)
    when Prism::ConstantPathWriteNode
      patch_constant_path_write(subnode, patches, analysis)
    when Prism::ClassNode
      patch_class_node(subnode, patches, analysis)
    when Prism::ModuleNode
      patch_module_node(subnode, patches, analysis)
    when Prism::DefNode
      patch_def_receiver(subnode.receiver, patches, analysis) if subnode.receiver && !subnode.receiver.is_a?(Prism::SelfNode)
    end
  end
end

#finish(ctx) ⇒ void

This method returns an undefined value.

Parameters:



33
34
35
36
37
38
39
40
41
42
# File 'lib/ryac/pipeline/constant_aliaser.rb', line 33

def finish(ctx)
  mapping = @constant_mapping
  return unless mapping

  prefix_decls = mapping.generate_prefix_declarations
  ctx.preamble = [ctx.preamble, prefix_decls.join(';')].reject(&:empty?).join(';') if prefix_decls.any?

  alias_decls = mapping.generate_alias_declarations
  ctx.aliases = [ctx.aliases, alias_decls.join(';')].reject(&:empty?).join(';') if alias_decls.any?
end

#get_short_cpath(cpath) ⇒ String

Parameters:

  • cpath (Array[Symbol])

Returns:

  • (String)


213
214
215
216
217
218
219
# File 'lib/ryac/pipeline/constant_aliaser.rb', line 213

def get_short_cpath(cpath)
  if @constant_mapping.user_defined_path?(cpath)
    render_short_cpath(cpath)
  else
    cpath.map(&:to_s).join('::')
  end
end

#mark_constant_children(node) ⇒ void

This method returns an undefined value.

Parameters:

  • node (Prism::Node)


182
183
184
185
186
187
188
189
190
# File 'lib/ryac/pipeline/constant_aliaser.rb', line 182

def mark_constant_children(node)
  case node
  when Prism::ConstantPathNode
    @class_module_cpath_offsets << node.location.start_offset
    mark_constant_children(node.parent) if node.parent
  when Prism::ConstantReadNode
    @class_module_cpath_offsets << node.location.start_offset
  end
end

#needs_analysis? ⇒ Boolean

Returns:

  • (Boolean)


11
# File 'lib/ryac/pipeline/constant_aliaser.rb', line 11

def needs_analysis? = true

#patch_class_node(node, patches, analysis) ⇒ Object

Parameters:

  • node (Prism::ClassNode)
  • patches (Array[patch_entry])
  • analysis (AnalysisResult)

Returns:

  • (Object)


130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
# File 'lib/ryac/pipeline/constant_aliaser.rb', line 130

def patch_class_node(node, patches, analysis)
  key = prism_location_key(node)
  patch_definition_path(node, patches, analysis.class_cpath_map[key], analysis)

  if node.superclass
    superclass_path = analysis.superclass_resolution_map[key]
    superclass_path ||= analysis.const_resolution_map[prism_location_key(node.superclass)]
    if superclass_path
      short = get_short_cpath(superclass_path)
      sc_loc = node.superclass.location
      patches << { start: sc_loc.start_offset, end: sc_loc.end_offset, replacement: short }
      @class_module_cpath_offsets << sc_loc.start_offset
      mark_constant_children(node.superclass)
    end
  end
end

#patch_constant_path_write(node, patches, analysis) ⇒ Object

Parameters:

  • node (Prism::ConstantPathWriteNode)
  • patches (Array[patch_entry])
  • analysis (AnalysisResult)

Returns:

  • (Object)


118
119
120
121
122
123
124
125
126
127
128
# File 'lib/ryac/pipeline/constant_aliaser.rb', line 118

def patch_constant_path_write(node, patches, analysis)
  key = prism_location_key(node)
  static_cpath = analysis.const_write_cpath_map[key]
  return unless static_cpath

  path_str = render_short_cpath(static_cpath)
  target_loc = node.target.location
  patches << { start: target_loc.start_offset, end: target_loc.end_offset, replacement: path_str }
  @class_module_cpath_offsets << target_loc.start_offset
  mark_constant_children(node.target)
end

#patch_constant_ref(node, patches, analysis) ⇒ Object

The patch_* helpers are called for their side effects from the walk_prism block, whose declared result is Symbol? (:skip_children or nil) — untyped rather than void keeps the block body compatible.

Parameters:

  • node (Prism::ConstantReadNode, Prism::ConstantPathNode)
  • patches (Array[patch_entry])
  • analysis (AnalysisResult)

Returns:

  • (Object)


22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
# File 'sig/ryac/pipeline/constant_aliaser.rbs', line 22

def patch_constant_ref(node, patches, analysis)
  return if @class_module_cpath_offsets.include?(node.location.start_offset)

  key = prism_location_key(node)
  resolved_cpath = analysis.const_resolution_map[key]
  full_path = analysis.const_full_path_map[key]
  prefix_alias = full_path && @constant_mapping&.short_name_for_prefix(full_path)

  if resolved_cpath && @constant_mapping&.user_defined_path?(resolved_cpath)
    short = if node.is_a?(Prism::ConstantReadNode)
      # Bare name reference (e.g., CONST) — use only the leaf short name.
      # Expanding to a fully qualified path would break constants defined
      # in `class << self` (metaclass constants are not accessible as Foo::X).
      @constant_mapping.short_name_for_path(resolved_cpath) || node.name.to_s
    else
      get_short_cpath(resolved_cpath)
    end
    loc = node.location
    patches << { start: loc.start_offset, end: loc.end_offset, replacement: short }
  elsif node.is_a?(Prism::ConstantPathNode) && resolved_cpath &&
        (short = build_renamed_via_user_prefix(node, analysis))
    loc = node.location
    patches << { start: loc.start_offset, end: loc.end_offset, replacement: short }
  elsif prefix_alias
    short = "#{prefix_alias}::#{node.name}"
    loc = node.location
    patches << { start: loc.start_offset, end: loc.end_offset, replacement: short }
  end
end

#patch_constant_write(node, patches, analysis) ⇒ Object

Parameters:

  • node (Prism::ConstantWriteNode, Prism::ConstantTargetNode)
  • patches (Array[patch_entry])
  • analysis (AnalysisResult)

Returns:

  • (Object)


104
105
106
107
108
109
110
111
112
113
114
115
116
# File 'lib/ryac/pipeline/constant_aliaser.rb', line 104

def patch_constant_write(node, patches, analysis)
  key = prism_location_key(node)
  static_cpath = analysis.const_write_cpath_map[key]
  return unless static_cpath

  short_name = @constant_mapping.short_name_for_path(static_cpath)
  return unless short_name

  # A ConstantTargetNode is entirely the name; a ConstantWriteNode also
  # spans `= value`, so only its name_loc belongs to us.
  name_loc = node.is_a?(Prism::ConstantTargetNode) ? node.location : node.name_loc
  patches << { start: name_loc.start_offset, end: name_loc.end_offset, replacement: short_name }
end

#patch_def_receiver(receiver, patches, analysis) ⇒ Object

Parameters:

  • receiver (Prism::Node)
  • patches (Array[patch_entry])
  • analysis (AnalysisResult)

Returns:

  • (Object)


221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
# File 'lib/ryac/pipeline/constant_aliaser.rb', line 221

def patch_def_receiver(receiver, patches, analysis)
  return unless @constant_mapping

  @class_module_cpath_offsets.add(receiver.location.start_offset)
  key = prism_location_key(receiver)
  resolved_cpath = case receiver
                   when Prism::ConstantReadNode
                     analysis.const_resolution_map[key] || [receiver.name]
                   when Prism::ConstantPathNode
                     analysis.const_resolution_map[key]
                   end
  return unless resolved_cpath && @constant_mapping.user_defined_path?(resolved_cpath)

  short = get_short_cpath(resolved_cpath)
  loc = receiver.location
  patches << { start: loc.start_offset, end: loc.end_offset, replacement: short }
end

#patch_definition_path(node, patches, class_cpath, analysis) ⇒ void

This method returns an undefined value.

A definition's written path names the class relative to its lexical nesting, and the enclosing modules are renamed right along with it — so the renamed spelling only needs the written segments' short names. Re-qualifying with the full path (module A; class A::B) costs bytes and says nothing the nesting doesn't already say: class B inside module A creates and reopens exactly A::B.

Parameters:

  • node (Prism::ClassNode, Prism::ModuleNode)
  • patches (Array[patch_entry])
  • class_cpath (Array[Symbol], nil)
  • analysis (AnalysisResult)


157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
# File 'lib/ryac/pipeline/constant_aliaser.rb', line 157

def patch_definition_path(node, patches, class_cpath, analysis)
  return unless class_cpath

  # class_cpath exists only when path_segments succeeded on this node,
  # and in the absolute case it equals the segments outright.
  segments, = Nesting.path_segments(node.constant_path) #: [Array[Symbol], bool]
  rendered = render_short_cpath(class_cpath, from: class_cpath.size - segments.size)
  if rendered != node.constant_path.slice
    cpath_loc = node.constant_path.location
    patches << { start: cpath_loc.start_offset, end: cpath_loc.end_offset, replacement: rendered }
  end

  @class_module_cpath_offsets << node.constant_path.location.start_offset
  mark_constant_children(node.constant_path)
end

#patch_module_node(node, patches, analysis) ⇒ Object

Parameters:

  • node (Prism::ModuleNode)
  • patches (Array[patch_entry])
  • analysis (AnalysisResult)

Returns:

  • (Object)


147
148
149
# File 'lib/ryac/pipeline/constant_aliaser.rb', line 147

def patch_module_node(node, patches, analysis)
  patch_definition_path(node, patches, analysis.class_cpath_map[prism_location_key(node)], analysis)
end

#render_short_cpath(cpath, from: 0) ⇒ String

Each prefix of the cpath may carry its own short name; render from from onward, falling back to the original segment where none was assigned.

Parameters:

  • cpath (Array[Symbol])
  • from: (Integer) (defaults to: 0)

Returns:

  • (String)


176
177
178
179
180
# File 'lib/ryac/pipeline/constant_aliaser.rb', line 176

def render_short_cpath(cpath, from: 0)
  (from...cpath.size).map { |i|
    @constant_mapping.short_name_for_path(cpath[0..i]) || cpath[i].to_s
  }.join('::')
end