Class: Ast::Merge::MergeResultBase

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

Overview

Base class for tracking merge results in AST merge libraries. Provides shared decision constants and base functionality for file-type-specific implementations.

Examples:

Basic usage in a subclass

class MyMergeResult < Ast::Merge::MergeResultBase
  def add_node(node, decision:, source:)
    # File-type-specific node handling
  end
end

Direct Known Subclasses

Text::MergeResult

Constant Summary collapse

DECISION_KEPT_TEMPLATE =

Line was kept from template (no conflict or template preferred). Used when template content is included without modification.

:kept_template
DECISION_KEPT_DEST =

Line was kept from destination (no conflict or destination preferred). Used when destination content is included without modification.

:kept_destination
DECISION_MERGED =

Line was merged from both sources. Used when content was combined from template and destination.

:merged
DECISION_ADDED =

Line was added from template (template-only content). Used for content that exists only in template and is added to result.

:added
DECISION_FREEZE_BLOCK =

Line from destination freeze block (always preserved). Used for content within freeze markers that must be kept from destination regardless of template content.

:freeze_block
DECISION_REPLACED =

Line replaced matching content (signature match with preference applied). Used when template and destination have nodes with same signature but different content, and one version replaced the other based on preference.

:replaced
DECISION_APPENDED =

Line was appended from destination (destination-only content). Used for content that exists only in destination and is added to result.

:appended
DECISION_UNRESOLVED =

Line was emitted with a provisional winner while review remains required.

:unresolved

Instance Attribute Summary collapse

Instance Method Summary collapse

Constructor Details

#initialize(template_analysis: nil, dest_analysis: nil, conflicts: [], frozen_blocks: [], stats: {}, unresolved_cases: [], **_options) ⇒ MergeResultBase

Initialize a new merge result.

This unified constructor accepts all parameters that any *-merge gem might need. Subclasses should call super with the parameters they use.

Parameters:

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

    Analysis of the template file

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

    Analysis of the destination file

  • conflicts (Array<Hash>) (defaults to: [])

    Conflicts detected during merge

  • frozen_blocks (Array) (defaults to: [])

    Frozen blocks preserved during merge

  • stats (Hash) (defaults to: {})

    Statistics about the merge

  • options (Hash)

    Additional options for forward compatibility



88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
# File 'lib/ast/merge/merge_result_base.rb', line 88

def initialize(
  template_analysis: nil,
  dest_analysis: nil,
  conflicts: [],
  frozen_blocks: [],
  stats: {},
  unresolved_cases: [],
  **_options
)
  @template_analysis = template_analysis
  @dest_analysis = dest_analysis
  @lines = []
  @decisions = []
  @conflicts = conflicts
  @frozen_blocks = frozen_blocks
  @stats = stats
  @unresolved_cases = unresolved_cases
  # **options captured for forward compatibility - subclasses may use additional options
end

Instance Attribute Details

#conflictsArray<Hash> (readonly)

Returns Conflicts detected during merge.

Returns:

  • (Array<Hash>)

    Conflicts detected during merge



66
67
68
# File 'lib/ast/merge/merge_result_base.rb', line 66

def conflicts
  @conflicts
end

#decisionsArray<Hash> (readonly)

Returns Decisions made during merge.

Returns:

  • (Array<Hash>)

    Decisions made during merge



57
58
59
# File 'lib/ast/merge/merge_result_base.rb', line 57

def decisions
  @decisions
end

#dest_analysisObject? (readonly)

Returns Analysis of the destination file.

Returns:

  • (Object, nil)

    Analysis of the destination file



63
64
65
# File 'lib/ast/merge/merge_result_base.rb', line 63

def dest_analysis
  @dest_analysis
end

#frozen_blocksArray (readonly)

Returns Frozen blocks preserved during merge.

Returns:

  • (Array)

    Frozen blocks preserved during merge



69
70
71
# File 'lib/ast/merge/merge_result_base.rb', line 69

def frozen_blocks
  @frozen_blocks
end

#linesArray<String> (readonly)

Returns Lines in the result (canonical storage for line-by-line merging).

Returns:

  • (Array<String>)

    Lines in the result (canonical storage for line-by-line merging)



54
55
56
# File 'lib/ast/merge/merge_result_base.rb', line 54

def lines
  @lines
end

#statsHash (readonly)

Returns Statistics about the merge.

Returns:

  • (Hash)

    Statistics about the merge



72
73
74
# File 'lib/ast/merge/merge_result_base.rb', line 72

def stats
  @stats
end

#template_analysisObject? (readonly)

Returns Analysis of the template file.

Returns:

  • (Object, nil)

    Analysis of the template file



60
61
62
# File 'lib/ast/merge/merge_result_base.rb', line 60

def template_analysis
  @template_analysis
end

#unresolved_casesArray<Ast::Merge::Runtime::ResolutionCase, Hash> (readonly)

Returns Reviewable unresolved cases.

Returns:



75
76
77
# File 'lib/ast/merge/merge_result_base.rb', line 75

def unresolved_cases
  @unresolved_cases
end

Instance Method Details

#add_unresolved_case(resolution_case) ⇒ Object



237
238
239
240
# File 'lib/ast/merge/merge_result_base.rb', line 237

def add_unresolved_case(resolution_case)
  @unresolved_cases << resolution_case
  resolution_case
end

#apply_unresolved_resolutions!(resolutions) ⇒ Object



281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
# File 'lib/ast/merge/merge_result_base.rb', line 281

def apply_unresolved_resolutions!(resolutions)
  normalized = normalize_unresolved_resolutions(resolutions)
  remaining_cases = []

  @unresolved_cases.each do |resolution_case|
    selection = normalized[resolution_case.case_id]
    if selection.nil?
      remaining_cases << resolution_case
      next
    end

    apply_unresolved_resolution!(resolution_case, selection)
    @conflicts.reject! { |conflict| conflict[:case_id].to_s == resolution_case.case_id }
  end

  @unresolved_cases = remaining_cases
  self
end

#apply_unresolved_review_state!(review_state) ⇒ Object



309
310
311
312
313
# File 'lib/ast/merge/merge_result_base.rb', line 309

def apply_unresolved_review_state!(review_state)
  state = Ast::Merge::UnresolvedReviewState.coerce(review_state)
  validate_review_state_compatibility!(state)
  apply_unresolved_resolutions!(state.selections)
end

#blank_lines?(candidate_lines) ⇒ Boolean

Check whether every provided line is blank.

Parameters:

  • candidate_lines (Array<String>)

    Lines to classify

Returns:

  • (Boolean)


154
155
156
# File 'lib/ast/merge/merge_result_base.rb', line 154

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

#contentArray<String>

Get content - returns @lines array for most gems. Subclasses may override for different content models (e.g., string).

Returns:

  • (Array<String>)

    The merged content as array of lines



112
113
114
# File 'lib/ast/merge/merge_result_base.rb', line 112

def content
  @lines
end

#content=(value) ⇒ Object

Set content from a string (splits on newlines). Used when region substitution replaces the merged content.

Parameters:

  • value (String)

    The new content



120
121
122
# File 'lib/ast/merge/merge_result_base.rb', line 120

def content=(value)
  @lines = value.to_s.split("\n", -1)
end

#content?Boolean

Check if content has been built (has any lines).

Returns:

  • (Boolean)


140
141
142
# File 'lib/ast/merge/merge_result_base.rb', line 140

def content?
  !@lines.empty?
end

#decision_summaryHash<Symbol, Integer>

Get summary of decisions made

Returns:

  • (Hash<Symbol, Integer>)


317
318
319
320
321
# File 'lib/ast/merge/merge_result_base.rb', line 317

def decision_summary
  summary = Hash.new(0)
  @decisions.each { |d| summary[d[:decision]] += 1 }
  summary
end

#empty?Boolean

Check if the result is empty

Returns:

  • (Boolean)


146
147
148
# File 'lib/ast/merge/merge_result_base.rb', line 146

def empty?
  @lines.empty?
end

#ends_with_blank_line?Boolean

Check whether the current result ends with a blank line.

Returns:

  • (Boolean)


161
162
163
# File 'lib/ast/merge/merge_result_base.rb', line 161

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

#inspectString

String representation

Returns:

  • (String)


325
326
327
# File 'lib/ast/merge/merge_result_base.rb', line 325

def inspect
  "#<#{self.class.name} lines=#{line_count} decisions=#{@decisions.length}>"
end

#line_countInteger

Get the number of lines

Returns:

  • (Integer)


167
168
169
# File 'lib/ast/merge/merge_result_base.rb', line 167

def line_count
  @lines.length
end

#normalize_blank_line_runs!(max: 1) ⇒ Integer

Normalize repeated blank-line runs in the current result.

This is a safety repair for line-oriented semantic merges after ownership-aware layout handling has run. It prevents removed/skipped owners from leaving impossible interstitial blank runs in generated output.

Parameters:

  • max (Integer) (defaults to: 1)

    Maximum number of consecutive blank lines to retain

Returns:

  • (Integer)

    Number of blank lines removed

Raises:

  • (ArgumentError)


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

def normalize_blank_line_runs!(max: 1)
  max = Integer(max)
  raise ArgumentError, 'max must be >= 0' if max.negative?

  normalized_lines = []
   = [] if instance_variable_defined?(:@line_metadata) && @line_metadata.respond_to?(:each)
  blank_count = 0
  removed = 0

  @lines.each_with_index do |line, index|
    if line.to_s.strip.empty?
      blank_count += 1
      if blank_count > max
        removed += 1
        next
      end
    else
      blank_count = 0
    end

    normalized_lines << line
     << @line_metadata[index] if 
  end

  @lines = normalized_lines
  @line_metadata =  if 
  removed
end

#record_unresolved_choice(template_text:, destination_text:, provisional_winner:, case_id:, surface_path: nil, reason: :conflict, metadata: {}, conflict_fields: {}) ⇒ Object



242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
# File 'lib/ast/merge/merge_result_base.rb', line 242

def record_unresolved_choice(
  template_text:,
  destination_text:,
  provisional_winner:,
  case_id:,
  surface_path: nil,
  reason: :conflict,
  metadata: {},
  conflict_fields: {}
)
  return if template_text == destination_text

  conflict = {
    case_id: case_id,
    reason: reason,
    template: template_text,
    destination: destination_text,
    provisional_winner: provisional_winner
  }.merge(compact_hash(conflict_fields))
  @conflicts << conflict

  resolution_case = Ast::Merge::Runtime::ResolutionCase.new(
    case_id: case_id,
    reason: reason,
    candidates: {
      template: template_text,
      destination: destination_text
    },
    provisional_winner: provisional_winner,
    surface_path: surface_path,
    metadata: compact_hash()
  )
  add_unresolved_case(resolution_case)
end

#remove_trailing_blank_lines(max: nil) ⇒ Integer

Remove blank lines from the end of the current result.

Layout-aware merge adapters use this when they later discover that a removed owner controlled a leading blank-line gap already emitted as part of the preceding output.

Parameters:

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

    Maximum number of blank lines to remove

Returns:

  • (Integer)

    Number of lines removed



179
180
181
182
183
184
185
186
187
188
189
190
191
# File 'lib/ast/merge/merge_result_base.rb', line 179

def remove_trailing_blank_lines(max: nil)
  removed = 0

  while @lines.any? && @lines.last.to_s.strip.empty?
    break if max && removed >= max

    @lines.pop
    @line_metadata.pop if instance_variable_defined?(:@line_metadata) && @line_metadata.respond_to?(:pop)
    removed += 1
  end

  removed
end

#to_sString

Get content as a string. This is the canonical method for converting the merge result to a string. Ensures a trailing newline for non-empty content, matching standard file conventions and the pattern used by EmitterBase#to_s, Psych::Merge::MergeResult#to_yaml, and Bash::Merge::MergeResult#to_bash.

Returns:

  • (String)

    Content as string joined with newlines



131
132
133
134
135
# File 'lib/ast/merge/merge_result_base.rb', line 131

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

#to_unresolved_review_state(selections: {}, metadata: {}) ⇒ Object



300
301
302
303
304
305
306
307
# File 'lib/ast/merge/merge_result_base.rb', line 300

def to_unresolved_review_state(selections: {}, metadata: {})
  normalized_selections = normalize_unresolved_resolutions(selections)
  Ast::Merge::UnresolvedReviewState.new(
    cases: unresolved_cases,
    selections: normalized_selections,
    metadata: (, normalized_selections)
  )
end

#unresolved?Boolean Also known as: review_required?

Returns:

  • (Boolean)


231
232
233
# File 'lib/ast/merge/merge_result_base.rb', line 231

def unresolved?
  @unresolved_cases.any?
end

#unresolved_case(case_id) ⇒ Object



277
278
279
# File 'lib/ast/merge/merge_result_base.rb', line 277

def unresolved_case(case_id)
  @unresolved_cases.find { |resolution_case| resolution_case.case_id == case_id.to_s }
end