json-merge 7.0.0 → 7.1.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- checksums.yaml.gz.sig +0 -0
- data/LICENSE.md +13 -0
- data/README.md +600 -0
- data/lib/json/merge/comment_tracker.rb +27 -0
- data/lib/json/merge/conflict_resolver.rb +1379 -0
- data/lib/json/merge/debug_logger.rb +41 -0
- data/lib/json/merge/emitter.rb +227 -0
- data/lib/json/merge/file_analysis.rb +355 -0
- data/lib/json/merge/freeze_node.rb +62 -0
- data/lib/json/merge/merge_result.rb +156 -0
- data/lib/json/merge/node_wrapper.rb +258 -0
- data/lib/json/merge/object_match_refiner.rb +338 -0
- data/lib/json/merge/provider.rb +696 -0
- data/lib/json/merge/smart_merger.rb +276 -0
- data/lib/json/merge/source_locator.rb +68 -0
- data/lib/json/merge/three_way_decision.rb +168 -0
- data/lib/json/merge/version.rb +5 -3
- data/lib/json/merge.rb +393 -266
- data/lib/json-merge.rb +7 -1
- data/sig/json/merge.rbs +54 -0
- data.tar.gz.sig +0 -0
- metadata +264 -15
- metadata.gz.sig +0 -0
|
@@ -0,0 +1,1379 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Json
|
|
4
|
+
module Merge
|
|
5
|
+
# Resolves conflicts between template and destination JSON content
|
|
6
|
+
# using structural signatures and configurable preferences.
|
|
7
|
+
#
|
|
8
|
+
# @example Basic usage
|
|
9
|
+
# resolver = ConflictResolver.new(template_analysis, dest_analysis)
|
|
10
|
+
# resolver.resolve(result)
|
|
11
|
+
class ConflictResolver < Ast::Merge::ConflictResolverBase
|
|
12
|
+
include Ast::Merge::CommentLayoutEmissionSupport
|
|
13
|
+
include Ast::Merge::StructuredEmitterProvenanceSupport
|
|
14
|
+
|
|
15
|
+
class MissingSharedInlineRegionError < Json::Merge::Error; end
|
|
16
|
+
|
|
17
|
+
include ::Ast::Merge::TrailingGroups::DestIterate
|
|
18
|
+
|
|
19
|
+
attr_reader :corruption_handling
|
|
20
|
+
|
|
21
|
+
# Creates a new ConflictResolver
|
|
22
|
+
#
|
|
23
|
+
# @param template_analysis [FileAnalysis] Analyzed template file
|
|
24
|
+
# @param dest_analysis [FileAnalysis] Analyzed destination file
|
|
25
|
+
# @param preference [Symbol, Hash] Which version to prefer when
|
|
26
|
+
# nodes have matching signatures:
|
|
27
|
+
# - :destination (default) - Keep destination version (customizations)
|
|
28
|
+
# - :template - Use template version (updates)
|
|
29
|
+
# @param add_template_only_nodes [Boolean] Whether to add nodes only in template
|
|
30
|
+
# @param match_refiner [#call, nil] Optional match refiner for fuzzy matching
|
|
31
|
+
# @param options [Hash] Additional options for forward compatibility
|
|
32
|
+
# @param node_typing [Hash{Symbol,String => #call}, nil] Node typing configuration
|
|
33
|
+
# for per-node-type preferences
|
|
34
|
+
def initialize(template_analysis, dest_analysis, preference: :destination, add_template_only_nodes: false,
|
|
35
|
+
remove_template_missing_nodes: false, resolution_mode: :eager, corruption_handling: :heal, match_refiner: nil, node_typing: nil, merge_arrays: true, preserve_atomic_formatting: false, **options)
|
|
36
|
+
super(
|
|
37
|
+
strategy: :batch,
|
|
38
|
+
preference: preference,
|
|
39
|
+
template_analysis: template_analysis,
|
|
40
|
+
dest_analysis: dest_analysis,
|
|
41
|
+
add_template_only_nodes: add_template_only_nodes,
|
|
42
|
+
remove_template_missing_nodes: remove_template_missing_nodes,
|
|
43
|
+
match_refiner: match_refiner,
|
|
44
|
+
**options
|
|
45
|
+
)
|
|
46
|
+
@resolution_mode = resolution_mode
|
|
47
|
+
@corruption_handling = ::Ast::Merge::Healer.normalize_mode(corruption_handling)
|
|
48
|
+
@node_typing = node_typing
|
|
49
|
+
@merge_arrays = merge_arrays
|
|
50
|
+
@preserve_atomic_formatting = preserve_atomic_formatting
|
|
51
|
+
@emitter = Emitter.new
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
protected
|
|
55
|
+
|
|
56
|
+
# Resolve conflicts and populate the result using tree-based merging
|
|
57
|
+
#
|
|
58
|
+
# @param result [MergeResult] Result object to populate
|
|
59
|
+
def resolve_batch(result)
|
|
60
|
+
DebugLogger.time('ConflictResolver#resolve') do
|
|
61
|
+
@result = result
|
|
62
|
+
template_statements = @template_analysis.statements
|
|
63
|
+
dest_statements = @dest_analysis.statements
|
|
64
|
+
|
|
65
|
+
# Clear emitter for fresh merge
|
|
66
|
+
@emitter.clear
|
|
67
|
+
@emitted_leading_comment_texts = ::Set.new
|
|
68
|
+
|
|
69
|
+
emit_document_prelude(@dest_analysis, nodes: dest_statements)
|
|
70
|
+
|
|
71
|
+
# Merge root-level statements via emitter
|
|
72
|
+
merge_node_lists_to_emitter(
|
|
73
|
+
template_statements,
|
|
74
|
+
dest_statements,
|
|
75
|
+
@template_analysis,
|
|
76
|
+
@dest_analysis
|
|
77
|
+
)
|
|
78
|
+
|
|
79
|
+
emit_document_postlude(@dest_analysis, fallback_node: dest_statements.last)
|
|
80
|
+
|
|
81
|
+
# Transfer emitter output to result
|
|
82
|
+
transfer_emitter_output(result)
|
|
83
|
+
|
|
84
|
+
DebugLogger.debug('Conflict resolution complete', {
|
|
85
|
+
template_statements: template_statements.size,
|
|
86
|
+
dest_statements: dest_statements.size,
|
|
87
|
+
result_lines: result.line_count
|
|
88
|
+
})
|
|
89
|
+
end
|
|
90
|
+
end
|
|
91
|
+
|
|
92
|
+
public
|
|
93
|
+
|
|
94
|
+
def freeze_node?(node)
|
|
95
|
+
return false unless node
|
|
96
|
+
return node.freeze_node? if node.respond_to?(:freeze_node?)
|
|
97
|
+
|
|
98
|
+
node.is_a?(FreezeNode)
|
|
99
|
+
end
|
|
100
|
+
|
|
101
|
+
private
|
|
102
|
+
|
|
103
|
+
# Recursively merge two lists of nodes, emitting to emitter
|
|
104
|
+
# @param template_nodes [Array<NodeWrapper>] Template nodes
|
|
105
|
+
# @param dest_nodes [Array<NodeWrapper>] Destination nodes
|
|
106
|
+
# @param template_analysis [FileAnalysis] Template analysis for line access
|
|
107
|
+
# @param dest_analysis [FileAnalysis] Destination analysis for line access
|
|
108
|
+
def merge_node_lists_to_emitter(template_nodes, dest_nodes, template_analysis, dest_analysis, dest_owners: nil)
|
|
109
|
+
# Build signature maps for matching
|
|
110
|
+
template_by_sig = build_signature_map(template_nodes, template_analysis)
|
|
111
|
+
dest_by_sig = build_signature_map(dest_nodes, dest_analysis)
|
|
112
|
+
|
|
113
|
+
# Build refined matches for nodes that don't match by signature
|
|
114
|
+
refined_matches = build_refined_matches(template_nodes, dest_nodes, template_by_sig, dest_by_sig)
|
|
115
|
+
refined_dest_to_template = refined_matches.invert
|
|
116
|
+
|
|
117
|
+
# Track consumed individual node indices (not just signatures) so that
|
|
118
|
+
# multiple nodes sharing the same signature are matched 1:1 in order
|
|
119
|
+
# rather than collapsed into a single match.
|
|
120
|
+
consumed_template_indices = ::Set.new
|
|
121
|
+
sig_cursor = Hash.new(0)
|
|
122
|
+
|
|
123
|
+
# Pre-compute position-aware trailing groups for template-only nodes.
|
|
124
|
+
dest_sigs = ::Set.new
|
|
125
|
+
dest_nodes.each do |n|
|
|
126
|
+
sig = dest_analysis.generate_signature(n)
|
|
127
|
+
dest_sigs << sig if sig
|
|
128
|
+
end
|
|
129
|
+
refined_template_ids = ::Set.new(refined_matches.keys.map(&:object_id))
|
|
130
|
+
|
|
131
|
+
trailing_groups, all_matched_indices = build_dest_iterate_trailing_groups(
|
|
132
|
+
template_nodes: template_nodes,
|
|
133
|
+
dest_sigs: dest_sigs,
|
|
134
|
+
signature_for: ->(node) { template_analysis.generate_signature(node) },
|
|
135
|
+
refined_template_ids: refined_template_ids,
|
|
136
|
+
add_template_only_nodes: @add_template_only_nodes
|
|
137
|
+
)
|
|
138
|
+
|
|
139
|
+
# Emit template-only nodes that precede the first matched template node.
|
|
140
|
+
emit_prefix_trailing_group(trailing_groups, consumed_template_indices) do |info|
|
|
141
|
+
next if freeze_node?(info[:node])
|
|
142
|
+
|
|
143
|
+
emit_atomic_node(info[:node], template_analysis)
|
|
144
|
+
end
|
|
145
|
+
|
|
146
|
+
# First pass: Process destination nodes
|
|
147
|
+
dest_nodes.each do |dest_node|
|
|
148
|
+
dest_sig = dest_analysis.generate_signature(dest_node)
|
|
149
|
+
|
|
150
|
+
if freeze_node?(dest_node)
|
|
151
|
+
emit_freeze_block(dest_node)
|
|
152
|
+
next
|
|
153
|
+
end
|
|
154
|
+
|
|
155
|
+
# Check for signature match
|
|
156
|
+
if dest_sig && template_by_sig[dest_sig]
|
|
157
|
+
# Find the next unconsumed template node with this signature
|
|
158
|
+
candidates = template_by_sig[dest_sig]
|
|
159
|
+
cursor = sig_cursor[dest_sig]
|
|
160
|
+
template_info = nil
|
|
161
|
+
|
|
162
|
+
while cursor < candidates.size
|
|
163
|
+
candidate = candidates[cursor]
|
|
164
|
+
unless consumed_template_indices.include?(candidate[:index])
|
|
165
|
+
template_info = candidate
|
|
166
|
+
break
|
|
167
|
+
end
|
|
168
|
+
cursor += 1
|
|
169
|
+
end
|
|
170
|
+
|
|
171
|
+
if template_info
|
|
172
|
+
template_node = template_info[:node]
|
|
173
|
+
|
|
174
|
+
# Both have this node - merge them (recursively if containers)
|
|
175
|
+
merge_matched_nodes_to_emitter(template_node, dest_node, template_analysis, dest_analysis,
|
|
176
|
+
dest_owners: dest_nodes)
|
|
177
|
+
|
|
178
|
+
consumed_template_indices << template_info[:index]
|
|
179
|
+
sig_cursor[dest_sig] = cursor + 1
|
|
180
|
+
else
|
|
181
|
+
# All template copies consumed — keep dest copy
|
|
182
|
+
emit_atomic_node(dest_node, dest_analysis)
|
|
183
|
+
end
|
|
184
|
+
elsif refined_dest_to_template.key?(dest_node)
|
|
185
|
+
# Found refined match
|
|
186
|
+
template_node = refined_dest_to_template[dest_node]
|
|
187
|
+
template_sig = template_analysis.generate_signature(template_node)
|
|
188
|
+
|
|
189
|
+
# Find and consume the matching template index
|
|
190
|
+
if template_sig && template_by_sig[template_sig]
|
|
191
|
+
template_by_sig[template_sig].each do |info|
|
|
192
|
+
unless consumed_template_indices.include?(info[:index])
|
|
193
|
+
consumed_template_indices << info[:index]
|
|
194
|
+
break
|
|
195
|
+
end
|
|
196
|
+
end
|
|
197
|
+
end
|
|
198
|
+
|
|
199
|
+
# Merge matched nodes
|
|
200
|
+
merge_matched_nodes_to_emitter(template_node, dest_node, template_analysis, dest_analysis,
|
|
201
|
+
dest_owners: dest_nodes)
|
|
202
|
+
elsif @remove_template_missing_nodes
|
|
203
|
+
emit_removed_destination_node_comments(dest_node, dest_analysis)
|
|
204
|
+
else
|
|
205
|
+
# Destination-only node - always keep
|
|
206
|
+
emit_atomic_node(dest_node, dest_analysis)
|
|
207
|
+
end
|
|
208
|
+
|
|
209
|
+
# Flush interior trailing groups (between two matches) that are ready
|
|
210
|
+
flush_ready_trailing_groups(
|
|
211
|
+
trailing_groups: trailing_groups,
|
|
212
|
+
matched_indices: all_matched_indices,
|
|
213
|
+
consumed_indices: consumed_template_indices
|
|
214
|
+
) do |info|
|
|
215
|
+
next if freeze_node?(info[:node])
|
|
216
|
+
|
|
217
|
+
emit_atomic_node(info[:node], template_analysis)
|
|
218
|
+
end
|
|
219
|
+
end
|
|
220
|
+
|
|
221
|
+
# Emit remaining trailing groups (tail groups after last match + safety net)
|
|
222
|
+
emit_remaining_trailing_groups(
|
|
223
|
+
trailing_groups: trailing_groups,
|
|
224
|
+
consumed_indices: consumed_template_indices
|
|
225
|
+
) do |info|
|
|
226
|
+
next if freeze_node?(info[:node])
|
|
227
|
+
|
|
228
|
+
emit_atomic_node(info[:node], template_analysis)
|
|
229
|
+
end
|
|
230
|
+
end
|
|
231
|
+
|
|
232
|
+
def trailing_group_node_matched?(node, _signature)
|
|
233
|
+
freeze_node?(node)
|
|
234
|
+
end
|
|
235
|
+
|
|
236
|
+
# Merge two matched nodes - for containers, recursively merge children
|
|
237
|
+
# Emits to emitter instead of result
|
|
238
|
+
# @param template_node [NodeWrapper] Template node
|
|
239
|
+
# @param dest_node [NodeWrapper] Destination node
|
|
240
|
+
# @param template_analysis [FileAnalysis] Template analysis
|
|
241
|
+
# @param dest_analysis [FileAnalysis] Destination analysis
|
|
242
|
+
def merge_matched_nodes_to_emitter(template_node, dest_node, template_analysis, dest_analysis, dest_owners: nil)
|
|
243
|
+
if preference_for_pair(template_node, dest_node) == :template
|
|
244
|
+
emit_retained_gap_before_matched_template_node(dest_node, dest_analysis, owners: dest_owners)
|
|
245
|
+
end
|
|
246
|
+
|
|
247
|
+
if dest_node.container? && template_node.container?
|
|
248
|
+
# Both are containers - recursively merge their children
|
|
249
|
+
merge_container_to_emitter(template_node, dest_node, template_analysis, dest_analysis)
|
|
250
|
+
elsif dest_node.pair? && template_node.pair?
|
|
251
|
+
# Both are pairs - check if their values are OBJECTS (not arrays) that need recursive merge
|
|
252
|
+
template_value = template_node.value_node
|
|
253
|
+
dest_value = dest_node.value_node
|
|
254
|
+
|
|
255
|
+
# Only recursively merge if BOTH values are objects.
|
|
256
|
+
# Arrays are file-owned atoms unless a caller explicitly changes the
|
|
257
|
+
# preference; their element order and formatting must survive.
|
|
258
|
+
if recursively_merge_pair_values?(template_value, dest_value)
|
|
259
|
+
key_name = dest_node.key_name || template_node.key_name
|
|
260
|
+
comment_source_node, comment_source_analysis = preferred_comment_source(
|
|
261
|
+
dest_node,
|
|
262
|
+
dest_analysis,
|
|
263
|
+
fallback_node: template_node,
|
|
264
|
+
fallback_analysis: template_analysis
|
|
265
|
+
)
|
|
266
|
+
comment_attachment = shared_line_comment_attachment_for(comment_source_node, comment_source_analysis)
|
|
267
|
+
inline_source_node, inline_source_analysis, inline_attachment = preferred_available_inline_attachment(
|
|
268
|
+
template_node,
|
|
269
|
+
template_analysis,
|
|
270
|
+
dest_node,
|
|
271
|
+
dest_analysis
|
|
272
|
+
)
|
|
273
|
+
|
|
274
|
+
emit_preferred_leading_comments_for(comment_source_node, comment_source_analysis,
|
|
275
|
+
shared_attachment: comment_attachment)
|
|
276
|
+
trailing_source_node, trailing_source_analysis = preferred_container_comment_source(
|
|
277
|
+
dest_value,
|
|
278
|
+
dest_analysis,
|
|
279
|
+
fallback_node: template_value,
|
|
280
|
+
fallback_analysis: template_analysis
|
|
281
|
+
)
|
|
282
|
+
compact_source_node = trailing_source_node || dest_value || template_value
|
|
283
|
+
|
|
284
|
+
with_resolution_path_segment(dest_node, template_node) do
|
|
285
|
+
if compact_empty_container?(template_value, compact_source_node, trailing_source_analysis)
|
|
286
|
+
emit_with_preferred_inline_comment(inline_source_node, inline_source_analysis,
|
|
287
|
+
shared_attachment: inline_attachment) do |inline_text|
|
|
288
|
+
@emitter.emit_pair(key_name, compact_container_literal_for(template_value),
|
|
289
|
+
inline_comment: inline_text)
|
|
290
|
+
end
|
|
291
|
+
elsif template_value.object?
|
|
292
|
+
emit_with_preferred_inline_comment(inline_source_node, inline_source_analysis,
|
|
293
|
+
shared_attachment: inline_attachment) do |inline_text|
|
|
294
|
+
@emitter.emit_nested_object_start(key_name, inline_comment: inline_text)
|
|
295
|
+
end
|
|
296
|
+
elsif template_value.array?
|
|
297
|
+
emit_with_preferred_inline_comment(inline_source_node, inline_source_analysis,
|
|
298
|
+
shared_attachment: inline_attachment) do |inline_text|
|
|
299
|
+
@emitter.emit_array_start(key_name, inline_comment: inline_text)
|
|
300
|
+
end
|
|
301
|
+
end
|
|
302
|
+
|
|
303
|
+
unless compact_empty_container?(template_value, compact_source_node, trailing_source_analysis)
|
|
304
|
+
merge_node_lists_to_emitter(
|
|
305
|
+
template_value.mergeable_children,
|
|
306
|
+
dest_value.mergeable_children,
|
|
307
|
+
template_analysis,
|
|
308
|
+
dest_analysis,
|
|
309
|
+
dest_owners: dest_value.mergeable_children
|
|
310
|
+
)
|
|
311
|
+
|
|
312
|
+
emit_container_trailing_lines(trailing_source_node, trailing_source_analysis)
|
|
313
|
+
|
|
314
|
+
if template_value.object?
|
|
315
|
+
@emitter.emit_nested_object_end
|
|
316
|
+
elsif template_value.array?
|
|
317
|
+
@emitter.emit_array_end
|
|
318
|
+
end
|
|
319
|
+
end
|
|
320
|
+
end
|
|
321
|
+
elsif preference_for_pair(template_node, dest_node) == :destination
|
|
322
|
+
# Values are not both objects, or one/both are arrays - use preference and emit
|
|
323
|
+
# Arrays are always replaced, not merged
|
|
324
|
+
record_unresolved_choice(
|
|
325
|
+
template_node: template_node,
|
|
326
|
+
dest_node: dest_node,
|
|
327
|
+
match_kind: :pair_value
|
|
328
|
+
)
|
|
329
|
+
emit_atomic_node(dest_node, dest_analysis)
|
|
330
|
+
else
|
|
331
|
+
record_unresolved_choice(
|
|
332
|
+
template_node: template_node,
|
|
333
|
+
dest_node: dest_node,
|
|
334
|
+
match_kind: :pair_value
|
|
335
|
+
)
|
|
336
|
+
emit_atomic_node(
|
|
337
|
+
template_node,
|
|
338
|
+
template_analysis,
|
|
339
|
+
comment_source_node: dest_node,
|
|
340
|
+
comment_analysis: dest_analysis
|
|
341
|
+
)
|
|
342
|
+
end
|
|
343
|
+
elsif preference_for_pair(template_node, dest_node) == :destination
|
|
344
|
+
# Leaf nodes or mismatched types - use preference
|
|
345
|
+
record_unresolved_choice(
|
|
346
|
+
template_node: template_node,
|
|
347
|
+
dest_node: dest_node,
|
|
348
|
+
match_kind: :node_value
|
|
349
|
+
)
|
|
350
|
+
emit_atomic_node(dest_node, dest_analysis)
|
|
351
|
+
else
|
|
352
|
+
record_unresolved_choice(
|
|
353
|
+
template_node: template_node,
|
|
354
|
+
dest_node: dest_node,
|
|
355
|
+
match_kind: :node_value
|
|
356
|
+
)
|
|
357
|
+
emit_atomic_node(
|
|
358
|
+
template_node,
|
|
359
|
+
template_analysis,
|
|
360
|
+
comment_source_node: dest_node,
|
|
361
|
+
comment_analysis: dest_analysis
|
|
362
|
+
)
|
|
363
|
+
end
|
|
364
|
+
end
|
|
365
|
+
|
|
366
|
+
def emit_retained_gap_before_matched_template_node(dest_node, dest_analysis, owners: nil)
|
|
367
|
+
gap_lines = retained_owner_leading_gap_lines_for(dest_node, dest_analysis, owners: owners)
|
|
368
|
+
return if gap_lines.empty?
|
|
369
|
+
return if @emitter.blank_lines?(gap_lines) && @emitter.ends_with_blank_line?
|
|
370
|
+
|
|
371
|
+
gap = retained_owner_leading_gap_for(dest_node, dest_analysis, owners: owners)
|
|
372
|
+
@emitter.emit_raw_lines(gap_lines, metadata: emitter_block_metadata(dest_analysis, gap.start_line))
|
|
373
|
+
end
|
|
374
|
+
|
|
375
|
+
# Merge container nodes by emitting via emitter
|
|
376
|
+
# @param template_node [NodeWrapper] Template container node
|
|
377
|
+
# @param dest_node [NodeWrapper] Destination container node
|
|
378
|
+
# @param template_analysis [FileAnalysis] Template analysis
|
|
379
|
+
# @param dest_analysis [FileAnalysis] Destination analysis
|
|
380
|
+
def merge_container_to_emitter(template_node, dest_node, template_analysis, dest_analysis)
|
|
381
|
+
if dest_node.object?
|
|
382
|
+
@emitter.emit_object_start
|
|
383
|
+
elsif dest_node.array?
|
|
384
|
+
@emitter.emit_array_start
|
|
385
|
+
end
|
|
386
|
+
|
|
387
|
+
merge_node_lists_to_emitter(
|
|
388
|
+
template_node.mergeable_children,
|
|
389
|
+
dest_node.mergeable_children,
|
|
390
|
+
template_analysis,
|
|
391
|
+
dest_analysis
|
|
392
|
+
)
|
|
393
|
+
|
|
394
|
+
trailing_source_node, trailing_source_analysis = preferred_container_comment_source(
|
|
395
|
+
dest_node,
|
|
396
|
+
dest_analysis,
|
|
397
|
+
fallback_node: template_node,
|
|
398
|
+
fallback_analysis: template_analysis
|
|
399
|
+
)
|
|
400
|
+
emit_container_trailing_lines(trailing_source_node, trailing_source_analysis)
|
|
401
|
+
|
|
402
|
+
if dest_node.object?
|
|
403
|
+
@emitter.emit_object_end
|
|
404
|
+
elsif dest_node.array?
|
|
405
|
+
@emitter.emit_array_end
|
|
406
|
+
end
|
|
407
|
+
end
|
|
408
|
+
|
|
409
|
+
def preference_for_pair(template_node, dest_node)
|
|
410
|
+
return @preference unless @preference.is_a?(Hash)
|
|
411
|
+
|
|
412
|
+
typed_template = apply_node_typing(template_node)
|
|
413
|
+
typed_dest = apply_node_typing(dest_node)
|
|
414
|
+
|
|
415
|
+
if Ast::Merge::NodeTyping.typed_node?(typed_template)
|
|
416
|
+
merge_type = Ast::Merge::NodeTyping.merge_type_for(typed_template)
|
|
417
|
+
return @preference.fetch(merge_type) { default_preference } if merge_type
|
|
418
|
+
end
|
|
419
|
+
|
|
420
|
+
if Ast::Merge::NodeTyping.typed_node?(typed_dest)
|
|
421
|
+
merge_type = Ast::Merge::NodeTyping.merge_type_for(typed_dest)
|
|
422
|
+
return @preference.fetch(merge_type) { default_preference } if merge_type
|
|
423
|
+
end
|
|
424
|
+
|
|
425
|
+
default_preference
|
|
426
|
+
end
|
|
427
|
+
|
|
428
|
+
def apply_node_typing(node)
|
|
429
|
+
return node unless @node_typing
|
|
430
|
+
return node unless node
|
|
431
|
+
|
|
432
|
+
Ast::Merge::NodeTyping.process(node, @node_typing)
|
|
433
|
+
end
|
|
434
|
+
|
|
435
|
+
def record_unresolved_choice(template_node:, dest_node:, match_kind:)
|
|
436
|
+
return unless unresolved_mode?
|
|
437
|
+
return unless template_node && dest_node
|
|
438
|
+
|
|
439
|
+
template_text = node_resolution_text(template_node)
|
|
440
|
+
dest_text = node_resolution_text(dest_node)
|
|
441
|
+
return if template_text == dest_text
|
|
442
|
+
|
|
443
|
+
provisional_winner = preference_for_pair(template_node, dest_node) == :template ? :template : :destination
|
|
444
|
+
key_name = resolution_key_name(template_node, dest_node)
|
|
445
|
+
surface_path = resolution_surface_path(template_node, dest_node)
|
|
446
|
+
metadata = {
|
|
447
|
+
match_kind: match_kind,
|
|
448
|
+
node_type: dest_node.respond_to?(:type) ? dest_node.type : nil,
|
|
449
|
+
key_name: key_name,
|
|
450
|
+
review_identity: review_identity_for_unresolved_choice(
|
|
451
|
+
template_text: template_text,
|
|
452
|
+
destination_text: dest_text,
|
|
453
|
+
provisional_winner: provisional_winner,
|
|
454
|
+
surface_path: surface_path,
|
|
455
|
+
match_kind: match_kind,
|
|
456
|
+
key_name: key_name
|
|
457
|
+
)
|
|
458
|
+
}.compact
|
|
459
|
+
|
|
460
|
+
record_unresolved_node_choice(
|
|
461
|
+
result: @result,
|
|
462
|
+
template_node: template_node,
|
|
463
|
+
destination_node: dest_node,
|
|
464
|
+
template_text: template_text,
|
|
465
|
+
destination_text: dest_text,
|
|
466
|
+
provisional_winner: provisional_winner,
|
|
467
|
+
case_prefix: 'json',
|
|
468
|
+
case_parts: [match_kind, metadata[:key_name]],
|
|
469
|
+
surface_path: surface_path,
|
|
470
|
+
metadata: metadata,
|
|
471
|
+
conflict_fields: {
|
|
472
|
+
match_kind: match_kind,
|
|
473
|
+
key_name: metadata[:key_name]
|
|
474
|
+
}
|
|
475
|
+
)
|
|
476
|
+
end
|
|
477
|
+
|
|
478
|
+
def node_resolution_text(node)
|
|
479
|
+
return unless node.respond_to?(:text)
|
|
480
|
+
|
|
481
|
+
node.text
|
|
482
|
+
end
|
|
483
|
+
|
|
484
|
+
def resolution_key_name(template_node, dest_node)
|
|
485
|
+
unresolved_identifier_for_nodes(dest_node, template_node, methods: [:key_name])
|
|
486
|
+
end
|
|
487
|
+
|
|
488
|
+
def resolution_surface_path(template_node, dest_node)
|
|
489
|
+
segment = resolution_path_segment_for(template_node, dest_node)
|
|
490
|
+
line = dest_node.respond_to?(:start_line) ? dest_node.start_line : nil
|
|
491
|
+
unresolved_surface_path_for(segment, fallback_segment: (line ? "line[#{line}]" : nil))
|
|
492
|
+
end
|
|
493
|
+
|
|
494
|
+
def resolution_path_segment_for(template_node, dest_node)
|
|
495
|
+
key_name = resolution_key_name(template_node, dest_node)
|
|
496
|
+
return "pair[#{key_name.inspect}]" if key_name
|
|
497
|
+
|
|
498
|
+
nil
|
|
499
|
+
end
|
|
500
|
+
|
|
501
|
+
def with_resolution_path_segment(*nodes, &block)
|
|
502
|
+
with_first_unresolved_path_segment(*nodes, segment_builder: lambda { |node|
|
|
503
|
+
resolution_path_segment_for(node, node)
|
|
504
|
+
}, &block)
|
|
505
|
+
end
|
|
506
|
+
|
|
507
|
+
# Emit a single node to the emitter
|
|
508
|
+
# @param node [NodeWrapper] Node to emit
|
|
509
|
+
# @param analysis [FileAnalysis] Analysis for accessing source
|
|
510
|
+
def emit_node(node, analysis, comment_source_node: nil, comment_analysis: analysis)
|
|
511
|
+
return if freeze_node?(node)
|
|
512
|
+
|
|
513
|
+
source_node = comment_source_node || node
|
|
514
|
+
source_analysis = comment_source_node ? comment_analysis : analysis
|
|
515
|
+
source_attachment = shared_line_comment_attachment_for(source_node, source_analysis)
|
|
516
|
+
_inline_source_node, _inline_source_analysis, inline_attachment =
|
|
517
|
+
preferred_available_inline_attachment(
|
|
518
|
+
node,
|
|
519
|
+
analysis,
|
|
520
|
+
comment_source_node,
|
|
521
|
+
source_analysis,
|
|
522
|
+
preferred_node: node,
|
|
523
|
+
preferred_analysis: analysis
|
|
524
|
+
)
|
|
525
|
+
|
|
526
|
+
emit_preferred_leading_comments_for(source_node, source_analysis, shared_attachment: source_attachment)
|
|
527
|
+
|
|
528
|
+
if node.pair?
|
|
529
|
+
# Emit as pair
|
|
530
|
+
key = node.key_name
|
|
531
|
+
value_node = node.value_node
|
|
532
|
+
source_value_node = source_node.respond_to?(:value_node) ? source_node.value_node : nil
|
|
533
|
+
|
|
534
|
+
if value_node
|
|
535
|
+
# Check if value is an object (not array) and needs recursive emission
|
|
536
|
+
if value_node.container?
|
|
537
|
+
container_comment_source = source_value_node || value_node
|
|
538
|
+
|
|
539
|
+
if compact_empty_container?(value_node, container_comment_source, source_analysis)
|
|
540
|
+
emit_with_preferred_inline_comment(node, analysis,
|
|
541
|
+
shared_attachment: inline_attachment) do |inline_text|
|
|
542
|
+
if key
|
|
543
|
+
@emitter.emit_pair(
|
|
544
|
+
key,
|
|
545
|
+
compact_container_literal_for(value_node),
|
|
546
|
+
inline_comment: inline_text,
|
|
547
|
+
metadata: emitter_line_metadata(analysis, line_number: node.start_line)
|
|
548
|
+
)
|
|
549
|
+
end
|
|
550
|
+
end
|
|
551
|
+
elsif value_node.object?
|
|
552
|
+
emit_with_preferred_inline_comment(node, analysis,
|
|
553
|
+
shared_attachment: inline_attachment) do |inline_text|
|
|
554
|
+
@emitter.emit_nested_object_start(
|
|
555
|
+
key,
|
|
556
|
+
inline_comment: inline_text,
|
|
557
|
+
metadata: emitter_line_metadata(analysis, line_number: node.start_line)
|
|
558
|
+
)
|
|
559
|
+
end
|
|
560
|
+
elsif value_node.array?
|
|
561
|
+
emit_with_preferred_inline_comment(node, analysis,
|
|
562
|
+
shared_attachment: inline_attachment) do |inline_text|
|
|
563
|
+
@emitter.emit_array_start(
|
|
564
|
+
key,
|
|
565
|
+
inline_comment: inline_text,
|
|
566
|
+
metadata: emitter_line_metadata(analysis, line_number: node.start_line)
|
|
567
|
+
)
|
|
568
|
+
end
|
|
569
|
+
end
|
|
570
|
+
|
|
571
|
+
unless compact_empty_container?(value_node, container_comment_source, source_analysis)
|
|
572
|
+
value_node.mergeable_children.each do |child|
|
|
573
|
+
emit_node(child, analysis)
|
|
574
|
+
end
|
|
575
|
+
|
|
576
|
+
emit_container_trailing_lines(container_comment_source, source_analysis)
|
|
577
|
+
|
|
578
|
+
if value_node.object?
|
|
579
|
+
@emitter.emit_nested_object_end(metadata: emitter_line_metadata(analysis, line_number: node.end_line))
|
|
580
|
+
elsif value_node.array?
|
|
581
|
+
@emitter.emit_array_end(metadata: emitter_line_metadata(analysis, line_number: node.end_line))
|
|
582
|
+
end
|
|
583
|
+
end
|
|
584
|
+
else
|
|
585
|
+
emit_with_preferred_inline_comment(node, analysis, shared_attachment: inline_attachment) do |inline_text|
|
|
586
|
+
if key
|
|
587
|
+
@emitter.emit_pair(
|
|
588
|
+
key,
|
|
589
|
+
value_node.text,
|
|
590
|
+
inline_comment: inline_text,
|
|
591
|
+
metadata: emitter_line_metadata(analysis, line_number: node.start_line)
|
|
592
|
+
)
|
|
593
|
+
end
|
|
594
|
+
end
|
|
595
|
+
end
|
|
596
|
+
end
|
|
597
|
+
elsif node.container?
|
|
598
|
+
if node.object?
|
|
599
|
+
@emitter.emit_object_start(metadata: emitter_line_metadata(analysis, line_number: node.start_line))
|
|
600
|
+
elsif node.array?
|
|
601
|
+
@emitter.emit_array_start(metadata: emitter_line_metadata(analysis, line_number: node.start_line))
|
|
602
|
+
end
|
|
603
|
+
|
|
604
|
+
node.mergeable_children.each do |child|
|
|
605
|
+
emit_node(child, analysis)
|
|
606
|
+
end
|
|
607
|
+
|
|
608
|
+
emit_container_trailing_lines(source_node, source_analysis)
|
|
609
|
+
|
|
610
|
+
if node.object?
|
|
611
|
+
@emitter.emit_object_end(metadata: emitter_line_metadata(analysis, line_number: node.end_line))
|
|
612
|
+
elsif node.array?
|
|
613
|
+
@emitter.emit_array_end(metadata: emitter_line_metadata(analysis, line_number: node.end_line))
|
|
614
|
+
end
|
|
615
|
+
elsif node.start_line && node.end_line
|
|
616
|
+
if node.start_line == node.end_line
|
|
617
|
+
emit_with_preferred_inline_comment(node, analysis, shared_attachment: inline_attachment) do |inline_text|
|
|
618
|
+
@emitter.emit_array_element(
|
|
619
|
+
node.text,
|
|
620
|
+
inline_comment: inline_text,
|
|
621
|
+
metadata: emitter_line_metadata(analysis, line_number: node.start_line)
|
|
622
|
+
)
|
|
623
|
+
end
|
|
624
|
+
else
|
|
625
|
+
lines = []
|
|
626
|
+
(node.start_line..node.end_line).each do |ln|
|
|
627
|
+
line = analysis.line_at(ln)
|
|
628
|
+
lines << line if line
|
|
629
|
+
end
|
|
630
|
+
@emitter.emit_raw_lines(lines, metadata: emitter_block_metadata(analysis, node.start_line))
|
|
631
|
+
end
|
|
632
|
+
end
|
|
633
|
+
end
|
|
634
|
+
|
|
635
|
+
def emit_atomic_node(node, analysis, comment_source_node: nil, comment_analysis: analysis)
|
|
636
|
+
return if freeze_node?(node)
|
|
637
|
+
unless @preserve_atomic_formatting
|
|
638
|
+
return emit_node(node, analysis, comment_source_node: comment_source_node,
|
|
639
|
+
comment_analysis: comment_analysis)
|
|
640
|
+
end
|
|
641
|
+
unless node.respond_to?(:text)
|
|
642
|
+
return emit_node(node, analysis, comment_source_node: comment_source_node,
|
|
643
|
+
comment_analysis: comment_analysis)
|
|
644
|
+
end
|
|
645
|
+
|
|
646
|
+
source_node = comment_source_node || node
|
|
647
|
+
source_analysis = comment_source_node ? comment_analysis : analysis
|
|
648
|
+
source_attachment = shared_line_comment_attachment_for(source_node, source_analysis)
|
|
649
|
+
inline_attachment = shared_inline_comment_attachment_for(source_node, source_analysis)
|
|
650
|
+
|
|
651
|
+
emit_preferred_leading_comments_for(source_node, source_analysis, shared_attachment: source_attachment)
|
|
652
|
+
emit_with_preferred_inline_comment(node, analysis, shared_attachment: inline_attachment) do |inline_text|
|
|
653
|
+
fragment = reindented_source_fragment(node, analysis)
|
|
654
|
+
fragment = "#{fragment} // #{inline_text}" if inline_text && !inline_text.empty?
|
|
655
|
+
@emitter.emit_raw_fragment(
|
|
656
|
+
fragment,
|
|
657
|
+
metadata: emitter_block_metadata(analysis, node.start_line)
|
|
658
|
+
)
|
|
659
|
+
end
|
|
660
|
+
end
|
|
661
|
+
|
|
662
|
+
def reindented_source_fragment(node, analysis)
|
|
663
|
+
fragment = node.text.to_s
|
|
664
|
+
source_line = node.respond_to?(:start_line) && node.start_line ? analysis.line_at(node.start_line).to_s : ''
|
|
665
|
+
source_indent = leading_indent(source_line)
|
|
666
|
+
return fragment if source_indent.empty?
|
|
667
|
+
|
|
668
|
+
fragment.lines(chomp: true).map do |line|
|
|
669
|
+
line.start_with?(source_indent) ? line.delete_prefix(source_indent) : line
|
|
670
|
+
end.join("\n")
|
|
671
|
+
end
|
|
672
|
+
|
|
673
|
+
def leading_indent(line)
|
|
674
|
+
indent = +''
|
|
675
|
+
line.each_char do |char|
|
|
676
|
+
break unless [' ', "\t"].include?(char)
|
|
677
|
+
|
|
678
|
+
indent << char
|
|
679
|
+
end
|
|
680
|
+
indent
|
|
681
|
+
end
|
|
682
|
+
|
|
683
|
+
def recursively_merge_pair_values?(template_value, dest_value)
|
|
684
|
+
return true if template_value&.object? && dest_value&.object?
|
|
685
|
+
return true if @merge_arrays && template_value&.array? && dest_value&.array?
|
|
686
|
+
|
|
687
|
+
false
|
|
688
|
+
end
|
|
689
|
+
|
|
690
|
+
def preferred_comment_source(node, analysis, fallback_node: nil, fallback_analysis: nil)
|
|
691
|
+
return [node, analysis] if node_has_emittable_leading_comments?(node, analysis)
|
|
692
|
+
return [fallback_node, fallback_analysis] if fallback_node && node_has_emittable_leading_comments?(
|
|
693
|
+
fallback_node, fallback_analysis
|
|
694
|
+
)
|
|
695
|
+
|
|
696
|
+
[node, analysis]
|
|
697
|
+
end
|
|
698
|
+
|
|
699
|
+
def preferred_available_inline_attachment(template_node, template_analysis, dest_node, dest_analysis,
|
|
700
|
+
preferred_node: nil, preferred_analysis: nil)
|
|
701
|
+
if preferred_node && preferred_analysis
|
|
702
|
+
primary_node = preferred_node
|
|
703
|
+
primary_analysis = preferred_analysis
|
|
704
|
+
fallback_node = preferred_node.equal?(template_node) && preferred_analysis.equal?(template_analysis) ? dest_node : template_node
|
|
705
|
+
fallback_analysis = fallback_node.equal?(dest_node) ? dest_analysis : template_analysis
|
|
706
|
+
elsif preference_for_pair(template_node, dest_node) == :destination
|
|
707
|
+
primary_node = dest_node
|
|
708
|
+
primary_analysis = dest_analysis
|
|
709
|
+
fallback_node = template_node
|
|
710
|
+
fallback_analysis = template_analysis
|
|
711
|
+
else
|
|
712
|
+
primary_node = template_node
|
|
713
|
+
primary_analysis = template_analysis
|
|
714
|
+
fallback_node = dest_node
|
|
715
|
+
fallback_analysis = dest_analysis
|
|
716
|
+
end
|
|
717
|
+
|
|
718
|
+
primary_attachment = shared_inline_comment_attachment_for(primary_node, primary_analysis)
|
|
719
|
+
if primary_attachment&.inline_region && !primary_attachment.inline_region.empty?
|
|
720
|
+
return [primary_node, primary_analysis,
|
|
721
|
+
primary_attachment]
|
|
722
|
+
end
|
|
723
|
+
|
|
724
|
+
fallback_attachment = shared_inline_comment_attachment_for(fallback_node, fallback_analysis)
|
|
725
|
+
if fallback_attachment&.inline_region && !fallback_attachment.inline_region.empty?
|
|
726
|
+
return [fallback_node, fallback_analysis,
|
|
727
|
+
fallback_attachment]
|
|
728
|
+
end
|
|
729
|
+
|
|
730
|
+
[primary_node, primary_analysis, nil]
|
|
731
|
+
end
|
|
732
|
+
|
|
733
|
+
def preferred_container_comment_source(node, analysis, fallback_node: nil, fallback_analysis: nil)
|
|
734
|
+
return [node, analysis] if container_has_trailing_comments?(node, analysis)
|
|
735
|
+
return [fallback_node, fallback_analysis] if fallback_node && container_has_trailing_comments?(fallback_node,
|
|
736
|
+
fallback_analysis)
|
|
737
|
+
|
|
738
|
+
[node, analysis]
|
|
739
|
+
end
|
|
740
|
+
|
|
741
|
+
def node_has_emittable_leading_comments?(node, analysis)
|
|
742
|
+
return false unless node.respond_to?(:start_line) && node.start_line
|
|
743
|
+
|
|
744
|
+
analysis.comment_tracker.leading_comments_before(node.start_line).any?
|
|
745
|
+
end
|
|
746
|
+
|
|
747
|
+
def emit_preferred_leading_comments_for(node, analysis, shared_attachment: nil)
|
|
748
|
+
attachment = shared_attachment || shared_line_comment_attachment_for(node, analysis)
|
|
749
|
+
region = canonical_leading_comment_region(attachment&.leading_region, analysis: analysis, node: node)
|
|
750
|
+
|
|
751
|
+
unless region && !region.empty?
|
|
752
|
+
emit_leading_comments_for(node, analysis)
|
|
753
|
+
return
|
|
754
|
+
end
|
|
755
|
+
|
|
756
|
+
# Bidirectional dedup: skip this region if an identical comment block
|
|
757
|
+
# was already emitted by a preceding node (from either source).
|
|
758
|
+
normalized = region.normalized_content
|
|
759
|
+
if normalized && !normalized.empty? && @emitted_leading_comment_texts&.include?(normalized)
|
|
760
|
+
should_heal = handle_suspected_corruption(
|
|
761
|
+
kind: :comment_ownership_overlap,
|
|
762
|
+
message: 'leading comment region overlaps previously emitted JSON comment ownership',
|
|
763
|
+
context: dedup_warning_context(region: region, analysis: analysis, node: node)
|
|
764
|
+
)
|
|
765
|
+
if should_heal
|
|
766
|
+
emit_blank_lines_in_range((region.end_line || node.start_line).to_i + 1, node.start_line.to_i - 1, analysis)
|
|
767
|
+
return
|
|
768
|
+
end
|
|
769
|
+
end
|
|
770
|
+
@emitted_leading_comment_texts&.add(normalized) if normalized && !normalized.empty?
|
|
771
|
+
|
|
772
|
+
emit_blank_lines_before_leading_comments(region.start_line, analysis)
|
|
773
|
+
if attachment&.leading_region.equal?(region)
|
|
774
|
+
@emitter.emit_comment_attachment(attachment, leading: true, inline: false, source_lines: analysis.lines)
|
|
775
|
+
else
|
|
776
|
+
@emitter.emit_comment_region(region, source_lines: analysis.lines)
|
|
777
|
+
end
|
|
778
|
+
emit_blank_lines_in_range((region.end_line || node.start_line).to_i + 1, node.start_line.to_i - 1, analysis)
|
|
779
|
+
end
|
|
780
|
+
|
|
781
|
+
def emit_leading_comments_for(node, analysis)
|
|
782
|
+
return unless node.respond_to?(:start_line) && node.start_line
|
|
783
|
+
|
|
784
|
+
leading = analysis.comment_tracker.leading_comments_before(node.start_line)
|
|
785
|
+
leading = canonical_tracked_leading_comments(leading, analysis: analysis, node: node)
|
|
786
|
+
return if leading.empty?
|
|
787
|
+
|
|
788
|
+
# Bidirectional dedup: build normalized text from tracked comments
|
|
789
|
+
# and skip if already emitted by a preceding node.
|
|
790
|
+
normalized = leading.map { |c| c[:text].to_s.strip }.join("\n")
|
|
791
|
+
if @emitted_leading_comment_texts&.include?(normalized)
|
|
792
|
+
should_heal = handle_suspected_corruption(
|
|
793
|
+
kind: :comment_ownership_overlap,
|
|
794
|
+
message: 'tracked leading comments overlap previously emitted JSON comment ownership',
|
|
795
|
+
context: dedup_warning_context(
|
|
796
|
+
region: nil,
|
|
797
|
+
analysis: analysis,
|
|
798
|
+
node: node,
|
|
799
|
+
normalized_content: normalized,
|
|
800
|
+
region_lines: [leading.first[:line], comment_end_line(leading.last)]
|
|
801
|
+
)
|
|
802
|
+
)
|
|
803
|
+
if should_heal
|
|
804
|
+
emit_blank_lines_in_range(comment_end_line(leading.last) + 1, node.start_line - 1, analysis)
|
|
805
|
+
return
|
|
806
|
+
end
|
|
807
|
+
end
|
|
808
|
+
@emitted_leading_comment_texts&.add(normalized)
|
|
809
|
+
|
|
810
|
+
emit_blank_lines_before_leading_comments(leading.first[:line], analysis)
|
|
811
|
+
emit_tracked_comments_with_internal_blank_lines(leading, analysis)
|
|
812
|
+
|
|
813
|
+
emit_blank_lines_in_range(comment_end_line(leading.last) + 1, node.start_line - 1, analysis) if leading.any?
|
|
814
|
+
end
|
|
815
|
+
|
|
816
|
+
def canonical_leading_comment_region(region, analysis:, node:)
|
|
817
|
+
return region unless region && !region.empty?
|
|
818
|
+
return region unless analysis.equal?(@dest_analysis)
|
|
819
|
+
return region unless first_statement?(node, analysis)
|
|
820
|
+
|
|
821
|
+
template_region = first_leading_comment_region(@template_analysis)
|
|
822
|
+
return region unless template_region && !template_region.empty?
|
|
823
|
+
|
|
824
|
+
template_nodes = Array(template_region.nodes)
|
|
825
|
+
region_nodes = Array(region.nodes)
|
|
826
|
+
return region if template_nodes.empty? || region_nodes.length < template_nodes.length
|
|
827
|
+
|
|
828
|
+
repeat_count = leading_repeat_count(region_nodes, template_nodes) do |left, right|
|
|
829
|
+
normalized_comment_unit(left) == normalized_comment_unit(right)
|
|
830
|
+
end
|
|
831
|
+
return region if repeat_count < 2
|
|
832
|
+
|
|
833
|
+
remaining_nodes = region_nodes.drop(repeat_count * template_nodes.length)
|
|
834
|
+
return region if remaining_nodes.empty?
|
|
835
|
+
|
|
836
|
+
should_heal = handle_suspected_corruption(
|
|
837
|
+
kind: :duplicate_template_preamble_prefix,
|
|
838
|
+
message: 'leading JSON comment region begins with duplicated template-owned preamble comments',
|
|
839
|
+
context: {
|
|
840
|
+
template_comment_lines: template_nodes.length,
|
|
841
|
+
merged_comment_lines: region_nodes.length,
|
|
842
|
+
destination_specific_comment_lines: remaining_nodes.length
|
|
843
|
+
}
|
|
844
|
+
)
|
|
845
|
+
return region unless should_heal
|
|
846
|
+
|
|
847
|
+
::Ast::Merge::Comment::Region.new(
|
|
848
|
+
kind: region.kind,
|
|
849
|
+
nodes: remaining_nodes,
|
|
850
|
+
metadata: region.metadata
|
|
851
|
+
)
|
|
852
|
+
end
|
|
853
|
+
|
|
854
|
+
def canonical_tracked_leading_comments(leading, analysis:, node:)
|
|
855
|
+
return leading unless analysis.equal?(@dest_analysis)
|
|
856
|
+
return leading unless first_statement?(node, analysis)
|
|
857
|
+
|
|
858
|
+
template_region = first_leading_comment_region(@template_analysis)
|
|
859
|
+
template_units = Array(template_region&.nodes).map { |comment| normalized_comment_unit(comment) }
|
|
860
|
+
return leading if template_units.empty? || leading.length < template_units.length
|
|
861
|
+
|
|
862
|
+
leading_units = leading.map { |comment| normalized_comment_unit(comment) }
|
|
863
|
+
repeat_count = leading_repeat_count(leading_units, template_units)
|
|
864
|
+
return leading if repeat_count < 2
|
|
865
|
+
|
|
866
|
+
remaining_comments = leading.drop(repeat_count * template_units.length)
|
|
867
|
+
return leading if remaining_comments.empty?
|
|
868
|
+
|
|
869
|
+
should_heal = handle_suspected_corruption(
|
|
870
|
+
kind: :duplicate_template_preamble_prefix,
|
|
871
|
+
message: 'tracked JSON leading comments begin with duplicated template-owned preamble comments',
|
|
872
|
+
context: {
|
|
873
|
+
template_comment_lines: template_units.length,
|
|
874
|
+
merged_comment_lines: leading.length,
|
|
875
|
+
destination_specific_comment_lines: remaining_comments.length
|
|
876
|
+
}
|
|
877
|
+
)
|
|
878
|
+
return leading unless should_heal
|
|
879
|
+
|
|
880
|
+
remaining_comments
|
|
881
|
+
end
|
|
882
|
+
|
|
883
|
+
def tracked_inline_comment_for(node, analysis)
|
|
884
|
+
return unless node.respond_to?(:start_line) && node.start_line
|
|
885
|
+
|
|
886
|
+
analysis.comment_tracker.inline_comment_at(inline_comment_line_for(node))
|
|
887
|
+
end
|
|
888
|
+
|
|
889
|
+
def dedup_warning_context(region:, analysis:, node:, normalized_content: nil, region_lines: nil)
|
|
890
|
+
{
|
|
891
|
+
file: analysis.respond_to?(:path) ? analysis.path : nil,
|
|
892
|
+
owner_type: node.respond_to?(:type) ? node.type : node.class.name.split('::').last,
|
|
893
|
+
region_lines: region_lines || [region.respond_to?(:start_line) ? region.start_line : nil,
|
|
894
|
+
region.respond_to?(:end_line) ? region.end_line : nil],
|
|
895
|
+
normalized_content: normalized_content || region&.normalized_content
|
|
896
|
+
}.compact
|
|
897
|
+
end
|
|
898
|
+
|
|
899
|
+
def handle_suspected_corruption(kind:, message:, context:)
|
|
900
|
+
::Ast::Merge::Healer.handle(
|
|
901
|
+
mode: corruption_handling,
|
|
902
|
+
kind: kind,
|
|
903
|
+
message: message,
|
|
904
|
+
prefix: '[json-merge]',
|
|
905
|
+
error_class: Json::Merge::CorruptionDetectedError,
|
|
906
|
+
warner: ->(formatted) { DebugLogger.debug_warning(formatted, context) }
|
|
907
|
+
)
|
|
908
|
+
end
|
|
909
|
+
|
|
910
|
+
def first_leading_comment_region(analysis)
|
|
911
|
+
first_statement = first_owned_node(analysis)
|
|
912
|
+
return unless first_statement
|
|
913
|
+
|
|
914
|
+
shared_line_comment_attachment_for(first_statement, analysis)&.leading_region
|
|
915
|
+
end
|
|
916
|
+
|
|
917
|
+
def first_statement?(node, analysis)
|
|
918
|
+
equivalent_owner?(first_owned_node(analysis), node)
|
|
919
|
+
end
|
|
920
|
+
|
|
921
|
+
def first_owned_node(analysis)
|
|
922
|
+
first_statement = Array(analysis&.statements).first
|
|
923
|
+
return unless first_statement
|
|
924
|
+
|
|
925
|
+
if first_statement.respond_to?(:container?) && first_statement.container? &&
|
|
926
|
+
first_statement.respond_to?(:mergeable_children)
|
|
927
|
+
first_child = Array(first_statement.mergeable_children).first
|
|
928
|
+
return first_child if first_child
|
|
929
|
+
end
|
|
930
|
+
|
|
931
|
+
first_statement
|
|
932
|
+
end
|
|
933
|
+
|
|
934
|
+
def equivalent_owner?(left, right)
|
|
935
|
+
return false unless left && right
|
|
936
|
+
return true if left.equal?(right)
|
|
937
|
+
return false unless left.respond_to?(:type) && right.respond_to?(:type) && left.type == right.type
|
|
938
|
+
unless left.respond_to?(:start_line) && right.respond_to?(:start_line) && left.start_line == right.start_line
|
|
939
|
+
return false
|
|
940
|
+
end
|
|
941
|
+
unless left.respond_to?(:end_line) && right.respond_to?(:end_line) && left.end_line == right.end_line
|
|
942
|
+
return false
|
|
943
|
+
end
|
|
944
|
+
|
|
945
|
+
if left.respond_to?(:key_name) && right.respond_to?(:key_name)
|
|
946
|
+
left.key_name == right.key_name
|
|
947
|
+
else
|
|
948
|
+
true
|
|
949
|
+
end
|
|
950
|
+
end
|
|
951
|
+
|
|
952
|
+
def leading_repeat_count(lines, prefix, &comparator)
|
|
953
|
+
return 0 if prefix.empty? || lines.length < prefix.length
|
|
954
|
+
|
|
955
|
+
comparator ||= ->(left, right) { left == right }
|
|
956
|
+
count = 0
|
|
957
|
+
count += 1 while prefix_match?(lines.drop(count * prefix.length).first(prefix.length), prefix, comparator)
|
|
958
|
+
count
|
|
959
|
+
end
|
|
960
|
+
|
|
961
|
+
def prefix_match?(candidate, prefix, comparator)
|
|
962
|
+
return false unless candidate && candidate.length == prefix.length
|
|
963
|
+
|
|
964
|
+
candidate.zip(prefix).all? { |left, right| comparator.call(left, right) }
|
|
965
|
+
end
|
|
966
|
+
|
|
967
|
+
def normalized_comment_unit(comment)
|
|
968
|
+
return comment.normalized_content if comment.respond_to?(:normalized_content)
|
|
969
|
+
return comment[:text].to_s.strip if comment.is_a?(Hash)
|
|
970
|
+
|
|
971
|
+
comment.to_s.strip
|
|
972
|
+
end
|
|
973
|
+
|
|
974
|
+
def emit_with_preferred_inline_comment(node, analysis, shared_attachment: nil)
|
|
975
|
+
tracked_inline_comment = tracked_inline_comment_for(node, analysis)
|
|
976
|
+
attachment = shared_attachment || shared_line_comment_attachment_for(node, analysis)
|
|
977
|
+
inline_region = attachment&.inline_region
|
|
978
|
+
|
|
979
|
+
unless inline_region && !inline_region.empty?
|
|
980
|
+
if tracked_inline_comment
|
|
981
|
+
raise MissingSharedInlineRegionError,
|
|
982
|
+
"Expected shared inline region for tracked inline comment at line #{tracked_inline_comment[:line]}"
|
|
983
|
+
end
|
|
984
|
+
|
|
985
|
+
yield nil
|
|
986
|
+
return
|
|
987
|
+
end
|
|
988
|
+
|
|
989
|
+
yield nil
|
|
990
|
+
@emitter.emit_comment_attachment(attachment, leading: false, inline: true, source_lines: analysis.lines)
|
|
991
|
+
end
|
|
992
|
+
|
|
993
|
+
def shared_line_comment_attachment_for(node, analysis)
|
|
994
|
+
return unless node && analysis
|
|
995
|
+
return unless node.respond_to?(:start_line) && node.start_line
|
|
996
|
+
|
|
997
|
+
tracker = analysis.comment_tracker
|
|
998
|
+
leading_comments = tracker.leading_comments_before(node.start_line)
|
|
999
|
+
return if leading_comments.any? { |comment| comment[:block] }
|
|
1000
|
+
|
|
1001
|
+
inline_comment = tracker.inline_comment_at(inline_comment_line_for(node))
|
|
1002
|
+
return unless leading_comments.any? || inline_comment
|
|
1003
|
+
|
|
1004
|
+
analysis.comment_attachment_for(
|
|
1005
|
+
node,
|
|
1006
|
+
line_num: node.start_line,
|
|
1007
|
+
leading_comments: leading_comments,
|
|
1008
|
+
inline_comment: inline_comment
|
|
1009
|
+
)
|
|
1010
|
+
end
|
|
1011
|
+
|
|
1012
|
+
def shared_inline_comment_attachment_for(node, analysis)
|
|
1013
|
+
return unless node && analysis
|
|
1014
|
+
return unless node.respond_to?(:start_line) && node.start_line
|
|
1015
|
+
|
|
1016
|
+
inline_comment = analysis.comment_tracker.inline_comment_at(inline_comment_line_for(node))
|
|
1017
|
+
return unless inline_comment
|
|
1018
|
+
|
|
1019
|
+
analysis.comment_attachment_for(
|
|
1020
|
+
node,
|
|
1021
|
+
line_num: node.start_line,
|
|
1022
|
+
leading_comments: [],
|
|
1023
|
+
inline_comment: inline_comment
|
|
1024
|
+
)
|
|
1025
|
+
end
|
|
1026
|
+
|
|
1027
|
+
def inline_comment_line_for(node)
|
|
1028
|
+
return unless node
|
|
1029
|
+
return node.start_line if node.respond_to?(:pair?) && node.pair?
|
|
1030
|
+
return node.start_line if node.respond_to?(:container?) && node.container?
|
|
1031
|
+
|
|
1032
|
+
node.end_line || node.start_line
|
|
1033
|
+
end
|
|
1034
|
+
|
|
1035
|
+
def emit_container_trailing_lines(container_node, analysis)
|
|
1036
|
+
range = trailing_container_line_range(container_node)
|
|
1037
|
+
return unless range
|
|
1038
|
+
|
|
1039
|
+
region = shared_trailing_line_comment_region_for(range, analysis)
|
|
1040
|
+
unless region && !region.empty?
|
|
1041
|
+
emit_comment_and_blank_lines_in_range(range.begin, range.end, analysis)
|
|
1042
|
+
return
|
|
1043
|
+
end
|
|
1044
|
+
|
|
1045
|
+
emit_blank_lines_in_range(range.begin, region.start_line - 1, analysis)
|
|
1046
|
+
@emitter.emit_comment_region(region, source_lines: analysis.lines)
|
|
1047
|
+
emit_blank_lines_in_range(region.end_line + 1, range.end, analysis)
|
|
1048
|
+
end
|
|
1049
|
+
|
|
1050
|
+
def container_has_trailing_comments?(container_node, analysis)
|
|
1051
|
+
range = trailing_container_line_range(container_node)
|
|
1052
|
+
return false unless range
|
|
1053
|
+
|
|
1054
|
+
range.any? do |line_num|
|
|
1055
|
+
stripped = analysis.line_at(line_num).to_s.strip
|
|
1056
|
+
comment_like_line?(stripped)
|
|
1057
|
+
end
|
|
1058
|
+
end
|
|
1059
|
+
|
|
1060
|
+
def trailing_container_line_range(container_node)
|
|
1061
|
+
return unless container_node&.container?
|
|
1062
|
+
return unless container_node.respond_to?(:start_line) && container_node.respond_to?(:end_line)
|
|
1063
|
+
return unless container_node.start_line && container_node.end_line
|
|
1064
|
+
|
|
1065
|
+
children = container_node.mergeable_children
|
|
1066
|
+
start_line = if children.any?
|
|
1067
|
+
last_child = children.last
|
|
1068
|
+
(last_child.end_line || last_child.start_line) + 1
|
|
1069
|
+
else
|
|
1070
|
+
container_node.start_line + 1
|
|
1071
|
+
end
|
|
1072
|
+
end_line = container_node.end_line - 1
|
|
1073
|
+
return if end_line < start_line
|
|
1074
|
+
|
|
1075
|
+
start_line..end_line
|
|
1076
|
+
end
|
|
1077
|
+
|
|
1078
|
+
def emit_comment_and_blank_lines_in_range(start_line, end_line, analysis)
|
|
1079
|
+
return unless start_line && end_line
|
|
1080
|
+
return if end_line < start_line
|
|
1081
|
+
|
|
1082
|
+
lines = []
|
|
1083
|
+
(start_line..end_line).each do |line_num|
|
|
1084
|
+
line = analysis.line_at(line_num)
|
|
1085
|
+
next unless line
|
|
1086
|
+
|
|
1087
|
+
stripped = line.strip
|
|
1088
|
+
next unless stripped.empty? || comment_like_line?(stripped)
|
|
1089
|
+
|
|
1090
|
+
lines << line
|
|
1091
|
+
end
|
|
1092
|
+
|
|
1093
|
+
@emitter.emit_raw_lines(lines) if lines.any?
|
|
1094
|
+
end
|
|
1095
|
+
|
|
1096
|
+
def shared_trailing_line_comment_region_for(range, analysis)
|
|
1097
|
+
return unless range && analysis
|
|
1098
|
+
return unless trailing_range_supports_shared_line_region?(range, analysis)
|
|
1099
|
+
|
|
1100
|
+
region = analysis.comment_region_for_range(range, kind: :trailing, full_line_only: true)
|
|
1101
|
+
return unless region && !region.empty?
|
|
1102
|
+
|
|
1103
|
+
region
|
|
1104
|
+
end
|
|
1105
|
+
|
|
1106
|
+
def trailing_range_supports_shared_line_region?(range, analysis)
|
|
1107
|
+
range.each do |line_num|
|
|
1108
|
+
stripped = analysis.line_at(line_num).to_s.strip
|
|
1109
|
+
next if stripped.empty?
|
|
1110
|
+
return false unless stripped.start_with?('//')
|
|
1111
|
+
return false unless analysis.comment_tracker.full_line_comment?(line_num)
|
|
1112
|
+
end
|
|
1113
|
+
|
|
1114
|
+
true
|
|
1115
|
+
end
|
|
1116
|
+
|
|
1117
|
+
def comment_like_line?(stripped_line)
|
|
1118
|
+
stripped_line.start_with?('//', '/*', '*', '*/')
|
|
1119
|
+
end
|
|
1120
|
+
|
|
1121
|
+
def emit_freeze_block(freeze_node)
|
|
1122
|
+
@emitter.emit_raw_lines(freeze_node.lines)
|
|
1123
|
+
end
|
|
1124
|
+
|
|
1125
|
+
def emit_removed_destination_node_comments(node, analysis)
|
|
1126
|
+
return unless node.respond_to?(:start_line) && node.start_line
|
|
1127
|
+
|
|
1128
|
+
leading_comments = analysis.comment_tracker.leading_comments_before(node.start_line)
|
|
1129
|
+
emit_preferred_leading_comments_for(node, analysis)
|
|
1130
|
+
|
|
1131
|
+
inline_comment = removed_inline_comment_for(node, analysis)
|
|
1132
|
+
if inline_comment
|
|
1133
|
+
@emitter.emit_tracked_comment(normalize_comment_indent(
|
|
1134
|
+
inline_comment.merge(
|
|
1135
|
+
indent: current_emitter_indent,
|
|
1136
|
+
full_line: true,
|
|
1137
|
+
block: inline_comment[:block] || false
|
|
1138
|
+
)
|
|
1139
|
+
))
|
|
1140
|
+
end
|
|
1141
|
+
|
|
1142
|
+
emit_following_removed_node_blank_lines(node, analysis) if leading_comments.any? || inline_comment
|
|
1143
|
+
end
|
|
1144
|
+
|
|
1145
|
+
def emit_following_removed_node_blank_lines(node, analysis)
|
|
1146
|
+
line_num = (node.end_line || node.start_line) + 1
|
|
1147
|
+
first_nonblank_line = line_num
|
|
1148
|
+
|
|
1149
|
+
while first_nonblank_line <= analysis.lines.length && analysis.comment_tracker.blank_line?(first_nonblank_line)
|
|
1150
|
+
first_nonblank_line += 1
|
|
1151
|
+
end
|
|
1152
|
+
|
|
1153
|
+
return if analysis.comment_tracker.full_line_comment?(first_nonblank_line)
|
|
1154
|
+
|
|
1155
|
+
while line_num <= analysis.lines.length && analysis.comment_tracker.blank_line?(line_num)
|
|
1156
|
+
@emitter.emit_blank_line
|
|
1157
|
+
line_num += 1
|
|
1158
|
+
end
|
|
1159
|
+
end
|
|
1160
|
+
|
|
1161
|
+
def removed_inline_comment_for(node, analysis)
|
|
1162
|
+
line_num = inline_comment_line_for(node)
|
|
1163
|
+
return unless line_num
|
|
1164
|
+
|
|
1165
|
+
region = analysis.comment_tracker.inline_comment_region_at(line_num)
|
|
1166
|
+
tracked = Array(region&.metadata&.dig(:tracked_hashes)).first
|
|
1167
|
+
return tracked if tracked
|
|
1168
|
+
|
|
1169
|
+
analysis.comment_tracker.inline_comment_at(line_num) || removed_inline_block_comment_at(line_num, analysis)
|
|
1170
|
+
end
|
|
1171
|
+
|
|
1172
|
+
def removed_inline_block_comment_at(line_num, analysis)
|
|
1173
|
+
line = analysis.line_at(line_num).to_s
|
|
1174
|
+
return if line.empty?
|
|
1175
|
+
|
|
1176
|
+
start_idx = line.index('/*')
|
|
1177
|
+
end_idx = start_idx && line.index('*/', start_idx + 2)
|
|
1178
|
+
return unless start_idx && end_idx
|
|
1179
|
+
|
|
1180
|
+
before_comment = line[0...start_idx].to_s
|
|
1181
|
+
after_comment = line[(end_idx + 2)..].to_s
|
|
1182
|
+
return if before_comment.strip.empty?
|
|
1183
|
+
return unless after_comment.strip.empty?
|
|
1184
|
+
|
|
1185
|
+
quote_count = before_comment.count('"') - before_comment.scan('\\"').count
|
|
1186
|
+
return unless quote_count.even?
|
|
1187
|
+
|
|
1188
|
+
{
|
|
1189
|
+
line: line_num,
|
|
1190
|
+
indent: 0,
|
|
1191
|
+
text: line[(start_idx + 2)...end_idx].to_s.strip,
|
|
1192
|
+
full_line: false,
|
|
1193
|
+
block: true,
|
|
1194
|
+
raw: line[start_idx..(end_idx + 1)]
|
|
1195
|
+
}
|
|
1196
|
+
end
|
|
1197
|
+
|
|
1198
|
+
def emit_tracked_comments_with_internal_blank_lines(comments, analysis)
|
|
1199
|
+
Array(comments).each_with_index do |comment, index|
|
|
1200
|
+
emit_tracked_comment_preserving_raw_layout(comment, analysis)
|
|
1201
|
+
|
|
1202
|
+
next_comment = comments[index + 1]
|
|
1203
|
+
next unless next_comment
|
|
1204
|
+
|
|
1205
|
+
emit_blank_lines_in_range(comment_end_line(comment) + 1, next_comment[:line] - 1, analysis)
|
|
1206
|
+
end
|
|
1207
|
+
end
|
|
1208
|
+
|
|
1209
|
+
def emit_tracked_comment_preserving_raw_layout(comment, analysis)
|
|
1210
|
+
if multiline_block_comment?(comment)
|
|
1211
|
+
lines = (comment[:line]..comment_end_line(comment)).map { |line_num| analysis.line_at(line_num) }.compact
|
|
1212
|
+
@emitter.emit_raw_lines(lines) if lines.any?
|
|
1213
|
+
return
|
|
1214
|
+
end
|
|
1215
|
+
|
|
1216
|
+
@emitter.emit_tracked_comment(normalize_comment_indent(comment))
|
|
1217
|
+
end
|
|
1218
|
+
|
|
1219
|
+
def multiline_block_comment?(comment)
|
|
1220
|
+
comment && comment[:block] && comment_end_line(comment) > comment[:line]
|
|
1221
|
+
end
|
|
1222
|
+
|
|
1223
|
+
def comment_end_line(comment)
|
|
1224
|
+
return unless comment
|
|
1225
|
+
|
|
1226
|
+
comment[:end_line] || comment[:line]
|
|
1227
|
+
end
|
|
1228
|
+
|
|
1229
|
+
def emit_document_prelude(analysis, nodes: [])
|
|
1230
|
+
augmenter = document_comment_augmenter_for(analysis)
|
|
1231
|
+
return unless augmenter
|
|
1232
|
+
|
|
1233
|
+
normalized_nodes = Array(nodes)
|
|
1234
|
+
regions = []
|
|
1235
|
+
preamble = augmenter.preamble_region
|
|
1236
|
+
regions << preamble if preamble && !preamble.empty?
|
|
1237
|
+
|
|
1238
|
+
if normalized_nodes.any?
|
|
1239
|
+
first_attachment = augmenter.attachment_for(normalized_nodes.first)
|
|
1240
|
+
first_leading = first_attachment&.leading_region
|
|
1241
|
+
if first_leading && !first_leading.empty?
|
|
1242
|
+
duplicate = regions.any? do |region|
|
|
1243
|
+
region.start_line == first_leading.start_line && region.end_line == first_leading.end_line
|
|
1244
|
+
end
|
|
1245
|
+
regions << first_leading unless duplicate
|
|
1246
|
+
end
|
|
1247
|
+
end
|
|
1248
|
+
|
|
1249
|
+
if normalized_nodes.empty?
|
|
1250
|
+
augmenter.orphan_regions.each do |region|
|
|
1251
|
+
regions << region if region && !region.empty?
|
|
1252
|
+
end
|
|
1253
|
+
end
|
|
1254
|
+
|
|
1255
|
+
regions.each do |region|
|
|
1256
|
+
@emitter.emit_comment_region(region, source_lines: analysis.lines)
|
|
1257
|
+
end
|
|
1258
|
+
|
|
1259
|
+
return if regions.empty?
|
|
1260
|
+
|
|
1261
|
+
last_region_end = regions.last.end_line
|
|
1262
|
+
if normalized_nodes.any?
|
|
1263
|
+
first_node_start = normalized_nodes.first.start_line
|
|
1264
|
+
if last_region_end && first_node_start
|
|
1265
|
+
emit_blank_lines_in_range(last_region_end + 1, first_node_start - 1,
|
|
1266
|
+
analysis)
|
|
1267
|
+
end
|
|
1268
|
+
elsif last_region_end
|
|
1269
|
+
emit_blank_lines_in_range(last_region_end + 1, analysis.lines.length, analysis)
|
|
1270
|
+
end
|
|
1271
|
+
end
|
|
1272
|
+
|
|
1273
|
+
def emit_document_postlude(analysis, fallback_node: nil)
|
|
1274
|
+
augmenter = document_comment_augmenter_for(analysis)
|
|
1275
|
+
regions = []
|
|
1276
|
+
postlude = augmenter&.postlude_region
|
|
1277
|
+
regions << postlude if postlude && !postlude.empty?
|
|
1278
|
+
|
|
1279
|
+
if fallback_node
|
|
1280
|
+
last_attachment = augmenter&.attachment_for(fallback_node)
|
|
1281
|
+
last_trailing = last_attachment&.trailing_region
|
|
1282
|
+
if last_trailing && !last_trailing.empty?
|
|
1283
|
+
duplicate = regions.any? do |region|
|
|
1284
|
+
region.start_line == last_trailing.start_line && region.end_line == last_trailing.end_line
|
|
1285
|
+
end
|
|
1286
|
+
regions << last_trailing unless duplicate
|
|
1287
|
+
end
|
|
1288
|
+
end
|
|
1289
|
+
|
|
1290
|
+
return if regions.empty?
|
|
1291
|
+
|
|
1292
|
+
first_region = regions.first
|
|
1293
|
+
if fallback_node && first_region.respond_to?(:start_line) && first_region.start_line && fallback_node.respond_to?(:end_line) && fallback_node.end_line && fallback_node.respond_to?(:end_line) && fallback_node.end_line
|
|
1294
|
+
emit_blank_lines_in_range(fallback_node.end_line + 1, first_region.start_line - 1,
|
|
1295
|
+
analysis)
|
|
1296
|
+
end
|
|
1297
|
+
|
|
1298
|
+
regions.each do |region|
|
|
1299
|
+
@emitter.emit_comment_region(region, source_lines: analysis.lines)
|
|
1300
|
+
end
|
|
1301
|
+
end
|
|
1302
|
+
|
|
1303
|
+
def document_comment_augmenter_for(analysis)
|
|
1304
|
+
@document_comment_augmenters ||= {}
|
|
1305
|
+
@document_comment_augmenters[analysis.object_id] ||= analysis.comment_augmenter
|
|
1306
|
+
end
|
|
1307
|
+
|
|
1308
|
+
def emit_blank_lines_in_range(start_line, end_line, analysis)
|
|
1309
|
+
return unless start_line && end_line
|
|
1310
|
+
return if end_line < start_line
|
|
1311
|
+
|
|
1312
|
+
(start_line..end_line).each do |line_num|
|
|
1313
|
+
@emitter.emit_blank_line if analysis.comment_tracker.blank_line?(line_num)
|
|
1314
|
+
end
|
|
1315
|
+
end
|
|
1316
|
+
|
|
1317
|
+
def emit_blank_lines_before_leading_comments(first_comment_line, analysis)
|
|
1318
|
+
return unless first_comment_line
|
|
1319
|
+
|
|
1320
|
+
blank_lines = []
|
|
1321
|
+
line_num = first_comment_line - 1
|
|
1322
|
+
while line_num >= 1 && analysis.comment_tracker.blank_line?(line_num)
|
|
1323
|
+
blank_lines << line_num
|
|
1324
|
+
line_num -= 1
|
|
1325
|
+
end
|
|
1326
|
+
|
|
1327
|
+
blank_lines.reverse_each { @emitter.emit_blank_line }
|
|
1328
|
+
end
|
|
1329
|
+
|
|
1330
|
+
def normalize_comment_indent(comment)
|
|
1331
|
+
return comment unless comment
|
|
1332
|
+
|
|
1333
|
+
comment.merge(indent: current_emitter_indent)
|
|
1334
|
+
end
|
|
1335
|
+
|
|
1336
|
+
def current_emitter_indent
|
|
1337
|
+
@emitter.indent_level * @emitter.indent_size
|
|
1338
|
+
end
|
|
1339
|
+
|
|
1340
|
+
def compact_empty_container?(container_node, source_node, source_analysis)
|
|
1341
|
+
return false unless container_node&.container?
|
|
1342
|
+
return false unless container_node.mergeable_children.empty?
|
|
1343
|
+
|
|
1344
|
+
!container_has_trailing_comments?(source_node || container_node, source_analysis)
|
|
1345
|
+
end
|
|
1346
|
+
|
|
1347
|
+
def compact_container_literal_for(container_node)
|
|
1348
|
+
container_node.object? ? '{}' : '[]'
|
|
1349
|
+
end
|
|
1350
|
+
|
|
1351
|
+
def build_refined_matches(template_nodes, dest_nodes, template_by_sig, dest_by_sig)
|
|
1352
|
+
return {} unless @match_refiner
|
|
1353
|
+
|
|
1354
|
+
matched_sigs = template_by_sig.keys & dest_by_sig.keys
|
|
1355
|
+
|
|
1356
|
+
unmatched_template = template_nodes.reject do |node|
|
|
1357
|
+
sig = @template_analysis.generate_signature(node)
|
|
1358
|
+
sig && matched_sigs.include?(sig)
|
|
1359
|
+
end
|
|
1360
|
+
|
|
1361
|
+
unmatched_dest = dest_nodes.reject do |node|
|
|
1362
|
+
sig = @dest_analysis.generate_signature(node)
|
|
1363
|
+
sig && matched_sigs.include?(sig)
|
|
1364
|
+
end
|
|
1365
|
+
|
|
1366
|
+
return {} if unmatched_template.empty? || unmatched_dest.empty?
|
|
1367
|
+
|
|
1368
|
+
matches = @match_refiner.call(unmatched_template, unmatched_dest, {
|
|
1369
|
+
template_analysis: @template_analysis,
|
|
1370
|
+
dest_analysis: @dest_analysis
|
|
1371
|
+
})
|
|
1372
|
+
|
|
1373
|
+
matches.each_with_object({}) do |match, hash|
|
|
1374
|
+
hash[match.template_node] = match.dest_node
|
|
1375
|
+
end
|
|
1376
|
+
end
|
|
1377
|
+
end
|
|
1378
|
+
end
|
|
1379
|
+
end
|