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