Class: Ast::Merge::EmitterBase

Inherits:
Object
  • Object
show all
Defined in:
lib/ast/merge/emitter_base.rb

Overview

Base class for emitters that convert AST structures back to text. Provides common functionality for tracking indentation, managing output lines, and handling comments.

Subclasses implement format-specific emission methods (e.g., emit_pair for JSON, emit_variable_assignment for Bash, etc.)

Ownership boundary:

  • shared structural recomposition and attachment preservation belong here
  • syntax-aware normalization and serializer polish belong in the relevant family layer or concrete emitter subclass unless they prove reusable across unrelated formats

Examples:

Implementing a custom emitter

class MyEmitter < Ast::Merge::EmitterBase
  def emit_my_construct(data)
    add_comma_if_needed if @needs_separator
    @lines << "#{current_indent}my_syntax: #{data}"
    @needs_separator = true
  end
end

Defined Under Namespace

Classes: UnsupportedCommentNodeError

Instance Attribute Summary collapse

Instance Method Summary collapse

Constructor Details

#initialize(indent_size: 2, **options) ⇒ EmitterBase

Initialize a new emitter

Parameters:

  • indent_size (Integer) (defaults to: 2)

    Number of spaces per indent level

  • options (Hash)

    Additional options for subclasses



42
43
44
45
46
47
# File 'lib/ast/merge/emitter_base.rb', line 42

def initialize(indent_size: 2, **options)
  @lines = []
  @indent_level = 0
  @indent_size = indent_size
  initialize_subclass_state(**options)
end

Instance Attribute Details

#indent_levelInteger (readonly)

Returns Current indentation level.

Returns:

  • (Integer)

    Current indentation level



33
34
35
# File 'lib/ast/merge/emitter_base.rb', line 33

def indent_level
  @indent_level
end

#indent_sizeInteger (readonly)

Returns Spaces per indent level.

Returns:

  • (Integer)

    Spaces per indent level



36
37
38
# File 'lib/ast/merge/emitter_base.rb', line 36

def indent_size
  @indent_size
end

#linesArray<String> (readonly)

Returns Output lines.

Returns:

  • (Array<String>)

    Output lines



30
31
32
# File 'lib/ast/merge/emitter_base.rb', line 30

def lines
  @lines
end

Instance Method Details

#blank_lines?(candidate_lines) ⇒ Boolean

Check whether every provided line is blank.

Parameters:

  • candidate_lines (Array<String>)

    Lines to classify

Returns:

  • (Boolean)


241
242
243
# File 'lib/ast/merge/emitter_base.rb', line 241

def blank_lines?(candidate_lines)
  Array(candidate_lines).all? { |line| line.to_s.strip.empty? }
end

#clearObject

Clear the emitter state



253
254
255
256
257
# File 'lib/ast/merge/emitter_base.rb', line 253

def clear
  @lines = []
  @indent_level = 0
  clear_subclass_state
end

#clear_subclass_stateObject

Hook for subclasses to clear their own state



260
261
262
# File 'lib/ast/merge/emitter_base.rb', line 260

def clear_subclass_state
  # Override in subclasses if needed
end

#dedentObject

Decrease indentation level



270
271
272
# File 'lib/ast/merge/emitter_base.rb', line 270

def dedent
  @indent_level -= 1 if @indent_level.positive?
end

#emit_blank_lineObject

Emit a blank line



56
57
58
# File 'lib/ast/merge/emitter_base.rb', line 56

def emit_blank_line
  @lines << ''
end

#emit_comment(text, inline: false) ⇒ Object

Emit a comment using the emitter's native syntax. Subclasses should override this to support full-line and inline emission.

Parameters:

  • text (String)

    Comment text without the delimiter

  • inline (Boolean) (defaults to: false)

    Whether this is an inline comment

Raises:

  • (NotImplementedError)


82
83
84
# File 'lib/ast/merge/emitter_base.rb', line 82

def emit_comment(text, inline: false)
  raise NotImplementedError, 'Subclasses must implement emit_comment'
end

#emit_comment_attachment(attachment, leading: true, inline: false, trailing: false, orphan: false, source_lines: nil) ⇒ Object

Emit selected regions from a shared comment attachment.

Parameters:

  • attachment (Comment::Attachment, nil)

    Attachment to emit

  • leading (Boolean) (defaults to: true)

    Whether to emit the leading region

  • inline (Boolean) (defaults to: false)

    Whether to emit the inline region

  • trailing (Boolean) (defaults to: false)

    Whether to emit the trailing region

  • orphan (Boolean) (defaults to: false)

    Whether to emit orphan regions in order

  • source_lines (Array<String>, nil) (defaults to: nil)

    Original source lines for gap preservation



120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
# File 'lib/ast/merge/emitter_base.rb', line 120

def emit_comment_attachment(attachment, leading: true, inline: false, trailing: false, orphan: false,
                            source_lines: nil)
  return unless attachment
  return unless attachment.respond_to?(:leading_region) && attachment.respond_to?(:inline_region)

  regions = []
  regions << attachment.leading_region if leading && attachment.leading_region
  regions << attachment.inline_region if inline && attachment.inline_region
  if trailing && attachment.respond_to?(:trailing_region) && attachment.trailing_region
    regions << attachment.trailing_region
  end
  regions.concat(Array(attachment.orphan_regions)) if orphan && attachment.respond_to?(:orphan_regions)

  previous_region_end_line = nil
  regions.each do |region|
    current_region_start_line = region.respond_to?(:start_line) ? region.start_line : nil
    emit_region_gap_lines(previous_region_end_line, current_region_start_line, source_lines)
    emit_comment_region(region, inline: region.respond_to?(:inline?) ? region.inline? : nil,
                                source_lines: source_lines)
    previous_region_end_line = region.respond_to?(:end_line) ? region.end_line : previous_region_end_line
  end
end

#emit_comment_region(region, inline: nil, source_lines: nil) ⇒ Object

Emit a shared normalized comment region.

Preserves explicit blank-line nodes and can also recreate blank gaps between comment lines by consulting original source lines when those gaps are not already represented as Comment::Empty nodes.

Parameters:

  • region (Comment::Region, nil)

    Region to emit

  • inline (Boolean, nil) (defaults to: nil)

    Force inline emission mode

  • source_lines (Array<String>, nil) (defaults to: nil)

    Original source lines for gap preservation



95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
# File 'lib/ast/merge/emitter_base.rb', line 95

def emit_comment_region(region, inline: nil, source_lines: nil)
  return unless region
  return unless region.respond_to?(:nodes)
  return if region.respond_to?(:empty?) && region.empty?

  inline = region.inline? if inline.nil? && region.respond_to?(:inline?)
  return emit_inline_comment_region(region) if inline

  previous_line = nil
  comment_region_nodes(region).each do |node|
    current_line = comment_region_line_number(node)
    emit_region_gap_lines(previous_line, current_line, source_lines)
    emit_comment_node(node)
    previous_line = current_line
  end
end

#emit_layout_attachment(attachment, leading: true, trailing: false, source_lines: nil, retained_owners: nil, removed_owners: nil, leading_last_emitted_source_line: nil, trailing_last_emitted_source_line: nil) ⇒ Hash{Symbol=>Integer}

Emit selected leading/trailing layout gaps from an attachment.

Works with both Layout::Attachment and Comment::Attachment because both expose owner/leading_gap/trailing_gap.

Parameters:

  • attachment (Layout::Attachment, Comment::Attachment, nil)
  • leading (Boolean) (defaults to: true)

    Whether to emit the leading gap

  • trailing (Boolean) (defaults to: false)

    Whether to emit the trailing gap

  • source_lines (Array<String>, nil) (defaults to: nil)

    Original source lines for exact whitespace preservation

  • retained_owners (Array<Object>, nil) (defaults to: nil)

    Explicit retained owners for controller fallback

  • removed_owners (Array<Object>, nil) (defaults to: nil)

    Explicit removed owners for controller fallback

  • leading_last_emitted_source_line (Integer, nil) (defaults to: nil)

    Skip leading gap lines up to and including this source line

  • trailing_last_emitted_source_line (Integer, nil) (defaults to: nil)

    Skip trailing gap lines up to and including this source line

Returns:

  • (Hash{Symbol=>Integer})

    Last emitted source line by selected gap side



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
# File 'lib/ast/merge/emitter_base.rb', line 186

def emit_layout_attachment(attachment, leading: true, trailing: false, source_lines: nil, retained_owners: nil,
                           removed_owners: nil, leading_last_emitted_source_line: nil, trailing_last_emitted_source_line: nil)
  return {} unless attachment
  unless attachment.respond_to?(:owner) && attachment.respond_to?(:leading_gap) && attachment.respond_to?(:trailing_gap)
    return {}
  end

  emitted_lines = {}

  if leading && attachment.leading_gap
    emitted_lines[:leading] = emit_layout_gap(
      attachment.leading_gap,
      owner: attachment.owner,
      source_lines: source_lines,
      retained_owners: retained_owners,
      removed_owners: removed_owners,
      last_emitted_source_line: leading_last_emitted_source_line
    )
  end

  if trailing && attachment.trailing_gap
    emitted_lines[:trailing] = emit_layout_gap(
      attachment.trailing_gap,
      owner: attachment.owner,
      source_lines: source_lines,
      retained_owners: retained_owners,
      removed_owners: removed_owners,
      last_emitted_source_line: trailing_last_emitted_source_line
    )
  end

  emitted_lines.compact
end

#emit_layout_gap(gap, owner: nil, source_lines: nil, retained_owners: nil, removed_owners: nil, last_emitted_source_line: nil) ⇒ Integer?

Emit a shared layout gap when the requesting owner controls output.

Parameters:

  • gap (Layout::Gap, nil)

    Gap to emit

  • owner (Object, nil) (defaults to: nil)

    Owner requesting emission; defaults to the gap's effective controller

  • source_lines (Array<String>, nil) (defaults to: nil)

    Original source lines for exact whitespace preservation

  • retained_owners (Array<Object>, nil) (defaults to: nil)

    Explicit retained owners for controller fallback

  • removed_owners (Array<Object>, nil) (defaults to: nil)

    Explicit removed owners for controller fallback

  • last_emitted_source_line (Integer, nil) (defaults to: nil)

    Skip gap lines up to and including this source line

Returns:

  • (Integer, nil)

    Last emitted source line number



152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
# File 'lib/ast/merge/emitter_base.rb', line 152

def emit_layout_gap(gap, owner: nil, source_lines: nil, retained_owners: nil, removed_owners: nil,
                    last_emitted_source_line: nil)
  return unless gap

  emitting_owner = owner || gap.effective_controller(retained_owners: retained_owners,
                                                     removed_owners: removed_owners) || gap.controller
  return unless emitting_owner
  return unless gap.controls_output_for?(emitting_owner, retained_owners: retained_owners,
                                                         removed_owners: removed_owners)

  start_line = if last_emitted_source_line
                 [gap.start_line, last_emitted_source_line + 1].max
               else
                 gap.start_line
               end
  return if start_line > gap.end_line

  emit_layout_gap_lines(gap, source_lines: source_lines, line_numbers: start_line..gap.end_line)
end

#emit_leading_comments(comments) ⇒ Object

Emit leading comments from CommentTracker

Parameters:

  • comments (Array<Hash>)

    Comment hashes with :text, :indent, etc.



63
64
65
66
67
# File 'lib/ast/merge/emitter_base.rb', line 63

def emit_leading_comments(comments)
  comments.each do |comment|
    emit_tracked_comment(comment)
  end
end

#emit_raw_lines(raw_lines) ⇒ Object

Emit raw lines as-is (for preserving exact formatting)

Parameters:

  • raw_lines (Array<String>)

    Lines to emit without modification



223
224
225
# File 'lib/ast/merge/emitter_base.rb', line 223

def emit_raw_lines(raw_lines)
  raw_lines.each { |line| @lines << line.chomp }
end

#emit_tracked_comment(comment) ⇒ Object

Emit a comment from CommentTracker hash Subclasses should override this to handle format-specific comment syntax

Parameters:

  • comment (Hash)

    Comment hash with :text, :indent, :block, etc.

Raises:

  • (NotImplementedError)


73
74
75
# File 'lib/ast/merge/emitter_base.rb', line 73

def emit_tracked_comment(comment)
  raise NotImplementedError, 'Subclasses must implement emit_tracked_comment'
end

#ends_with_blank_line?Boolean

Check whether the current emitter output ends with a blank line.

Returns:

  • (Boolean)


248
249
250
# File 'lib/ast/merge/emitter_base.rb', line 248

def ends_with_blank_line?
  @lines.any? && blank_lines?([@lines.last])
end

#indentObject

Increase indentation level



265
266
267
# File 'lib/ast/merge/emitter_base.rb', line 265

def indent
  @indent_level += 1
end

#initialize_subclass_state(**options) ⇒ Object

Hook for subclasses to initialize their own state

Parameters:

  • options (Hash)

    Additional options



51
52
53
# File 'lib/ast/merge/emitter_base.rb', line 51

def initialize_subclass_state(**options)
  # Override in subclasses if needed
end

#to_sString

Get the output as a single string Subclasses may override to customize output format (e.g., to_json, to_yaml)

Returns:

  • (String)


231
232
233
234
235
# File 'lib/ast/merge/emitter_base.rb', line 231

def to_s
  content = @lines.join("\n")
  content += "\n" unless content.empty? || content.end_with?("\n")
  content
end