prism-merge 7.1.1 → 7.1.4
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 +3 -11
- data/lib/prism/merge/provider.rb +717 -0
- data/lib/prism/merge/version.rb +1 -1
- data/lib/prism/merge.rb +44 -0
- data/sig/prism/merge.rbs +13 -0
- data.tar.gz.sig +0 -0
- metadata +47 -18
- 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: 7d32fbdcd833e576a54414849501db40ddacd349498c05e4d233159f2e5521c1
|
|
4
|
+
data.tar.gz: 063f4adb55b355f984f09eb376652dded9e8a8d2451d993ebc4a954eb403ca0c
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 291ed360148759fb7f32d3c28da2d0c8da02733022b60b9ada62781cd0cfc61d0295164e43f84985816d2416f92f433862e04010ec2ac59c200f953918e8c540
|
|
7
|
+
data.tar.gz: 75c2f6a59e5910332a80945f0f12426d5a019872d4e128834cebabfc496db6c30d2ab389940a7bc10910a084694bf68d59e9bcd81281dbbc173857d86b6a59a5
|
checksums.yaml.gz.sig
CHANGED
|
Binary file
|
data/README.md
CHANGED
|
@@ -83,7 +83,7 @@ File.write("merged.rb", result)
|
|
|
83
83
|
|
|
84
84
|
### Compatibility
|
|
85
85
|
|
|
86
|
-
Compatible with MRI Ruby 4.0.0+,
|
|
86
|
+
Compatible with MRI Ruby 4.0.0+, JRuby, and TruffleRuby.
|
|
87
87
|
CI workflows and Appraisals are generated for MRI Ruby 4.0.0+.
|
|
88
88
|
This test floor is configured by `ruby.test_minimum` in `.kettle-jem.yml` and
|
|
89
89
|
may be higher than the gem's runtime compatibility floor when legacy Rubies are
|
|
@@ -1099,20 +1099,12 @@ See [SECURITY.md][🔐security].
|
|
|
1099
1099
|
## 🤝 Contributing
|
|
1100
1100
|
|
|
1101
1101
|
If you need some ideas of where to help, you could work on adding more code coverage,
|
|
1102
|
-
|
|
1103
|
-
or use the gem and think about how it could be better.
|
|
1102
|
+
check [issues][🤝gh-issues] or [PRs][🤝gh-pulls], or use the gem and think about how it could be better.
|
|
1104
1103
|
|
|
1105
1104
|
We [![Keep A Changelog][📗keep-changelog-img]][📗keep-changelog] so if you make changes, remember to update it.
|
|
1106
1105
|
|
|
1107
1106
|
See [CONTRIBUTING.md][🤝contributing] for more detailed instructions.
|
|
1108
1107
|
|
|
1109
|
-
### Code Coverage
|
|
1110
|
-
|
|
1111
|
-
<details markdown="1">
|
|
1112
|
-
<summary>Coverage service badges</summary>
|
|
1113
|
-
|
|
1114
|
-
</details>
|
|
1115
|
-
|
|
1116
1108
|
## 📌 Versioning
|
|
1117
1109
|
|
|
1118
1110
|
This library follows [![Semantic Versioning 2.0.0][📌semver-img]][📌semver] for its public API where practical.
|
|
@@ -1252,7 +1244,7 @@ If none of the available licenses suit your use case, please [contact us](mailto
|
|
|
1252
1244
|
[📌gitmoji]: https://gitmoji.dev
|
|
1253
1245
|
[📌gitmoji-img]: https://img.shields.io/badge/gitmoji_commits-%20%F0%9F%98%9C%20%F0%9F%98%8D-34495e.svg?style=flat-square
|
|
1254
1246
|
[🧮kloc]: https://www.youtube.com/watch?v=dQw4w9WgXcQ
|
|
1255
|
-
[🧮kloc-img]: https://img.shields.io/badge/KLOC-4.
|
|
1247
|
+
[🧮kloc-img]: https://img.shields.io/badge/KLOC-4.550-FFDD67.svg?style=for-the-badge&logo=YouTube&logoColor=blue
|
|
1256
1248
|
[🔐security]: https://github.com/structuredmerge/structuredmerge-ruby/blob/main/SECURITY.md
|
|
1257
1249
|
[🔐security-img]: https://img.shields.io/badge/security-policy-259D6C.svg?style=flat
|
|
1258
1250
|
[📄copyright-notice-explainer]: https://opensource.stackexchange.com/questions/5778/why-do-licenses-such-as-the-mit-license-specify-a-single-year
|
|
@@ -0,0 +1,717 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'digest'
|
|
4
|
+
|
|
5
|
+
module Prism
|
|
6
|
+
module Merge
|
|
7
|
+
# Base-aware, source-preserving Ruby provider backed by Prism top-level owners.
|
|
8
|
+
# rubocop:disable Metrics/AbcSize, Metrics/ClassLength, Metrics/CyclomaticComplexity, Metrics/MethodLength, Metrics/ParameterLists, Metrics/PerceivedComplexity -- provider decisions, source plans, and verification form one boundary
|
|
9
|
+
class Provider
|
|
10
|
+
DEFAULT_PROFILE = :source_preserving
|
|
11
|
+
Owner = Data.define(:id, :signature, :fingerprint, :start_line, :end_line, :role)
|
|
12
|
+
Document = Data.define(:source, :analysis, :owners, :by_id, :unmanaged_fingerprint)
|
|
13
|
+
Decision = Data.define(:changes, :conflicts, :choices)
|
|
14
|
+
|
|
15
|
+
def provider_id = 'ruby.ruby.prism'
|
|
16
|
+
def family = 'ruby'
|
|
17
|
+
|
|
18
|
+
def capabilities
|
|
19
|
+
{
|
|
20
|
+
operations: Ast::Merge::ProviderContract::OPERATIONS,
|
|
21
|
+
dialects: %i[ruby],
|
|
22
|
+
backends: %i[prism],
|
|
23
|
+
profiles: [DEFAULT_PROFILE],
|
|
24
|
+
role: :backend,
|
|
25
|
+
source_preservation: %i[exact_source owner_fragments line_provenance reparse semantic_verification]
|
|
26
|
+
}.freeze
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
def analyze(request)
|
|
30
|
+
document = parse_document(:analyze, request, :source)
|
|
31
|
+
return document if provider_failure?(document)
|
|
32
|
+
|
|
33
|
+
result(
|
|
34
|
+
:analyze,
|
|
35
|
+
request,
|
|
36
|
+
analysis: {
|
|
37
|
+
backend: 'prism',
|
|
38
|
+
valid: true,
|
|
39
|
+
owners: document.owners.map { |owner| owner_description(owner) }
|
|
40
|
+
},
|
|
41
|
+
verification: { source_parsed: true }
|
|
42
|
+
)
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
def diff2(request)
|
|
46
|
+
before = parse_document(:diff2, request, :before)
|
|
47
|
+
return before if provider_failure?(before)
|
|
48
|
+
|
|
49
|
+
after = parse_document(:diff2, request, :after)
|
|
50
|
+
return after if provider_failure?(after)
|
|
51
|
+
|
|
52
|
+
changes = diff_documents(before, after)
|
|
53
|
+
result(
|
|
54
|
+
:diff2,
|
|
55
|
+
request,
|
|
56
|
+
diff: { changes: changes },
|
|
57
|
+
changes: changes,
|
|
58
|
+
verification: { before_parsed: true, after_parsed: true }
|
|
59
|
+
)
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
def merge2(request)
|
|
63
|
+
merged = Prism::Merge.merge_ruby(
|
|
64
|
+
request.fetch(:incoming_source),
|
|
65
|
+
request.fetch(:current_source),
|
|
66
|
+
'ruby',
|
|
67
|
+
backend: request[:backend],
|
|
68
|
+
add_template_only_nodes: true
|
|
69
|
+
)
|
|
70
|
+
return merge2_failure(request, merged) unless merged[:ok]
|
|
71
|
+
|
|
72
|
+
output = merged.fetch(:output)
|
|
73
|
+
parsed = parse_source(output, :output)
|
|
74
|
+
return render_failure(:merge2, request, parsed) if provider_failure?(parsed)
|
|
75
|
+
|
|
76
|
+
result(
|
|
77
|
+
:merge2,
|
|
78
|
+
request,
|
|
79
|
+
output: output,
|
|
80
|
+
render_report: { strategy: :prism_smart_merger, synthesis: :family_emission },
|
|
81
|
+
verification: { output_reparsed: true }
|
|
82
|
+
)
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
def merge3(request)
|
|
86
|
+
documents = parse_merge3_documents(request)
|
|
87
|
+
return documents if provider_failure?(documents)
|
|
88
|
+
|
|
89
|
+
exact_role = exact_revision_role(request)
|
|
90
|
+
decision = decide(documents, include_unmanaged_conflict: exact_role.nil?)
|
|
91
|
+
return render_conflicts(request, documents, decision) unless decision.conflicts.empty?
|
|
92
|
+
|
|
93
|
+
return render_exact(request, documents, decision, exact_role) if exact_role
|
|
94
|
+
|
|
95
|
+
render_composite(request, documents, decision)
|
|
96
|
+
end
|
|
97
|
+
|
|
98
|
+
private
|
|
99
|
+
|
|
100
|
+
def parse_merge3_documents(request)
|
|
101
|
+
%i[base ours theirs].each_with_object({}) do |role, documents|
|
|
102
|
+
document = parse_document(:merge3, request, role)
|
|
103
|
+
return document if provider_failure?(document)
|
|
104
|
+
|
|
105
|
+
documents[role] = document
|
|
106
|
+
end
|
|
107
|
+
end
|
|
108
|
+
|
|
109
|
+
def parse_document(operation, request, role)
|
|
110
|
+
source = request.fetch(role == :source ? :source : :"#{role}_source")
|
|
111
|
+
parsed = parse_source(source, role)
|
|
112
|
+
return parse_failure(operation, request, role, parsed) if provider_failure?(parsed)
|
|
113
|
+
|
|
114
|
+
parsed
|
|
115
|
+
end
|
|
116
|
+
|
|
117
|
+
def parse_source(source, role)
|
|
118
|
+
analysis = Prism::Merge::FileAnalysis.new(source, source_label: role.to_s)
|
|
119
|
+
unless analysis.valid?
|
|
120
|
+
return {
|
|
121
|
+
parse_error: analysis.errors.map(&:message).join('; '),
|
|
122
|
+
source_role: role
|
|
123
|
+
}
|
|
124
|
+
end
|
|
125
|
+
|
|
126
|
+
owners = analysis.nodes_with_comments.map do |node_info|
|
|
127
|
+
signature = node_info.fetch(:signature)
|
|
128
|
+
Owner.new(
|
|
129
|
+
id: owner_id(signature),
|
|
130
|
+
signature: signature,
|
|
131
|
+
fingerprint: owner_fingerprint(source, node_info),
|
|
132
|
+
start_line: node_info.fetch(:line_range).begin,
|
|
133
|
+
end_line: node_info.fetch(:line_range).end,
|
|
134
|
+
role: role
|
|
135
|
+
)
|
|
136
|
+
end
|
|
137
|
+
duplicate = owners.group_by(&:id).find { |_id, matches| matches.length > 1 }
|
|
138
|
+
return { ambiguous_owner: duplicate.first, source_role: role } if duplicate
|
|
139
|
+
|
|
140
|
+
Document.new(
|
|
141
|
+
source: source,
|
|
142
|
+
analysis: analysis,
|
|
143
|
+
owners: owners.freeze,
|
|
144
|
+
by_id: owners.to_h { |owner| [owner.id, owner] }.freeze,
|
|
145
|
+
unmanaged_fingerprint: unmanaged_fingerprint(source, owners)
|
|
146
|
+
)
|
|
147
|
+
end
|
|
148
|
+
|
|
149
|
+
def unmanaged_fingerprint(source, owners)
|
|
150
|
+
owned_lines = owners.each_with_object({}) do |owner, lines|
|
|
151
|
+
(owner.start_line..owner.end_line).each { |line| lines[line] = true }
|
|
152
|
+
end
|
|
153
|
+
source.lines.each_with_index.filter_map do |line, index|
|
|
154
|
+
line unless owned_lines[index + 1]
|
|
155
|
+
end.join
|
|
156
|
+
end
|
|
157
|
+
|
|
158
|
+
def semantic_fingerprint(value)
|
|
159
|
+
case value
|
|
160
|
+
when ::Prism::Node
|
|
161
|
+
fields = value.deconstruct_keys(nil).reject do |key, _item|
|
|
162
|
+
key == :node_id || key == :location || key.to_s.end_with?('_loc')
|
|
163
|
+
end
|
|
164
|
+
[value.type, fields.map { |key, item| [key, semantic_fingerprint(item)] }]
|
|
165
|
+
when Array
|
|
166
|
+
value.map { |item| semantic_fingerprint(item) }
|
|
167
|
+
when Hash
|
|
168
|
+
value.map { |key, item| [key, semantic_fingerprint(item)] }
|
|
169
|
+
else
|
|
170
|
+
value
|
|
171
|
+
end
|
|
172
|
+
end
|
|
173
|
+
|
|
174
|
+
def owner_fingerprint(source, node_info)
|
|
175
|
+
range = node_info.fetch(:line_range)
|
|
176
|
+
{
|
|
177
|
+
ast: semantic_fingerprint(node_info.fetch(:node)),
|
|
178
|
+
source: source.lines.slice(range.begin - 1, range.end - range.begin + 1).join.delete_suffix("\n"),
|
|
179
|
+
leading_comments: node_info.fetch(:leading_comments).map(&:slice),
|
|
180
|
+
inline_comments: node_info.fetch(:inline_comments).map(&:slice)
|
|
181
|
+
}.freeze
|
|
182
|
+
end
|
|
183
|
+
|
|
184
|
+
def owner_id(signature)
|
|
185
|
+
Digest::SHA256.hexdigest(Marshal.dump(signature))
|
|
186
|
+
end
|
|
187
|
+
|
|
188
|
+
def owner_path(owner)
|
|
189
|
+
owner ? owner.signature.inspect : '<document>'
|
|
190
|
+
end
|
|
191
|
+
|
|
192
|
+
def owner_description(owner)
|
|
193
|
+
{
|
|
194
|
+
path: owner_path(owner),
|
|
195
|
+
signature: owner.signature,
|
|
196
|
+
line_range: [owner.start_line, owner.end_line]
|
|
197
|
+
}
|
|
198
|
+
end
|
|
199
|
+
|
|
200
|
+
def diff_documents(before, after)
|
|
201
|
+
ordered_ids(before, after).filter_map do |id|
|
|
202
|
+
left = before.by_id[id]
|
|
203
|
+
right = after.by_id[id]
|
|
204
|
+
next if left && right && left.fingerprint == right.fingerprint
|
|
205
|
+
|
|
206
|
+
{
|
|
207
|
+
path: owner_path(left || right),
|
|
208
|
+
ours: :unchanged,
|
|
209
|
+
theirs: change_kind(left, right)
|
|
210
|
+
}.freeze
|
|
211
|
+
end.freeze
|
|
212
|
+
end
|
|
213
|
+
|
|
214
|
+
def ordered_ids(*documents)
|
|
215
|
+
documents.flat_map { |document| document.owners.map(&:id) }.uniq
|
|
216
|
+
end
|
|
217
|
+
|
|
218
|
+
def change_kind(before, after)
|
|
219
|
+
return :added unless before
|
|
220
|
+
return :deleted unless after
|
|
221
|
+
|
|
222
|
+
:edited
|
|
223
|
+
end
|
|
224
|
+
|
|
225
|
+
def decide(documents, include_unmanaged_conflict:)
|
|
226
|
+
changes = []
|
|
227
|
+
conflicts = []
|
|
228
|
+
choices = {}
|
|
229
|
+
append_unmanaged_conflict(changes, conflicts, documents) if include_unmanaged_conflict
|
|
230
|
+
ordered_ids(*documents.values).each do |id|
|
|
231
|
+
base = documents.fetch(:base).by_id[id]
|
|
232
|
+
ours = documents.fetch(:ours).by_id[id]
|
|
233
|
+
theirs = documents.fetch(:theirs).by_id[id]
|
|
234
|
+
ours_change = side_change(base, ours)
|
|
235
|
+
theirs_change = side_change(base, theirs)
|
|
236
|
+
next choices[id] = :ours if ours_change == :unchanged && theirs_change == :unchanged
|
|
237
|
+
|
|
238
|
+
change = {
|
|
239
|
+
path: owner_path(base || ours || theirs),
|
|
240
|
+
ours: ours_change,
|
|
241
|
+
theirs: theirs_change
|
|
242
|
+
}.freeze
|
|
243
|
+
changes << change
|
|
244
|
+
choice = owner_choice(base, ours, theirs)
|
|
245
|
+
if choice == :conflict
|
|
246
|
+
conflicts << conflict_for(id, base, ours, theirs, change)
|
|
247
|
+
else
|
|
248
|
+
choices[id] = choice
|
|
249
|
+
end
|
|
250
|
+
end
|
|
251
|
+
Decision.new(changes: changes.freeze, conflicts: conflicts.freeze, choices: choices.freeze)
|
|
252
|
+
end
|
|
253
|
+
|
|
254
|
+
def append_unmanaged_conflict(changes, conflicts, documents)
|
|
255
|
+
base = documents.fetch(:base).unmanaged_fingerprint
|
|
256
|
+
ours = documents.fetch(:ours).unmanaged_fingerprint
|
|
257
|
+
theirs = documents.fetch(:theirs).unmanaged_fingerprint
|
|
258
|
+
return if ours == theirs || base == theirs
|
|
259
|
+
|
|
260
|
+
change = {
|
|
261
|
+
path: '<unmanaged-source>',
|
|
262
|
+
ours: base == ours ? :unchanged : :edited,
|
|
263
|
+
theirs: base == theirs ? :unchanged : :edited
|
|
264
|
+
}.freeze
|
|
265
|
+
changes << change
|
|
266
|
+
conflicts << {
|
|
267
|
+
conflict_id: "ruby-unmanaged-#{Digest::SHA256.hexdigest([base, ours, theirs].join("\0"))[0, 16]}",
|
|
268
|
+
category: :unmanaged_source_change,
|
|
269
|
+
path: change.fetch(:path),
|
|
270
|
+
owner_id: nil,
|
|
271
|
+
change_classification: change
|
|
272
|
+
}.freeze
|
|
273
|
+
end
|
|
274
|
+
|
|
275
|
+
def side_change(base, side)
|
|
276
|
+
return :unchanged if base && side && base.fingerprint == side.fingerprint
|
|
277
|
+
return :added if !base && side
|
|
278
|
+
return :deleted if base && !side
|
|
279
|
+
return :unchanged unless base || side
|
|
280
|
+
|
|
281
|
+
:edited
|
|
282
|
+
end
|
|
283
|
+
|
|
284
|
+
def owner_choice(base, ours, theirs)
|
|
285
|
+
return :ours if equivalent?(ours, theirs)
|
|
286
|
+
return :theirs if equivalent?(base, ours)
|
|
287
|
+
return :ours if equivalent?(base, theirs)
|
|
288
|
+
return nil if ours.nil? && theirs.nil?
|
|
289
|
+
|
|
290
|
+
:conflict
|
|
291
|
+
end
|
|
292
|
+
|
|
293
|
+
def equivalent?(left, right)
|
|
294
|
+
return true if left.nil? && right.nil?
|
|
295
|
+
return false unless left && right
|
|
296
|
+
|
|
297
|
+
left.fingerprint == right.fingerprint
|
|
298
|
+
end
|
|
299
|
+
|
|
300
|
+
def conflict_for(id, base, ours, theirs, change)
|
|
301
|
+
{
|
|
302
|
+
conflict_id: "ruby-owner-#{id[0, 16]}",
|
|
303
|
+
category: base && (!ours || !theirs) ? :delete_edit : :edit_edit,
|
|
304
|
+
path: change.fetch(:path),
|
|
305
|
+
owner_id: id,
|
|
306
|
+
base: owner_state(base),
|
|
307
|
+
ours: owner_state(ours),
|
|
308
|
+
theirs: owner_state(theirs),
|
|
309
|
+
change_classification: change
|
|
310
|
+
}.freeze
|
|
311
|
+
end
|
|
312
|
+
|
|
313
|
+
def owner_state(owner)
|
|
314
|
+
{ present: !owner.nil?, fingerprint: owner&.fingerprint }.freeze
|
|
315
|
+
end
|
|
316
|
+
|
|
317
|
+
def exact_revision_role(request)
|
|
318
|
+
base = request.fetch(:base_source)
|
|
319
|
+
ours = request.fetch(:ours_source)
|
|
320
|
+
theirs = request.fetch(:theirs_source)
|
|
321
|
+
return :ours if ours == theirs || base == theirs
|
|
322
|
+
|
|
323
|
+
:theirs if base == ours
|
|
324
|
+
end
|
|
325
|
+
|
|
326
|
+
def render_exact(request, documents, decision, role)
|
|
327
|
+
rendered = render_plan(request, [whole_source_fragment(role, request.fetch(:"#{role}_source"))])
|
|
328
|
+
verification = verify_rendered(rendered.content, documents.fetch(role).owners, role: :output)
|
|
329
|
+
result(
|
|
330
|
+
:merge3,
|
|
331
|
+
request,
|
|
332
|
+
output: rendered.content,
|
|
333
|
+
changes: decision.changes,
|
|
334
|
+
render_report: render_report(rendered, :exact_revision),
|
|
335
|
+
verification: verification.merge(base_participated: true)
|
|
336
|
+
)
|
|
337
|
+
end
|
|
338
|
+
|
|
339
|
+
def render_composite(request, documents, decision)
|
|
340
|
+
fragments = composite_fragments(documents, decision)
|
|
341
|
+
rendered = render_plan(request, fragments)
|
|
342
|
+
expected = expected_owners(documents, decision)
|
|
343
|
+
verification = verify_rendered(rendered.content, expected, role: :output)
|
|
344
|
+
unless verification[:semantic_match]
|
|
345
|
+
return failure(
|
|
346
|
+
:merge3,
|
|
347
|
+
request,
|
|
348
|
+
category: :render_failure,
|
|
349
|
+
message: 'Prism composite did not match the planned top-level owner structure.',
|
|
350
|
+
changes: decision.changes,
|
|
351
|
+
render_report: render_report(rendered, :exact_owner_composite),
|
|
352
|
+
verification: verification.merge(base_participated: true)
|
|
353
|
+
)
|
|
354
|
+
end
|
|
355
|
+
|
|
356
|
+
result(
|
|
357
|
+
:merge3,
|
|
358
|
+
request,
|
|
359
|
+
output: rendered.content,
|
|
360
|
+
changes: decision.changes,
|
|
361
|
+
render_report: render_report(rendered, :exact_owner_composite),
|
|
362
|
+
verification: verification.merge(base_participated: true)
|
|
363
|
+
)
|
|
364
|
+
end
|
|
365
|
+
|
|
366
|
+
def composite_fragments(documents, decision)
|
|
367
|
+
ours = documents.fetch(:ours)
|
|
368
|
+
edits = replacement_edits(documents, decision)
|
|
369
|
+
fragments = []
|
|
370
|
+
cursor = 1
|
|
371
|
+
edits.sort_by { |edit| edit.fetch(:start_line) }.each do |edit|
|
|
372
|
+
append_range(fragments, :ours, cursor, edit.fetch(:start_line) - 1)
|
|
373
|
+
replacement = edit[:owner]
|
|
374
|
+
append_owner(fragments, replacement) if replacement
|
|
375
|
+
cursor = edit.fetch(:end_line) + 1
|
|
376
|
+
end
|
|
377
|
+
append_range(fragments, :ours, cursor, ours.source.lines.length)
|
|
378
|
+
append_additions(fragments, documents, decision)
|
|
379
|
+
fragments
|
|
380
|
+
end
|
|
381
|
+
|
|
382
|
+
def replacement_edits(documents, decision)
|
|
383
|
+
ours = documents.fetch(:ours)
|
|
384
|
+
decision.choices.filter_map do |id, role|
|
|
385
|
+
ours_owner = ours.by_id[id]
|
|
386
|
+
next unless ours_owner
|
|
387
|
+
|
|
388
|
+
chosen = role && documents.fetch(role).by_id[id]
|
|
389
|
+
next if role == :ours
|
|
390
|
+
|
|
391
|
+
{ start_line: ours_owner.start_line, end_line: ours_owner.end_line, owner: chosen }
|
|
392
|
+
end
|
|
393
|
+
end
|
|
394
|
+
|
|
395
|
+
def append_additions(fragments, documents, decision)
|
|
396
|
+
ours = documents.fetch(:ours)
|
|
397
|
+
additions = documents.fetch(:theirs).owners.select do |owner|
|
|
398
|
+
!ours.by_id.key?(owner.id) && decision.choices[owner.id] == :theirs
|
|
399
|
+
end
|
|
400
|
+
return if additions.empty?
|
|
401
|
+
|
|
402
|
+
ensure_plan_boundary!(fragments, documents)
|
|
403
|
+
additions.each_with_index do |owner, index|
|
|
404
|
+
append_owner(fragments, owner)
|
|
405
|
+
ensure_plan_boundary!(fragments, documents) if index < additions.length - 1
|
|
406
|
+
end
|
|
407
|
+
end
|
|
408
|
+
|
|
409
|
+
def ensure_plan_boundary!(fragments, documents)
|
|
410
|
+
fragment = fragments.last
|
|
411
|
+
return if fragment.nil? || fragment_content(fragment, documents).end_with?("\n")
|
|
412
|
+
|
|
413
|
+
if fragment.is_a?(Ast::Merge::SourceRender::SourceFragment) && fragment.start_line < fragment.end_line
|
|
414
|
+
fragments[-1] = source_range_fragment(fragment.revision, fragment.start_line, fragment.end_line - 1)
|
|
415
|
+
else
|
|
416
|
+
fragments.pop
|
|
417
|
+
end
|
|
418
|
+
content = if fragment.is_a?(Ast::Merge::SourceRender::SourceFragment)
|
|
419
|
+
documents.fetch(fragment.revision).source.lines.fetch(fragment.end_line - 1)
|
|
420
|
+
else
|
|
421
|
+
fragment.content
|
|
422
|
+
end
|
|
423
|
+
fragments << Ast::Merge::SourceRender::SynthesizedFragment.new(
|
|
424
|
+
content: "#{content}\n",
|
|
425
|
+
reason: :owner_separator,
|
|
426
|
+
producer: provider_id,
|
|
427
|
+
metadata: { copied_source: true }
|
|
428
|
+
)
|
|
429
|
+
end
|
|
430
|
+
|
|
431
|
+
def fragment_content(fragment, documents)
|
|
432
|
+
return fragment.content if fragment.is_a?(Ast::Merge::SourceRender::SynthesizedFragment)
|
|
433
|
+
|
|
434
|
+
document = documents.fetch(fragment.revision)
|
|
435
|
+
document.source.lines.slice(fragment.start_line - 1, fragment.end_line - fragment.start_line + 1).join
|
|
436
|
+
end
|
|
437
|
+
|
|
438
|
+
def append_owner(fragments, owner)
|
|
439
|
+
fragments << source_range_fragment(owner.role, owner.start_line, owner.end_line)
|
|
440
|
+
end
|
|
441
|
+
|
|
442
|
+
def append_range(fragments, role, start_line, end_line)
|
|
443
|
+
return if start_line > end_line
|
|
444
|
+
|
|
445
|
+
fragments << source_range_fragment(role, start_line, end_line)
|
|
446
|
+
end
|
|
447
|
+
|
|
448
|
+
def expected_owners(documents, decision)
|
|
449
|
+
ours = documents.fetch(:ours)
|
|
450
|
+
expected = ours.owners.filter_map do |owner|
|
|
451
|
+
role = decision.choices[owner.id]
|
|
452
|
+
role && documents.fetch(role).by_id[owner.id]
|
|
453
|
+
end
|
|
454
|
+
documents.fetch(:theirs).owners.each do |owner|
|
|
455
|
+
next if ours.by_id.key?(owner.id)
|
|
456
|
+
next unless decision.choices[owner.id] == :theirs
|
|
457
|
+
|
|
458
|
+
expected << owner
|
|
459
|
+
end
|
|
460
|
+
expected
|
|
461
|
+
end
|
|
462
|
+
|
|
463
|
+
def verify_rendered(output, expected, role:)
|
|
464
|
+
parsed = parse_source(output, role)
|
|
465
|
+
if provider_failure?(parsed)
|
|
466
|
+
return {
|
|
467
|
+
output_reparsed: false,
|
|
468
|
+
semantic_match: false,
|
|
469
|
+
parse_error: parsed[:parse_error]
|
|
470
|
+
}
|
|
471
|
+
end
|
|
472
|
+
|
|
473
|
+
actual = parsed.owners.map { |owner| [owner.id, owner.fingerprint] }
|
|
474
|
+
planned = expected.map { |owner| [owner.id, owner.fingerprint] }
|
|
475
|
+
{
|
|
476
|
+
output_reparsed: true,
|
|
477
|
+
semantic_match: actual == planned,
|
|
478
|
+
planned_owner_count: planned.length,
|
|
479
|
+
output_owner_count: actual.length
|
|
480
|
+
}
|
|
481
|
+
end
|
|
482
|
+
|
|
483
|
+
def render_conflicts(request, documents, decision)
|
|
484
|
+
localized = localized_conflict_fragments(request, documents, decision)
|
|
485
|
+
if localized
|
|
486
|
+
rendered = render_plan(request, localized)
|
|
487
|
+
return conflict_failure(request, decision, rendered, :owner_localized_conflict)
|
|
488
|
+
end
|
|
489
|
+
|
|
490
|
+
rendered = render_plan(request, [whole_document_conflict(request, decision)])
|
|
491
|
+
conflict_failure(
|
|
492
|
+
request,
|
|
493
|
+
decision,
|
|
494
|
+
rendered,
|
|
495
|
+
:full_file_conflict,
|
|
496
|
+
fallbacks: [{ from: :owner_localization, to: :full_file_conflict, reason: :owner_not_addressable }]
|
|
497
|
+
)
|
|
498
|
+
end
|
|
499
|
+
|
|
500
|
+
def localized_conflict_fragments(request, documents, decision)
|
|
501
|
+
ours = documents.fetch(:ours)
|
|
502
|
+
return unless decision.conflicts.all? do |conflict|
|
|
503
|
+
ours.by_id.key?(conflict.fetch(:owner_id))
|
|
504
|
+
end
|
|
505
|
+
|
|
506
|
+
ranges = decision.conflicts.map do |conflict|
|
|
507
|
+
[conflict, ours.by_id.fetch(conflict.fetch(:owner_id))]
|
|
508
|
+
end
|
|
509
|
+
return unless ranges.sort_by { |_conflict, owner| owner.start_line }.each_cons(2).none? do |left, right|
|
|
510
|
+
left.last.end_line >= right.last.start_line
|
|
511
|
+
end
|
|
512
|
+
|
|
513
|
+
fragments = []
|
|
514
|
+
cursor = 1
|
|
515
|
+
ranges.sort_by { |_conflict, owner| owner.start_line }.each do |conflict, owner|
|
|
516
|
+
append_range(fragments, :ours, cursor, owner.start_line - 1)
|
|
517
|
+
fragments << owner_conflict_fragment(request, documents, conflict)
|
|
518
|
+
cursor = owner.end_line + 1
|
|
519
|
+
end
|
|
520
|
+
append_range(fragments, :ours, cursor, ours.source.lines.length)
|
|
521
|
+
fragments
|
|
522
|
+
end
|
|
523
|
+
|
|
524
|
+
def owner_conflict_fragment(request, documents, conflict)
|
|
525
|
+
Ast::Merge::SourceRender::ConflictFragment.new(
|
|
526
|
+
conflict_id: conflict.fetch(:conflict_id),
|
|
527
|
+
base: conflict_owner_side(documents, conflict, :base),
|
|
528
|
+
ours: conflict_owner_side(documents, conflict, :ours),
|
|
529
|
+
theirs: conflict_owner_side(documents, conflict, :theirs),
|
|
530
|
+
labels: request.fetch(:labels, {}),
|
|
531
|
+
marker_size: request.fetch(:conflict_marker_size, 7),
|
|
532
|
+
metadata: { path: conflict.fetch(:path), category: conflict.fetch(:category) }
|
|
533
|
+
)
|
|
534
|
+
end
|
|
535
|
+
|
|
536
|
+
def conflict_owner_side(documents, conflict, role)
|
|
537
|
+
document = documents.fetch(role)
|
|
538
|
+
owner = document.by_id[conflict.fetch(:owner_id)]
|
|
539
|
+
owner ? [conflict_source_fragment(owner, document.source)] : []
|
|
540
|
+
end
|
|
541
|
+
|
|
542
|
+
def whole_document_conflict(request, decision)
|
|
543
|
+
Ast::Merge::SourceRender::ConflictFragment.new(
|
|
544
|
+
conflict_id: decision.conflicts.first.fetch(:conflict_id),
|
|
545
|
+
base: [conflict_whole_source_fragment(:base, request.fetch(:base_source))],
|
|
546
|
+
ours: [conflict_whole_source_fragment(:ours, request.fetch(:ours_source))],
|
|
547
|
+
theirs: [conflict_whole_source_fragment(:theirs, request.fetch(:theirs_source))],
|
|
548
|
+
labels: request.fetch(:labels, {}),
|
|
549
|
+
marker_size: request.fetch(:conflict_marker_size, 7),
|
|
550
|
+
metadata: { conflicts: decision.conflicts.map { |conflict| conflict[:conflict_id] } }
|
|
551
|
+
)
|
|
552
|
+
end
|
|
553
|
+
|
|
554
|
+
def conflict_source_fragment(owner, source)
|
|
555
|
+
fragment_source = source.lines.slice(owner.start_line - 1, owner.end_line - owner.start_line + 1).join
|
|
556
|
+
return source_range_fragment(owner.role, owner.start_line, owner.end_line) if fragment_source.end_with?("\n")
|
|
557
|
+
|
|
558
|
+
Ast::Merge::SourceRender::SynthesizedFragment.new(
|
|
559
|
+
content: "#{fragment_source}\n",
|
|
560
|
+
reason: :conflict_line_boundary,
|
|
561
|
+
producer: provider_id,
|
|
562
|
+
metadata: { source_role: owner.role, copied_source: true }
|
|
563
|
+
)
|
|
564
|
+
end
|
|
565
|
+
|
|
566
|
+
def conflict_whole_source_fragment(role, source)
|
|
567
|
+
return whole_source_fragment(role, source) if source.empty? || source.end_with?("\n")
|
|
568
|
+
|
|
569
|
+
Ast::Merge::SourceRender::SynthesizedFragment.new(
|
|
570
|
+
content: "#{source}\n",
|
|
571
|
+
reason: :conflict_line_boundary,
|
|
572
|
+
producer: provider_id,
|
|
573
|
+
metadata: { source_role: role, copied_source: true }
|
|
574
|
+
)
|
|
575
|
+
end
|
|
576
|
+
|
|
577
|
+
def whole_source_fragment(role, source)
|
|
578
|
+
return empty_source_fragment(role) if source.empty?
|
|
579
|
+
|
|
580
|
+
source_range_fragment(role, 1, source.lines.length)
|
|
581
|
+
end
|
|
582
|
+
|
|
583
|
+
def empty_source_fragment(role)
|
|
584
|
+
Ast::Merge::SourceRender::SynthesizedFragment.new(
|
|
585
|
+
content: '',
|
|
586
|
+
reason: :exact_empty_source,
|
|
587
|
+
producer: provider_id,
|
|
588
|
+
metadata: { source_role: role }
|
|
589
|
+
)
|
|
590
|
+
end
|
|
591
|
+
|
|
592
|
+
def source_range_fragment(role, start_line, end_line)
|
|
593
|
+
Ast::Merge::SourceRender::SourceFragment.new(
|
|
594
|
+
revision: role,
|
|
595
|
+
start_line: start_line,
|
|
596
|
+
end_line: end_line,
|
|
597
|
+
metadata: { source_role: role }
|
|
598
|
+
)
|
|
599
|
+
end
|
|
600
|
+
|
|
601
|
+
def render_plan(request, fragments)
|
|
602
|
+
Ast::Merge::SourceRender::Renderer.new.render(
|
|
603
|
+
Ast::Merge::SourceRender::Plan.new(
|
|
604
|
+
sources: {
|
|
605
|
+
base: request[:base_source].to_s,
|
|
606
|
+
ours: request[:ours_source].to_s,
|
|
607
|
+
theirs: request[:theirs_source].to_s
|
|
608
|
+
},
|
|
609
|
+
fragments: fragments,
|
|
610
|
+
metadata: { provider_id: provider_id }
|
|
611
|
+
)
|
|
612
|
+
)
|
|
613
|
+
end
|
|
614
|
+
|
|
615
|
+
def render_report(rendered, strategy)
|
|
616
|
+
{
|
|
617
|
+
strategy: strategy,
|
|
618
|
+
line_records: rendered.line_records,
|
|
619
|
+
synthesized_fragments: rendered.synthesized_fragments,
|
|
620
|
+
conflicts: rendered.conflicts,
|
|
621
|
+
verification_input: rendered.verification_input
|
|
622
|
+
}
|
|
623
|
+
end
|
|
624
|
+
|
|
625
|
+
def conflict_failure(request, decision, rendered, strategy, fallbacks: [])
|
|
626
|
+
failure(
|
|
627
|
+
:merge3,
|
|
628
|
+
request,
|
|
629
|
+
category: :merge_conflict,
|
|
630
|
+
message: 'Ruby top-level owner changed incompatibly on both sides.',
|
|
631
|
+
changes: decision.changes,
|
|
632
|
+
conflicts: decision.conflicts,
|
|
633
|
+
conflicted_output: rendered.content,
|
|
634
|
+
render_report: render_report(rendered, strategy),
|
|
635
|
+
verification: { base_participated: true },
|
|
636
|
+
fallbacks: fallbacks
|
|
637
|
+
)
|
|
638
|
+
end
|
|
639
|
+
|
|
640
|
+
def merge2_failure(request, merged)
|
|
641
|
+
failure(
|
|
642
|
+
:merge2,
|
|
643
|
+
request,
|
|
644
|
+
category: :merge_failure,
|
|
645
|
+
message: Array(merged[:diagnostics]).map { |item| item[:message] }.compact.join('; ')
|
|
646
|
+
)
|
|
647
|
+
end
|
|
648
|
+
|
|
649
|
+
def parse_failure(operation, request, role, parsed)
|
|
650
|
+
detail = parsed[:parse_error] || "ambiguous duplicate owner #{parsed[:ambiguous_owner].inspect}"
|
|
651
|
+
failure(
|
|
652
|
+
operation,
|
|
653
|
+
request,
|
|
654
|
+
category: parsed[:parse_error] ? :parse_error : :ambiguous_owner,
|
|
655
|
+
message: "#{role} parse error: #{detail}",
|
|
656
|
+
source_role: role
|
|
657
|
+
)
|
|
658
|
+
end
|
|
659
|
+
|
|
660
|
+
def render_failure(operation, request, parsed)
|
|
661
|
+
failure(
|
|
662
|
+
operation,
|
|
663
|
+
request,
|
|
664
|
+
category: :render_failure,
|
|
665
|
+
message: "Prism emitter produced invalid output: #{parsed[:parse_error]}",
|
|
666
|
+
source_role: :output
|
|
667
|
+
)
|
|
668
|
+
end
|
|
669
|
+
|
|
670
|
+
def provider_failure?(value)
|
|
671
|
+
value.is_a?(Hash) && (value[:ok] == false || value.key?(:parse_error) || value.key?(:ambiguous_owner))
|
|
672
|
+
end
|
|
673
|
+
|
|
674
|
+
def result(operation, request, changes: [], render_report: {}, verification: {}, **payload)
|
|
675
|
+
Ast::Merge::ProviderResult.build(
|
|
676
|
+
operation: operation,
|
|
677
|
+
success: true,
|
|
678
|
+
envelope: envelope(request, changes: changes, render_report: render_report, verification: verification),
|
|
679
|
+
**payload
|
|
680
|
+
)
|
|
681
|
+
end
|
|
682
|
+
|
|
683
|
+
def failure(operation, request, category:, message:, changes: [], conflicts: [], fallbacks: [],
|
|
684
|
+
render_report: {}, verification: {}, **payload)
|
|
685
|
+
Ast::Merge::ProviderResult.build(
|
|
686
|
+
operation: operation,
|
|
687
|
+
success: false,
|
|
688
|
+
envelope: envelope(
|
|
689
|
+
request,
|
|
690
|
+
changes: changes,
|
|
691
|
+
conflicts: conflicts,
|
|
692
|
+
fallbacks: fallbacks,
|
|
693
|
+
diagnostics: [{ category: category, severity: :error, message: message, blocking: true }],
|
|
694
|
+
render_report: render_report,
|
|
695
|
+
verification: verification
|
|
696
|
+
),
|
|
697
|
+
**payload
|
|
698
|
+
)
|
|
699
|
+
end
|
|
700
|
+
|
|
701
|
+
def envelope(request, **fields)
|
|
702
|
+
{
|
|
703
|
+
provider: {
|
|
704
|
+
provider_id: provider_id,
|
|
705
|
+
family: family,
|
|
706
|
+
dialect: request[:dialect] || :ruby,
|
|
707
|
+
backend: request[:backend] || :prism,
|
|
708
|
+
package: Prism::Merge::PACKAGE_NAME,
|
|
709
|
+
package_version: Prism::Merge::Version::VERSION
|
|
710
|
+
},
|
|
711
|
+
profile: { profile_id: request[:profile_id] || DEFAULT_PROFILE }
|
|
712
|
+
}.merge(fields)
|
|
713
|
+
end
|
|
714
|
+
end
|
|
715
|
+
# rubocop:enable Metrics/AbcSize, Metrics/ClassLength, Metrics/CyclomaticComplexity, Metrics/MethodLength, Metrics/ParameterLists, Metrics/PerceivedComplexity
|
|
716
|
+
end
|
|
717
|
+
end
|
data/lib/prism/merge/version.rb
CHANGED
|
@@ -5,7 +5,7 @@ module Prism
|
|
|
5
5
|
# Version namespace for this gem.
|
|
6
6
|
module Version
|
|
7
7
|
# Current gem version.
|
|
8
|
-
VERSION = '7.1.
|
|
8
|
+
VERSION = '7.1.4'
|
|
9
9
|
end
|
|
10
10
|
# Current gem version exposed at the traditional constant location.
|
|
11
11
|
VERSION = Version::VERSION # Traditional Constant Location
|
data/lib/prism/merge.rb
CHANGED
|
@@ -7,6 +7,9 @@ require 'tree_haver'
|
|
|
7
7
|
require_relative 'merge/version'
|
|
8
8
|
|
|
9
9
|
module Prism
|
|
10
|
+
# This public facade keeps provider protocol and backend registration
|
|
11
|
+
# entrypoints together; implementation details live in autoloaded files.
|
|
12
|
+
# rubocop:disable Metrics/ModuleLength
|
|
10
13
|
module Merge
|
|
11
14
|
module_function
|
|
12
15
|
|
|
@@ -62,6 +65,7 @@ module Prism
|
|
|
62
65
|
autoload :NodeWrapper, 'prism/merge/node_wrapper'
|
|
63
66
|
autoload :PartialTemplateMerger, 'prism/merge/partial_template_merger'
|
|
64
67
|
autoload :PartialTemplateNode, 'prism/merge/partial_template_node'
|
|
68
|
+
autoload :Provider, 'prism/merge/provider'
|
|
65
69
|
autoload :RecursiveMergePolicy, 'prism/merge/recursive_merge_policy'
|
|
66
70
|
autoload :RecursiveNodeBodyMerger, 'prism/merge/recursive_node_body_merger'
|
|
67
71
|
autoload :RubyDocSurfaceAnalyzer, 'prism/merge/ruby_doc_surface_analyzer'
|
|
@@ -90,6 +94,14 @@ module Prism
|
|
|
90
94
|
Ruby::Merge.ruby_feature_profile
|
|
91
95
|
end
|
|
92
96
|
|
|
97
|
+
def merge_provider
|
|
98
|
+
@merge_provider ||= Provider.new
|
|
99
|
+
end
|
|
100
|
+
|
|
101
|
+
def register_provider!(replace: false)
|
|
102
|
+
Ast::Merge.register_provider(merge_provider, replace: replace)
|
|
103
|
+
end
|
|
104
|
+
|
|
93
105
|
def available_ruby_backends
|
|
94
106
|
[BACKEND_REFERENCE]
|
|
95
107
|
end
|
|
@@ -300,6 +312,8 @@ module Prism
|
|
|
300
312
|
}
|
|
301
313
|
end
|
|
302
314
|
|
|
315
|
+
# This projection mirrors the nested public schema intentionally.
|
|
316
|
+
# rubocop:disable Metrics/AbcSize, Metrics/MethodLength
|
|
303
317
|
def ruby_structured_edit_batch_report_projection
|
|
304
318
|
content = "class App\n # managed snippet\n old_call\n\n anchor_call\n\n # obsolete snippet\n obsolete_call\nend\n"
|
|
305
319
|
{
|
|
@@ -441,7 +455,10 @@ module Prism
|
|
|
441
455
|
)
|
|
442
456
|
}
|
|
443
457
|
end
|
|
458
|
+
# rubocop:enable Metrics/AbcSize, Metrics/MethodLength
|
|
444
459
|
|
|
460
|
+
# Keep parser validation and diagnostics in one boundary method.
|
|
461
|
+
# rubocop:disable Metrics/AbcSize, Metrics/MethodLength
|
|
445
462
|
def parse_ruby(source, dialect, backend: nil)
|
|
446
463
|
requested = backend.to_s.empty? ? BACKEND_REFERENCE.id : backend.to_s
|
|
447
464
|
return unsupported_feature_result("Unsupported Ruby dialect #{dialect}.") unless dialect == 'ruby'
|
|
@@ -469,6 +486,7 @@ module Prism
|
|
|
469
486
|
policies: []
|
|
470
487
|
}
|
|
471
488
|
end
|
|
489
|
+
# rubocop:enable Metrics/AbcSize, Metrics/MethodLength
|
|
472
490
|
|
|
473
491
|
def analyze_ruby_document(source, parse_result)
|
|
474
492
|
normalized_source = prism_ruby_normalize_source(source)
|
|
@@ -485,6 +503,8 @@ module Prism
|
|
|
485
503
|
}
|
|
486
504
|
end
|
|
487
505
|
|
|
506
|
+
# Normalization builds the complete TreeHaver contract in one pass.
|
|
507
|
+
# rubocop:disable Metrics/AbcSize, Metrics/CyclomaticComplexity, Metrics/MethodLength, Metrics/PerceivedComplexity
|
|
488
508
|
def parse_ruby_normalized(source, dialect = 'ruby', backend: nil)
|
|
489
509
|
requested = backend.to_s.empty? ? BACKEND_REFERENCE.id : backend.to_s
|
|
490
510
|
return unsupported_feature_result("Unsupported Ruby dialect #{dialect}.") unless dialect == 'ruby'
|
|
@@ -566,7 +586,10 @@ module Prism
|
|
|
566
586
|
metadata: prism_normalized_metadata
|
|
567
587
|
).to_h
|
|
568
588
|
end
|
|
589
|
+
# rubocop:enable Metrics/AbcSize, Metrics/CyclomaticComplexity, Metrics/MethodLength, Metrics/PerceivedComplexity
|
|
569
590
|
|
|
591
|
+
# This adapter validates, edits, reparses, and reports one projection atomically.
|
|
592
|
+
# rubocop:disable Metrics/AbcSize, Metrics/CyclomaticComplexity, Metrics/MethodLength, Metrics/PerceivedComplexity
|
|
570
593
|
def apply_edit_projection(request)
|
|
571
594
|
source = request.fetch(:source)
|
|
572
595
|
provider_id = request.fetch(:provider_id)
|
|
@@ -638,6 +661,7 @@ module Prism
|
|
|
638
661
|
[]
|
|
639
662
|
).to_h
|
|
640
663
|
end
|
|
664
|
+
# rubocop:enable Metrics/AbcSize, Metrics/CyclomaticComplexity, Metrics/MethodLength, Metrics/PerceivedComplexity
|
|
641
665
|
|
|
642
666
|
def match_ruby_owners(template, destination)
|
|
643
667
|
Ruby::Merge.match_ruby_owners(template, destination)
|
|
@@ -721,6 +745,8 @@ module Prism
|
|
|
721
745
|
end
|
|
722
746
|
end
|
|
723
747
|
|
|
748
|
+
# Alias normalization intentionally handles the supported input shapes inline.
|
|
749
|
+
# rubocop:disable Metrics/AbcSize, Metrics/CyclomaticComplexity, Metrics/MethodLength
|
|
724
750
|
def normalize_ruby_require_aliases(require_aliases)
|
|
725
751
|
Array(require_aliases).each_with_object({}) do |entry, aliases|
|
|
726
752
|
case entry
|
|
@@ -741,6 +767,7 @@ module Prism
|
|
|
741
767
|
aliases[to] = canonical
|
|
742
768
|
end
|
|
743
769
|
end
|
|
770
|
+
# rubocop:enable Metrics/AbcSize, Metrics/CyclomaticComplexity, Metrics/MethodLength
|
|
744
771
|
|
|
745
772
|
def ruby_dsl_call_node(node)
|
|
746
773
|
return node if node.is_a?(::Prism::CallNode)
|
|
@@ -765,6 +792,8 @@ module Prism
|
|
|
765
792
|
end
|
|
766
793
|
end
|
|
767
794
|
|
|
795
|
+
# The reviewed merge boundary must retain all protocol roles for replay.
|
|
796
|
+
# rubocop:disable Metrics/AbcSize, Metrics/MethodLength, Metrics/ParameterLists
|
|
768
797
|
def merge_ruby_with_reviewed_nested_outputs(template_source, destination_source, dialect, review_state,
|
|
769
798
|
applied_children, backend: nil)
|
|
770
799
|
requested = backend.to_s.empty? ? BACKEND_REFERENCE.id : backend.to_s
|
|
@@ -797,6 +826,7 @@ module Prism
|
|
|
797
826
|
}
|
|
798
827
|
)
|
|
799
828
|
end
|
|
829
|
+
# rubocop:enable Metrics/AbcSize, Metrics/MethodLength, Metrics/ParameterLists
|
|
800
830
|
|
|
801
831
|
def merge_ruby_with_reviewed_nested_outputs_from_replay_bundle(template_source, destination_source, dialect,
|
|
802
832
|
replay_bundle, backend: nil)
|
|
@@ -924,6 +954,8 @@ module Prism
|
|
|
924
954
|
end.sort_by { |owner| owner[:path] }
|
|
925
955
|
end
|
|
926
956
|
|
|
957
|
+
# Surface discovery intentionally keeps line ownership and comment state together.
|
|
958
|
+
# rubocop:disable Metrics/AbcSize, Metrics/MethodLength
|
|
927
959
|
def prism_ruby_discovered_surfaces(source, body)
|
|
928
960
|
owners_by_line = body.filter_map do |node|
|
|
929
961
|
name = prism_ruby_declaration_name(node)
|
|
@@ -954,7 +986,10 @@ module Prism
|
|
|
954
986
|
|
|
955
987
|
surfaces
|
|
956
988
|
end
|
|
989
|
+
# rubocop:enable Metrics/AbcSize, Metrics/MethodLength
|
|
957
990
|
|
|
991
|
+
# A surface record is assembled as one schema-shaped value.
|
|
992
|
+
# rubocop:disable Metrics/AbcSize, Metrics/MethodLength
|
|
958
993
|
def prism_ruby_surfaces_for_owner(owner_name, comment_entries)
|
|
959
994
|
filtered_entries = comment_entries.filter { |entry| prism_ruby_doc_comment_content?(entry.fetch(:raw)) }
|
|
960
995
|
return [] if filtered_entries.empty?
|
|
@@ -979,7 +1014,10 @@ module Prism
|
|
|
979
1014
|
|
|
980
1015
|
[doc_surface] + prism_ruby_example_surfaces(doc_surface)
|
|
981
1016
|
end
|
|
1017
|
+
# rubocop:enable Metrics/AbcSize, Metrics/MethodLength
|
|
982
1018
|
|
|
1019
|
+
# Example surfaces preserve the complete delegated-child metadata contract.
|
|
1020
|
+
# rubocop:disable Metrics/AbcSize, Metrics/MethodLength
|
|
983
1021
|
def prism_ruby_example_surfaces(surface)
|
|
984
1022
|
entries = Array(surface.dig(:metadata, :entries))
|
|
985
1023
|
Ruby::Merge::DocCommentSupport.example_blocks(entries).map do |block|
|
|
@@ -1004,6 +1042,7 @@ module Prism
|
|
|
1004
1042
|
)
|
|
1005
1043
|
end
|
|
1006
1044
|
end
|
|
1045
|
+
# rubocop:enable Metrics/AbcSize, Metrics/MethodLength
|
|
1007
1046
|
|
|
1008
1047
|
def prism_ruby_declaration_name(node)
|
|
1009
1048
|
case node
|
|
@@ -1162,6 +1201,8 @@ module Prism
|
|
|
1162
1201
|
end
|
|
1163
1202
|
end
|
|
1164
1203
|
|
|
1204
|
+
# Comment nodes retain backend metadata in a single normalized schema value.
|
|
1205
|
+
# rubocop:disable Metrics/AbcSize, Metrics/MethodLength
|
|
1165
1206
|
def prism_comment_node(source, comment, index, magic_comment, parent_id:)
|
|
1166
1207
|
directive = prism_comment_directive(
|
|
1167
1208
|
source.byteslice(comment.location.start_offset...comment.location.end_offset), magic_comment
|
|
@@ -1192,6 +1233,7 @@ module Prism
|
|
|
1192
1233
|
}
|
|
1193
1234
|
)
|
|
1194
1235
|
end
|
|
1236
|
+
# rubocop:enable Metrics/AbcSize, Metrics/MethodLength
|
|
1195
1237
|
|
|
1196
1238
|
def prism_comment_directive(comment_text, magic_comment)
|
|
1197
1239
|
if comment_text.strip.match?(/\A#\s*smorg:freeze\b/)
|
|
@@ -1330,9 +1372,11 @@ module Prism
|
|
|
1330
1372
|
:source_span_for_location
|
|
1331
1373
|
)
|
|
1332
1374
|
end
|
|
1375
|
+
# rubocop:enable Metrics/ModuleLength
|
|
1333
1376
|
end
|
|
1334
1377
|
|
|
1335
1378
|
Prism::Merge.register_backend!
|
|
1379
|
+
Prism::Merge.register_provider!
|
|
1336
1380
|
|
|
1337
1381
|
if defined?(Ast::Merge::RSpec::MergeGemRegistry)
|
|
1338
1382
|
Ast::Merge::RSpec::MergeGemRegistry.register(
|
data/sig/prism/merge.rbs
CHANGED
|
@@ -1,5 +1,18 @@
|
|
|
1
1
|
module Prism
|
|
2
2
|
module Merge
|
|
3
|
+
class Provider
|
|
4
|
+
def provider_id: () -> String
|
|
5
|
+
def family: () -> String
|
|
6
|
+
def capabilities: () -> Ast::Merge::provider_capabilities
|
|
7
|
+
def analyze: (Ast::Merge::provider_request request) -> Ast::Merge::provider_result
|
|
8
|
+
def diff2: (Ast::Merge::provider_request request) -> Ast::Merge::provider_result
|
|
9
|
+
def merge2: (Ast::Merge::provider_request request) -> Ast::Merge::provider_result
|
|
10
|
+
def merge3: (Ast::Merge::provider_request request) -> Ast::Merge::provider_result
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
def self.merge_provider: () -> Provider
|
|
14
|
+
def self.register_provider!: (?replace: bool) -> Ast::Merge::_MergeProvider
|
|
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: prism-merge
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 7.1.
|
|
4
|
+
version: 7.1.4
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Peter H. Boling
|
|
@@ -37,6 +37,20 @@ cert_chain:
|
|
|
37
37
|
-----END CERTIFICATE-----
|
|
38
38
|
date: 1980-01-02 00:00:00.000000000 Z
|
|
39
39
|
dependencies:
|
|
40
|
+
- !ruby/object:Gem::Dependency
|
|
41
|
+
name: ast-merge
|
|
42
|
+
requirement: !ruby/object:Gem::Requirement
|
|
43
|
+
requirements:
|
|
44
|
+
- - '='
|
|
45
|
+
- !ruby/object:Gem::Version
|
|
46
|
+
version: 7.1.4
|
|
47
|
+
type: :runtime
|
|
48
|
+
prerelease: false
|
|
49
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
50
|
+
requirements:
|
|
51
|
+
- - '='
|
|
52
|
+
- !ruby/object:Gem::Version
|
|
53
|
+
version: 7.1.4
|
|
40
54
|
- !ruby/object:Gem::Dependency
|
|
41
55
|
name: prism
|
|
42
56
|
requirement: !ruby/object:Gem::Requirement
|
|
@@ -57,14 +71,28 @@ dependencies:
|
|
|
57
71
|
requirements:
|
|
58
72
|
- - '='
|
|
59
73
|
- !ruby/object:Gem::Version
|
|
60
|
-
version: 7.1.
|
|
74
|
+
version: 7.1.4
|
|
75
|
+
type: :runtime
|
|
76
|
+
prerelease: false
|
|
77
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
78
|
+
requirements:
|
|
79
|
+
- - '='
|
|
80
|
+
- !ruby/object:Gem::Version
|
|
81
|
+
version: 7.1.4
|
|
82
|
+
- !ruby/object:Gem::Dependency
|
|
83
|
+
name: tree_haver
|
|
84
|
+
requirement: !ruby/object:Gem::Requirement
|
|
85
|
+
requirements:
|
|
86
|
+
- - '='
|
|
87
|
+
- !ruby/object:Gem::Version
|
|
88
|
+
version: 7.1.4
|
|
61
89
|
type: :runtime
|
|
62
90
|
prerelease: false
|
|
63
91
|
version_requirements: !ruby/object:Gem::Requirement
|
|
64
92
|
requirements:
|
|
65
93
|
- - '='
|
|
66
94
|
- !ruby/object:Gem::Version
|
|
67
|
-
version: 7.1.
|
|
95
|
+
version: 7.1.4
|
|
68
96
|
- !ruby/object:Gem::Dependency
|
|
69
97
|
name: version_gem
|
|
70
98
|
requirement: !ruby/object:Gem::Requirement
|
|
@@ -91,20 +119,20 @@ dependencies:
|
|
|
91
119
|
requirements:
|
|
92
120
|
- - "~>"
|
|
93
121
|
- !ruby/object:Gem::Version
|
|
94
|
-
version: '
|
|
122
|
+
version: '3.0'
|
|
95
123
|
- - ">="
|
|
96
124
|
- !ruby/object:Gem::Version
|
|
97
|
-
version:
|
|
125
|
+
version: 3.0.7
|
|
98
126
|
type: :development
|
|
99
127
|
prerelease: false
|
|
100
128
|
version_requirements: !ruby/object:Gem::Requirement
|
|
101
129
|
requirements:
|
|
102
130
|
- - "~>"
|
|
103
131
|
- !ruby/object:Gem::Version
|
|
104
|
-
version: '
|
|
132
|
+
version: '3.0'
|
|
105
133
|
- - ">="
|
|
106
134
|
- !ruby/object:Gem::Version
|
|
107
|
-
version:
|
|
135
|
+
version: 3.0.7
|
|
108
136
|
- !ruby/object:Gem::Dependency
|
|
109
137
|
name: bundler-audit
|
|
110
138
|
requirement: !ruby/object:Gem::Requirement
|
|
@@ -182,7 +210,7 @@ dependencies:
|
|
|
182
210
|
version: '3.2'
|
|
183
211
|
- - ">="
|
|
184
212
|
- !ruby/object:Gem::Version
|
|
185
|
-
version: 3.2.
|
|
213
|
+
version: 3.2.2
|
|
186
214
|
type: :development
|
|
187
215
|
prerelease: false
|
|
188
216
|
version_requirements: !ruby/object:Gem::Requirement
|
|
@@ -192,7 +220,7 @@ dependencies:
|
|
|
192
220
|
version: '3.2'
|
|
193
221
|
- - ">="
|
|
194
222
|
- !ruby/object:Gem::Version
|
|
195
|
-
version: 3.2.
|
|
223
|
+
version: 3.2.2
|
|
196
224
|
- !ruby/object:Gem::Dependency
|
|
197
225
|
name: kettle-test
|
|
198
226
|
requirement: !ruby/object:Gem::Requirement
|
|
@@ -202,7 +230,7 @@ dependencies:
|
|
|
202
230
|
version: '2.0'
|
|
203
231
|
- - ">="
|
|
204
232
|
- !ruby/object:Gem::Version
|
|
205
|
-
version: 2.0.
|
|
233
|
+
version: 2.0.20
|
|
206
234
|
type: :development
|
|
207
235
|
prerelease: false
|
|
208
236
|
version_requirements: !ruby/object:Gem::Requirement
|
|
@@ -212,7 +240,7 @@ dependencies:
|
|
|
212
240
|
version: '2.0'
|
|
213
241
|
- - ">="
|
|
214
242
|
- !ruby/object:Gem::Version
|
|
215
|
-
version: 2.0.
|
|
243
|
+
version: 2.0.20
|
|
216
244
|
- !ruby/object:Gem::Dependency
|
|
217
245
|
name: turbo_tests2
|
|
218
246
|
requirement: !ruby/object:Gem::Requirement
|
|
@@ -222,7 +250,7 @@ dependencies:
|
|
|
222
250
|
version: '3.2'
|
|
223
251
|
- - ">="
|
|
224
252
|
- !ruby/object:Gem::Version
|
|
225
|
-
version: 3.2.
|
|
253
|
+
version: 3.2.6
|
|
226
254
|
type: :development
|
|
227
255
|
prerelease: false
|
|
228
256
|
version_requirements: !ruby/object:Gem::Requirement
|
|
@@ -232,7 +260,7 @@ dependencies:
|
|
|
232
260
|
version: '3.2'
|
|
233
261
|
- - ">="
|
|
234
262
|
- !ruby/object:Gem::Version
|
|
235
|
-
version: 3.2.
|
|
263
|
+
version: 3.2.6
|
|
236
264
|
- !ruby/object:Gem::Dependency
|
|
237
265
|
name: ruby-progressbar
|
|
238
266
|
requirement: !ruby/object:Gem::Requirement
|
|
@@ -276,7 +304,7 @@ dependencies:
|
|
|
276
304
|
version: '2.0'
|
|
277
305
|
- - ">="
|
|
278
306
|
- !ruby/object:Gem::Version
|
|
279
|
-
version: 2.0.
|
|
307
|
+
version: 2.0.12
|
|
280
308
|
type: :development
|
|
281
309
|
prerelease: false
|
|
282
310
|
version_requirements: !ruby/object:Gem::Requirement
|
|
@@ -286,7 +314,7 @@ dependencies:
|
|
|
286
314
|
version: '2.0'
|
|
287
315
|
- - ">="
|
|
288
316
|
- !ruby/object:Gem::Version
|
|
289
|
-
version: 2.0.
|
|
317
|
+
version: 2.0.12
|
|
290
318
|
description: "☯️ Prism-backed Ruby provider gem for the Structured Merge Ruby family."
|
|
291
319
|
email:
|
|
292
320
|
- floss@galtzo.com
|
|
@@ -330,6 +358,7 @@ files:
|
|
|
330
358
|
- lib/prism/merge/node_wrapper.rb
|
|
331
359
|
- lib/prism/merge/partial_template_merger.rb
|
|
332
360
|
- lib/prism/merge/partial_template_node.rb
|
|
361
|
+
- lib/prism/merge/provider.rb
|
|
333
362
|
- lib/prism/merge/recursive_merge_policy.rb
|
|
334
363
|
- lib/prism/merge/recursive_node_body_merger.rb
|
|
335
364
|
- lib/prism/merge/ruby_doc_surface_analyzer.rb
|
|
@@ -346,10 +375,10 @@ licenses:
|
|
|
346
375
|
- PolyForm-Small-Business-1.0.0
|
|
347
376
|
metadata:
|
|
348
377
|
homepage_uri: https://structuredmerge.org
|
|
349
|
-
source_code_uri: https://github.com/structuredmerge/structuredmerge-ruby/tree/v7.1.
|
|
350
|
-
changelog_uri: https://github.com/structuredmerge/structuredmerge-ruby/blob/v7.1.
|
|
378
|
+
source_code_uri: https://github.com/structuredmerge/structuredmerge-ruby/tree/v7.1.4
|
|
379
|
+
changelog_uri: https://github.com/structuredmerge/structuredmerge-ruby/blob/v7.1.4/CHANGELOG.md
|
|
351
380
|
bug_tracker_uri: https://github.com/structuredmerge/structuredmerge-ruby/issues
|
|
352
|
-
documentation_uri: https://www.rubydoc.info/gems/prism-merge/7.1.
|
|
381
|
+
documentation_uri: https://www.rubydoc.info/gems/prism-merge/7.1.4
|
|
353
382
|
funding_uri: https://github.com/sponsors/pboling
|
|
354
383
|
wiki_uri: https://github.com/structuredmerge/structuredmerge-ruby/wiki
|
|
355
384
|
news_uri: https://www.railsbling.com/tags/prism-merge
|
metadata.gz.sig
CHANGED
|
Binary file
|