Class: Prism::Merge::TopLevelMergeRunner

Inherits:
Object
  • Object
show all
Includes:
Ast::Merge::JaccardSimilarity, Ast::Merge::TrailingGroups::DestIterate
Defined in:
lib/prism/merge/top_level_merge_runner.rb

Overview

Orchestrates the top-level merge of two Ruby files parsed by Prism.

Uses a three-phase matching strategy to pair template nodes with destination nodes before emitting the merged result:

  1. Phase 1 — Exact signature match. Pairs nodes whose structural signatures (method name + params, call name + args, etc.) are identical. This is the fastest phase and handles the vast majority of nodes.

  2. Phase 2 — Similarity match at the same depth. For nodes left unmatched after Phase 1, computes body-text Jaccard similarity (Ast::Merge::JaccardSimilarity) to pair probable renames or minor refactors. Only operates on the residual unmatched sets, keeping cost proportional to orphan count.

  3. Phase 3 — Cross-depth match. For nodes still unmatched after Phase 2, searches recursively into destination subtrees (conditionals, begin/rescue, call blocks) to detect "moved" nodes — e.g. a template top-level eval_gemfile that the destination wrapped inside an if block. Only runs on the tiny residual set surviving both previous phases.

The phase ordering is critical: exact matches are locked in first so that fuzzy Phase 2 scoring cannot accidentally consume a node that belongs to a later exact match at a different position.

Examples:

Basic usage (internal — called by SmartMerger)

runner = TopLevelMergeRunner.new(merger: smart_merger)
result = runner.merge   # => MergeResult

See Also:

  • Jaccard token-set similarity
  • Template-only node positioning

Constant Summary collapse

SIMILARITY_THRESHOLD =

Minimum Jaccard score for Phase 2 body-text matching. Below this threshold, two nodes are too dissimilar to pair.

Returns:

  • (Float)
0.6
MIN_BODY_TOKENS =

Minimum token count for meaningful Jaccard comparison. Nodes with fewer body tokens than this are skipped in Phase 2 to avoid spurious matches on trivially short bodies.

Returns:

  • (Integer)
3

Instance Attribute Summary collapse

Instance Method Summary collapse

Constructor Details

#initialize(merger:) ⇒ TopLevelMergeRunner

Returns a new instance of TopLevelMergeRunner.

Parameters:



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

def initialize(merger:)
  @merger = merger
end

Instance Attribute Details

#mergerSmartMerger (readonly)

Returns The merger instance driving this run.

Returns:

  • (SmartMerger)

    The merger instance driving this run



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

def merger
  @merger
end

Instance Method Details

#mergeMergeResult

Execute the three-phase merge and return the result.

Returns:



66
67
68
69
70
71
72
73
74
75
76
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
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
148
149
150
151
152
153
154
155
156
157
158
# File 'lib/prism/merge/top_level_merge_runner.rb', line 66

def merge
  root_operation = start_runtime_session!

  merge_result = if comment_only_merge?
                   merger.send(:comment_only_file_merger).merge
                 else
                   template_by_signature = merger.send(:build_signature_map, merger.template_analysis)
                   dest_by_signature = merger.send(:build_signature_map, merger.dest_analysis)
                   prepare_comment_augmenters!(template_by_signature: template_by_signature,
                                               dest_by_signature: dest_by_signature)
                   consumed_template_indices = Set.new
                   sig_cursor = Hash.new(0)
                   output_dest_line_ranges = []
                   last_output_dest_line = merger.send(:emit_dest_prefix_lines, merger.result,
                                                       merger.dest_analysis)

                   # Phase 1: exact signature match (existing behavior).
                   dest_sigs = ::Set.new(dest_by_signature.keys)

                   # Phase 2: compute similarity-matched pairs from residual orphans.
                   @similarity_pairs = compute_similarity_pairs(template_by_signature, dest_by_signature)

                   # Phase 3: cross-depth search, but only for orphans surviving Phases 1+2.
                   @deep_dest_sigs = compute_deep_sigs_for_orphans(
                     template_by_signature, dest_sigs
                   )

                   trailing_groups, _matched_indices = build_dest_iterate_trailing_groups(
                     template_nodes: merger.template_analysis.statements,
                     dest_sigs: dest_sigs,
                     signature_for: ->(node) { merger.template_analysis.generate_signature(node) },
                     add_template_only_nodes: merger.add_template_only_nodes
                   )
                   destination_template_indices = destination_template_index_sequence(
                     template_by_signature: template_by_signature
                   )
                   destination_signatures = merger.dest_analysis.statements.map do |dest_node|
                     merger.dest_analysis.generate_signature(dest_node)
                   end

                   unless destination_tail_template_only_placement?
                     emit_prefix_trailing_group(trailing_groups, consumed_template_indices) do |info|
                       emit_template_only_node(info, consumed_template_indices)
                     end
                   end

                   merger.dest_analysis.statements.each_with_index do |dest_node, dest_position|
                     last_output_dest_line = process_dest_node(
                       dest_node: dest_node,
                       template_by_signature: template_by_signature,
                       consumed_template_indices: consumed_template_indices,
                       sig_cursor: sig_cursor,
                       output_dest_line_ranges: output_dest_line_ranges,
                       last_output_dest_line: last_output_dest_line,
                       dest_position: dest_position,
                       destination_signatures: destination_signatures,
                       destination_template_indices: destination_template_indices
                     )
                     next if destination_tail_template_only_placement?

                     emit_available_trailing_groups(
                       trailing_groups: trailing_groups,
                       consumed_indices: consumed_template_indices,
                       dest_position: dest_position,
                       destination_template_indices: destination_template_indices
                     )
                   end

                   # Safety net: emit any trailing groups whose anchor was never consumed
                   unless destination_tail_template_only_placement?
                     emit_remaining_trailing_groups(
                       trailing_groups: trailing_groups,
                       consumed_indices: consumed_template_indices
                     ) do |info|
                       emit_template_only_node(info, consumed_template_indices)
                     end
                   end
                   if destination_tail_template_only_placement?
                     emit_tail_template_only_nodes(consumed_template_indices)
                   end

                   emit_dest_postlude_lines(last_output_dest_line)
                   normalize_layout_blank_runs(merger.result)

                   merger.result
                 end

  complete_runtime_session!(root_operation, merge_result)
  merge_result
rescue StandardError => e
  fail_runtime_session!(root_operation, e)
  raise
end