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

Constant Summary collapse

CORRUPTION_HANDLINGS =
::Ast::Merge::Healer::HANDLINGS

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, remove_template_missing_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, corruption_handling: :heal, merge_template_requires: false, template_only_placement: :after_anchor, **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

  • remove_template_missing_nodes (Boolean) (defaults to: false)

    Whether to remove destination-only nodes while preserving or promoting their attached comments

  • 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



96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
# File 'lib/prism/merge/smart_merger.rb', line 96

def initialize(
  template_content,
  dest_content,
  signature_generator: nil,
  preference: :destination,
  add_template_only_nodes: false,
  remove_template_missing_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,
  corruption_handling: :heal,
  merge_template_requires: false,
  template_only_placement: :after_anchor,
  **options
)
  @max_recursion_depth = max_recursion_depth
  @current_depth = current_depth
  @text_merger_options = text_merger_options
  @remove_template_missing_nodes = remove_template_missing_nodes
  @corruption_handling = normalize_corruption_handling(corruption_handling)
  @merge_template_requires = merge_template_requires
  @template_only_placement = normalize_template_only_placement(template_only_placement)
  @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,
    remove_template_missing_nodes: remove_template_missing_nodes,
    freeze_token: freeze_token,
    match_refiner: match_refiner,
    regions: regions,
    region_placeholder: region_placeholder,
    node_typing: node_typing,
    **options
  )
end

Instance Attribute Details

#corruption_handlingSymbol (readonly)

Returns How suspected corruption should be handled (:heal, :warn, :error, :skip).

Returns:

  • (Symbol)

    How suspected corruption should be handled (:heal, :warn, :error, :skip)



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

def corruption_handling
  @corruption_handling
end

#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

#merge_template_requiresBoolean (readonly)

Returns Whether template-only require calls may be emitted.

Returns:

  • (Boolean)

    Whether template-only require calls may be emitted



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

def merge_template_requires
  @merge_template_requires
end

#remove_template_missing_nodesBoolean (readonly)

Returns Whether to remove destination-only nodes that are missing from the template.

Returns:

  • (Boolean)

    Whether to remove destination-only nodes that are missing from the template



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

def remove_template_missing_nodes
  @remove_template_missing_nodes
end

#runtime_sessionAst::Merge::Runtime::Session? (readonly)

Returns Runtime-charter state recorded during merge.

Returns:

  • (Ast::Merge::Runtime::Session, nil)

    Runtime-charter state recorded during merge



68
69
70
# File 'lib/prism/merge/smart_merger.rb', line 68

def runtime_session
  @runtime_session
end

#template_only_placementSymbol (readonly)

Returns Where template-only nodes are emitted.

Returns:

  • (Symbol)

    Where template-only nodes are emitted



65
66
67
# File 'lib/prism/merge/smart_merger.rb', line 65

def template_only_placement
  @template_only_placement
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)


164
165
166
# File 'lib/prism/merge/smart_merger.rb', line 164

def comment_only_file?(analysis)
  comment_only_file_merger.comment_only_file?(analysis)
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



171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
# File 'lib/prism/merge/smart_merger.rb', line 171

def merge_with_debug
  result_obj = merge_result
  template_analysis_debug = {
    valid: @template_analysis&.valid? || false,
    statements: @template_analysis&.statements&.size || 0
  }
  dest_analysis_debug = {
    valid: @dest_analysis&.valid? || false,
    statements: @dest_analysis&.statements&.size || 0
  }
  {
    content: result_obj.to_s,
    debug: {
      template_statements: template_analysis_debug[:statements],
      dest_statements: dest_analysis_debug[:statements],
      preference: @preference,
      add_template_only_nodes: @add_template_only_nodes,
      freeze_token: @freeze_token,
      corruption_handling: @corruption_handling,
      runtime_operation_count: runtime_session&.operations&.size || 0,
      runtime_diagnostic_count: runtime_session&.diagnostics&.size || 0
    },
    runtime: runtime_session&.to_h,
    statistics: result_obj.respond_to?(:statistics) ? result_obj.statistics : result_obj.decision_summary,
    decisions: result_obj.respond_to?(:decision_summary) ? result_obj.decision_summary : result_obj.statistics,
    template_analysis: template_analysis_debug,
    dest_analysis: dest_analysis_debug
  }
end

#normalize_template_only_placement(value) ⇒ Object

Raises:

  • (ArgumentError)


149
150
151
152
153
154
# File 'lib/prism/merge/smart_merger.rb', line 149

def normalize_template_only_placement(value)
  normalized = value.to_s.empty? ? 'after_anchor' : value.to_s
  return normalized.to_sym if %w[after_anchor destination_tail].include?(normalized)

  raise ArgumentError, "Unsupported template-only placement #{value.inspect}"
end