Class: Prism::Merge::SmartMerger

Inherits:
Object
  • Object
show all
Defined in:
lib/prism/merge/smart_merger.rb

Overview

Orchestrates the smart merge process using FileAnalysis, FileAligner, ConflictResolver, and MergeResult to merge two Ruby files intelligently.

SmartMerger provides flexible configuration for different merge scenarios. When matching class or module definitions are found in both files, the merger automatically performs recursive merging of their bodies, intelligently combining nested methods, constants, and other definitions.

Examples:

Basic merge (destination customizations preserved)

merger = SmartMerger.new(template_content, dest_content)
result = merger.merge

Version file merge (template updates win)

merger = SmartMerger.new(
  template_content,
  dest_content,
  signature_match_preference: :template,
  add_template_only_nodes: true
)
result = merger.merge
# Result: VERSION = "2.0.0" (from template), new constants added

Appraisals merge (destination customizations preserved)

merger = SmartMerger.new(
  template_content,
  dest_content,
  signature_match_preference: :destination,  # default
  add_template_only_nodes: false             # default
)
result = merger.merge
# Result: Custom gem versions preserved, template-only blocks skipped

Custom signature matching

sig_gen = ->(node) { [node.class.name, node.name] }
merger = SmartMerger.new(
  template_content,
  dest_content,
  signature_generator: sig_gen
)

See Also:

Instance Attribute Summary collapse

Instance Method Summary collapse

Constructor Details

#initialize(template_content, dest_content, signature_generator: nil, signature_match_preference: :destination, add_template_only_nodes: false, freeze_token: FileAnalysis::DEFAULT_FREEZE_TOKEN, max_recursion_depth: Float::INFINITY, current_depth: 0) ⇒ SmartMerger

Creates a new SmartMerger for intelligent Ruby file merging.

Examples:

Basic usage

merger = SmartMerger.new(template, destination)
result = merger.merge

Template updates win (version files)

merger = SmartMerger.new(
  template,
  destination,
  signature_match_preference: :template,
  add_template_only_nodes: true
)

Destination customizations win (Appraisals)

merger = SmartMerger.new(
  template,
  destination,
  signature_match_preference: :destination,
  add_template_only_nodes: false
)

Custom signature matching with fallthrough

sig_gen = lambda do |node|
  case node
  when Prism::CallNode
    # Custom handling for gem calls - match by gem name
    if node.name == :gem
      return [:gem, node.arguments&.arguments&.first&.unescaped]
    end
  end
  # Return the node to fall through to default signature computation
  node
end

merger = SmartMerger.new(
  template,
  destination,
  signature_generator: sig_gen
)

Parameters:

  • template_content (String)

    Template Ruby source code

  • dest_content (String)

    Destination Ruby source code

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

    Optional proc to generate custom node signatures. The proc receives a Prism node (or FreezeNode) and should return one of:

    • An array representing the node’s signature (e.g., ‘[:gem, “foo”]`)

    • ‘nil` to indicate the node should have no signature (won’t be matched)

    • A ‘Prism::Node` or `FreezeNode` to fall through to default signature computation using that node. This allows custom generators to only override specific node types while delegating others to the built-in logic. Return the original node unchanged for simple fallthrough, or return a modified node to influence default matching.

    Nodes with identical signatures are considered matches during merge. Default: Uses FileAnalysis#compute_node_signature which matches:

    • Conditionals by condition only (not body)

    • Assignments by name only (not value)

    • Method calls by name and args (not block)

  • signature_match_preference (Symbol) (defaults to: :destination)

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

    • ‘:destination` (default) - Use destination version (preserves customizations). Use for Appraisals files, configs with project-specific values.

    • ‘:template` - Use template version (applies updates). Use for version files, canonical configs, conditional implementations.

  • add_template_only_nodes (Boolean) (defaults to: false)

    Controls whether to add nodes that only exist in template:

    • ‘false` (default) - Skip template-only nodes. Use for templates with placeholder/example content.

    • ‘true` - Add template-only nodes to result. Use when template has new required constants/methods to add.

  • freeze_token (String) (defaults to: FileAnalysis::DEFAULT_FREEZE_TOKEN)

    Token to use for freeze block markers. Default: “prism-merge” (looks for # prism-merge:freeze / # prism-merge:unfreeze) Freeze blocks preserve destination content unchanged during merge.

  • max_recursion_depth (Integer, Float) (defaults to: Float::INFINITY)

    Maximum depth for recursive body merging. Default: Float::INFINITY (no limit). This is a safety valve that users can set if they encounter edge cases. Normal merging terminates naturally based on body content analysis (blocks with non-mergeable content like literals are not recursed into).

Raises:



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

def initialize(template_content, dest_content, signature_generator: nil, signature_match_preference: :destination, add_template_only_nodes: false, freeze_token: FileAnalysis::DEFAULT_FREEZE_TOKEN, max_recursion_depth: Float::INFINITY, current_depth: 0)
  @template_content = template_content
  @dest_content = dest_content
  @signature_match_preference = signature_match_preference
  @add_template_only_nodes = add_template_only_nodes
  @freeze_token = freeze_token
  @max_recursion_depth = max_recursion_depth
  @current_depth = current_depth
  @template_analysis = FileAnalysis.new(template_content, signature_generator: signature_generator, freeze_token: freeze_token)
  @dest_analysis = FileAnalysis.new(dest_content, signature_generator: signature_generator, freeze_token: freeze_token)
  @aligner = FileAligner.new(@template_analysis, @dest_analysis)
  @resolver = ConflictResolver.new(
    @template_analysis,
    @dest_analysis,
    signature_match_preference: signature_match_preference,
    add_template_only_nodes: add_template_only_nodes,
  )
  @result = MergeResult.new
end

Instance Attribute Details

#alignerFileAligner (readonly)

Returns Aligner for finding matches and differences.

Returns:

  • (FileAligner)

    Aligner for finding matches and differences



57
58
59
# File 'lib/prism/merge/smart_merger.rb', line 57

def aligner
  @aligner
end

#dest_analysisFileAnalysis (readonly)

Returns Analysis of the destination file.

Returns:



54
55
56
# File 'lib/prism/merge/smart_merger.rb', line 54

def dest_analysis
  @dest_analysis
end

#resolverConflictResolver (readonly)

Returns Resolver for handling conflicting content.

Returns:



60
61
62
# File 'lib/prism/merge/smart_merger.rb', line 60

def resolver
  @resolver
end

#resultMergeResult (readonly)

Returns Result object tracking merged content.

Returns:

  • (MergeResult)

    Result object tracking merged content



63
64
65
# File 'lib/prism/merge/smart_merger.rb', line 63

def result
  @result
end

#template_analysisFileAnalysis (readonly)

Returns Analysis of the template file.

Returns:



51
52
53
# File 'lib/prism/merge/smart_merger.rb', line 51

def template_analysis
  @template_analysis
end

Instance Method Details

#mergeString

Performs the intelligent merge of template and destination files.

The merge process:

  1. Validates both files for syntax errors

  2. Finds anchors (matching sections) and boundaries (differences)

  3. Processes anchors and boundaries in order

  4. Returns merged content as a string

Merge behavior is controlled by constructor parameters:

  • ‘signature_match_preference`: Which version wins for matching nodes

  • ‘add_template_only_nodes`: Whether to add template-only content

Examples:

Basic merge

merger = SmartMerger.new(template, destination)
result = merger.merge
File.write("output.rb", result)

With error handling

begin
  result = merger.merge
rescue Prism::Merge::TemplateParseError => e
  puts "Template error: #{e.message}"
  puts "Parse errors: #{e.parse_result.errors}"
end

Returns:

  • (String)

    The merged Ruby source code

Raises:

See Also:



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

def merge
  # Handle invalid files
  unless @template_analysis.valid?
    raise Prism::Merge::TemplateParseError.new(
      "Template file has parsing errors",
      content: @template_content,
      parse_result: @template_analysis.parse_result,
    )
  end

  unless @dest_analysis.valid?
    raise Prism::Merge::DestinationParseError.new(
      "Destination file has parsing errors",
      content: @dest_content,
      parse_result: @dest_analysis.parse_result,
    )
  end

  # Find anchors and boundaries
  boundaries = @aligner.align

  # Process the merge by walking through anchors and boundaries in order
  process_merge(boundaries)

  # Return final content
  @result.to_s
end

#merge_with_debugHash

Performs merge and returns detailed debug information.

This method provides comprehensive information about merge decisions, useful for debugging, testing, and understanding merge behavior.

Examples:

Get merge statistics

result = merger.merge_with_debug
puts "Template lines: #{result[:statistics][:kept_template]}"
puts "Replaced lines: #{result[:statistics][:replaced]}"

Debug line provenance

result = merger.merge_with_debug
puts result[:debug]
# Output shows source file and decision for each line:
# Line 1: [KEPT_TEMPLATE] # frozen_string_literal: true
# Line 2: [KEPT_TEMPLATE]
# Line 3: [REPLACED] VERSION = "2.0.0"

Returns:

  • (Hash)

    Hash containing:

    • ‘:content` [String] - Final merged content

    • ‘:debug` [String] - Line-by-line provenance information

    • ‘:statistics` [Hash] - Counts of merge decisions:

      • ‘:kept_template` - Lines from template (no conflict)

      • ‘:kept_destination` - Lines from destination (no conflict)

      • ‘:replaced` - Template replaced matching destination

      • ‘:appended` - Destination-only content added

      • ‘:freeze_block` - Lines from freeze blocks

See Also:



259
260
261
262
263
264
265
266
# File 'lib/prism/merge/smart_merger.rb', line 259

def merge_with_debug
  content = merge
  {
    content: content,
    debug: @result.debug_output,
    statistics: @result.statistics,
  }
end