Class: Ast::Merge::SmartMergerBase Abstract

Inherits:
Object
  • Object
show all
Includes:
Detector::Mergeable
Defined in:
lib/ast/merge/smart_merger_base.rb

Overview

This class is abstract.

Subclass and implement #analysis_class and #perform_merge

Abstract base class for SmartMerger implementations across all *-merge gems.

SmartMergerBase provides the standard interface and common functionality
for intelligent file merging. Subclasses implement format-specific parsing,
analysis, and merge logic while inheriting the common API.

Standard Options

All SmartMerger implementations support these common options:

  • preference - :destination (default) or :template, or Hash for per-type
  • add_template_only_nodes - false (default) or true
  • signature_generator - Custom signature proc or nil
  • freeze_token - Token for freeze block markers
  • match_refiner - Fuzzy match refiner or nil
  • regions - Region configurations for nested merging
  • region_placeholder - Custom placeholder for regions
  • node_typing - Hash mapping node types to callables for per-type preferences

Implementing a SmartMerger

Subclasses must implement:

  • analysis_class - Returns the FileAnalysis class for this format
  • perform_merge - Performs the format-specific merge logic

Subclasses may override:

  • default_freeze_token - Format-specific default freeze token
  • resolver_class - Returns the ConflictResolver class (if different)
  • result_class - Returns the MergeResult class (if different)
  • aligner_class - Returns the FileAligner class (if used)
  • parse_content - Custom parsing logic
  • build_analysis_options - Additional analysis options
  • build_resolver_options - Additional resolver options

FileAnalysis Error Handling Pattern

All FileAnalysis classes must follow this consistent error handling pattern:

  1. Catch backend errors internally - Handle TreeHaver::NotAvailable and
    similar backend errors inside the FileAnalysis class, storing them in @errors
    and setting @ast = nil. Do NOT re-raise these errors.

  2. Collect parse errors without raising - When the parser detects syntax errors
    (e.g., has_error? returns true), collect them in @errors but do NOT raise.

  3. Implement valid? - Return false when there are errors or no AST:
    def valid?
      @errors.empty? && !@ast.nil?
    end
    
  4. SmartMergerBase handles the rest - After FileAnalysis creation,
    parse_and_analyze checks valid? and raises the appropriate parse error
    (TemplateParseError or DestinationParseError) if the analysis is invalid.

This pattern ensures:

  • Consistent error handling across all *-merge gems
  • TreeHaver::NotAvailable (which inherits from Exception) is handled safely
  • Parse errors are properly wrapped in format-specific error classes
  • No need to rescue Exception in SmartMergerBase

Examples:

FileAnalysis error handling

def parse_content
  parser = TreeHaver.parser_for(:myformat, library_path: @parser_path)
  @ast = parser.parse(@source)

  if @ast&.root_node&.has_error?
    collect_parse_errors(@ast.root_node)
    # Do NOT raise here - SmartMergerBase will check valid?
  end
rescue TreeHaver::NotAvailable => e
  @errors << e.message
  @ast = nil
  # Do NOT re-raise - SmartMergerBase will check valid?
rescue StandardError => e
  @errors << e
  @ast = nil
  # Do NOT re-raise - SmartMergerBase will check valid?
end

Implementing a custom SmartMerger

class MyFormat::SmartMerger < Ast::Merge::SmartMergerBase
  def analysis_class
    MyFormat::FileAnalysis
  end

  def default_freeze_token
    "myformat-merge"
  end

  private

  def perform_merge
    alignment = @aligner.align
    process_alignment(alignment)
    @result
  end
end

Direct Known Subclasses

Text::SmartMerger

Constant Summary

Constants included from Detector::Mergeable

Detector::Mergeable::DEFAULT_PLACEHOLDER_PREFIX, Detector::Mergeable::DEFAULT_PLACEHOLDER_SUFFIX

Instance Attribute Summary collapse

Instance Method Summary collapse

Methods included from Detector::Mergeable

#extract_dest_regions, #extract_template_regions, #regions_configured?, #setup_regions, #substitute_merged_regions

Constructor Details

#initialize(template_content, dest_content, signature_generator: nil, preference: :destination, add_template_only_nodes: false, freeze_token: nil, match_refiner: nil, regions: nil, region_placeholder: nil, node_typing: nil, **format_options) ⇒ SmartMergerBase

Creates a new SmartMerger for intelligent file merging.

Parameters:

  • template_content (String)

    Template source content

  • dest_content (String)

    Destination source content

  • signature_generator (Proc, nil) (defaults to: nil)

    Optional proc to generate custom signatures.
    The proc receives a node and should return one of:

    • An array representing the node’s signature
    • nil to indicate the node should have no signature
    • The original node to fall through to default signature computation
  • preference (Symbol, Hash) (defaults to: :destination)

    Controls which version to use
    when nodes have matching signatures but different content:

    • :destination (default) - Use destination version (preserves customizations)
    • :template - Use template version (applies updates)
    • Hash for per-type preferences: { default: :destination, special: :template }
  • add_template_only_nodes (Boolean) (defaults to: false)

    Controls whether to add nodes that only
    exist in template:

    • false (default) - Skip template-only nodes
    • true - Add template-only nodes to result
  • freeze_token (String, nil) (defaults to: nil)

    Token to use for freeze block markers.
    Default varies by format (e.g., “prism-merge”, “markly-merge”)

  • match_refiner (#call, nil) (defaults to: nil)

    Optional match refiner for fuzzy matching.
    Default: nil (fuzzy matching disabled)

  • regions (Array<Hash>, nil) (defaults to: nil)

    Region configurations for nested merging.
    Each hash should contain:

    • :detector - RegionDetectorBase instance
    • :merger_class - SmartMerger class for the region (optional)
    • :merger_options - Options for the region merger (optional)
    • :regions - Nested region configs (optional, for recursive regions)
  • region_placeholder (String, nil) (defaults to: nil)

    Custom placeholder prefix for regions.
    Default: “«<AST_MERGE_REGION_”

  • format_options (Hash)

    Format-specific parser options passed to FileAnalysis.
    These are merged with freeze_token and signature_generator in build_full_analysis_options.
    Examples:

    • Markly: flags: Markly::FOOTNOTES, extensions: [:table, :strikethrough]
    • Commonmarker: options: { parse: { smart: true } }
    • Prism: (no additional parser options needed)
  • node_typing (Hash{Symbol,String => #call}, nil) (defaults to: nil)

    Node typing configuration
    for per-node-type merge preferences. Maps node type names to callables that
    can wrap nodes with custom merge_types for use with Hash-based preference.
    @example
    node_typing = {
    CallNode: ->(node) {
    NodeTyping.with_merge_type(node, :special) if special_node?(node)
    }
    }

Raises:



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
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
# File 'lib/ast/merge/smart_merger_base.rb', line 206

def initialize(
  template_content,
  dest_content,
  signature_generator: nil,
  preference: :destination,
  add_template_only_nodes: false,
  freeze_token: nil,
  match_refiner: nil,
  regions: nil,
  region_placeholder: nil,
  node_typing: nil,
  **format_options
)
  @template_content = template_content
  @dest_content = dest_content
  @signature_generator = signature_generator
  @preference = preference
  @add_template_only_nodes = add_template_only_nodes
  @freeze_token = freeze_token || default_freeze_token
  @match_refiner = match_refiner
  @node_typing = node_typing
  @format_options = format_options

  # Validate node_typing if provided
  NodeTyping.validate!(node_typing) if node_typing

  # Set up region support
  setup_regions(regions: regions || [], region_placeholder: region_placeholder)

  # Extract regions before parsing (if configured)
  template_for_parsing = extract_template_regions(@template_content)
  dest_for_parsing = extract_dest_regions(@dest_content)

  # Parse and analyze both files
  @template_analysis = parse_and_analyze(template_for_parsing, :template)
  @dest_analysis = parse_and_analyze(dest_for_parsing, :destination)

  # Set up aligner (if applicable)
  @aligner = build_aligner if respond_to?(:aligner_class, true) && aligner_class

  # Set up resolver
  @resolver = build_resolver

  # Set up result
  @result = build_result
end

Instance Attribute Details

#add_template_only_nodesBoolean (readonly)

Returns Whether to add template-only nodes.

Returns:

  • (Boolean)

    Whether to add template-only nodes



135
136
137
# File 'lib/ast/merge/smart_merger_base.rb', line 135

def add_template_only_nodes
  @add_template_only_nodes
end

#alignerObject? (readonly)

Returns Aligner for finding matches (if applicable).

Returns:

  • (Object, nil)

    Aligner for finding matches (if applicable)



123
124
125
# File 'lib/ast/merge/smart_merger_base.rb', line 123

def aligner
  @aligner
end

#dest_analysisObject (readonly)

Returns Analysis of the destination file.

Returns:

  • (Object)

    Analysis of the destination file



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

def dest_analysis
  @dest_analysis
end

#dest_contentString (readonly)

Returns Destination source content.

Returns:

  • (String)

    Destination source content



114
115
116
# File 'lib/ast/merge/smart_merger_base.rb', line 114

def dest_content
  @dest_content
end

#freeze_tokenString (readonly)

Returns Token for freeze block markers.

Returns:

  • (String)

    Token for freeze block markers



138
139
140
# File 'lib/ast/merge/smart_merger_base.rb', line 138

def freeze_token
  @freeze_token
end

#match_refinerObject? (readonly)

Returns Match refiner for fuzzy matching.

Returns:

  • (Object, nil)

    Match refiner for fuzzy matching



144
145
146
# File 'lib/ast/merge/smart_merger_base.rb', line 144

def match_refiner
  @match_refiner
end

#node_typingHash{Symbol,String => #call}? (readonly)

Returns Node typing configuration.

Returns:

  • (Hash{Symbol,String => #call}, nil)

    Node typing configuration



147
148
149
# File 'lib/ast/merge/smart_merger_base.rb', line 147

def node_typing
  @node_typing
end

#preferenceSymbol, Hash (readonly)

Returns Preference for signature matches.

Returns:

  • (Symbol, Hash)

    Preference for signature matches



132
133
134
# File 'lib/ast/merge/smart_merger_base.rb', line 132

def preference
  @preference
end

#resolverObject (readonly)

Returns Resolver for handling conflicts.

Returns:

  • (Object)

    Resolver for handling conflicts



126
127
128
# File 'lib/ast/merge/smart_merger_base.rb', line 126

def resolver
  @resolver
end

#resultObject (readonly)

Returns Result object tracking merged content.

Returns:

  • (Object)

    Result object tracking merged content



129
130
131
# File 'lib/ast/merge/smart_merger_base.rb', line 129

def result
  @result
end

#signature_generatorProc? (readonly)

Returns Custom signature generator.

Returns:

  • (Proc, nil)

    Custom signature generator



141
142
143
# File 'lib/ast/merge/smart_merger_base.rb', line 141

def signature_generator
  @signature_generator
end

#template_analysisObject (readonly)

Returns Analysis of the template file.

Returns:

  • (Object)

    Analysis of the template file



117
118
119
# File 'lib/ast/merge/smart_merger_base.rb', line 117

def template_analysis
  @template_analysis
end

#template_contentString (readonly)

Returns Template source content.

Returns:

  • (String)

    Template source content



111
112
113
# File 'lib/ast/merge/smart_merger_base.rb', line 111

def template_content
  @template_content
end

Instance Method Details

#mergeString

Perform the merge operation and return the merged content as a string.

Returns:

  • (String)

    The merged content



256
257
258
# File 'lib/ast/merge/smart_merger_base.rb', line 256

def merge
  merge_result.to_s
end

#merge_resultObject

Perform the merge operation and return the full result object.

This method is memoized - subsequent calls return the cached result.

Returns:

  • (Object)

    The merge result (format-specific MergeResult subclass)



265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
# File 'lib/ast/merge/smart_merger_base.rb', line 265

def merge_result
  return @merge_result if @merge_result

  @merge_result = DebugLogger.time("#{self.class.name}#merge") do
    result = perform_merge

    # Substitute merged regions back into the result if configured
    if regions_configured? && (merged_content = result.to_s)
      final_content = substitute_merged_regions(merged_content)
      update_result_content(result, final_content)
    end

    result
  end
end

#merge_with_debugHash

Perform the merge and return detailed debug information.

Returns:

  • (Hash)

    Hash containing:

    • :content [String] - Final merged content
    • :statistics [Hash] - Merge decision counts


286
287
288
289
290
291
292
293
# File 'lib/ast/merge/smart_merger_base.rb', line 286

def merge_with_debug
  content = merge

  {
    content: content,
    statistics: @result.decision_summary,
  }
end

#statsHash

Get merge statistics.

Returns:

  • (Hash)

    Statistics about the merge



298
299
300
301
# File 'lib/ast/merge/smart_merger_base.rb', line 298

def stats
  merge_result # Ensure merge has run
  @result.decision_summary
end