Class: Prism::Merge::SmartMerger

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

Overview

A merger that uses section-based semantics with recursive body merging for cleaner merging.

SmartMerger:

  1. Converts each top-level node into a “section” identified by its signature
  2. Uses SectionTyping-style merge logic to decide which sections to include
  3. Recursively merges matching class/module/block bodies
  4. Outputs each selected node exactly once (with its comments)

This approach avoids the complexity of tracking line ranges for anchors
and boundaries, which can lead to duplicate content when comments are
attached to multiple overlapping ranges.

Merge Algorithm

  1. Parse both template and destination files
  2. Generate signatures for all top-level nodes in both files
  3. Build a signature -> node map for destination
  4. Walk template nodes in order:
    • If signature matches a dest node:
      • If class/module/block with mergeable body: recursively merge bodies
      • Otherwise: output based on preference
    • If template-only: output if add_template_only_nodes is true
  5. Output any remaining dest-only nodes

Recursive Body Merging

When matching class/module definitions or CallNodes with blocks are found,
the merger recursively merges their body contents. This allows template
updates to nested methods/constants to be merged with destination customizations.

Examples:

Basic merge

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

Template wins with additions

merger = SmartMerger.new(
  template_content,
  dest_content,
  preference: :template,
  add_template_only_nodes: true
)
result = merger.merge

Instance Attribute Summary collapse

Instance Method Summary collapse

Constructor Details

#initialize(template_content, dest_content, signature_generator: nil, preference: :destination, add_template_only_nodes: false, freeze_token: nil, node_typing: nil, max_recursion_depth: Float::INFINITY, current_depth: 0, match_refiner: nil, regions: nil, region_placeholder: nil, text_merger_options: nil, **options) ⇒ SmartMerger

Creates a new SmartMerger.

Parameters:

  • template_content (String)

    Template Ruby source code

  • dest_content (String)

    Destination Ruby source code

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

    Custom signature generator

  • preference (Symbol, Hash) (defaults to: :destination)

    :template, :destination, or per-type Hash

  • add_template_only_nodes (Boolean) (defaults to: false)

    Whether to add template-only nodes

  • freeze_token (String, nil) (defaults to: nil)

    Token for freeze block markers

  • node_typing (Hash{Symbol,String => #call}, nil) (defaults to: nil)

    Node typing configuration
    for per-node-type merge preferences

  • 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.

  • current_depth (Integer) (defaults to: 0)

    Current recursion depth (internal use)

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

    Optional match refiner (unused but accepted for API compatibility)

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

    Region configurations (unused but accepted for API compatibility)

  • region_placeholder (String, nil) (defaults to: nil)

    Custom placeholder prefix (unused but accepted for API compatibility)

  • text_merger_options (Hash, nil) (defaults to: nil)

    Options to pass to Text::SmartMerger when
    merging comment-only files (files with no Ruby code statements). Supported options:

    • :freeze_token - Token for freeze block markers (defaults to @freeze_token or “text-merge”)
    • Any other options supported by Ast::Merge::Text::SmartMerger
  • options (Hash)

    Additional options for forward compatibility



77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
# File 'lib/prism/merge/smart_merger.rb', line 77

def initialize(
  template_content,
  dest_content,
  signature_generator: nil,
  preference: :destination,
  add_template_only_nodes: false,
  freeze_token: nil,
  node_typing: nil,
  max_recursion_depth: Float::INFINITY,
  current_depth: 0,
  match_refiner: nil,
  regions: nil,
  region_placeholder: nil,
  text_merger_options: nil,
  **options
)
  @max_recursion_depth = max_recursion_depth
  @current_depth = current_depth
  @text_merger_options = text_merger_options
  @dest_prefix_comment_lines = nil

  # Store the raw (unwrapped) signature_generator so that
  # merge_node_body_recursively can pass it to inner SmartMergers
  # without double-wrapping.
  @raw_signature_generator = signature_generator

  # Wrap signature_generator to include node_typing processing
  effective_signature_generator = build_effective_signature_generator(signature_generator, node_typing)

  super(
    template_content,
    dest_content,
    signature_generator: effective_signature_generator,
    preference: preference,
    add_template_only_nodes: add_template_only_nodes,
    freeze_token: freeze_token,
    match_refiner: match_refiner,
    regions: regions,
    region_placeholder: region_placeholder,
    node_typing: node_typing,
    **options
  )
end

Instance Attribute Details

#max_recursion_depthInteger, Float (readonly)

Returns Maximum recursion depth for body merging.

Returns:

  • (Integer, Float)

    Maximum recursion depth for body merging



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

def max_recursion_depth
  @max_recursion_depth
end

#text_merger_optionsHash? (readonly)

Returns Options to pass to Text::SmartMerger for comment-only files.

Returns:

  • (Hash, nil)

    Options to pass to Text::SmartMerger for comment-only files



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

def text_merger_options
  @text_merger_options
end

Instance Method Details

#comment_only_file?(analysis) ⇒ Boolean

Determine whether the given analysis represents a comment-only file.

Returns true when every top-level statement is a comment/block/empty
node produced by the comment parsers. This is used to decide whether to
delegate to the comment-only merger logic.

Parameters:

Returns:

  • (Boolean)


129
130
131
132
133
134
135
136
137
138
139
# File 'lib/prism/merge/smart_merger.rb', line 129

def comment_only_file?(analysis)
  stmts = analysis.statements
  return false if stmts.nil? || stmts.empty?

  stmts.all? do |s|
    # AST comment nodes (Prism-specific ones inherit from these)
    s.is_a?(Ast::Merge::Comment::Empty) ||
      s.is_a?(Ast::Merge::Comment::Block) ||
      s.is_a?(Ast::Merge::Comment::Line)
  end
end

#merge_with_debugHash

Perform the merge and return a hash with content, debug info, and statistics.

Returns:

  • (Hash)

    Hash with :content, :debug, and :statistics keys



144
145
146
147
148
149
150
151
152
153
154
155
156
157
# File 'lib/prism/merge/smart_merger.rb', line 144

def merge_with_debug
  result_obj = merge_result
  {
    content: result_obj.to_s,
    debug: {
      template_statements: @template_analysis&.statements&.size || 0,
      dest_statements: @dest_analysis&.statements&.size || 0,
      preference: @preference,
      add_template_only_nodes: @add_template_only_nodes,
      freeze_token: @freeze_token,
    },
    statistics: result_obj.respond_to?(:statistics) ? result_obj.statistics : result_obj.decision_summary,
  }
end