rbs-merge 7.1.1 → 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/README.md +2 -10
- data/lib/rbs/merge/provider.rb +891 -0
- data/lib/rbs/merge/version.rb +1 -1
- data/lib/rbs/merge.rb +11 -0
- data/sig/rbs/merge.rbs +13 -0
- data.tar.gz.sig +0 -0
- metadata +17 -16
- metadata.gz.sig +0 -0
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 3ff859ca9d5b341aeafadc243b34eb38be17976d5508c472d34357512790b6f2
|
|
4
|
+
data.tar.gz: 1cb7bb66b2472e9bae12c4a773cefb9d9123572f56f1bfbdb8db05fabb796947
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 58b8b4f4fee83df915c21f86c8617f28fa3ec9b8fb4400993eb29336ac751a4141db444e9ce547e930042276d293b0bfb80859a781e672562c2b67e5fc1500bf
|
|
7
|
+
data.tar.gz: 5a2948e0f81c006710f73cfc52e15be93ee00c08ba97d6cb17a1bc67f00374d77e59a9de0efa0b0d6faf635b7f70d6b32bd53024a16889989cae0728bf74e437
|
checksums.yaml.gz.sig
CHANGED
|
Binary file
|
data/README.md
CHANGED
|
@@ -93,7 +93,7 @@ File.write("merged.rbs", result.to_s)
|
|
|
93
93
|
|
|
94
94
|
### Compatibility
|
|
95
95
|
|
|
96
|
-
Compatible with MRI Ruby 4.0.0+,
|
|
96
|
+
Compatible with MRI Ruby 4.0.0+, JRuby, and TruffleRuby.
|
|
97
97
|
CI workflows and Appraisals are generated for MRI Ruby 4.0.0+.
|
|
98
98
|
This test floor is configured by `ruby.test_minimum` in `.kettle-jem.yml` and
|
|
99
99
|
may be higher than the gem's runtime compatibility floor when legacy Rubies are
|
|
@@ -442,20 +442,12 @@ See [SECURITY.md][🔐security].
|
|
|
442
442
|
## 🤝 Contributing
|
|
443
443
|
|
|
444
444
|
If you need some ideas of where to help, you could work on adding more code coverage,
|
|
445
|
-
|
|
446
|
-
or use the gem and think about how it could be better.
|
|
445
|
+
check [issues][🤝gh-issues] or [PRs][🤝gh-pulls], or use the gem and think about how it could be better.
|
|
447
446
|
|
|
448
447
|
We [![Keep A Changelog][📗keep-changelog-img]][📗keep-changelog] so if you make changes, remember to update it.
|
|
449
448
|
|
|
450
449
|
See [CONTRIBUTING.md][🤝contributing] for more detailed instructions.
|
|
451
450
|
|
|
452
|
-
### Code Coverage
|
|
453
|
-
|
|
454
|
-
<details markdown="1">
|
|
455
|
-
<summary>Coverage service badges</summary>
|
|
456
|
-
|
|
457
|
-
</details>
|
|
458
|
-
|
|
459
451
|
## 📌 Versioning
|
|
460
452
|
|
|
461
453
|
This library follows [![Semantic Versioning 2.0.0][📌semver-img]][📌semver] for its public API where practical.
|
|
@@ -0,0 +1,891 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'digest'
|
|
4
|
+
require 'json'
|
|
5
|
+
|
|
6
|
+
module Rbs
|
|
7
|
+
module Merge
|
|
8
|
+
# Base-aware, source-preserving provider for native RBS declarations.
|
|
9
|
+
# rubocop:disable Metrics/AbcSize, Metrics/ClassLength, Metrics/CyclomaticComplexity, Metrics/MethodLength, Metrics/ParameterLists, Metrics/PerceivedComplexity -- provider decisions, source plans, and verification form one boundary
|
|
10
|
+
class Provider
|
|
11
|
+
DEFAULT_PROFILE = :source_preserving
|
|
12
|
+
Owner = Data.define(:id, :signature, :fingerprint, :start_line, :end_line, :role)
|
|
13
|
+
Document = Data.define(:source, :analysis, :owners, :by_id, :unmanaged_fingerprint)
|
|
14
|
+
ExactDocument = Data.define(:source, :analysis, :declarations, :issues, :role)
|
|
15
|
+
Decision = Data.define(:changes, :conflicts, :choices)
|
|
16
|
+
|
|
17
|
+
def provider_id = 'ruby.rbs'
|
|
18
|
+
def family = 'rbs'
|
|
19
|
+
|
|
20
|
+
def capabilities
|
|
21
|
+
{
|
|
22
|
+
operations: Ast::Merge::ProviderContract::OPERATIONS,
|
|
23
|
+
dialects: %i[rbs],
|
|
24
|
+
backends: %i[rbs],
|
|
25
|
+
profiles: [DEFAULT_PROFILE],
|
|
26
|
+
role: :workflow,
|
|
27
|
+
source_preservation: %i[exact_source declaration_fragments line_provenance reparse semantic_verification]
|
|
28
|
+
}.freeze
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
def analyze(request)
|
|
32
|
+
document = parse_document(:analyze, request, :source)
|
|
33
|
+
return document if provider_failure?(document)
|
|
34
|
+
|
|
35
|
+
result(
|
|
36
|
+
:analyze,
|
|
37
|
+
request,
|
|
38
|
+
analysis: {
|
|
39
|
+
backend: 'rbs',
|
|
40
|
+
valid: true,
|
|
41
|
+
declarations: document.owners.map { |owner| owner_description(owner) }
|
|
42
|
+
},
|
|
43
|
+
verification: { source_parsed: true }
|
|
44
|
+
)
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
def diff2(request)
|
|
48
|
+
before = parse_document(:diff2, request, :before)
|
|
49
|
+
return before if provider_failure?(before)
|
|
50
|
+
|
|
51
|
+
after = parse_document(:diff2, request, :after)
|
|
52
|
+
return after if provider_failure?(after)
|
|
53
|
+
|
|
54
|
+
changes = diff_documents(before, after)
|
|
55
|
+
result(
|
|
56
|
+
:diff2,
|
|
57
|
+
request,
|
|
58
|
+
diff: { changes: changes },
|
|
59
|
+
changes: changes,
|
|
60
|
+
verification: { before_parsed: true, after_parsed: true }
|
|
61
|
+
)
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
def merge2(request)
|
|
65
|
+
merge3_request = request.merge(
|
|
66
|
+
base_source: request.fetch(:current_source),
|
|
67
|
+
ours_source: request.fetch(:current_source),
|
|
68
|
+
theirs_source: request.fetch(:incoming_source)
|
|
69
|
+
)
|
|
70
|
+
merged = merge3(merge3_request)
|
|
71
|
+
merged.merge(
|
|
72
|
+
operation: :merge2,
|
|
73
|
+
verification: merged.fetch(:verification).except(:base_participated)
|
|
74
|
+
)
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
def merge3(request)
|
|
78
|
+
exact_role = exact_revision_role(request)
|
|
79
|
+
return merge3_exact(request, exact_role) if exact_role
|
|
80
|
+
|
|
81
|
+
documents = parse_merge3_documents(request)
|
|
82
|
+
return documents if provider_failure?(documents)
|
|
83
|
+
|
|
84
|
+
decision = decide(documents, include_unmanaged_conflict: true)
|
|
85
|
+
return render_conflicts(request, documents, decision) unless decision.conflicts.empty?
|
|
86
|
+
|
|
87
|
+
render_composite(request, documents, decision)
|
|
88
|
+
end
|
|
89
|
+
|
|
90
|
+
private
|
|
91
|
+
|
|
92
|
+
def parse_merge3_documents(request)
|
|
93
|
+
%i[base ours theirs].each_with_object({}) do |role, documents|
|
|
94
|
+
source = request.fetch(:"#{role}_source")
|
|
95
|
+
document = parse_source(source, role)
|
|
96
|
+
if document.is_a?(Hash) && document[:parse_error]
|
|
97
|
+
return parse_failure(:merge3, request, role, document)
|
|
98
|
+
elsif provider_failure?(document)
|
|
99
|
+
return unsafe_document_failure(request, role, document)
|
|
100
|
+
end
|
|
101
|
+
|
|
102
|
+
documents[role] = document
|
|
103
|
+
end
|
|
104
|
+
end
|
|
105
|
+
|
|
106
|
+
def parse_document(operation, request, role)
|
|
107
|
+
source = request.fetch(role == :source ? :source : :"#{role}_source")
|
|
108
|
+
parsed = parse_source(source, role)
|
|
109
|
+
return parse_failure(operation, request, role, parsed) if provider_failure?(parsed)
|
|
110
|
+
|
|
111
|
+
parsed
|
|
112
|
+
end
|
|
113
|
+
|
|
114
|
+
def parse_source(source, role)
|
|
115
|
+
analysis = TreeHaver.with_backend(:rbs) { FileAnalysis.new(source) }
|
|
116
|
+
unless analysis.errors.empty? && analysis.backend == :rbs
|
|
117
|
+
return { parse_error: analysis.errors.map(&:to_s).join('; '), source_role: role }
|
|
118
|
+
end
|
|
119
|
+
|
|
120
|
+
owners = analysis.declarations.map do |declaration|
|
|
121
|
+
wrapper = NodeWrapper.new(
|
|
122
|
+
declaration,
|
|
123
|
+
lines: source.split("\n", -1),
|
|
124
|
+
source: source,
|
|
125
|
+
backend: :rbs
|
|
126
|
+
)
|
|
127
|
+
return { unsafe_range: wrapper.signature, source_role: role } unless safe_range?(source, declaration.location)
|
|
128
|
+
|
|
129
|
+
signature = wrapper.signature
|
|
130
|
+
Owner.new(
|
|
131
|
+
id: owner_id(signature),
|
|
132
|
+
signature: signature,
|
|
133
|
+
fingerprint: owner_fingerprint(source, declaration),
|
|
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
|
+
return { unsafe_range: :overlapping_declarations, source_role: role } unless non_overlapping?(owners)
|
|
142
|
+
|
|
143
|
+
Document.new(
|
|
144
|
+
source: source,
|
|
145
|
+
analysis: analysis,
|
|
146
|
+
owners: owners.freeze,
|
|
147
|
+
by_id: owners.to_h { |owner| [owner.id, owner] }.freeze,
|
|
148
|
+
unmanaged_fingerprint: unmanaged_fingerprint(source, owners)
|
|
149
|
+
)
|
|
150
|
+
rescue StandardError => e
|
|
151
|
+
{ parse_error: e.message, source_role: role }
|
|
152
|
+
end
|
|
153
|
+
|
|
154
|
+
def parse_exact_source(source, role)
|
|
155
|
+
analysis = TreeHaver.with_backend(:rbs) { FileAnalysis.new(source) }
|
|
156
|
+
unless analysis.errors.empty? && analysis.backend == :rbs
|
|
157
|
+
return { parse_error: analysis.errors.map(&:to_s).join('; '), source_role: role }
|
|
158
|
+
end
|
|
159
|
+
|
|
160
|
+
declarations = analysis.declarations.map do |declaration|
|
|
161
|
+
wrapper = NodeWrapper.new(
|
|
162
|
+
declaration,
|
|
163
|
+
lines: source.split("\n", -1),
|
|
164
|
+
source: source,
|
|
165
|
+
backend: :rbs
|
|
166
|
+
)
|
|
167
|
+
[wrapper.signature, semantic_attributes(declaration)]
|
|
168
|
+
end.freeze
|
|
169
|
+
duplicate = declarations.group_by(&:first).find { |_signature, matches| matches.length > 1 }
|
|
170
|
+
issues = if duplicate
|
|
171
|
+
[{
|
|
172
|
+
category: :ambiguous_owner,
|
|
173
|
+
declaration: duplicate.first,
|
|
174
|
+
message: duplicate_message(duplicate.first)
|
|
175
|
+
}]
|
|
176
|
+
else
|
|
177
|
+
[]
|
|
178
|
+
end
|
|
179
|
+
ExactDocument.new(
|
|
180
|
+
source: source,
|
|
181
|
+
analysis: analysis,
|
|
182
|
+
declarations: declarations,
|
|
183
|
+
issues: issues.freeze,
|
|
184
|
+
role: role
|
|
185
|
+
)
|
|
186
|
+
rescue StandardError => e
|
|
187
|
+
{ parse_error: e.message, source_role: role }
|
|
188
|
+
end
|
|
189
|
+
|
|
190
|
+
def safe_range?(source, location)
|
|
191
|
+
return false unless location
|
|
192
|
+
return false unless location.start_line.positive? && location.end_line >= location.start_line
|
|
193
|
+
return false unless location.start_column.zero?
|
|
194
|
+
return false if location.start_pos.negative? || location.end_pos > source.bytesize
|
|
195
|
+
|
|
196
|
+
line_end = source.index("\n", location.end_pos) || source.bytesize
|
|
197
|
+
source.byteslice(location.end_pos...line_end).to_s.each_byte.all? { |byte| [9, 13, 32].include?(byte) }
|
|
198
|
+
end
|
|
199
|
+
|
|
200
|
+
def non_overlapping?(owners)
|
|
201
|
+
owners.each_cons(2).all? { |left, right| left.end_line < right.start_line }
|
|
202
|
+
end
|
|
203
|
+
|
|
204
|
+
def unmanaged_fingerprint(source, owners)
|
|
205
|
+
owned_lines = owners.each_with_object({}) do |owner, lines|
|
|
206
|
+
(owner.start_line..owner.end_line).each { |line| lines[line] = true }
|
|
207
|
+
end
|
|
208
|
+
source.lines.each_with_index.filter_map do |line, index|
|
|
209
|
+
line unless owned_lines[index + 1]
|
|
210
|
+
end.join
|
|
211
|
+
end
|
|
212
|
+
|
|
213
|
+
def semantic_attributes(value)
|
|
214
|
+
case value
|
|
215
|
+
when Array
|
|
216
|
+
value.map { |item| semantic_attributes(item) }
|
|
217
|
+
when Hash
|
|
218
|
+
value.map { |key, item| [semantic_attributes(key), semantic_attributes(item)] }
|
|
219
|
+
when String, Symbol, Numeric, true, false, nil
|
|
220
|
+
value
|
|
221
|
+
else
|
|
222
|
+
attributes = value.instance_variables.reject { |name| %i[@location @comment].include?(name) }
|
|
223
|
+
[
|
|
224
|
+
value.class.name,
|
|
225
|
+
attributes.map { |name| [name, semantic_attributes(value.instance_variable_get(name))] }
|
|
226
|
+
]
|
|
227
|
+
end
|
|
228
|
+
end
|
|
229
|
+
|
|
230
|
+
def owner_fingerprint(source, declaration)
|
|
231
|
+
location = declaration.location
|
|
232
|
+
exact_source = source.byteslice(location.start_pos...location.end_pos)
|
|
233
|
+
Digest::SHA256.hexdigest(JSON.generate(Ast::Merge.json_ready([semantic_attributes(declaration), exact_source])))
|
|
234
|
+
end
|
|
235
|
+
|
|
236
|
+
def owner_id(signature)
|
|
237
|
+
Digest::SHA256.hexdigest(JSON.generate(Ast::Merge.json_ready(signature)))
|
|
238
|
+
end
|
|
239
|
+
|
|
240
|
+
def owner_path(owner)
|
|
241
|
+
owner ? owner.signature.inspect : '<document>'
|
|
242
|
+
end
|
|
243
|
+
|
|
244
|
+
def owner_description(owner)
|
|
245
|
+
{
|
|
246
|
+
path: owner_path(owner),
|
|
247
|
+
signature: owner.signature,
|
|
248
|
+
source_role: owner.role,
|
|
249
|
+
line_range: [owner.start_line, owner.end_line]
|
|
250
|
+
}
|
|
251
|
+
end
|
|
252
|
+
|
|
253
|
+
def diff_documents(before, after)
|
|
254
|
+
ordered_ids(before, after).filter_map do |id|
|
|
255
|
+
left = before.by_id[id]
|
|
256
|
+
right = after.by_id[id]
|
|
257
|
+
next if equivalent?(left, right)
|
|
258
|
+
|
|
259
|
+
{
|
|
260
|
+
path: owner_path(left || right),
|
|
261
|
+
before: change_side(left),
|
|
262
|
+
after: change_side(right),
|
|
263
|
+
change: change_kind(left, right)
|
|
264
|
+
}.freeze
|
|
265
|
+
end.freeze
|
|
266
|
+
end
|
|
267
|
+
|
|
268
|
+
def change_side(owner)
|
|
269
|
+
return { present: false } unless owner
|
|
270
|
+
|
|
271
|
+
{ present: true, source_role: owner.role, line_range: [owner.start_line, owner.end_line] }
|
|
272
|
+
end
|
|
273
|
+
|
|
274
|
+
def ordered_ids(*documents)
|
|
275
|
+
documents.flat_map { |document| document.owners.map(&:id) }.uniq
|
|
276
|
+
end
|
|
277
|
+
|
|
278
|
+
def change_kind(before, after)
|
|
279
|
+
return :added unless before
|
|
280
|
+
return :deleted unless after
|
|
281
|
+
|
|
282
|
+
:edited
|
|
283
|
+
end
|
|
284
|
+
|
|
285
|
+
def decide(documents, include_unmanaged_conflict:)
|
|
286
|
+
changes = []
|
|
287
|
+
conflicts = []
|
|
288
|
+
choices = {}
|
|
289
|
+
if include_unmanaged_conflict
|
|
290
|
+
append_unmanaged_conflict(changes, conflicts, documents)
|
|
291
|
+
append_directive_conflict(changes, conflicts, documents)
|
|
292
|
+
end
|
|
293
|
+
ordered_ids(*documents.values).each do |id|
|
|
294
|
+
base = documents.fetch(:base).by_id[id]
|
|
295
|
+
ours = documents.fetch(:ours).by_id[id]
|
|
296
|
+
theirs = documents.fetch(:theirs).by_id[id]
|
|
297
|
+
ours_change = side_change(base, ours)
|
|
298
|
+
theirs_change = side_change(base, theirs)
|
|
299
|
+
next choices[id] = :ours if ours_change == :unchanged && theirs_change == :unchanged
|
|
300
|
+
|
|
301
|
+
change = {
|
|
302
|
+
path: owner_path(base || ours || theirs),
|
|
303
|
+
ours: ours_change,
|
|
304
|
+
theirs: theirs_change
|
|
305
|
+
}.freeze
|
|
306
|
+
changes << change
|
|
307
|
+
choice = owner_choice(base, ours, theirs)
|
|
308
|
+
if choice == :conflict
|
|
309
|
+
conflicts << conflict_for(id, base, ours, theirs, change)
|
|
310
|
+
else
|
|
311
|
+
choices[id] = choice
|
|
312
|
+
end
|
|
313
|
+
end
|
|
314
|
+
Decision.new(changes: changes.freeze, conflicts: conflicts.freeze, choices: choices.freeze)
|
|
315
|
+
end
|
|
316
|
+
|
|
317
|
+
def append_unmanaged_conflict(changes, conflicts, documents)
|
|
318
|
+
base = documents.fetch(:base).unmanaged_fingerprint
|
|
319
|
+
ours = documents.fetch(:ours).unmanaged_fingerprint
|
|
320
|
+
theirs = documents.fetch(:theirs).unmanaged_fingerprint
|
|
321
|
+
return if ours == theirs
|
|
322
|
+
|
|
323
|
+
change = {
|
|
324
|
+
path: '<unmanaged-source>',
|
|
325
|
+
ours: base == ours ? :unchanged : :edited,
|
|
326
|
+
theirs: base == theirs ? :unchanged : :edited
|
|
327
|
+
}.freeze
|
|
328
|
+
changes << change
|
|
329
|
+
conflicts << {
|
|
330
|
+
conflict_id: "rbs-unmanaged-#{Digest::SHA256.hexdigest([base, ours, theirs].join("\0"))[0, 16]}",
|
|
331
|
+
category: :unmanaged_source_change,
|
|
332
|
+
path: change.fetch(:path),
|
|
333
|
+
owner_id: nil,
|
|
334
|
+
change_classification: change
|
|
335
|
+
}.freeze
|
|
336
|
+
end
|
|
337
|
+
|
|
338
|
+
def append_directive_conflict(changes, conflicts, documents)
|
|
339
|
+
return unless documents.values.any? { |document| document.analysis.directives.any? }
|
|
340
|
+
|
|
341
|
+
change = { path: '<directives>', ours: :unsafe, theirs: :unsafe }.freeze
|
|
342
|
+
changes << change
|
|
343
|
+
conflicts << {
|
|
344
|
+
conflict_id: "rbs-directives-#{Digest::SHA256.hexdigest(documents.values.map(&:source).join("\0"))[0, 16]}",
|
|
345
|
+
category: :unsafe_directives,
|
|
346
|
+
path: change.fetch(:path),
|
|
347
|
+
owner_id: nil,
|
|
348
|
+
change_classification: change
|
|
349
|
+
}.freeze
|
|
350
|
+
end
|
|
351
|
+
|
|
352
|
+
def side_change(base, side)
|
|
353
|
+
return :unchanged if equivalent?(base, side)
|
|
354
|
+
return :added if !base && side
|
|
355
|
+
return :deleted if base && !side
|
|
356
|
+
return :unchanged unless base || side
|
|
357
|
+
|
|
358
|
+
:edited
|
|
359
|
+
end
|
|
360
|
+
|
|
361
|
+
def owner_choice(base, ours, theirs)
|
|
362
|
+
return :ours if equivalent?(ours, theirs)
|
|
363
|
+
return :theirs if equivalent?(base, ours)
|
|
364
|
+
return :ours if equivalent?(base, theirs)
|
|
365
|
+
return if ours.nil? && theirs.nil?
|
|
366
|
+
|
|
367
|
+
:conflict
|
|
368
|
+
end
|
|
369
|
+
|
|
370
|
+
def equivalent?(left, right)
|
|
371
|
+
return true if left.nil? && right.nil?
|
|
372
|
+
return false unless left && right
|
|
373
|
+
|
|
374
|
+
left.fingerprint == right.fingerprint
|
|
375
|
+
end
|
|
376
|
+
|
|
377
|
+
def conflict_for(id, base, ours, theirs, change)
|
|
378
|
+
{
|
|
379
|
+
conflict_id: "rbs-declaration-#{id[0, 16]}",
|
|
380
|
+
category: base && (!ours || !theirs) ? :delete_edit : :edit_edit,
|
|
381
|
+
path: change.fetch(:path),
|
|
382
|
+
owner_id: id,
|
|
383
|
+
base: owner_state(base),
|
|
384
|
+
ours: owner_state(ours),
|
|
385
|
+
theirs: owner_state(theirs),
|
|
386
|
+
change_classification: change
|
|
387
|
+
}.freeze
|
|
388
|
+
end
|
|
389
|
+
|
|
390
|
+
def owner_state(owner)
|
|
391
|
+
{
|
|
392
|
+
present: !owner.nil?,
|
|
393
|
+
fingerprint: owner&.fingerprint,
|
|
394
|
+
source_role: owner&.role,
|
|
395
|
+
line_range: owner && [owner.start_line, owner.end_line]
|
|
396
|
+
}
|
|
397
|
+
end
|
|
398
|
+
|
|
399
|
+
def exact_revision_role(request)
|
|
400
|
+
base = request.fetch(:base_source)
|
|
401
|
+
ours = request.fetch(:ours_source)
|
|
402
|
+
theirs = request.fetch(:theirs_source)
|
|
403
|
+
return :ours if ours == theirs || base == theirs
|
|
404
|
+
|
|
405
|
+
:theirs if base == ours
|
|
406
|
+
end
|
|
407
|
+
|
|
408
|
+
def merge3_exact(request, role)
|
|
409
|
+
documents = %i[base ours theirs].to_h do |source_role|
|
|
410
|
+
[source_role, parse_exact_source(request.fetch(:"#{source_role}_source"), source_role)]
|
|
411
|
+
end
|
|
412
|
+
winner = documents.fetch(role)
|
|
413
|
+
return parse_failure(:merge3, request, role, winner) if provider_failure?(winner)
|
|
414
|
+
|
|
415
|
+
render_exact(request, documents, role)
|
|
416
|
+
end
|
|
417
|
+
|
|
418
|
+
def render_exact(request, documents, role)
|
|
419
|
+
winner = documents.fetch(role)
|
|
420
|
+
rendered = render_plan(request, [whole_source_fragment(role, request.fetch(:"#{role}_source"))])
|
|
421
|
+
verification = verify_exact_rendered(rendered.content, winner)
|
|
422
|
+
unless verification[:semantic_match] && verification[:byte_exact]
|
|
423
|
+
return failure(
|
|
424
|
+
:merge3,
|
|
425
|
+
request,
|
|
426
|
+
category: :render_failure,
|
|
427
|
+
message: 'RBS exact revision did not remain byte-exact and semantically identical after native reparse.',
|
|
428
|
+
source_role: role,
|
|
429
|
+
render_report: render_report(rendered, :exact_revision),
|
|
430
|
+
verification: verification.merge(base_participated: true)
|
|
431
|
+
)
|
|
432
|
+
end
|
|
433
|
+
|
|
434
|
+
result(
|
|
435
|
+
:merge3,
|
|
436
|
+
request,
|
|
437
|
+
output: rendered.content,
|
|
438
|
+
diagnostics: exact_diagnostics(documents),
|
|
439
|
+
render_report: render_report(rendered, :exact_revision),
|
|
440
|
+
verification: verification.merge(base_participated: true)
|
|
441
|
+
)
|
|
442
|
+
end
|
|
443
|
+
|
|
444
|
+
def verify_exact_rendered(output, expected)
|
|
445
|
+
parsed = parse_exact_source(output, :output)
|
|
446
|
+
if provider_failure?(parsed)
|
|
447
|
+
return {
|
|
448
|
+
output_reparsed: false,
|
|
449
|
+
byte_exact: output == expected.source,
|
|
450
|
+
semantic_match: false,
|
|
451
|
+
parse_error: failure_detail(parsed),
|
|
452
|
+
source_role: expected.role
|
|
453
|
+
}
|
|
454
|
+
end
|
|
455
|
+
|
|
456
|
+
actual_signatures = parsed.declarations.map(&:first)
|
|
457
|
+
expected_signatures = expected.declarations.map(&:first)
|
|
458
|
+
actual_attributes = parsed.declarations.map(&:last)
|
|
459
|
+
expected_attributes = expected.declarations.map(&:last)
|
|
460
|
+
{
|
|
461
|
+
output_reparsed: true,
|
|
462
|
+
byte_exact: output == expected.source,
|
|
463
|
+
semantic_match: parsed.declarations == expected.declarations,
|
|
464
|
+
ordered_declaration_signatures_verified: actual_signatures == expected_signatures,
|
|
465
|
+
ast_attributes_verified: actual_attributes == expected_attributes,
|
|
466
|
+
planned_declaration_count: expected.declarations.length,
|
|
467
|
+
output_declaration_count: parsed.declarations.length,
|
|
468
|
+
source_role: expected.role
|
|
469
|
+
}
|
|
470
|
+
end
|
|
471
|
+
|
|
472
|
+
def exact_diagnostics(documents)
|
|
473
|
+
documents.flat_map do |role, document|
|
|
474
|
+
if provider_failure?(document)
|
|
475
|
+
[{
|
|
476
|
+
category: :parse_error,
|
|
477
|
+
severity: :warning,
|
|
478
|
+
message: "#{role} parse error: #{failure_detail(document)}",
|
|
479
|
+
blocking: false,
|
|
480
|
+
source_role: role
|
|
481
|
+
}]
|
|
482
|
+
else
|
|
483
|
+
document.issues.map do |issue|
|
|
484
|
+
issue.merge(severity: :warning, blocking: false, source_role: role).freeze
|
|
485
|
+
end
|
|
486
|
+
end
|
|
487
|
+
end.freeze
|
|
488
|
+
end
|
|
489
|
+
|
|
490
|
+
def duplicate_message(signature)
|
|
491
|
+
"ambiguous duplicate declaration #{signature.inspect}"
|
|
492
|
+
end
|
|
493
|
+
|
|
494
|
+
def render_composite(request, documents, decision)
|
|
495
|
+
fragments = composite_fragments(documents, decision)
|
|
496
|
+
rendered = render_plan(request, fragments)
|
|
497
|
+
expected = expected_owners(documents, decision)
|
|
498
|
+
verification = verify_rendered(rendered.content, expected)
|
|
499
|
+
unless verification[:semantic_match]
|
|
500
|
+
return failure(
|
|
501
|
+
:merge3,
|
|
502
|
+
request,
|
|
503
|
+
category: :render_failure,
|
|
504
|
+
message: 'RBS composite did not match the planned ordered declaration structure and AST attributes.',
|
|
505
|
+
changes: decision.changes,
|
|
506
|
+
render_report: render_report(rendered, :exact_declaration_composite),
|
|
507
|
+
verification: verification.merge(base_participated: true)
|
|
508
|
+
)
|
|
509
|
+
end
|
|
510
|
+
|
|
511
|
+
result(
|
|
512
|
+
:merge3,
|
|
513
|
+
request,
|
|
514
|
+
output: rendered.content,
|
|
515
|
+
changes: decision.changes,
|
|
516
|
+
render_report: render_report(rendered, :exact_declaration_composite),
|
|
517
|
+
verification: verification.merge(base_participated: true)
|
|
518
|
+
)
|
|
519
|
+
end
|
|
520
|
+
|
|
521
|
+
def composite_fragments(documents, decision)
|
|
522
|
+
ours = documents.fetch(:ours)
|
|
523
|
+
fragments = []
|
|
524
|
+
cursor = 1
|
|
525
|
+
replacement_edits(documents, decision).sort_by { |edit| edit.fetch(:start_line) }.each do |edit|
|
|
526
|
+
append_range(fragments, :ours, cursor, edit.fetch(:start_line) - 1)
|
|
527
|
+
append_owner(fragments, edit[:owner]) if edit[:owner]
|
|
528
|
+
cursor = edit.fetch(:end_line) + 1
|
|
529
|
+
end
|
|
530
|
+
append_range(fragments, :ours, cursor, ours.source.lines.length)
|
|
531
|
+
append_additions(fragments, documents, decision)
|
|
532
|
+
fragments
|
|
533
|
+
end
|
|
534
|
+
|
|
535
|
+
def replacement_edits(documents, decision)
|
|
536
|
+
ours = documents.fetch(:ours)
|
|
537
|
+
decision.choices.filter_map do |id, role|
|
|
538
|
+
ours_owner = ours.by_id[id]
|
|
539
|
+
next unless ours_owner
|
|
540
|
+
next if role == :ours
|
|
541
|
+
|
|
542
|
+
{
|
|
543
|
+
start_line: ours_owner.start_line,
|
|
544
|
+
end_line: ours_owner.end_line,
|
|
545
|
+
owner: role && documents.fetch(role).by_id[id]
|
|
546
|
+
}
|
|
547
|
+
end
|
|
548
|
+
end
|
|
549
|
+
|
|
550
|
+
def append_additions(fragments, documents, decision)
|
|
551
|
+
ours = documents.fetch(:ours)
|
|
552
|
+
additions = documents.fetch(:theirs).owners.select do |owner|
|
|
553
|
+
!ours.by_id.key?(owner.id) && decision.choices[owner.id] == :theirs
|
|
554
|
+
end
|
|
555
|
+
return if additions.empty?
|
|
556
|
+
|
|
557
|
+
ensure_plan_boundary!(fragments, documents)
|
|
558
|
+
additions.each_with_index do |owner, index|
|
|
559
|
+
append_owner(fragments, owner)
|
|
560
|
+
ensure_plan_boundary!(fragments, documents) if index < additions.length - 1
|
|
561
|
+
end
|
|
562
|
+
end
|
|
563
|
+
|
|
564
|
+
def ensure_plan_boundary!(fragments, documents)
|
|
565
|
+
fragment = fragments.last
|
|
566
|
+
return if fragment.nil? || fragment_content(fragment, documents).end_with?("\n")
|
|
567
|
+
|
|
568
|
+
if fragment.is_a?(Ast::Merge::SourceRender::SourceFragment) && fragment.start_line < fragment.end_line
|
|
569
|
+
fragments[-1] = source_range_fragment(fragment.revision, fragment.start_line, fragment.end_line - 1)
|
|
570
|
+
else
|
|
571
|
+
fragments.pop
|
|
572
|
+
end
|
|
573
|
+
content = if fragment.is_a?(Ast::Merge::SourceRender::SourceFragment)
|
|
574
|
+
documents.fetch(fragment.revision).source.lines.fetch(fragment.end_line - 1)
|
|
575
|
+
else
|
|
576
|
+
fragment.content
|
|
577
|
+
end
|
|
578
|
+
fragments << Ast::Merge::SourceRender::SynthesizedFragment.new(
|
|
579
|
+
content: "#{content}\n",
|
|
580
|
+
reason: :declaration_separator,
|
|
581
|
+
producer: provider_id,
|
|
582
|
+
metadata: { source_role: fragment.respond_to?(:revision) ? fragment.revision : nil, copied_source: true }
|
|
583
|
+
)
|
|
584
|
+
end
|
|
585
|
+
|
|
586
|
+
def fragment_content(fragment, documents)
|
|
587
|
+
return fragment.content if fragment.is_a?(Ast::Merge::SourceRender::SynthesizedFragment)
|
|
588
|
+
|
|
589
|
+
document = documents.fetch(fragment.revision)
|
|
590
|
+
document.source.lines.slice(fragment.start_line - 1, fragment.end_line - fragment.start_line + 1).join
|
|
591
|
+
end
|
|
592
|
+
|
|
593
|
+
def append_owner(fragments, owner)
|
|
594
|
+
fragments << source_range_fragment(owner.role, owner.start_line, owner.end_line)
|
|
595
|
+
end
|
|
596
|
+
|
|
597
|
+
def append_range(fragments, role, start_line, end_line)
|
|
598
|
+
return if start_line > end_line
|
|
599
|
+
|
|
600
|
+
fragments << source_range_fragment(role, start_line, end_line)
|
|
601
|
+
end
|
|
602
|
+
|
|
603
|
+
def expected_owners(documents, decision)
|
|
604
|
+
ours = documents.fetch(:ours)
|
|
605
|
+
expected = ours.owners.filter_map do |owner|
|
|
606
|
+
role = decision.choices[owner.id]
|
|
607
|
+
role && documents.fetch(role).by_id[owner.id]
|
|
608
|
+
end
|
|
609
|
+
documents.fetch(:theirs).owners.each do |owner|
|
|
610
|
+
next if ours.by_id.key?(owner.id)
|
|
611
|
+
next unless decision.choices[owner.id] == :theirs
|
|
612
|
+
|
|
613
|
+
expected << owner
|
|
614
|
+
end
|
|
615
|
+
expected
|
|
616
|
+
end
|
|
617
|
+
|
|
618
|
+
def verify_rendered(output, expected)
|
|
619
|
+
parsed = parse_source(output, :output)
|
|
620
|
+
if provider_failure?(parsed)
|
|
621
|
+
return {
|
|
622
|
+
output_reparsed: false,
|
|
623
|
+
semantic_match: false,
|
|
624
|
+
parse_error: failure_detail(parsed)
|
|
625
|
+
}
|
|
626
|
+
end
|
|
627
|
+
|
|
628
|
+
actual = parsed.owners.map { |owner| [owner.signature, owner.fingerprint] }
|
|
629
|
+
planned = expected.map { |owner| [owner.signature, owner.fingerprint] }
|
|
630
|
+
{
|
|
631
|
+
output_reparsed: true,
|
|
632
|
+
semantic_match: actual == planned,
|
|
633
|
+
ordered_declaration_signatures_verified: actual.map(&:first) == planned.map(&:first),
|
|
634
|
+
ast_attributes_verified: actual.map(&:last) == planned.map(&:last),
|
|
635
|
+
planned_declaration_count: planned.length,
|
|
636
|
+
output_declaration_count: actual.length
|
|
637
|
+
}
|
|
638
|
+
end
|
|
639
|
+
|
|
640
|
+
def render_conflicts(request, documents, decision)
|
|
641
|
+
localized = localized_conflict_fragments(request, documents, decision)
|
|
642
|
+
if localized
|
|
643
|
+
rendered = render_plan(request, localized)
|
|
644
|
+
return conflict_failure(request, decision, rendered, :declaration_localized_conflict)
|
|
645
|
+
end
|
|
646
|
+
|
|
647
|
+
rendered = render_plan(request, [whole_document_conflict(request, decision)])
|
|
648
|
+
conflict_failure(
|
|
649
|
+
request,
|
|
650
|
+
decision,
|
|
651
|
+
rendered,
|
|
652
|
+
:full_file_conflict,
|
|
653
|
+
fallbacks: [{ from: :declaration_localization, to: :full_file_conflict, reason: :source_ownership_unproven }]
|
|
654
|
+
)
|
|
655
|
+
end
|
|
656
|
+
|
|
657
|
+
def localized_conflict_fragments(request, documents, decision)
|
|
658
|
+
return if decision.conflicts.any? { |conflict| conflict[:owner_id].nil? }
|
|
659
|
+
|
|
660
|
+
ours = documents.fetch(:ours)
|
|
661
|
+
return unless decision.conflicts.all? { |conflict| ours.by_id.key?(conflict.fetch(:owner_id)) }
|
|
662
|
+
|
|
663
|
+
ranges = decision.conflicts.map { |conflict| [conflict, ours.by_id.fetch(conflict.fetch(:owner_id))] }
|
|
664
|
+
return unless ranges.sort_by { |_conflict, owner| owner.start_line }.each_cons(2).none? do |left, right|
|
|
665
|
+
left.last.end_line >= right.last.start_line
|
|
666
|
+
end
|
|
667
|
+
|
|
668
|
+
fragments = []
|
|
669
|
+
cursor = 1
|
|
670
|
+
ranges.sort_by { |_conflict, owner| owner.start_line }.each do |conflict, owner|
|
|
671
|
+
append_range(fragments, :ours, cursor, owner.start_line - 1)
|
|
672
|
+
fragments << declaration_conflict_fragment(request, documents, conflict)
|
|
673
|
+
cursor = owner.end_line + 1
|
|
674
|
+
end
|
|
675
|
+
append_range(fragments, :ours, cursor, ours.source.lines.length)
|
|
676
|
+
fragments
|
|
677
|
+
end
|
|
678
|
+
|
|
679
|
+
def declaration_conflict_fragment(request, documents, conflict)
|
|
680
|
+
Ast::Merge::SourceRender::ConflictFragment.new(
|
|
681
|
+
conflict_id: conflict.fetch(:conflict_id),
|
|
682
|
+
base: conflict_declaration_side(documents, conflict, :base),
|
|
683
|
+
ours: conflict_declaration_side(documents, conflict, :ours),
|
|
684
|
+
theirs: conflict_declaration_side(documents, conflict, :theirs),
|
|
685
|
+
labels: request.fetch(:labels, {}),
|
|
686
|
+
marker_size: request.fetch(:conflict_marker_size, 7),
|
|
687
|
+
metadata: { path: conflict.fetch(:path), category: conflict.fetch(:category) }
|
|
688
|
+
)
|
|
689
|
+
end
|
|
690
|
+
|
|
691
|
+
def conflict_declaration_side(documents, conflict, role)
|
|
692
|
+
document = documents.fetch(role)
|
|
693
|
+
owner = document.by_id[conflict.fetch(:owner_id)]
|
|
694
|
+
owner ? [conflict_source_fragment(owner, document.source)] : []
|
|
695
|
+
end
|
|
696
|
+
|
|
697
|
+
def unsafe_document_failure(request, role, parsed)
|
|
698
|
+
return parsed unless parsed[:unsafe_range] || parsed[:ambiguous_owner]
|
|
699
|
+
|
|
700
|
+
conflict_id = "rbs-unsafe-#{Digest::SHA256.hexdigest([role, failure_detail(parsed)].join("\0"))[0, 16]}"
|
|
701
|
+
conflict = {
|
|
702
|
+
conflict_id: conflict_id,
|
|
703
|
+
category: parsed[:ambiguous_owner] ? :ambiguous_owner : :unsafe_source_range,
|
|
704
|
+
path: '<document>',
|
|
705
|
+
owner_id: nil,
|
|
706
|
+
source_role: role
|
|
707
|
+
}
|
|
708
|
+
rendered = render_plan(
|
|
709
|
+
request,
|
|
710
|
+
[whole_document_conflict(request, Decision.new(changes: [], conflicts: [conflict], choices: {}))]
|
|
711
|
+
)
|
|
712
|
+
failure(
|
|
713
|
+
:merge3,
|
|
714
|
+
request,
|
|
715
|
+
category: conflict.fetch(:category),
|
|
716
|
+
message: "#{role} cannot be merged safely: #{failure_detail(parsed)}",
|
|
717
|
+
conflicts: [conflict],
|
|
718
|
+
conflicted_output: rendered.content,
|
|
719
|
+
source_role: role,
|
|
720
|
+
render_report: render_report(rendered, :full_file_conflict),
|
|
721
|
+
verification: { base_participated: true }
|
|
722
|
+
)
|
|
723
|
+
end
|
|
724
|
+
|
|
725
|
+
def whole_document_conflict(request, decision)
|
|
726
|
+
Ast::Merge::SourceRender::ConflictFragment.new(
|
|
727
|
+
conflict_id: decision.conflicts.first.fetch(:conflict_id),
|
|
728
|
+
base: [conflict_whole_source_fragment(:base, request.fetch(:base_source))],
|
|
729
|
+
ours: [conflict_whole_source_fragment(:ours, request.fetch(:ours_source))],
|
|
730
|
+
theirs: [conflict_whole_source_fragment(:theirs, request.fetch(:theirs_source))],
|
|
731
|
+
labels: request.fetch(:labels, {}),
|
|
732
|
+
marker_size: request.fetch(:conflict_marker_size, 7),
|
|
733
|
+
metadata: { conflicts: decision.conflicts.map { |conflict| conflict[:conflict_id] } }
|
|
734
|
+
)
|
|
735
|
+
end
|
|
736
|
+
|
|
737
|
+
def conflict_source_fragment(owner, source)
|
|
738
|
+
fragment_source = source.lines.slice(owner.start_line - 1, owner.end_line - owner.start_line + 1).join
|
|
739
|
+
return source_range_fragment(owner.role, owner.start_line, owner.end_line) if fragment_source.end_with?("\n")
|
|
740
|
+
|
|
741
|
+
synthesized_copy("#{fragment_source}\n", :conflict_line_boundary, owner.role)
|
|
742
|
+
end
|
|
743
|
+
|
|
744
|
+
def conflict_whole_source_fragment(role, source)
|
|
745
|
+
return whole_source_fragment(role, source) if source.empty? || source.end_with?("\n")
|
|
746
|
+
|
|
747
|
+
synthesized_copy("#{source}\n", :conflict_line_boundary, role)
|
|
748
|
+
end
|
|
749
|
+
|
|
750
|
+
def synthesized_copy(content, reason, role)
|
|
751
|
+
Ast::Merge::SourceRender::SynthesizedFragment.new(
|
|
752
|
+
content: content,
|
|
753
|
+
reason: reason,
|
|
754
|
+
producer: provider_id,
|
|
755
|
+
metadata: { source_role: role, copied_source: true }
|
|
756
|
+
)
|
|
757
|
+
end
|
|
758
|
+
|
|
759
|
+
def whole_source_fragment(role, source)
|
|
760
|
+
return synthesized_copy('', :exact_empty_source, role) if source.empty?
|
|
761
|
+
|
|
762
|
+
source_range_fragment(role, 1, source.lines.length)
|
|
763
|
+
end
|
|
764
|
+
|
|
765
|
+
def source_range_fragment(role, start_line, end_line)
|
|
766
|
+
Ast::Merge::SourceRender::SourceFragment.new(
|
|
767
|
+
revision: role,
|
|
768
|
+
start_line: start_line,
|
|
769
|
+
end_line: end_line,
|
|
770
|
+
metadata: { source_role: role }
|
|
771
|
+
)
|
|
772
|
+
end
|
|
773
|
+
|
|
774
|
+
def render_plan(request, fragments)
|
|
775
|
+
Ast::Merge::SourceRender::Renderer.new.render(
|
|
776
|
+
Ast::Merge::SourceRender::Plan.new(
|
|
777
|
+
sources: {
|
|
778
|
+
base: request[:base_source].to_s,
|
|
779
|
+
ours: request[:ours_source].to_s,
|
|
780
|
+
theirs: request[:theirs_source].to_s
|
|
781
|
+
},
|
|
782
|
+
fragments: fragments,
|
|
783
|
+
metadata: { provider_id: provider_id }
|
|
784
|
+
)
|
|
785
|
+
)
|
|
786
|
+
end
|
|
787
|
+
|
|
788
|
+
def render_report(rendered, strategy)
|
|
789
|
+
{
|
|
790
|
+
strategy: strategy,
|
|
791
|
+
line_records: rendered.line_records,
|
|
792
|
+
synthesized_fragments: rendered.synthesized_fragments,
|
|
793
|
+
conflicts: rendered.conflicts,
|
|
794
|
+
verification_input: rendered.verification_input
|
|
795
|
+
}
|
|
796
|
+
end
|
|
797
|
+
|
|
798
|
+
def conflict_failure(request, decision, rendered, strategy, fallbacks: [])
|
|
799
|
+
failure(
|
|
800
|
+
:merge3,
|
|
801
|
+
request,
|
|
802
|
+
category: :merge_conflict,
|
|
803
|
+
message: 'RBS top-level declaration changed incompatibly on both sides.',
|
|
804
|
+
changes: decision.changes,
|
|
805
|
+
conflicts: decision.conflicts,
|
|
806
|
+
conflicted_output: rendered.content,
|
|
807
|
+
render_report: render_report(rendered, strategy),
|
|
808
|
+
verification: { base_participated: true },
|
|
809
|
+
fallbacks: fallbacks
|
|
810
|
+
)
|
|
811
|
+
end
|
|
812
|
+
|
|
813
|
+
def parse_failure(operation, request, role, parsed)
|
|
814
|
+
category = if parsed[:parse_error]
|
|
815
|
+
:parse_error
|
|
816
|
+
elsif parsed[:ambiguous_owner]
|
|
817
|
+
:ambiguous_owner
|
|
818
|
+
else
|
|
819
|
+
:unsafe_source_range
|
|
820
|
+
end
|
|
821
|
+
failure(
|
|
822
|
+
operation,
|
|
823
|
+
request,
|
|
824
|
+
category: category,
|
|
825
|
+
message: "#{role} parse error: #{failure_detail(parsed)}",
|
|
826
|
+
source_role: role
|
|
827
|
+
)
|
|
828
|
+
end
|
|
829
|
+
|
|
830
|
+
def failure_detail(parsed)
|
|
831
|
+
return parsed[:parse_error] if parsed[:parse_error]
|
|
832
|
+
return "ambiguous duplicate declaration #{parsed[:ambiguous_owner].inspect}" if parsed[:ambiguous_owner]
|
|
833
|
+
|
|
834
|
+
"unsafe declaration range #{parsed[:unsafe_range].inspect}"
|
|
835
|
+
end
|
|
836
|
+
|
|
837
|
+
def provider_failure?(value)
|
|
838
|
+
value.is_a?(Hash) &&
|
|
839
|
+
(value[:ok] == false || value.key?(:parse_error) || value.key?(:ambiguous_owner) || value.key?(:unsafe_range))
|
|
840
|
+
end
|
|
841
|
+
|
|
842
|
+
def result(operation, request, changes: [], diagnostics: [], render_report: {}, verification: {}, **payload)
|
|
843
|
+
Ast::Merge::ProviderResult.build(
|
|
844
|
+
operation: operation,
|
|
845
|
+
success: true,
|
|
846
|
+
envelope: envelope(
|
|
847
|
+
request,
|
|
848
|
+
changes: changes,
|
|
849
|
+
diagnostics: diagnostics,
|
|
850
|
+
render_report: render_report,
|
|
851
|
+
verification: verification
|
|
852
|
+
),
|
|
853
|
+
**payload
|
|
854
|
+
)
|
|
855
|
+
end
|
|
856
|
+
|
|
857
|
+
def failure(operation, request, category:, message:, changes: [], conflicts: [], fallbacks: [],
|
|
858
|
+
render_report: {}, verification: {}, **payload)
|
|
859
|
+
Ast::Merge::ProviderResult.build(
|
|
860
|
+
operation: operation,
|
|
861
|
+
success: false,
|
|
862
|
+
envelope: envelope(
|
|
863
|
+
request,
|
|
864
|
+
changes: changes,
|
|
865
|
+
conflicts: conflicts,
|
|
866
|
+
fallbacks: fallbacks,
|
|
867
|
+
diagnostics: [{ category: category, severity: :error, message: message, blocking: true }],
|
|
868
|
+
render_report: render_report,
|
|
869
|
+
verification: verification
|
|
870
|
+
),
|
|
871
|
+
**payload
|
|
872
|
+
)
|
|
873
|
+
end
|
|
874
|
+
|
|
875
|
+
def envelope(request, **fields)
|
|
876
|
+
{
|
|
877
|
+
provider: {
|
|
878
|
+
provider_id: provider_id,
|
|
879
|
+
family: family,
|
|
880
|
+
dialect: request[:dialect] || :rbs,
|
|
881
|
+
backend: request[:backend] || :rbs,
|
|
882
|
+
package: 'rbs-merge',
|
|
883
|
+
package_version: Rbs::Merge::Version::VERSION
|
|
884
|
+
},
|
|
885
|
+
profile: { profile_id: request[:profile_id] || DEFAULT_PROFILE }
|
|
886
|
+
}.merge(fields)
|
|
887
|
+
end
|
|
888
|
+
end
|
|
889
|
+
# rubocop:enable Metrics/AbcSize, Metrics/ClassLength, Metrics/CyclomaticComplexity, Metrics/MethodLength, Metrics/ParameterLists, Metrics/PerceivedComplexity
|
|
890
|
+
end
|
|
891
|
+
end
|
data/lib/rbs/merge/version.rb
CHANGED
|
@@ -5,7 +5,7 @@ module Rbs
|
|
|
5
5
|
# Version namespace for this gem.
|
|
6
6
|
module Version
|
|
7
7
|
# Current gem version.
|
|
8
|
-
VERSION = '7.1.
|
|
8
|
+
VERSION = '7.1.3'
|
|
9
9
|
end
|
|
10
10
|
# Current gem version exposed at the traditional constant location.
|
|
11
11
|
VERSION = Version::VERSION # Traditional Constant Location
|
data/lib/rbs/merge.rb
CHANGED
|
@@ -137,12 +137,23 @@ module Rbs
|
|
|
137
137
|
backends << TREE_SITTER_BACKEND_REFERENCE if registrations.key?(:tree_sitter) || registrations.key?(:tslp)
|
|
138
138
|
backends
|
|
139
139
|
end
|
|
140
|
+
|
|
141
|
+
def merge_provider
|
|
142
|
+
@merge_provider ||= Provider.new
|
|
143
|
+
end
|
|
144
|
+
|
|
145
|
+
def register_provider!(replace: false)
|
|
146
|
+
Ast::Merge.register_provider(merge_provider, replace: replace)
|
|
147
|
+
end
|
|
140
148
|
end
|
|
141
149
|
end
|
|
142
150
|
end
|
|
143
151
|
|
|
152
|
+
Rbs::Merge.autoload :Provider, 'rbs/merge/provider'
|
|
153
|
+
|
|
144
154
|
# Register the RBS backend with TreeHaver when this gem is loaded
|
|
145
155
|
Rbs::Merge.register_backend!
|
|
156
|
+
Rbs::Merge.register_provider!
|
|
146
157
|
|
|
147
158
|
# Register with ast-merge's MergeGemRegistry for RSpec dependency tags
|
|
148
159
|
# Only register if MergeGemRegistry is loaded (i.e., in test environment)
|
data/sig/rbs/merge.rbs
CHANGED
|
@@ -1,5 +1,18 @@
|
|
|
1
1
|
module Rbs
|
|
2
2
|
module Merge
|
|
3
|
+
class Provider
|
|
4
|
+
def provider_id: () -> String
|
|
5
|
+
def family: () -> String
|
|
6
|
+
def capabilities: () -> Hash[Symbol, untyped]
|
|
7
|
+
def analyze: (Hash[Symbol, untyped] request) -> Hash[Symbol, untyped]
|
|
8
|
+
def diff2: (Hash[Symbol, untyped] request) -> Hash[Symbol, untyped]
|
|
9
|
+
def merge2: (Hash[Symbol, untyped] request) -> Hash[Symbol, untyped]
|
|
10
|
+
def merge3: (Hash[Symbol, untyped] request) -> Hash[Symbol, untyped]
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
def self.merge_provider: () -> Provider
|
|
14
|
+
def self.register_provider!: (?replace: bool) -> untyped
|
|
15
|
+
|
|
3
16
|
module Version
|
|
4
17
|
VERSION: String
|
|
5
18
|
end
|
data.tar.gz.sig
CHANGED
|
Binary file
|
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: rbs-merge
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 7.1.
|
|
4
|
+
version: 7.1.3
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Peter H. Boling
|
|
@@ -43,14 +43,14 @@ dependencies:
|
|
|
43
43
|
requirements:
|
|
44
44
|
- - '='
|
|
45
45
|
- !ruby/object:Gem::Version
|
|
46
|
-
version: 7.1.
|
|
46
|
+
version: 7.1.3
|
|
47
47
|
type: :runtime
|
|
48
48
|
prerelease: false
|
|
49
49
|
version_requirements: !ruby/object:Gem::Requirement
|
|
50
50
|
requirements:
|
|
51
51
|
- - '='
|
|
52
52
|
- !ruby/object:Gem::Version
|
|
53
|
-
version: 7.1.
|
|
53
|
+
version: 7.1.3
|
|
54
54
|
- !ruby/object:Gem::Dependency
|
|
55
55
|
name: rbs
|
|
56
56
|
requirement: !ruby/object:Gem::Requirement
|
|
@@ -71,14 +71,14 @@ dependencies:
|
|
|
71
71
|
requirements:
|
|
72
72
|
- - '='
|
|
73
73
|
- !ruby/object:Gem::Version
|
|
74
|
-
version: 7.1.
|
|
74
|
+
version: 7.1.3
|
|
75
75
|
type: :runtime
|
|
76
76
|
prerelease: false
|
|
77
77
|
version_requirements: !ruby/object:Gem::Requirement
|
|
78
78
|
requirements:
|
|
79
79
|
- - '='
|
|
80
80
|
- !ruby/object:Gem::Version
|
|
81
|
-
version: 7.1.
|
|
81
|
+
version: 7.1.3
|
|
82
82
|
- !ruby/object:Gem::Dependency
|
|
83
83
|
name: version_gem
|
|
84
84
|
requirement: !ruby/object:Gem::Requirement
|
|
@@ -105,20 +105,20 @@ dependencies:
|
|
|
105
105
|
requirements:
|
|
106
106
|
- - "~>"
|
|
107
107
|
- !ruby/object:Gem::Version
|
|
108
|
-
version: '
|
|
108
|
+
version: '3.0'
|
|
109
109
|
- - ">="
|
|
110
110
|
- !ruby/object:Gem::Version
|
|
111
|
-
version:
|
|
111
|
+
version: 3.0.0
|
|
112
112
|
type: :development
|
|
113
113
|
prerelease: false
|
|
114
114
|
version_requirements: !ruby/object:Gem::Requirement
|
|
115
115
|
requirements:
|
|
116
116
|
- - "~>"
|
|
117
117
|
- !ruby/object:Gem::Version
|
|
118
|
-
version: '
|
|
118
|
+
version: '3.0'
|
|
119
119
|
- - ">="
|
|
120
120
|
- !ruby/object:Gem::Version
|
|
121
|
-
version:
|
|
121
|
+
version: 3.0.0
|
|
122
122
|
- !ruby/object:Gem::Dependency
|
|
123
123
|
name: bundler-audit
|
|
124
124
|
requirement: !ruby/object:Gem::Requirement
|
|
@@ -196,7 +196,7 @@ dependencies:
|
|
|
196
196
|
version: '3.2'
|
|
197
197
|
- - ">="
|
|
198
198
|
- !ruby/object:Gem::Version
|
|
199
|
-
version: 3.2.
|
|
199
|
+
version: 3.2.2
|
|
200
200
|
type: :development
|
|
201
201
|
prerelease: false
|
|
202
202
|
version_requirements: !ruby/object:Gem::Requirement
|
|
@@ -206,7 +206,7 @@ dependencies:
|
|
|
206
206
|
version: '3.2'
|
|
207
207
|
- - ">="
|
|
208
208
|
- !ruby/object:Gem::Version
|
|
209
|
-
version: 3.2.
|
|
209
|
+
version: 3.2.2
|
|
210
210
|
- !ruby/object:Gem::Dependency
|
|
211
211
|
name: kettle-test
|
|
212
212
|
requirement: !ruby/object:Gem::Requirement
|
|
@@ -236,7 +236,7 @@ dependencies:
|
|
|
236
236
|
version: '3.2'
|
|
237
237
|
- - ">="
|
|
238
238
|
- !ruby/object:Gem::Version
|
|
239
|
-
version: 3.2.
|
|
239
|
+
version: 3.2.5
|
|
240
240
|
type: :development
|
|
241
241
|
prerelease: false
|
|
242
242
|
version_requirements: !ruby/object:Gem::Requirement
|
|
@@ -246,7 +246,7 @@ dependencies:
|
|
|
246
246
|
version: '3.2'
|
|
247
247
|
- - ">="
|
|
248
248
|
- !ruby/object:Gem::Version
|
|
249
|
-
version: 3.2.
|
|
249
|
+
version: 3.2.5
|
|
250
250
|
- !ruby/object:Gem::Dependency
|
|
251
251
|
name: ruby-progressbar
|
|
252
252
|
requirement: !ruby/object:Gem::Requirement
|
|
@@ -323,6 +323,7 @@ files:
|
|
|
323
323
|
- lib/rbs/merge/merge_result.rb
|
|
324
324
|
- lib/rbs/merge/node_type_normalizer.rb
|
|
325
325
|
- lib/rbs/merge/node_wrapper.rb
|
|
326
|
+
- lib/rbs/merge/provider.rb
|
|
326
327
|
- lib/rbs/merge/smart_merger.rb
|
|
327
328
|
- lib/rbs/merge/version.rb
|
|
328
329
|
- sig/rbs/merge.rbs
|
|
@@ -332,10 +333,10 @@ licenses:
|
|
|
332
333
|
- PolyForm-Small-Business-1.0.0
|
|
333
334
|
metadata:
|
|
334
335
|
homepage_uri: https://structuredmerge.org
|
|
335
|
-
source_code_uri: https://github.com/structuredmerge/structuredmerge-ruby/tree/v7.1.
|
|
336
|
-
changelog_uri: https://github.com/structuredmerge/structuredmerge-ruby/blob/v7.1.
|
|
336
|
+
source_code_uri: https://github.com/structuredmerge/structuredmerge-ruby/tree/v7.1.3
|
|
337
|
+
changelog_uri: https://github.com/structuredmerge/structuredmerge-ruby/blob/v7.1.3/CHANGELOG.md
|
|
337
338
|
bug_tracker_uri: https://github.com/structuredmerge/structuredmerge-ruby/issues
|
|
338
|
-
documentation_uri: https://www.rubydoc.info/gems/rbs-merge/7.1.
|
|
339
|
+
documentation_uri: https://www.rubydoc.info/gems/rbs-merge/7.1.3
|
|
339
340
|
funding_uri: https://github.com/sponsors/pboling
|
|
340
341
|
wiki_uri: https://github.com/structuredmerge/structuredmerge-ruby/wiki
|
|
341
342
|
news_uri: https://www.railsbling.com/tags/rbs-merge
|
metadata.gz.sig
CHANGED
|
Binary file
|