Class: Prism::Merge::RecursiveNodeBodyMerger
- Inherits:
-
Object
- Object
- Prism::Merge::RecursiveNodeBodyMerger
- Defined in:
- lib/prism/merge/recursive_node_body_merger.rb
Instance Attribute Summary collapse
-
#merger ⇒ Object
readonly
Returns the value of attribute merger.
Instance Method Summary collapse
-
#initialize(merger:) ⇒ RecursiveNodeBodyMerger
constructor
A new instance of RecursiveNodeBodyMerger.
- #merge(template_node:, dest_node:) ⇒ Object
Constructor Details
#initialize(merger:) ⇒ RecursiveNodeBodyMerger
Returns a new instance of RecursiveNodeBodyMerger.
8 9 10 |
# File 'lib/prism/merge/recursive_node_body_merger.rb', line 8 def initialize(merger:) @merger = merger end |
Instance Attribute Details
#merger ⇒ Object (readonly)
Returns the value of attribute merger.
6 7 8 |
# File 'lib/prism/merge/recursive_node_body_merger.rb', line 6 def merger @merger end |
Instance Method Details
#merge(template_node:, dest_node:) ⇒ Object
12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 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 159 160 161 162 163 164 165 166 167 168 169 170 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 200 201 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 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 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 |
# File 'lib/prism/merge/recursive_node_body_merger.rb', line 12 def merge(template_node:, dest_node:) actual_template = template_node.respond_to?(:unwrap) ? template_node.unwrap : template_node actual_dest = dest_node.respond_to?(:unwrap) ? dest_node.unwrap : dest_node template_layout = merger.send(:node_body_layout_for, actual_template, merger.template_analysis) dest_layout = merger.send(:node_body_layout_for, actual_dest, merger.dest_analysis) template_body = merger.send(:extract_node_body, actual_template, merger.template_analysis) dest_body = merger.send(:extract_node_body, actual_dest, merger.dest_analysis) # When the gemspec block uses different variable names (e.g. |gem| vs |spec|), # rewrite the destination body to match the template's variable name. # The template variable ALWAYS wins regardless of merge preference because the # downstream pipeline (DependencySectionPolicy regex, DevelopmentDependencySyncPolicy # line insertion, etc.) is built around a single canonical variable name. # Uses AST-directed byte-offset replacement — no regular expressions. template_var = merger.template_analysis.respond_to?(:gemspec_block_var) ? merger.template_analysis.gemspec_block_var : nil dest_var = merger.dest_analysis.respond_to?(:gemspec_block_var) ? merger.dest_analysis.gemspec_block_var : nil preferred_var = Ruby::Merge::GemspecSupport.preferred_block_var(template_var, dest_var) dest_body = GemspecVarRenamer.rename(dest_body, old_var: dest_var, new_var: preferred_var) if preferred_var body_merger = merger.class.new( template_body, dest_body, signature_generator: merger.instance_variable_get(:@raw_signature_generator), preference: merger.preference, resolution_mode: merger.resolution_mode, unresolved_policy: merger.unresolved_policy, add_template_only_nodes: merger.add_template_only_nodes, remove_template_missing_nodes: merger.remove_template_missing_nodes, corruption_handling: merger.corruption_handling, freeze_token: merger.freeze_token, max_recursion_depth: merger.max_recursion_depth, current_depth: merger.instance_variable_get(:@current_depth) + 1, node_typing: merger.node_typing, # Thread gemspec block-variable names so inner FileAnalysis objects can normalise # assignment signatures even when the Gem::Specification.new wrapper is absent # from the extracted body text (auto-detection cannot fire without the wrapper). # When renaming was applied above, both sides now use preferred_var. template_gemspec_block_var: Ruby::Merge::GemspecSupport.merged_block_var(template_var, preferred_var), dest_gemspec_block_var: Ruby::Merge::GemspecSupport.merged_block_var(dest_var, preferred_var) ) body_result = if template_body.empty? && dest_body.empty? nil else body_merger.merge_result end node_preference = merger.send(:preference_for_node, template_node, dest_node) last_emitted_dest_line = nil template_comments = actual_template.location.respond_to?(:leading_comments) ? actual_template.location.leading_comments : [] dest_comments = actual_dest.location.respond_to?(:leading_comments) ? actual_dest.location.leading_comments : [] dest_prefix_comment_lines = merger.instance_variable_get(:@dest_prefix_comment_lines) template_prefix_line_numbers = Prism::Merge::MagicCommentSupport.prefix_comment_line_numbers_for_comments(template_comments) dest_claimed = merger.dest_analysis.respond_to?(:claimed_lines) ? merger.dest_analysis.claimed_lines : Set.new template_claimed = merger.template_analysis.respond_to?(:claimed_lines) ? merger.template_analysis.claimed_lines : Set.new dest_comments = dest_comments.reject do |comment| ln = comment.location.start_line dest_prefix_comment_lines&.include?(ln) || dest_claimed.include?(ln) end last_skipped_template_line = nil if dest_prefix_comment_lines&.any? || template_claimed.any? template_comments = template_comments.reject do |comment| ln = comment.location.start_line if template_prefix_line_numbers.include?(ln) || template_claimed.include?(ln) last_skipped_template_line = ln true end end end if node_preference == :template && template_comments.empty? && dest_comments.any? comment_source = :destination leading_comments = dest_comments comment_analysis = merger.dest_analysis elsif node_preference == :template comment_source = :template leading_comments = template_comments comment_analysis = merger.template_analysis else comment_source = :destination leading_comments = dest_comments comment_analysis = merger.dest_analysis end source_analysis = node_preference == :template ? merger.template_analysis : merger.dest_analysis source_node = node_preference == :template ? actual_template : actual_dest source_layout = merger.send(:node_body_layout_for, source_node, source_analysis) decision = MergeResult::DECISION_REPLACED template_inline_by_line = merger.send(:wrapper_inline_comment_entries_by_line, merger.template_analysis, actual_template) dest_inline_by_line = merger.send(:wrapper_inline_comment_entries_by_line, merger.dest_analysis, actual_dest) merged_body_lines = body_result ? body_result.lines.dup : [] = body_result&.&.dup || [] remapped_result_lines = {} # Recursive body merges emit comments through SmartMerger directly, so # apply the same ownership filters used by NodeEmissionSupport. Without # this, a floating comment can be emitted once as a matched node's # external template trailing comment and again as a nested leading # comment when both inputs already contain it. emission_support = merger.send(:node_emission_support) last_filtered_comment_line = nil leading_comments, trailing_filtered_line = emission_support.send( :filter_emitted_template_trailing_comments, leading_comments ) if comment_source == :template leading_comments, leading_filtered_line = emission_support.send( :filter_already_emitted_leading_comments, leading_comments ) last_filtered_comment_line = leading_filtered_line || trailing_filtered_line emission_support.send(:track_emitted_template_leading_comments, leading_comments) else leading_comments, template_leading_filtered_line = emission_support.send( :filter_emitted_template_leading_comments, leading_comments, last_filtered_line: trailing_filtered_line ) last_filtered_comment_line = template_leading_filtered_line || trailing_filtered_line emission_support.send(:track_emitted_dest_leading_comments, leading_comments) end prev_comment_line = if comment_source == :template [last_skipped_template_line, last_filtered_comment_line].compact.max else last_filtered_comment_line end merger.send( :emit_leading_comments, merger.result, leading_comments, analysis: comment_analysis, source: comment_source, decision: decision, prev_comment_line: prev_comment_line ) if comment_source == :destination && leading_comments.any? last_emitted_dest_line = leading_comments.last.location.start_line end if leading_comments.any? emitted_gap_line = merger.send( :emit_blank_lines_between, merger.result, last_comment_line: leading_comments.last.location.start_line, next_content_line: source_node.location.start_line, analysis: comment_analysis, source: comment_source, decision: decision ) last_emitted_dest_line = emitted_gap_line if comment_source == :destination && emitted_gap_line end opening_line = source_layout.opening_line_text # When the opening line comes from dest but the template var is canonical, # rename the block parameter in the opening line (e.g. |gem| → |spec|). opening_line = Ruby::Merge::GemspecSupport.opening_line_with_preferred_block_var( opening_line, dest_var: dest_var, preferred_var: preferred_var, node_preference: node_preference ) if node_preference == :template && template_inline_by_line[actual_template.location.start_line].empty? && !source_layout.body_starts_on_opening_line? dest_opening_inline = dest_inline_by_line[actual_dest.location.start_line] if dest_opening_inline.any? opening_line = merger.send(:append_inline_comment_entries, opening_line.to_s.chomp, dest_opening_inline) end end if source_layout.body_starts_on_opening_line? && merged_body_lines.any? opening_line = "#{opening_line}#{merged_body_lines.shift}" .shift end merger.result.add_line( opening_line.chomp, decision: decision, template_line: node_preference == :template ? source_node.location.start_line : nil, dest_line: node_preference == :destination ? source_node.location.start_line : nil ) closing_body_line = nil if source_layout.body_ends_on_closing_line? && merged_body_lines.any? closing_body_line = merged_body_lines.pop .pop end merged_body_lines.each_with_index do |line, index| = [index] || {} merger.result.add_line( line.chomp, decision: [:decision] || decision, template_line: remap_body_line([:template_line], template_layout), dest_line: remap_body_line([:dest_line], dest_layout), comment: [:comment] ) remapped_result_lines[[:result_line]] = merger.result.line_count if [:result_line] end remap_inner_unresolved_review_state!( body_result: body_result, remapped_result_lines: remapped_result_lines, template_layout: template_layout, dest_layout: dest_layout, parent_node: actual_dest || actual_template ) merger.send(:begin_node_plan_emitter).emit( template_node: actual_template, dest_node: actual_dest, node_preference: node_preference, decision: decision, template_inline_by_line: template_inline_by_line, dest_inline_by_line: dest_inline_by_line ) end_line = source_layout.closing_line_text if node_preference == :template && template_inline_by_line[actual_template.location.end_line].empty? dest_end_inline = dest_inline_by_line[actual_dest.location.end_line] if dest_end_inline.any? end_line = merger.send(:append_inline_comment_entries, end_line.to_s.chomp, dest_end_inline) end end end_line = "#{closing_body_line}#{end_line}" if closing_body_line merger.result.add_line( end_line.chomp, decision: decision, template_line: node_preference == :template ? source_node.location.end_line : nil, dest_line: node_preference == :destination ? source_node.location.end_line : nil ) template_trailing_comments = merger.send(:external_trailing_comments_for, actual_template, analysis: merger.template_analysis) dest_trailing_comments = merger.send(:external_trailing_comments_for, actual_dest, analysis: merger.dest_analysis) if node_preference == :template trailing_comments = template_trailing_comments.any? ? template_trailing_comments : dest_trailing_comments trailing_analysis = template_trailing_comments.any? ? merger.template_analysis : merger.dest_analysis else trailing_comments = dest_trailing_comments trailing_analysis = merger.dest_analysis end if trailing_comments.any? emitted_dest_line = merger.send( :emit_external_trailing_comments, merger.result, trailing_comments, source_node: trailing_analysis.equal?(merger.template_analysis) ? actual_template : actual_dest, analysis: trailing_analysis, source: trailing_analysis.equal?(merger.template_analysis) ? :template : :destination, decision: decision ) last_emitted_dest_line = emitted_dest_line if trailing_analysis.equal?(merger.dest_analysis) return { last_emitted_dest_line: last_emitted_dest_line } end trailing_line = source_node.location.end_line + 1 trailing_content = source_analysis.line_at(trailing_line) emitted_trailing_gap_line = emit_trailing_layout_gap_lines( analysis: source_analysis, owner: source_node, source: node_preference == :template ? :template : :destination, decision: decision ) if emitted_trailing_gap_line last_emitted_dest_line = emitted_trailing_gap_line if node_preference == :destination elsif trailing_content && trailing_content.strip.empty? if node_preference == :template merger.result.add_line('', decision: decision, template_line: trailing_line) else merger.result.add_line('', decision: decision, dest_line: trailing_line) last_emitted_dest_line = trailing_line end end { last_emitted_dest_line: last_emitted_dest_line } end |