Class: Ast::Merge::SmartMergerBase Abstract

Inherits:
Object
  • Object
show all
Includes:
Detector::Mergeable, UnresolvedSupport
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, backend_type: :tree_sitter)
  @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, resolution_mode: :eager, unresolved_policy: nil, template_path: nil, dest_path: 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:



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
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
# File 'lib/ast/merge/smart_merger_base.rb', line 214

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,
  resolution_mode: :eager,
  unresolved_policy: nil,
  template_path: nil,
  dest_path: 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
  @resolution_mode = resolution_mode
  @unresolved_policy = Ast::Merge::UnresolvedPolicy.coerce(unresolved_policy)
  @template_path = template_path
  @dest_path = dest_path
  @format_options = format_options

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

  # 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



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

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)



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

def aligner
  @aligner
end

#dest_analysisObject (readonly)

Returns Analysis of the destination file.

Returns:

  • (Object)

    Analysis of the destination file



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

def dest_analysis
  @dest_analysis
end

#dest_contentString (readonly)

Returns Destination source content.

Returns:

  • (String)

    Destination source content



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

def dest_content
  @dest_content
end

#freeze_tokenString (readonly)

Returns Token for freeze block markers.

Returns:

  • (String)

    Token for freeze block markers



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

def freeze_token
  @freeze_token
end

#match_refinerObject? (readonly)

Returns Match refiner for fuzzy matching.

Returns:

  • (Object, nil)

    Match refiner for fuzzy matching



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

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



150
151
152
# File 'lib/ast/merge/smart_merger_base.rb', line 150

def node_typing
  @node_typing
end

#preferenceSymbol, Hash (readonly)

Returns Preference for signature matches.

Returns:

  • (Symbol, Hash)

    Preference for signature matches



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

def preference
  @preference
end

#resolution_modeSymbol (readonly)

Returns How matching differences should be surfaced.

Returns:

  • (Symbol)

    How matching differences should be surfaced



153
154
155
# File 'lib/ast/merge/smart_merger_base.rb', line 153

def resolution_mode
  @resolution_mode
end

#resolverObject (readonly)

Returns Resolver for handling conflicts.

Returns:

  • (Object)

    Resolver for handling conflicts



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

def resolver
  @resolver
end

#resultObject (readonly)

Returns Result object tracking merged content.

Returns:

  • (Object)

    Result object tracking merged content



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

def result
  @result
end

#signature_generatorProc? (readonly)

Returns Custom signature generator.

Returns:

  • (Proc, nil)

    Custom signature generator



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

def signature_generator
  @signature_generator
end

#template_analysisObject (readonly)

Returns Analysis of the template file.

Returns:

  • (Object)

    Analysis of the template file



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

def template_analysis
  @template_analysis
end

#template_contentString (readonly)

Returns Template source content.

Returns:

  • (String)

    Template source content



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

def template_content
  @template_content
end

#unresolved_policyAst::Merge::UnresolvedPolicy (readonly)

Returns Caller-facing unresolved review policy.

Returns:



155
156
157
# File 'lib/ast/merge/smart_merger_base.rb', line 155

def unresolved_policy
  @unresolved_policy
end

Instance Method Details

#mergeString

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

Returns:

  • (String)

    The merged content



273
274
275
# File 'lib/ast/merge/smart_merger_base.rb', line 273

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)



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

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


303
304
305
306
307
308
309
310
# File 'lib/ast/merge/smart_merger_base.rb', line 303

def merge_with_debug
  content = merge

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

#statsHash

Get merge statistics.

Returns:

  • (Hash)

    Statistics about the merge



315
316
317
318
# File 'lib/ast/merge/smart_merger_base.rb', line 315

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