typescript-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 +286 -0
- data/lib/typescript/merge/file_analysis.rb +78 -0
- data/lib/typescript/merge/node_wrapper.rb +224 -0
- data/lib/typescript/merge/provider.rb +953 -0
- data/lib/typescript/merge/version.rb +5 -3
- data/lib/typescript/merge.rb +149 -56
- data/lib/typescript-merge.rb +7 -1
- data/sig/typescript/merge.rbs +54 -0
- data.tar.gz.sig +0 -0
- metadata +255 -15
- metadata.gz.sig +0 -0
|
@@ -0,0 +1,953 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'digest'
|
|
4
|
+
require 'json'
|
|
5
|
+
|
|
6
|
+
module TypeScript
|
|
7
|
+
module Merge
|
|
8
|
+
# Base-aware, source-preserving provider for native TypeScript declarations.
|
|
9
|
+
# Multi-declarator and declaration-merging forms are whole owners. Any
|
|
10
|
+
# ambiguous identity or changing membership is rejected before rendering.
|
|
11
|
+
# rubocop:disable Metrics/AbcSize, Metrics/ClassLength, Metrics/CyclomaticComplexity, Metrics/MethodLength, Metrics/ParameterLists, Metrics/PerceivedComplexity -- provider decisions, source plans, and verification form one boundary
|
|
12
|
+
class Provider
|
|
13
|
+
DEFAULT_PROFILE = :source_preserving
|
|
14
|
+
Owner = Data.define(:id, :signature, :fingerprint, :start_line, :end_line, :role)
|
|
15
|
+
Document = Data.define(:source, :analysis, :owners, :by_id, :unmanaged_fingerprint)
|
|
16
|
+
ExactDocument = Data.define(:source, :analysis, :declarations, :issues, :role)
|
|
17
|
+
Decision = Data.define(:changes, :conflicts, :choices)
|
|
18
|
+
|
|
19
|
+
def provider_id = 'ruby.typescript'
|
|
20
|
+
def family = 'typescript'
|
|
21
|
+
|
|
22
|
+
def capabilities
|
|
23
|
+
{
|
|
24
|
+
operations: Ast::Merge::ProviderContract::OPERATIONS,
|
|
25
|
+
dialects: %i[typescript tsx],
|
|
26
|
+
backends: [TREE_SITTER_BACKEND.id.to_sym],
|
|
27
|
+
profiles: [DEFAULT_PROFILE],
|
|
28
|
+
role: :workflow,
|
|
29
|
+
compound_declaration_ownership: :whole_declaration_with_stable_member_identity,
|
|
30
|
+
source_preservation: %i[exact_source declaration_fragments line_provenance reparse semantic_verification]
|
|
31
|
+
}.freeze
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
def analyze(request)
|
|
35
|
+
document = parse_document(:analyze, request, :source)
|
|
36
|
+
return document if provider_failure?(document)
|
|
37
|
+
|
|
38
|
+
result(
|
|
39
|
+
:analyze,
|
|
40
|
+
request,
|
|
41
|
+
analysis: {
|
|
42
|
+
backend: TREE_SITTER_BACKEND.id,
|
|
43
|
+
valid: true,
|
|
44
|
+
declarations: document.owners.map { |owner| owner_description(owner) }
|
|
45
|
+
},
|
|
46
|
+
verification: { source_parsed: true }
|
|
47
|
+
)
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
def diff2(request)
|
|
51
|
+
before = parse_document(:diff2, request, :before)
|
|
52
|
+
return before if provider_failure?(before)
|
|
53
|
+
|
|
54
|
+
after = parse_document(:diff2, request, :after)
|
|
55
|
+
return after if provider_failure?(after)
|
|
56
|
+
|
|
57
|
+
changes = diff_documents(before, after)
|
|
58
|
+
result(
|
|
59
|
+
:diff2,
|
|
60
|
+
request,
|
|
61
|
+
diff: { changes: changes },
|
|
62
|
+
changes: changes,
|
|
63
|
+
verification: { before_parsed: true, after_parsed: true }
|
|
64
|
+
)
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
def merge2(request)
|
|
68
|
+
merge3_request = request.merge(
|
|
69
|
+
base_source: request.fetch(:current_source),
|
|
70
|
+
ours_source: request.fetch(:current_source),
|
|
71
|
+
theirs_source: request.fetch(:incoming_source)
|
|
72
|
+
)
|
|
73
|
+
merged = merge3(merge3_request)
|
|
74
|
+
merged.merge(
|
|
75
|
+
operation: :merge2,
|
|
76
|
+
verification: merged.fetch(:verification).except(:base_participated)
|
|
77
|
+
)
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
def merge3(request)
|
|
81
|
+
exact_role = exact_revision_role(request)
|
|
82
|
+
return merge3_exact(request, exact_role) if exact_role
|
|
83
|
+
|
|
84
|
+
documents = parse_merge3_documents(request)
|
|
85
|
+
return documents if provider_failure?(documents)
|
|
86
|
+
|
|
87
|
+
grouped_failure = grouped_ownership_failure(request, documents)
|
|
88
|
+
return grouped_failure if grouped_failure
|
|
89
|
+
|
|
90
|
+
decision = decide(documents, include_unmanaged_conflict: true)
|
|
91
|
+
return render_conflicts(request, documents, decision) unless decision.conflicts.empty?
|
|
92
|
+
|
|
93
|
+
render_composite(request, documents, decision)
|
|
94
|
+
end
|
|
95
|
+
|
|
96
|
+
private
|
|
97
|
+
|
|
98
|
+
def parse_merge3_documents(request)
|
|
99
|
+
%i[base ours theirs].each_with_object({}) do |role, documents|
|
|
100
|
+
source = request.fetch(:"#{role}_source")
|
|
101
|
+
document = parse_source(source, role, request[:dialect])
|
|
102
|
+
if document.is_a?(Hash) && document[:parse_error]
|
|
103
|
+
return parse_failure(:merge3, request, role, document)
|
|
104
|
+
elsif provider_failure?(document)
|
|
105
|
+
return unsafe_document_failure(request, role, document)
|
|
106
|
+
end
|
|
107
|
+
|
|
108
|
+
documents[role] = document
|
|
109
|
+
end
|
|
110
|
+
end
|
|
111
|
+
|
|
112
|
+
def parse_document(operation, request, role)
|
|
113
|
+
source = request.fetch(role == :source ? :source : :"#{role}_source")
|
|
114
|
+
parsed = parse_source(source, role, request[:dialect])
|
|
115
|
+
return parse_failure(operation, request, role, parsed) if provider_failure?(parsed)
|
|
116
|
+
|
|
117
|
+
parsed
|
|
118
|
+
end
|
|
119
|
+
|
|
120
|
+
def parse_source(source, role, dialect = nil)
|
|
121
|
+
analysis = FileAnalysis.new(source, dialect: dialect || :typescript)
|
|
122
|
+
unless analysis.errors.empty? && analysis.backend == TREE_SITTER_BACKEND.id.to_sym
|
|
123
|
+
return { parse_error: analysis.errors.map(&:to_s).join('; '), source_role: role }
|
|
124
|
+
end
|
|
125
|
+
|
|
126
|
+
owners = analysis.declarations.map do |wrapper|
|
|
127
|
+
return { unsafe_range: wrapper.signature, source_role: role } unless safe_range?(source, wrapper)
|
|
128
|
+
|
|
129
|
+
signature = wrapper.signature
|
|
130
|
+
Owner.new(
|
|
131
|
+
id: owner_id(signature),
|
|
132
|
+
signature: signature,
|
|
133
|
+
fingerprint: owner_fingerprint(wrapper),
|
|
134
|
+
start_line: wrapper.start_line,
|
|
135
|
+
end_line: wrapper.end_line,
|
|
136
|
+
role: role
|
|
137
|
+
)
|
|
138
|
+
end
|
|
139
|
+
duplicate = owners.group_by(&:id).find { |_id, matches| matches.length > 1 }
|
|
140
|
+
return { ambiguous_owner: duplicate.first, source_role: role } if duplicate
|
|
141
|
+
|
|
142
|
+
declaration_merge = owners.group_by { |owner| declaration_merge_key(owner.signature) }
|
|
143
|
+
.find { |key, matches| key && matches.length > 1 }
|
|
144
|
+
return { ambiguous_owner: declaration_merge.first, source_role: role } if declaration_merge
|
|
145
|
+
return { unsafe_range: :overlapping_declarations, source_role: role } unless non_overlapping?(owners)
|
|
146
|
+
|
|
147
|
+
Document.new(
|
|
148
|
+
source: source,
|
|
149
|
+
analysis: analysis,
|
|
150
|
+
owners: owners.freeze,
|
|
151
|
+
by_id: owners.to_h { |owner| [owner.id, owner] }.freeze,
|
|
152
|
+
unmanaged_fingerprint: unmanaged_fingerprint(source, owners)
|
|
153
|
+
)
|
|
154
|
+
rescue StandardError => e
|
|
155
|
+
{ parse_error: e.message, source_role: role }
|
|
156
|
+
end
|
|
157
|
+
|
|
158
|
+
def parse_exact_source(source, role, dialect = nil)
|
|
159
|
+
analysis = FileAnalysis.new(source, dialect: dialect || :typescript)
|
|
160
|
+
unless analysis.errors.empty? && analysis.backend == TREE_SITTER_BACKEND.id.to_sym
|
|
161
|
+
return { parse_error: analysis.errors.map(&:to_s).join('; '), source_role: role }
|
|
162
|
+
end
|
|
163
|
+
|
|
164
|
+
declarations = analysis.declarations.map do |wrapper|
|
|
165
|
+
[wrapper.signature, wrapper.semantic_tree]
|
|
166
|
+
end.freeze
|
|
167
|
+
duplicate_keys = declarations.group_by(&:first).filter_map do |signature, matches|
|
|
168
|
+
signature if matches.length > 1
|
|
169
|
+
end
|
|
170
|
+
merge_groups = declarations.group_by { |signature, _tree| declaration_merge_key(signature) }
|
|
171
|
+
merge_keys = merge_groups.filter_map do |key, matches|
|
|
172
|
+
key if key && matches.length > 1
|
|
173
|
+
end
|
|
174
|
+
issues = (duplicate_keys + merge_keys).uniq.map do |identity|
|
|
175
|
+
{
|
|
176
|
+
category: :ambiguous_owner,
|
|
177
|
+
declaration: identity,
|
|
178
|
+
message: duplicate_message(identity)
|
|
179
|
+
}.freeze
|
|
180
|
+
end
|
|
181
|
+
ExactDocument.new(
|
|
182
|
+
source: source,
|
|
183
|
+
analysis: analysis,
|
|
184
|
+
declarations: declarations,
|
|
185
|
+
issues: issues.freeze,
|
|
186
|
+
role: role
|
|
187
|
+
)
|
|
188
|
+
rescue StandardError => e
|
|
189
|
+
{ parse_error: e.message, source_role: role }
|
|
190
|
+
end
|
|
191
|
+
|
|
192
|
+
def safe_range?(source, wrapper)
|
|
193
|
+
return false if wrapper.start_byte.negative? || wrapper.end_byte > source.bytesize
|
|
194
|
+
return false if wrapper.end_byte < wrapper.start_byte
|
|
195
|
+
|
|
196
|
+
line_start = source.rindex("\n", [wrapper.start_byte - 1, 0].max)
|
|
197
|
+
line_start = line_start ? line_start + 1 : 0
|
|
198
|
+
line_end = source.index("\n", wrapper.end_byte) || source.bytesize
|
|
199
|
+
horizontal_whitespace?(source.byteslice(line_start...wrapper.start_byte).to_s) &&
|
|
200
|
+
horizontal_whitespace?(source.byteslice(wrapper.end_byte...line_end).to_s)
|
|
201
|
+
end
|
|
202
|
+
|
|
203
|
+
def horizontal_whitespace?(value)
|
|
204
|
+
value.each_byte.all? { |byte| [9, 13, 32].include?(byte) }
|
|
205
|
+
end
|
|
206
|
+
|
|
207
|
+
def non_overlapping?(owners)
|
|
208
|
+
owners.each_cons(2).all? { |left, right| left.end_line < right.start_line }
|
|
209
|
+
end
|
|
210
|
+
|
|
211
|
+
def unmanaged_fingerprint(source, owners)
|
|
212
|
+
owned_lines = owners.each_with_object({}) do |owner, lines|
|
|
213
|
+
(owner.start_line..owner.end_line).each { |line| lines[line] = true }
|
|
214
|
+
end
|
|
215
|
+
source.lines.each_with_index.filter_map do |line, index|
|
|
216
|
+
line unless owned_lines[index + 1]
|
|
217
|
+
end.join
|
|
218
|
+
end
|
|
219
|
+
|
|
220
|
+
def owner_fingerprint(wrapper)
|
|
221
|
+
Digest::SHA256.hexdigest(
|
|
222
|
+
JSON.generate(Ast::Merge.json_ready([wrapper.semantic_tree, wrapper.source_text]))
|
|
223
|
+
)
|
|
224
|
+
end
|
|
225
|
+
|
|
226
|
+
def owner_id(signature)
|
|
227
|
+
Digest::SHA256.hexdigest(JSON.generate(Ast::Merge.json_ready(signature)))
|
|
228
|
+
end
|
|
229
|
+
|
|
230
|
+
def declaration_merge_key(signature)
|
|
231
|
+
value = signature.first == :export_declaration ? signature.fetch(1) : signature
|
|
232
|
+
return unless %i[class enum function interface namespace].include?(value.first)
|
|
233
|
+
|
|
234
|
+
value.last
|
|
235
|
+
end
|
|
236
|
+
|
|
237
|
+
def owner_path(owner)
|
|
238
|
+
owner ? owner.signature.inspect : '<document>'
|
|
239
|
+
end
|
|
240
|
+
|
|
241
|
+
def owner_description(owner)
|
|
242
|
+
{
|
|
243
|
+
path: owner_path(owner),
|
|
244
|
+
signature: owner.signature,
|
|
245
|
+
source_role: owner.role,
|
|
246
|
+
line_range: [owner.start_line, owner.end_line]
|
|
247
|
+
}
|
|
248
|
+
end
|
|
249
|
+
|
|
250
|
+
def diff_documents(before, after)
|
|
251
|
+
ordered_ids(before, after).filter_map do |id|
|
|
252
|
+
left = before.by_id[id]
|
|
253
|
+
right = after.by_id[id]
|
|
254
|
+
next if equivalent?(left, right)
|
|
255
|
+
|
|
256
|
+
{
|
|
257
|
+
path: owner_path(left || right),
|
|
258
|
+
before: change_side(left),
|
|
259
|
+
after: change_side(right),
|
|
260
|
+
change: change_kind(left, right)
|
|
261
|
+
}.freeze
|
|
262
|
+
end.freeze
|
|
263
|
+
end
|
|
264
|
+
|
|
265
|
+
def change_side(owner)
|
|
266
|
+
return { present: false } unless owner
|
|
267
|
+
|
|
268
|
+
{ present: true, source_role: owner.role, line_range: [owner.start_line, owner.end_line] }
|
|
269
|
+
end
|
|
270
|
+
|
|
271
|
+
def ordered_ids(*documents)
|
|
272
|
+
documents.flat_map { |document| document.owners.map(&:id) }.uniq
|
|
273
|
+
end
|
|
274
|
+
|
|
275
|
+
def change_kind(before, after)
|
|
276
|
+
return :added unless before
|
|
277
|
+
return :deleted unless after
|
|
278
|
+
|
|
279
|
+
:edited
|
|
280
|
+
end
|
|
281
|
+
|
|
282
|
+
def decide(documents, include_unmanaged_conflict:)
|
|
283
|
+
changes = []
|
|
284
|
+
conflicts = []
|
|
285
|
+
choices = {}
|
|
286
|
+
append_unmanaged_conflict(changes, conflicts, documents) if include_unmanaged_conflict
|
|
287
|
+
ordered_ids(*documents.values).each do |id|
|
|
288
|
+
base = documents.fetch(:base).by_id[id]
|
|
289
|
+
ours = documents.fetch(:ours).by_id[id]
|
|
290
|
+
theirs = documents.fetch(:theirs).by_id[id]
|
|
291
|
+
ours_change = side_change(base, ours)
|
|
292
|
+
theirs_change = side_change(base, theirs)
|
|
293
|
+
next choices[id] = :ours if ours_change == :unchanged && theirs_change == :unchanged
|
|
294
|
+
|
|
295
|
+
change = {
|
|
296
|
+
path: owner_path(base || ours || theirs),
|
|
297
|
+
ours: ours_change,
|
|
298
|
+
theirs: theirs_change
|
|
299
|
+
}.freeze
|
|
300
|
+
changes << change
|
|
301
|
+
choice = owner_choice(base, ours, theirs)
|
|
302
|
+
if choice == :conflict
|
|
303
|
+
conflicts << conflict_for(id, base, ours, theirs, change)
|
|
304
|
+
else
|
|
305
|
+
choices[id] = choice
|
|
306
|
+
end
|
|
307
|
+
end
|
|
308
|
+
Decision.new(changes: changes.freeze, conflicts: conflicts.freeze, choices: choices.freeze)
|
|
309
|
+
end
|
|
310
|
+
|
|
311
|
+
def append_unmanaged_conflict(changes, conflicts, documents)
|
|
312
|
+
base = documents.fetch(:base).unmanaged_fingerprint
|
|
313
|
+
ours = documents.fetch(:ours).unmanaged_fingerprint
|
|
314
|
+
theirs = documents.fetch(:theirs).unmanaged_fingerprint
|
|
315
|
+
return if ours == theirs
|
|
316
|
+
|
|
317
|
+
change = {
|
|
318
|
+
path: '<unmanaged-source>',
|
|
319
|
+
ours: base == ours ? :unchanged : :edited,
|
|
320
|
+
theirs: base == theirs ? :unchanged : :edited
|
|
321
|
+
}.freeze
|
|
322
|
+
changes << change
|
|
323
|
+
conflicts << {
|
|
324
|
+
conflict_id: "typescript-unmanaged-#{Digest::SHA256.hexdigest([base, ours, theirs].join("\0"))[0, 16]}",
|
|
325
|
+
category: :unmanaged_source_change,
|
|
326
|
+
path: change.fetch(:path),
|
|
327
|
+
owner_id: nil,
|
|
328
|
+
change_classification: change
|
|
329
|
+
}.freeze
|
|
330
|
+
end
|
|
331
|
+
|
|
332
|
+
def grouped_ownership_failure(request, documents)
|
|
333
|
+
signatures = documents.transform_values do |document|
|
|
334
|
+
document.owners.filter_map { |owner| owner.signature if grouped_owner?(owner) }
|
|
335
|
+
.sort_by { |signature| JSON.generate(Ast::Merge.json_ready(signature)) }
|
|
336
|
+
end
|
|
337
|
+
return if signatures.values.uniq.length == 1
|
|
338
|
+
|
|
339
|
+
signature_digest = Digest::SHA256.hexdigest(JSON.generate(Ast::Merge.json_ready(signatures)))
|
|
340
|
+
conflict = {
|
|
341
|
+
conflict_id: "typescript-compound-#{signature_digest[0, 16]}",
|
|
342
|
+
category: :unproven_compound_ownership,
|
|
343
|
+
path: '<compound-declarations>',
|
|
344
|
+
owner_id: nil,
|
|
345
|
+
member_signatures: signatures
|
|
346
|
+
}.freeze
|
|
347
|
+
decision = Decision.new(changes: [], conflicts: [conflict], choices: {})
|
|
348
|
+
rendered = render_plan(request, [whole_document_conflict(request, decision)])
|
|
349
|
+
failure(
|
|
350
|
+
:merge3,
|
|
351
|
+
request,
|
|
352
|
+
category: :unproven_compound_ownership,
|
|
353
|
+
message: 'Compound TypeScript declaration membership differs across revisions; ' \
|
|
354
|
+
'whole-owner identity is unproven.',
|
|
355
|
+
conflicts: [conflict],
|
|
356
|
+
conflicted_output: rendered.content,
|
|
357
|
+
render_report: render_report(rendered, :full_file_conflict),
|
|
358
|
+
verification: { base_participated: true },
|
|
359
|
+
fallbacks: [{
|
|
360
|
+
from: :compound_declaration_ownership,
|
|
361
|
+
to: :full_file_conflict,
|
|
362
|
+
reason: :compound_member_identity_changed
|
|
363
|
+
}]
|
|
364
|
+
)
|
|
365
|
+
end
|
|
366
|
+
|
|
367
|
+
def side_change(base, side)
|
|
368
|
+
return :unchanged if equivalent?(base, side)
|
|
369
|
+
return :added if !base && side
|
|
370
|
+
return :deleted if base && !side
|
|
371
|
+
return :unchanged unless base || side
|
|
372
|
+
|
|
373
|
+
:edited
|
|
374
|
+
end
|
|
375
|
+
|
|
376
|
+
def owner_choice(base, ours, theirs)
|
|
377
|
+
return :ours if equivalent?(ours, theirs)
|
|
378
|
+
return :theirs if equivalent?(base, ours)
|
|
379
|
+
return :ours if equivalent?(base, theirs)
|
|
380
|
+
return if ours.nil? && theirs.nil?
|
|
381
|
+
|
|
382
|
+
:conflict
|
|
383
|
+
end
|
|
384
|
+
|
|
385
|
+
def equivalent?(left, right)
|
|
386
|
+
return true if left.nil? && right.nil?
|
|
387
|
+
return false unless left && right
|
|
388
|
+
|
|
389
|
+
left.fingerprint == right.fingerprint
|
|
390
|
+
end
|
|
391
|
+
|
|
392
|
+
def conflict_for(id, base, ours, theirs, change)
|
|
393
|
+
{
|
|
394
|
+
conflict_id: "typescript-declaration-#{id[0, 16]}",
|
|
395
|
+
category: base && (!ours || !theirs) ? :delete_edit : :edit_edit,
|
|
396
|
+
path: change.fetch(:path),
|
|
397
|
+
owner_id: id,
|
|
398
|
+
base: owner_state(base),
|
|
399
|
+
ours: owner_state(ours),
|
|
400
|
+
theirs: owner_state(theirs),
|
|
401
|
+
change_classification: change
|
|
402
|
+
}.freeze
|
|
403
|
+
end
|
|
404
|
+
|
|
405
|
+
def owner_state(owner)
|
|
406
|
+
{
|
|
407
|
+
present: !owner.nil?,
|
|
408
|
+
fingerprint: owner&.fingerprint,
|
|
409
|
+
source_role: owner&.role,
|
|
410
|
+
line_range: owner && [owner.start_line, owner.end_line]
|
|
411
|
+
}
|
|
412
|
+
end
|
|
413
|
+
|
|
414
|
+
def exact_revision_role(request)
|
|
415
|
+
base = request.fetch(:base_source)
|
|
416
|
+
ours = request.fetch(:ours_source)
|
|
417
|
+
theirs = request.fetch(:theirs_source)
|
|
418
|
+
return :ours if ours == theirs || base == theirs
|
|
419
|
+
|
|
420
|
+
:theirs if base == ours
|
|
421
|
+
end
|
|
422
|
+
|
|
423
|
+
def merge3_exact(request, role)
|
|
424
|
+
documents = %i[base ours theirs].to_h do |source_role|
|
|
425
|
+
[source_role, parse_exact_source(request.fetch(:"#{source_role}_source"), source_role, request[:dialect])]
|
|
426
|
+
end
|
|
427
|
+
winner = documents.fetch(role)
|
|
428
|
+
return parse_failure(:merge3, request, role, winner) if provider_failure?(winner)
|
|
429
|
+
|
|
430
|
+
render_exact(request, documents, role)
|
|
431
|
+
end
|
|
432
|
+
|
|
433
|
+
def render_exact(request, documents, role)
|
|
434
|
+
winner = documents.fetch(role)
|
|
435
|
+
rendered = render_plan(request, [whole_source_fragment(role, request.fetch(:"#{role}_source"))])
|
|
436
|
+
verification = verify_exact_rendered(rendered.content, winner)
|
|
437
|
+
unless verification[:semantic_match] && verification[:byte_exact]
|
|
438
|
+
return failure(
|
|
439
|
+
:merge3,
|
|
440
|
+
request,
|
|
441
|
+
category: :render_failure,
|
|
442
|
+
message: 'TypeScript exact revision did not remain byte-exact and semantically identical ' \
|
|
443
|
+
'after native reparse.',
|
|
444
|
+
source_role: role,
|
|
445
|
+
render_report: render_report(rendered, :exact_revision),
|
|
446
|
+
verification: verification.merge(base_participated: true)
|
|
447
|
+
)
|
|
448
|
+
end
|
|
449
|
+
|
|
450
|
+
result(
|
|
451
|
+
:merge3,
|
|
452
|
+
request,
|
|
453
|
+
output: rendered.content,
|
|
454
|
+
diagnostics: exact_diagnostics(documents),
|
|
455
|
+
render_report: render_report(rendered, :exact_revision),
|
|
456
|
+
verification: verification.merge(base_participated: true)
|
|
457
|
+
)
|
|
458
|
+
end
|
|
459
|
+
|
|
460
|
+
def verify_exact_rendered(output, expected)
|
|
461
|
+
parsed = parse_exact_source(output, :output)
|
|
462
|
+
if provider_failure?(parsed)
|
|
463
|
+
return {
|
|
464
|
+
output_reparsed: false,
|
|
465
|
+
byte_exact: output == expected.source,
|
|
466
|
+
semantic_match: false,
|
|
467
|
+
parse_error: failure_detail(parsed),
|
|
468
|
+
source_role: expected.role
|
|
469
|
+
}
|
|
470
|
+
end
|
|
471
|
+
|
|
472
|
+
actual_signatures = parsed.declarations.map(&:first)
|
|
473
|
+
expected_signatures = expected.declarations.map(&:first)
|
|
474
|
+
actual_attributes = parsed.declarations.map(&:last)
|
|
475
|
+
expected_attributes = expected.declarations.map(&:last)
|
|
476
|
+
{
|
|
477
|
+
output_reparsed: true,
|
|
478
|
+
byte_exact: output == expected.source,
|
|
479
|
+
semantic_match: parsed.declarations == expected.declarations,
|
|
480
|
+
ordered_declaration_signatures_verified: actual_signatures == expected_signatures,
|
|
481
|
+
ast_attributes_verified: actual_attributes == expected_attributes,
|
|
482
|
+
planned_declaration_count: expected.declarations.length,
|
|
483
|
+
output_declaration_count: parsed.declarations.length,
|
|
484
|
+
source_role: expected.role
|
|
485
|
+
}
|
|
486
|
+
end
|
|
487
|
+
|
|
488
|
+
def exact_diagnostics(documents)
|
|
489
|
+
documents.flat_map do |role, document|
|
|
490
|
+
if provider_failure?(document)
|
|
491
|
+
[{
|
|
492
|
+
category: :parse_error,
|
|
493
|
+
severity: :warning,
|
|
494
|
+
message: "#{role} parse error: #{failure_detail(document)}",
|
|
495
|
+
blocking: false,
|
|
496
|
+
source_role: role
|
|
497
|
+
}]
|
|
498
|
+
else
|
|
499
|
+
document.issues.map do |issue|
|
|
500
|
+
issue.merge(severity: :warning, blocking: false, source_role: role).freeze
|
|
501
|
+
end
|
|
502
|
+
end
|
|
503
|
+
end.freeze
|
|
504
|
+
end
|
|
505
|
+
|
|
506
|
+
def duplicate_message(signature)
|
|
507
|
+
"ambiguous duplicate declaration #{signature.inspect}"
|
|
508
|
+
end
|
|
509
|
+
|
|
510
|
+
def render_composite(request, documents, decision)
|
|
511
|
+
fragments = composite_fragments(documents, decision)
|
|
512
|
+
rendered = render_plan(request, fragments)
|
|
513
|
+
expected = expected_owners(documents, decision)
|
|
514
|
+
verification = verify_rendered(rendered.content, expected, request[:dialect])
|
|
515
|
+
unless verification[:semantic_match]
|
|
516
|
+
return failure(
|
|
517
|
+
:merge3,
|
|
518
|
+
request,
|
|
519
|
+
category: :render_failure,
|
|
520
|
+
message: 'TypeScript composite did not match the planned ordered declaration structure and AST attributes.',
|
|
521
|
+
changes: decision.changes,
|
|
522
|
+
render_report: render_report(rendered, :exact_declaration_composite),
|
|
523
|
+
verification: verification.merge(base_participated: true)
|
|
524
|
+
)
|
|
525
|
+
end
|
|
526
|
+
|
|
527
|
+
result(
|
|
528
|
+
:merge3,
|
|
529
|
+
request,
|
|
530
|
+
output: rendered.content,
|
|
531
|
+
changes: decision.changes,
|
|
532
|
+
render_report: render_report(rendered, :exact_declaration_composite),
|
|
533
|
+
verification: verification.merge(base_participated: true)
|
|
534
|
+
)
|
|
535
|
+
end
|
|
536
|
+
|
|
537
|
+
def composite_fragments(documents, decision)
|
|
538
|
+
ours = documents.fetch(:ours)
|
|
539
|
+
fragments = []
|
|
540
|
+
cursor = 1
|
|
541
|
+
edits = composite_edits(documents, decision).sort_by do |edit|
|
|
542
|
+
insertion = edit.fetch(:end_line) < edit.fetch(:start_line)
|
|
543
|
+
[edit.fetch(:start_line), insertion ? 0 : 1]
|
|
544
|
+
end
|
|
545
|
+
edits.each do |edit|
|
|
546
|
+
append_range(fragments, :ours, cursor, edit.fetch(:start_line) - 1)
|
|
547
|
+
edit.fetch(:owners).each_with_index do |owner, index|
|
|
548
|
+
append_owner(fragments, owner)
|
|
549
|
+
ensure_plan_boundary!(fragments, documents) if index < edit.fetch(:owners).length - 1
|
|
550
|
+
end
|
|
551
|
+
cursor = [cursor, edit.fetch(:end_line) + 1].max
|
|
552
|
+
end
|
|
553
|
+
append_range(fragments, :ours, cursor, ours.source.lines.length)
|
|
554
|
+
append_declaration_additions(fragments, documents, decision)
|
|
555
|
+
fragments
|
|
556
|
+
end
|
|
557
|
+
|
|
558
|
+
def composite_edits(documents, decision)
|
|
559
|
+
replacement_edits(documents, decision) + import_insertion_edits(documents, decision)
|
|
560
|
+
end
|
|
561
|
+
|
|
562
|
+
def replacement_edits(documents, decision)
|
|
563
|
+
ours = documents.fetch(:ours)
|
|
564
|
+
decision.choices.filter_map do |id, role|
|
|
565
|
+
ours_owner = ours.by_id[id]
|
|
566
|
+
next unless ours_owner
|
|
567
|
+
next if role == :ours
|
|
568
|
+
|
|
569
|
+
{
|
|
570
|
+
start_line: ours_owner.start_line,
|
|
571
|
+
end_line: ours_owner.end_line,
|
|
572
|
+
owners: [role && documents.fetch(role).by_id[id]].compact
|
|
573
|
+
}
|
|
574
|
+
end
|
|
575
|
+
end
|
|
576
|
+
|
|
577
|
+
def import_insertion_edits(documents, decision)
|
|
578
|
+
additions = added_theirs_owners(documents, decision).select { |owner| import_owner?(owner) }
|
|
579
|
+
return [] if additions.empty?
|
|
580
|
+
|
|
581
|
+
ours = documents.fetch(:ours)
|
|
582
|
+
anchor = ours.owners.reverse.find { |owner| package_or_import_owner?(owner) }
|
|
583
|
+
insertion_line = anchor ? anchor.end_line + 1 : 1
|
|
584
|
+
[{
|
|
585
|
+
start_line: insertion_line,
|
|
586
|
+
end_line: insertion_line - 1,
|
|
587
|
+
owners: additions
|
|
588
|
+
}]
|
|
589
|
+
end
|
|
590
|
+
|
|
591
|
+
def append_declaration_additions(fragments, documents, decision)
|
|
592
|
+
additions = added_theirs_owners(documents, decision).reject { |owner| import_owner?(owner) }
|
|
593
|
+
return if additions.empty?
|
|
594
|
+
|
|
595
|
+
ensure_plan_boundary!(fragments, documents)
|
|
596
|
+
additions.each_with_index do |owner, index|
|
|
597
|
+
append_owner(fragments, owner)
|
|
598
|
+
ensure_plan_boundary!(fragments, documents) if index < additions.length - 1
|
|
599
|
+
end
|
|
600
|
+
end
|
|
601
|
+
|
|
602
|
+
def added_theirs_owners(documents, decision)
|
|
603
|
+
ours = documents.fetch(:ours)
|
|
604
|
+
documents.fetch(:theirs).owners.select do |owner|
|
|
605
|
+
!ours.by_id.key?(owner.id) && decision.choices[owner.id] == :theirs
|
|
606
|
+
end
|
|
607
|
+
end
|
|
608
|
+
|
|
609
|
+
def ensure_plan_boundary!(fragments, documents)
|
|
610
|
+
fragment = fragments.last
|
|
611
|
+
return if fragment.nil? || fragment_content(fragment, documents).end_with?("\n")
|
|
612
|
+
|
|
613
|
+
if fragment.is_a?(Ast::Merge::SourceRender::SourceFragment) && fragment.start_line < fragment.end_line
|
|
614
|
+
fragments[-1] = source_range_fragment(fragment.revision, fragment.start_line, fragment.end_line - 1)
|
|
615
|
+
else
|
|
616
|
+
fragments.pop
|
|
617
|
+
end
|
|
618
|
+
content = if fragment.is_a?(Ast::Merge::SourceRender::SourceFragment)
|
|
619
|
+
documents.fetch(fragment.revision).source.lines.fetch(fragment.end_line - 1)
|
|
620
|
+
else
|
|
621
|
+
fragment.content
|
|
622
|
+
end
|
|
623
|
+
fragments << Ast::Merge::SourceRender::SynthesizedFragment.new(
|
|
624
|
+
content: "#{content}\n",
|
|
625
|
+
reason: :declaration_separator,
|
|
626
|
+
producer: provider_id,
|
|
627
|
+
metadata: { source_role: fragment.respond_to?(:revision) ? fragment.revision : nil, copied_source: true }
|
|
628
|
+
)
|
|
629
|
+
end
|
|
630
|
+
|
|
631
|
+
def fragment_content(fragment, documents)
|
|
632
|
+
return fragment.content if fragment.is_a?(Ast::Merge::SourceRender::SynthesizedFragment)
|
|
633
|
+
|
|
634
|
+
document = documents.fetch(fragment.revision)
|
|
635
|
+
document.source.lines.slice(fragment.start_line - 1, fragment.end_line - fragment.start_line + 1).join
|
|
636
|
+
end
|
|
637
|
+
|
|
638
|
+
def append_owner(fragments, owner)
|
|
639
|
+
fragments << source_range_fragment(owner.role, owner.start_line, owner.end_line)
|
|
640
|
+
end
|
|
641
|
+
|
|
642
|
+
def append_range(fragments, role, start_line, end_line)
|
|
643
|
+
return if start_line > end_line
|
|
644
|
+
|
|
645
|
+
fragments << source_range_fragment(role, start_line, end_line)
|
|
646
|
+
end
|
|
647
|
+
|
|
648
|
+
def expected_owners(documents, decision)
|
|
649
|
+
ours = documents.fetch(:ours)
|
|
650
|
+
expected = ours.owners.filter_map do |owner|
|
|
651
|
+
role = decision.choices[owner.id]
|
|
652
|
+
role && documents.fetch(role).by_id[owner.id]
|
|
653
|
+
end
|
|
654
|
+
import_offset = expected.rindex { |owner| package_or_import_owner?(owner) }
|
|
655
|
+
documents.fetch(:theirs).owners.each do |owner|
|
|
656
|
+
next if ours.by_id.key?(owner.id)
|
|
657
|
+
next unless decision.choices[owner.id] == :theirs
|
|
658
|
+
|
|
659
|
+
if import_owner?(owner)
|
|
660
|
+
import_offset = import_offset ? import_offset + 1 : 0
|
|
661
|
+
expected.insert(import_offset, owner)
|
|
662
|
+
else
|
|
663
|
+
expected << owner
|
|
664
|
+
end
|
|
665
|
+
end
|
|
666
|
+
expected
|
|
667
|
+
end
|
|
668
|
+
|
|
669
|
+
def grouped_owner?(owner)
|
|
670
|
+
signature = owner.signature.first == :export_declaration ? owner.signature.fetch(1) : owner.signature
|
|
671
|
+
signature.first == :variables && signature.last.length > 1
|
|
672
|
+
end
|
|
673
|
+
|
|
674
|
+
def import_owner?(owner)
|
|
675
|
+
owner.signature.first == :import
|
|
676
|
+
end
|
|
677
|
+
|
|
678
|
+
alias package_or_import_owner? import_owner?
|
|
679
|
+
|
|
680
|
+
def verify_rendered(output, expected, dialect)
|
|
681
|
+
parsed = parse_source(output, :output, dialect)
|
|
682
|
+
if provider_failure?(parsed)
|
|
683
|
+
return {
|
|
684
|
+
output_reparsed: false,
|
|
685
|
+
semantic_match: false,
|
|
686
|
+
parse_error: failure_detail(parsed)
|
|
687
|
+
}
|
|
688
|
+
end
|
|
689
|
+
|
|
690
|
+
actual = parsed.owners.map { |owner| [owner.signature, owner.fingerprint] }
|
|
691
|
+
planned = expected.map { |owner| [owner.signature, owner.fingerprint] }
|
|
692
|
+
{
|
|
693
|
+
output_reparsed: true,
|
|
694
|
+
semantic_match: actual == planned,
|
|
695
|
+
ordered_declaration_signatures_verified: actual.map(&:first) == planned.map(&:first),
|
|
696
|
+
ast_attributes_verified: actual.map(&:last) == planned.map(&:last),
|
|
697
|
+
planned_declaration_count: planned.length,
|
|
698
|
+
output_declaration_count: actual.length
|
|
699
|
+
}
|
|
700
|
+
end
|
|
701
|
+
|
|
702
|
+
def render_conflicts(request, documents, decision)
|
|
703
|
+
localized = localized_conflict_fragments(request, documents, decision)
|
|
704
|
+
if localized
|
|
705
|
+
rendered = render_plan(request, localized)
|
|
706
|
+
return conflict_failure(request, decision, rendered, :declaration_localized_conflict)
|
|
707
|
+
end
|
|
708
|
+
|
|
709
|
+
rendered = render_plan(request, [whole_document_conflict(request, decision)])
|
|
710
|
+
conflict_failure(
|
|
711
|
+
request,
|
|
712
|
+
decision,
|
|
713
|
+
rendered,
|
|
714
|
+
:full_file_conflict,
|
|
715
|
+
fallbacks: [{ from: :declaration_localization, to: :full_file_conflict, reason: :source_ownership_unproven }]
|
|
716
|
+
)
|
|
717
|
+
end
|
|
718
|
+
|
|
719
|
+
def localized_conflict_fragments(request, documents, decision)
|
|
720
|
+
return if decision.conflicts.any? { |conflict| conflict[:owner_id].nil? }
|
|
721
|
+
|
|
722
|
+
ours = documents.fetch(:ours)
|
|
723
|
+
return unless decision.conflicts.all? { |conflict| ours.by_id.key?(conflict.fetch(:owner_id)) }
|
|
724
|
+
|
|
725
|
+
ranges = decision.conflicts.map { |conflict| [conflict, ours.by_id.fetch(conflict.fetch(:owner_id))] }
|
|
726
|
+
return unless ranges.sort_by { |_conflict, owner| owner.start_line }.each_cons(2).none? do |left, right|
|
|
727
|
+
left.last.end_line >= right.last.start_line
|
|
728
|
+
end
|
|
729
|
+
|
|
730
|
+
fragments = []
|
|
731
|
+
cursor = 1
|
|
732
|
+
ranges.sort_by { |_conflict, owner| owner.start_line }.each do |conflict, owner|
|
|
733
|
+
append_range(fragments, :ours, cursor, owner.start_line - 1)
|
|
734
|
+
fragments << declaration_conflict_fragment(request, documents, conflict)
|
|
735
|
+
cursor = owner.end_line + 1
|
|
736
|
+
end
|
|
737
|
+
append_range(fragments, :ours, cursor, ours.source.lines.length)
|
|
738
|
+
fragments
|
|
739
|
+
end
|
|
740
|
+
|
|
741
|
+
def declaration_conflict_fragment(request, documents, conflict)
|
|
742
|
+
Ast::Merge::SourceRender::ConflictFragment.new(
|
|
743
|
+
conflict_id: conflict.fetch(:conflict_id),
|
|
744
|
+
base: conflict_declaration_side(documents, conflict, :base),
|
|
745
|
+
ours: conflict_declaration_side(documents, conflict, :ours),
|
|
746
|
+
theirs: conflict_declaration_side(documents, conflict, :theirs),
|
|
747
|
+
labels: request.fetch(:labels, {}),
|
|
748
|
+
marker_size: request.fetch(:conflict_marker_size, 7),
|
|
749
|
+
metadata: { path: conflict.fetch(:path), category: conflict.fetch(:category) }
|
|
750
|
+
)
|
|
751
|
+
end
|
|
752
|
+
|
|
753
|
+
def conflict_declaration_side(documents, conflict, role)
|
|
754
|
+
document = documents.fetch(role)
|
|
755
|
+
owner = document.by_id[conflict.fetch(:owner_id)]
|
|
756
|
+
owner ? [conflict_source_fragment(owner, document.source)] : []
|
|
757
|
+
end
|
|
758
|
+
|
|
759
|
+
def unsafe_document_failure(request, role, parsed)
|
|
760
|
+
return parsed unless parsed[:unsafe_range] || parsed[:ambiguous_owner]
|
|
761
|
+
|
|
762
|
+
conflict_id = "typescript-unsafe-#{Digest::SHA256.hexdigest([role, failure_detail(parsed)].join("\0"))[0, 16]}"
|
|
763
|
+
conflict = {
|
|
764
|
+
conflict_id: conflict_id,
|
|
765
|
+
category: parsed[:ambiguous_owner] ? :ambiguous_owner : :unsafe_source_range,
|
|
766
|
+
path: '<document>',
|
|
767
|
+
owner_id: nil,
|
|
768
|
+
source_role: role
|
|
769
|
+
}
|
|
770
|
+
rendered = render_plan(
|
|
771
|
+
request,
|
|
772
|
+
[whole_document_conflict(request, Decision.new(changes: [], conflicts: [conflict], choices: {}))]
|
|
773
|
+
)
|
|
774
|
+
failure(
|
|
775
|
+
:merge3,
|
|
776
|
+
request,
|
|
777
|
+
category: conflict.fetch(:category),
|
|
778
|
+
message: "#{role} cannot be merged safely: #{failure_detail(parsed)}",
|
|
779
|
+
conflicts: [conflict],
|
|
780
|
+
conflicted_output: rendered.content,
|
|
781
|
+
source_role: role,
|
|
782
|
+
render_report: render_report(rendered, :full_file_conflict),
|
|
783
|
+
verification: { base_participated: true }
|
|
784
|
+
)
|
|
785
|
+
end
|
|
786
|
+
|
|
787
|
+
def whole_document_conflict(request, decision)
|
|
788
|
+
Ast::Merge::SourceRender::ConflictFragment.new(
|
|
789
|
+
conflict_id: decision.conflicts.first.fetch(:conflict_id),
|
|
790
|
+
base: [conflict_whole_source_fragment(:base, request.fetch(:base_source))],
|
|
791
|
+
ours: [conflict_whole_source_fragment(:ours, request.fetch(:ours_source))],
|
|
792
|
+
theirs: [conflict_whole_source_fragment(:theirs, request.fetch(:theirs_source))],
|
|
793
|
+
labels: request.fetch(:labels, {}),
|
|
794
|
+
marker_size: request.fetch(:conflict_marker_size, 7),
|
|
795
|
+
metadata: { conflicts: decision.conflicts.map { |conflict| conflict[:conflict_id] } }
|
|
796
|
+
)
|
|
797
|
+
end
|
|
798
|
+
|
|
799
|
+
def conflict_source_fragment(owner, source)
|
|
800
|
+
fragment_source = source.lines.slice(owner.start_line - 1, owner.end_line - owner.start_line + 1).join
|
|
801
|
+
return source_range_fragment(owner.role, owner.start_line, owner.end_line) if fragment_source.end_with?("\n")
|
|
802
|
+
|
|
803
|
+
synthesized_copy("#{fragment_source}\n", :conflict_line_boundary, owner.role)
|
|
804
|
+
end
|
|
805
|
+
|
|
806
|
+
def conflict_whole_source_fragment(role, source)
|
|
807
|
+
return whole_source_fragment(role, source) if source.empty? || source.end_with?("\n")
|
|
808
|
+
|
|
809
|
+
synthesized_copy("#{source}\n", :conflict_line_boundary, role)
|
|
810
|
+
end
|
|
811
|
+
|
|
812
|
+
def synthesized_copy(content, reason, role)
|
|
813
|
+
Ast::Merge::SourceRender::SynthesizedFragment.new(
|
|
814
|
+
content: content,
|
|
815
|
+
reason: reason,
|
|
816
|
+
producer: provider_id,
|
|
817
|
+
metadata: { source_role: role, copied_source: true }
|
|
818
|
+
)
|
|
819
|
+
end
|
|
820
|
+
|
|
821
|
+
def whole_source_fragment(role, source)
|
|
822
|
+
return synthesized_copy('', :exact_empty_source, role) if source.empty?
|
|
823
|
+
|
|
824
|
+
source_range_fragment(role, 1, source.lines.length)
|
|
825
|
+
end
|
|
826
|
+
|
|
827
|
+
def source_range_fragment(role, start_line, end_line)
|
|
828
|
+
Ast::Merge::SourceRender::SourceFragment.new(
|
|
829
|
+
revision: role,
|
|
830
|
+
start_line: start_line,
|
|
831
|
+
end_line: end_line,
|
|
832
|
+
metadata: { source_role: role }
|
|
833
|
+
)
|
|
834
|
+
end
|
|
835
|
+
|
|
836
|
+
def render_plan(request, fragments)
|
|
837
|
+
Ast::Merge::SourceRender::Renderer.new.render(
|
|
838
|
+
Ast::Merge::SourceRender::Plan.new(
|
|
839
|
+
sources: {
|
|
840
|
+
base: request[:base_source].to_s,
|
|
841
|
+
ours: request[:ours_source].to_s,
|
|
842
|
+
theirs: request[:theirs_source].to_s
|
|
843
|
+
},
|
|
844
|
+
fragments: fragments,
|
|
845
|
+
metadata: { provider_id: provider_id }
|
|
846
|
+
)
|
|
847
|
+
)
|
|
848
|
+
end
|
|
849
|
+
|
|
850
|
+
def render_report(rendered, strategy)
|
|
851
|
+
{
|
|
852
|
+
strategy: strategy,
|
|
853
|
+
line_records: rendered.line_records,
|
|
854
|
+
synthesized_fragments: rendered.synthesized_fragments,
|
|
855
|
+
conflicts: rendered.conflicts,
|
|
856
|
+
verification_input: rendered.verification_input
|
|
857
|
+
}
|
|
858
|
+
end
|
|
859
|
+
|
|
860
|
+
def conflict_failure(request, decision, rendered, strategy, fallbacks: [])
|
|
861
|
+
failure(
|
|
862
|
+
:merge3,
|
|
863
|
+
request,
|
|
864
|
+
category: :merge_conflict,
|
|
865
|
+
message: 'TypeScript top-level declaration changed incompatibly on both sides.',
|
|
866
|
+
changes: decision.changes,
|
|
867
|
+
conflicts: decision.conflicts,
|
|
868
|
+
conflicted_output: rendered.content,
|
|
869
|
+
render_report: render_report(rendered, strategy),
|
|
870
|
+
verification: { base_participated: true },
|
|
871
|
+
fallbacks: fallbacks
|
|
872
|
+
)
|
|
873
|
+
end
|
|
874
|
+
|
|
875
|
+
def parse_failure(operation, request, role, parsed)
|
|
876
|
+
category = if parsed[:parse_error]
|
|
877
|
+
:parse_error
|
|
878
|
+
elsif parsed[:ambiguous_owner]
|
|
879
|
+
:ambiguous_owner
|
|
880
|
+
else
|
|
881
|
+
:unsafe_source_range
|
|
882
|
+
end
|
|
883
|
+
failure(
|
|
884
|
+
operation,
|
|
885
|
+
request,
|
|
886
|
+
category: category,
|
|
887
|
+
message: "#{role} parse error: #{failure_detail(parsed)}",
|
|
888
|
+
source_role: role
|
|
889
|
+
)
|
|
890
|
+
end
|
|
891
|
+
|
|
892
|
+
def failure_detail(parsed)
|
|
893
|
+
return parsed[:parse_error] if parsed[:parse_error]
|
|
894
|
+
return "ambiguous duplicate declaration #{parsed[:ambiguous_owner].inspect}" if parsed[:ambiguous_owner]
|
|
895
|
+
|
|
896
|
+
"unsafe declaration range #{parsed[:unsafe_range].inspect}"
|
|
897
|
+
end
|
|
898
|
+
|
|
899
|
+
def provider_failure?(value)
|
|
900
|
+
value.is_a?(Hash) &&
|
|
901
|
+
(value[:ok] == false || value.key?(:parse_error) || value.key?(:ambiguous_owner) || value.key?(:unsafe_range))
|
|
902
|
+
end
|
|
903
|
+
|
|
904
|
+
def result(operation, request, changes: [], diagnostics: [], render_report: {}, verification: {}, **payload)
|
|
905
|
+
Ast::Merge::ProviderResult.build(
|
|
906
|
+
operation: operation,
|
|
907
|
+
success: true,
|
|
908
|
+
envelope: envelope(
|
|
909
|
+
request,
|
|
910
|
+
changes: changes,
|
|
911
|
+
diagnostics: diagnostics,
|
|
912
|
+
render_report: render_report,
|
|
913
|
+
verification: verification
|
|
914
|
+
),
|
|
915
|
+
**payload
|
|
916
|
+
)
|
|
917
|
+
end
|
|
918
|
+
|
|
919
|
+
def failure(operation, request, category:, message:, changes: [], conflicts: [], fallbacks: [],
|
|
920
|
+
render_report: {}, verification: {}, **payload)
|
|
921
|
+
Ast::Merge::ProviderResult.build(
|
|
922
|
+
operation: operation,
|
|
923
|
+
success: false,
|
|
924
|
+
envelope: envelope(
|
|
925
|
+
request,
|
|
926
|
+
changes: changes,
|
|
927
|
+
conflicts: conflicts,
|
|
928
|
+
fallbacks: fallbacks,
|
|
929
|
+
diagnostics: [{ category: category, severity: :error, message: message, blocking: true }],
|
|
930
|
+
render_report: render_report,
|
|
931
|
+
verification: verification
|
|
932
|
+
),
|
|
933
|
+
**payload
|
|
934
|
+
)
|
|
935
|
+
end
|
|
936
|
+
|
|
937
|
+
def envelope(request, **fields)
|
|
938
|
+
{
|
|
939
|
+
provider: {
|
|
940
|
+
provider_id: provider_id,
|
|
941
|
+
family: family,
|
|
942
|
+
dialect: request[:dialect] || :typescript,
|
|
943
|
+
backend: request[:backend] || TREE_SITTER_BACKEND.id.to_sym,
|
|
944
|
+
package: 'typescript-merge',
|
|
945
|
+
package_version: TypeScript::Merge::Version::VERSION
|
|
946
|
+
},
|
|
947
|
+
profile: { profile_id: request[:profile_id] || DEFAULT_PROFILE }
|
|
948
|
+
}.merge(fields)
|
|
949
|
+
end
|
|
950
|
+
end
|
|
951
|
+
# rubocop:enable Metrics/AbcSize, Metrics/ClassLength, Metrics/CyclomaticComplexity, Metrics/MethodLength, Metrics/ParameterLists, Metrics/PerceivedComplexity
|
|
952
|
+
end
|
|
953
|
+
end
|