json-merge 1.1.2 → 7.1.1
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 +190 -618
- data/lib/json/merge/comment_tracker.rb +27 -0
- data/lib/json/merge/conflict_resolver.rb +1156 -113
- data/lib/json/merge/debug_logger.rb +8 -8
- data/lib/json/merge/emitter.rb +101 -37
- data/lib/json/merge/file_analysis.rb +261 -96
- data/lib/json/merge/freeze_node.rb +62 -0
- data/lib/json/merge/merge_result.rb +25 -5
- data/lib/json/merge/node_wrapper.rb +82 -131
- data/lib/json/merge/object_match_refiner.rb +13 -14
- data/lib/json/merge/smart_merger.rb +136 -10
- data/lib/json/merge/version.rb +5 -4
- data/lib/json/merge.rb +498 -101
- data/lib/json-merge.rb +7 -4
- data/sig/json/merge.rbs +3 -196
- data.tar.gz.sig +0 -0
- metadata +99 -81
- metadata.gz.sig +0 -0
- data/CHANGELOG.md +0 -149
- data/CITATION.cff +0 -20
- data/CODE_OF_CONDUCT.md +0 -134
- data/CONTRIBUTING.md +0 -227
- data/FUNDING.md +0 -74
- data/LICENSE.txt +0 -21
- data/REEK +0 -0
- data/RUBOCOP.md +0 -71
- data/SECURITY.md +0 -21
|
@@ -8,7 +8,16 @@ module Json
|
|
|
8
8
|
# @example Basic usage
|
|
9
9
|
# resolver = ConflictResolver.new(template_analysis, dest_analysis)
|
|
10
10
|
# resolver.resolve(result)
|
|
11
|
-
class ConflictResolver <
|
|
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
|
+
|
|
12
21
|
# Creates a new ConflictResolver
|
|
13
22
|
#
|
|
14
23
|
# @param template_analysis [FileAnalysis] Analyzed template file
|
|
@@ -22,17 +31,23 @@ module Json
|
|
|
22
31
|
# @param options [Hash] Additional options for forward compatibility
|
|
23
32
|
# @param node_typing [Hash{Symbol,String => #call}, nil] Node typing configuration
|
|
24
33
|
# for per-node-type preferences
|
|
25
|
-
def initialize(template_analysis, dest_analysis, preference: :destination, add_template_only_nodes: false,
|
|
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)
|
|
26
36
|
super(
|
|
27
37
|
strategy: :batch,
|
|
28
38
|
preference: preference,
|
|
29
39
|
template_analysis: template_analysis,
|
|
30
40
|
dest_analysis: dest_analysis,
|
|
31
41
|
add_template_only_nodes: add_template_only_nodes,
|
|
42
|
+
remove_template_missing_nodes: remove_template_missing_nodes,
|
|
32
43
|
match_refiner: match_refiner,
|
|
33
44
|
**options
|
|
34
45
|
)
|
|
46
|
+
@resolution_mode = resolution_mode
|
|
47
|
+
@corruption_handling = ::Ast::Merge::Healer.normalize_mode(corruption_handling)
|
|
35
48
|
@node_typing = node_typing
|
|
49
|
+
@merge_arrays = merge_arrays
|
|
50
|
+
@preserve_atomic_formatting = preserve_atomic_formatting
|
|
36
51
|
@emitter = Emitter.new
|
|
37
52
|
end
|
|
38
53
|
|
|
@@ -42,37 +57,47 @@ module Json
|
|
|
42
57
|
#
|
|
43
58
|
# @param result [MergeResult] Result object to populate
|
|
44
59
|
def resolve_batch(result)
|
|
45
|
-
DebugLogger.time(
|
|
60
|
+
DebugLogger.time('ConflictResolver#resolve') do
|
|
61
|
+
@result = result
|
|
46
62
|
template_statements = @template_analysis.statements
|
|
47
63
|
dest_statements = @dest_analysis.statements
|
|
48
64
|
|
|
49
65
|
# Clear emitter for fresh merge
|
|
50
66
|
@emitter.clear
|
|
67
|
+
@emitted_leading_comment_texts = ::Set.new
|
|
68
|
+
|
|
69
|
+
emit_document_prelude(@dest_analysis, nodes: dest_statements)
|
|
51
70
|
|
|
52
71
|
# Merge root-level statements via emitter
|
|
53
72
|
merge_node_lists_to_emitter(
|
|
54
73
|
template_statements,
|
|
55
74
|
dest_statements,
|
|
56
75
|
@template_analysis,
|
|
57
|
-
@dest_analysis
|
|
76
|
+
@dest_analysis
|
|
58
77
|
)
|
|
59
78
|
|
|
79
|
+
emit_document_postlude(@dest_analysis, fallback_node: dest_statements.last)
|
|
80
|
+
|
|
60
81
|
# Transfer emitter output to result
|
|
61
|
-
|
|
62
|
-
unless emitted_content.empty?
|
|
63
|
-
emitted_content.lines.each do |line|
|
|
64
|
-
result.add_line(line.chomp, decision: MergeResult::DECISION_MERGED, source: :merged)
|
|
65
|
-
end
|
|
66
|
-
end
|
|
82
|
+
transfer_emitter_output(result)
|
|
67
83
|
|
|
68
|
-
DebugLogger.debug(
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
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
|
+
})
|
|
73
89
|
end
|
|
74
90
|
end
|
|
75
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
|
+
|
|
76
101
|
private
|
|
77
102
|
|
|
78
103
|
# Recursively merge two lists of nodes, emitting to emitter
|
|
@@ -80,7 +105,7 @@ module Json
|
|
|
80
105
|
# @param dest_nodes [Array<NodeWrapper>] Destination nodes
|
|
81
106
|
# @param template_analysis [FileAnalysis] Template analysis for line access
|
|
82
107
|
# @param dest_analysis [FileAnalysis] Destination analysis for line access
|
|
83
|
-
def merge_node_lists_to_emitter(template_nodes, dest_nodes, template_analysis, dest_analysis)
|
|
108
|
+
def merge_node_lists_to_emitter(template_nodes, dest_nodes, template_analysis, dest_analysis, dest_owners: nil)
|
|
84
109
|
# Build signature maps for matching
|
|
85
110
|
template_by_sig = build_signature_map(template_nodes, template_analysis)
|
|
86
111
|
dest_by_sig = build_signature_map(dest_nodes, dest_analysis)
|
|
@@ -89,63 +114,136 @@ module Json
|
|
|
89
114
|
refined_matches = build_refined_matches(template_nodes, dest_nodes, template_by_sig, dest_by_sig)
|
|
90
115
|
refined_dest_to_template = refined_matches.invert
|
|
91
116
|
|
|
92
|
-
# Track
|
|
93
|
-
|
|
94
|
-
|
|
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
|
|
95
145
|
|
|
96
146
|
# First pass: Process destination nodes
|
|
97
147
|
dest_nodes.each do |dest_node|
|
|
98
148
|
dest_sig = dest_analysis.generate_signature(dest_node)
|
|
99
149
|
|
|
150
|
+
if freeze_node?(dest_node)
|
|
151
|
+
emit_freeze_block(dest_node)
|
|
152
|
+
next
|
|
153
|
+
end
|
|
154
|
+
|
|
100
155
|
# Check for signature match
|
|
101
156
|
if dest_sig && template_by_sig[dest_sig]
|
|
102
|
-
|
|
103
|
-
|
|
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]
|
|
104
173
|
|
|
105
|
-
|
|
106
|
-
|
|
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)
|
|
107
177
|
|
|
108
|
-
|
|
109
|
-
|
|
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
|
|
110
184
|
elsif refined_dest_to_template.key?(dest_node)
|
|
111
185
|
# Found refined match
|
|
112
186
|
template_node = refined_dest_to_template[dest_node]
|
|
113
187
|
template_sig = template_analysis.generate_signature(template_node)
|
|
114
188
|
|
|
115
|
-
#
|
|
116
|
-
|
|
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
|
|
117
198
|
|
|
118
|
-
|
|
119
|
-
|
|
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)
|
|
120
204
|
else
|
|
121
205
|
# Destination-only node - always keep
|
|
122
|
-
|
|
123
|
-
processed_dest_sigs << dest_sig if dest_sig
|
|
206
|
+
emit_atomic_node(dest_node, dest_analysis)
|
|
124
207
|
end
|
|
125
|
-
end
|
|
126
208
|
|
|
127
|
-
|
|
128
|
-
|
|
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])
|
|
129
216
|
|
|
130
|
-
|
|
131
|
-
|
|
217
|
+
emit_atomic_node(info[:node], template_analysis)
|
|
218
|
+
end
|
|
219
|
+
end
|
|
132
220
|
|
|
133
|
-
|
|
134
|
-
|
|
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])
|
|
135
227
|
|
|
136
|
-
|
|
137
|
-
emit_node(template_node, template_analysis)
|
|
138
|
-
processed_template_sigs << template_sig if template_sig
|
|
228
|
+
emit_atomic_node(info[:node], template_analysis)
|
|
139
229
|
end
|
|
140
230
|
end
|
|
141
231
|
|
|
232
|
+
def trailing_group_node_matched?(node, _signature)
|
|
233
|
+
freeze_node?(node)
|
|
234
|
+
end
|
|
235
|
+
|
|
142
236
|
# Merge two matched nodes - for containers, recursively merge children
|
|
143
237
|
# Emits to emitter instead of result
|
|
144
238
|
# @param template_node [NodeWrapper] Template node
|
|
145
239
|
# @param dest_node [NodeWrapper] Destination node
|
|
146
240
|
# @param template_analysis [FileAnalysis] Template analysis
|
|
147
241
|
# @param dest_analysis [FileAnalysis] Destination analysis
|
|
148
|
-
def merge_matched_nodes_to_emitter(template_node, dest_node, template_analysis, dest_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
|
+
|
|
149
247
|
if dest_node.container? && template_node.container?
|
|
150
248
|
# Both are containers - recursively merge their children
|
|
151
249
|
merge_container_to_emitter(template_node, dest_node, template_analysis, dest_analysis)
|
|
@@ -154,63 +252,153 @@ module Json
|
|
|
154
252
|
template_value = template_node.value_node
|
|
155
253
|
dest_value = dest_node.value_node
|
|
156
254
|
|
|
157
|
-
# Only recursively merge if BOTH values are objects
|
|
158
|
-
# Arrays are
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
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,
|
|
168
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,
|
|
169
278
|
dest_analysis,
|
|
279
|
+
fallback_node: template_value,
|
|
280
|
+
fallback_analysis: template_analysis
|
|
170
281
|
)
|
|
282
|
+
compact_source_node = trailing_source_node || dest_value || template_value
|
|
171
283
|
|
|
172
|
-
|
|
173
|
-
|
|
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
|
|
174
321
|
elsif preference_for_pair(template_node, dest_node) == :destination
|
|
175
322
|
# Values are not both objects, or one/both are arrays - use preference and emit
|
|
176
323
|
# Arrays are always replaced, not merged
|
|
177
|
-
|
|
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)
|
|
178
330
|
else
|
|
179
|
-
|
|
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
|
+
)
|
|
180
342
|
end
|
|
181
343
|
elsif preference_for_pair(template_node, dest_node) == :destination
|
|
182
344
|
# Leaf nodes or mismatched types - use preference
|
|
183
|
-
|
|
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)
|
|
184
351
|
else
|
|
185
|
-
|
|
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
|
+
)
|
|
186
363
|
end
|
|
187
364
|
end
|
|
188
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
|
+
|
|
189
375
|
# Merge container nodes by emitting via emitter
|
|
190
376
|
# @param template_node [NodeWrapper] Template container node
|
|
191
377
|
# @param dest_node [NodeWrapper] Destination container node
|
|
192
378
|
# @param template_analysis [FileAnalysis] Template analysis
|
|
193
379
|
# @param dest_analysis [FileAnalysis] Destination analysis
|
|
194
380
|
def merge_container_to_emitter(template_node, dest_node, template_analysis, dest_analysis)
|
|
195
|
-
# Emit opening bracket
|
|
196
381
|
if dest_node.object?
|
|
197
382
|
@emitter.emit_object_start
|
|
198
383
|
elsif dest_node.array?
|
|
199
384
|
@emitter.emit_array_start
|
|
200
385
|
end
|
|
201
386
|
|
|
202
|
-
# Recursively merge the children
|
|
203
|
-
template_children = template_node.mergeable_children
|
|
204
|
-
dest_children = dest_node.mergeable_children
|
|
205
|
-
|
|
206
387
|
merge_node_lists_to_emitter(
|
|
207
|
-
|
|
208
|
-
|
|
388
|
+
template_node.mergeable_children,
|
|
389
|
+
dest_node.mergeable_children,
|
|
209
390
|
template_analysis,
|
|
391
|
+
dest_analysis
|
|
392
|
+
)
|
|
393
|
+
|
|
394
|
+
trailing_source_node, trailing_source_analysis = preferred_container_comment_source(
|
|
395
|
+
dest_node,
|
|
210
396
|
dest_analysis,
|
|
397
|
+
fallback_node: template_node,
|
|
398
|
+
fallback_analysis: template_analysis
|
|
211
399
|
)
|
|
400
|
+
emit_container_trailing_lines(trailing_source_node, trailing_source_analysis)
|
|
212
401
|
|
|
213
|
-
# Emit closing bracket
|
|
214
402
|
if dest_node.object?
|
|
215
403
|
@emitter.emit_object_end
|
|
216
404
|
elsif dest_node.array?
|
|
@@ -244,91 +432,946 @@ module Json
|
|
|
244
432
|
Ast::Merge::NodeTyping.process(node, @node_typing)
|
|
245
433
|
end
|
|
246
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
|
+
|
|
247
507
|
# Emit a single node to the emitter
|
|
248
508
|
# @param node [NodeWrapper] Node to emit
|
|
249
509
|
# @param analysis [FileAnalysis] Analysis for accessing source
|
|
250
|
-
def emit_node(node, analysis)
|
|
251
|
-
|
|
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
|
+
|
|
252
528
|
if node.pair?
|
|
253
529
|
# Emit as pair
|
|
254
530
|
key = node.key_name
|
|
255
531
|
value_node = node.value_node
|
|
532
|
+
source_value_node = source_node.respond_to?(:value_node) ? source_node.value_node : nil
|
|
256
533
|
|
|
257
534
|
if value_node
|
|
258
535
|
# Check if value is an object (not array) and needs recursive emission
|
|
259
|
-
if value_node.
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
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
|
|
265
583
|
end
|
|
266
|
-
@emitter.emit_nested_object_end
|
|
267
584
|
else
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
lines << analysis.line_at(ln)
|
|
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
|
+
)
|
|
277
593
|
end
|
|
278
|
-
lines.join("\n")
|
|
279
594
|
end
|
|
280
|
-
|
|
281
|
-
@emitter.emit_pair(key, value_text) if key && value_text
|
|
282
595
|
end
|
|
283
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
|
|
284
615
|
elsif node.start_line && node.end_line
|
|
285
|
-
# Emit raw content for non-pair nodes
|
|
286
616
|
if node.start_line == node.end_line
|
|
287
|
-
|
|
288
|
-
|
|
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
|
|
289
624
|
else
|
|
290
|
-
# Multi-line - collect and emit
|
|
291
625
|
lines = []
|
|
292
626
|
(node.start_line..node.end_line).each do |ln|
|
|
293
627
|
line = analysis.line_at(ln)
|
|
294
628
|
lines << line if line
|
|
295
629
|
end
|
|
296
|
-
@emitter.emit_raw_lines(lines)
|
|
630
|
+
@emitter.emit_raw_lines(lines, metadata: emitter_block_metadata(analysis, node.start_line))
|
|
297
631
|
end
|
|
298
632
|
end
|
|
299
633
|
end
|
|
300
634
|
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
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
|
+
|
|
307
1351
|
def build_refined_matches(template_nodes, dest_nodes, template_by_sig, dest_by_sig)
|
|
308
1352
|
return {} unless @match_refiner
|
|
309
1353
|
|
|
310
|
-
# Find unmatched nodes
|
|
311
1354
|
matched_sigs = template_by_sig.keys & dest_by_sig.keys
|
|
312
|
-
|
|
313
|
-
|
|
1355
|
+
|
|
1356
|
+
unmatched_template = template_nodes.reject do |node|
|
|
1357
|
+
sig = @template_analysis.generate_signature(node)
|
|
314
1358
|
sig && matched_sigs.include?(sig)
|
|
315
1359
|
end
|
|
316
|
-
|
|
317
|
-
|
|
1360
|
+
|
|
1361
|
+
unmatched_dest = dest_nodes.reject do |node|
|
|
1362
|
+
sig = @dest_analysis.generate_signature(node)
|
|
318
1363
|
sig && matched_sigs.include?(sig)
|
|
319
1364
|
end
|
|
320
1365
|
|
|
321
|
-
return {} if
|
|
1366
|
+
return {} if unmatched_template.empty? || unmatched_dest.empty?
|
|
322
1367
|
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
})
|
|
1368
|
+
matches = @match_refiner.call(unmatched_template, unmatched_dest, {
|
|
1369
|
+
template_analysis: @template_analysis,
|
|
1370
|
+
dest_analysis: @dest_analysis
|
|
1371
|
+
})
|
|
328
1372
|
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
h[match.template_node] = match.dest_node
|
|
1373
|
+
matches.each_with_object({}) do |match, hash|
|
|
1374
|
+
hash[match.template_node] = match.dest_node
|
|
332
1375
|
end
|
|
333
1376
|
end
|
|
334
1377
|
end
|