ast-merge 7.1.9 → 7.1.10
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/lib/ast/merge/provider_contract.rb +3 -3
- data/lib/ast/merge/rust_host_provider.rb +304 -0
- data/lib/ast/merge/structural_edit/README.md +11 -0
- data/lib/ast/merge/structural_edit/blank_line_support.rb +138 -0
- data/lib/ast/merge/structural_edit/remove_plan.rb +4 -0
- data/lib/ast/merge/structural_edit/splice_plan.rb +48 -21
- data/lib/ast/merge/structural_edit.rb +1 -0
- data/lib/ast/merge/typed_core_provider.rb +214 -0
- data/lib/ast/merge/version.rb +1 -1
- data/lib/ast/merge.rb +1 -0
- data.tar.gz.sig +0 -0
- metadata +24 -21
- 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: 54246c2a0a295278fc701f94b29176973d1a12f0a7241b82d0e0a9490cb4248d
|
|
4
|
+
data.tar.gz: 598d2c26c795c77e0d66d4105885210b0908755aa97e69155e7a8c3a7b1af853
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 570d218b5cb765f6d61f4bc7169ee1ffca21a032b61ee408e6556b285223a55715486387cb0f73d99abd7d3388bbec67e484f3407f0a2d1adc7fe224c15b26c7
|
|
7
|
+
data.tar.gz: c7868ee942c0a708f8af61c1020b1957433ca8715ee16e4a0c0506ccb5a5f5b8f26a9c9c63cc9f14fb5517147a0e1c504292a397cbdbba494126003dd57213ce
|
checksums.yaml.gz.sig
CHANGED
|
Binary file
|
|
@@ -133,9 +133,9 @@ module Ast
|
|
|
133
133
|
operations = normalize_identifiers(values)
|
|
134
134
|
unknown = operations - OPERATIONS
|
|
135
135
|
raise InvalidProviderError, "Unknown provider operations: #{unknown.join(', ')}" unless unknown.empty?
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
136
|
+
# Methods remain required so direct unsupported calls fail consistently;
|
|
137
|
+
# advertised capabilities must reflect only implemented operations.
|
|
138
|
+
raise InvalidProviderError, 'Provider must advertise at least one operation' if operations.empty?
|
|
139
139
|
|
|
140
140
|
operations
|
|
141
141
|
end
|
|
@@ -0,0 +1,304 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'json'
|
|
4
|
+
|
|
5
|
+
# rubocop:disable Metrics/AbcSize, Metrics/ClassLength, Metrics/CyclomaticComplexity, Metrics/MethodLength, Metrics/ParameterLists, Metrics/PerceivedComplexity
|
|
6
|
+
|
|
7
|
+
module Ast
|
|
8
|
+
module Merge
|
|
9
|
+
# Opt-in provider adapter for operations implemented by the compiled Rust
|
|
10
|
+
# host. Concrete format gems supply the host method names and metadata.
|
|
11
|
+
class RustHostProvider
|
|
12
|
+
class << self
|
|
13
|
+
def available?(required_methods = [])
|
|
14
|
+
require 'structuredmerge_core' unless defined?(::StructuredmergeCore)
|
|
15
|
+
required_methods.all? { |method_name| ::StructuredmergeCore.respond_to?(method_name) }
|
|
16
|
+
rescue LoadError
|
|
17
|
+
false
|
|
18
|
+
end
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
def initialize(provider_id:, family:, dialect:, package:, package_version:, host_methods:)
|
|
22
|
+
@provider_id = provider_id
|
|
23
|
+
@family = family
|
|
24
|
+
@dialect = dialect
|
|
25
|
+
@package = package
|
|
26
|
+
@package_version = package_version
|
|
27
|
+
@host_methods = host_methods.freeze
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
attr_reader :provider_id, :family
|
|
31
|
+
|
|
32
|
+
def capabilities
|
|
33
|
+
{
|
|
34
|
+
operations: ProviderContract::OPERATIONS,
|
|
35
|
+
dialects: [@dialect],
|
|
36
|
+
backends: [:rust_tslp],
|
|
37
|
+
profiles: [:source_preserving],
|
|
38
|
+
role: :workflow,
|
|
39
|
+
source_preservation: %i[exact_source declaration_fragments line_provenance reparse]
|
|
40
|
+
}.freeze
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
def analyze(request)
|
|
44
|
+
raw = call_host(:analyze, request.fetch(:source), request.fetch(:dialect).to_s)
|
|
45
|
+
return failure(:analyze, request, raw) unless raw.fetch('ok')
|
|
46
|
+
|
|
47
|
+
analysis = raw.fetch('analysis')
|
|
48
|
+
owners = Array(analysis['declarations'])
|
|
49
|
+
owners = Array(analysis['owners']) if owners.empty?
|
|
50
|
+
success(
|
|
51
|
+
:analyze,
|
|
52
|
+
request,
|
|
53
|
+
analysis: {
|
|
54
|
+
backend: :rust_tslp,
|
|
55
|
+
valid: true,
|
|
56
|
+
declarations: owners.map do |owner|
|
|
57
|
+
{
|
|
58
|
+
path: logical_owner_path(owner),
|
|
59
|
+
signature: logical_owner_path(owner),
|
|
60
|
+
source_role: :source,
|
|
61
|
+
line_range: owner.fetch('line_range', line_range(request.fetch(:source), owner['source_fragment']))
|
|
62
|
+
}
|
|
63
|
+
end
|
|
64
|
+
},
|
|
65
|
+
verification: { source_parsed: true, rust_host: true }
|
|
66
|
+
)
|
|
67
|
+
rescue KeyError, JSON::ParserError => e
|
|
68
|
+
failure(:analyze, request, error_payload('parse_error', e.message, 'source'))
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
def diff2(request)
|
|
72
|
+
before = analysis_for(request.fetch(:before_source), request.fetch(:dialect).to_s)
|
|
73
|
+
return failure(:diff2, request, before) unless before.fetch('ok')
|
|
74
|
+
|
|
75
|
+
after = analysis_for(request.fetch(:after_source), request.fetch(:dialect).to_s)
|
|
76
|
+
return failure(:diff2, request, after) unless after.fetch('ok')
|
|
77
|
+
|
|
78
|
+
before_owners = owner_map(
|
|
79
|
+
before,
|
|
80
|
+
source: request.fetch(:before_source),
|
|
81
|
+
source_role: :before
|
|
82
|
+
)
|
|
83
|
+
after_owners = owner_map(
|
|
84
|
+
after,
|
|
85
|
+
source: request.fetch(:after_source),
|
|
86
|
+
source_role: :after
|
|
87
|
+
)
|
|
88
|
+
changes = (before_owners.keys | after_owners.keys).filter_map do |path|
|
|
89
|
+
before_owner = before_owners[path]
|
|
90
|
+
after_owner = after_owners[path]
|
|
91
|
+
next if same_owner?(before_owner, after_owner)
|
|
92
|
+
|
|
93
|
+
change = if before_owners.key?(path)
|
|
94
|
+
after_owners.key?(path) ? :edited : :deleted
|
|
95
|
+
else
|
|
96
|
+
:added
|
|
97
|
+
end
|
|
98
|
+
{
|
|
99
|
+
path: path,
|
|
100
|
+
before: revision_record(before_owner, :before),
|
|
101
|
+
after: revision_record(after_owner, :after),
|
|
102
|
+
change: change
|
|
103
|
+
}
|
|
104
|
+
end
|
|
105
|
+
success(
|
|
106
|
+
:diff2,
|
|
107
|
+
request,
|
|
108
|
+
envelope: {
|
|
109
|
+
changes: changes,
|
|
110
|
+
verification: { before_parsed: true, after_parsed: true, rust_host: true }
|
|
111
|
+
},
|
|
112
|
+
diff: { changes: changes }
|
|
113
|
+
)
|
|
114
|
+
rescue KeyError, JSON::ParserError => e
|
|
115
|
+
failure(:diff2, request, error_payload('parse_error', e.message, 'source'))
|
|
116
|
+
end
|
|
117
|
+
|
|
118
|
+
def merge2(request)
|
|
119
|
+
raw = call_host(
|
|
120
|
+
:merge2,
|
|
121
|
+
request.fetch(:current_source),
|
|
122
|
+
request.fetch(:incoming_source),
|
|
123
|
+
request.fetch(:dialect).to_s
|
|
124
|
+
)
|
|
125
|
+
merge_result(:merge2, request, raw)
|
|
126
|
+
rescue KeyError, JSON::ParserError => e
|
|
127
|
+
failure(:merge2, request, error_payload('parse_error', e.message, 'source'))
|
|
128
|
+
end
|
|
129
|
+
|
|
130
|
+
def merge3(request)
|
|
131
|
+
raw = call_host(
|
|
132
|
+
:merge3,
|
|
133
|
+
request.fetch(:base_source),
|
|
134
|
+
request.fetch(:ours_source),
|
|
135
|
+
request.fetch(:theirs_source),
|
|
136
|
+
request.fetch(:dialect).to_s
|
|
137
|
+
)
|
|
138
|
+
merge_result(:merge3, request, raw)
|
|
139
|
+
rescue KeyError, JSON::ParserError => e
|
|
140
|
+
failure(:merge3, request, error_payload('parse_error', e.message, 'source'))
|
|
141
|
+
end
|
|
142
|
+
|
|
143
|
+
private
|
|
144
|
+
|
|
145
|
+
def host
|
|
146
|
+
require 'structuredmerge_core' unless defined?(::StructuredmergeCore)
|
|
147
|
+
::StructuredmergeCore
|
|
148
|
+
end
|
|
149
|
+
|
|
150
|
+
def call_host(operation, *args)
|
|
151
|
+
JSON.parse(host.public_send(@host_methods.fetch(operation), *args))
|
|
152
|
+
end
|
|
153
|
+
|
|
154
|
+
def analysis_for(source, dialect)
|
|
155
|
+
call_host(:analyze, source, dialect)
|
|
156
|
+
end
|
|
157
|
+
|
|
158
|
+
def owner_map(raw, source:, source_role:)
|
|
159
|
+
analysis = raw.fetch('analysis')
|
|
160
|
+
declarations = Array(analysis['declarations'])
|
|
161
|
+
unless declarations.empty?
|
|
162
|
+
return declarations.to_h do |declaration|
|
|
163
|
+
path = logical_owner_path(declaration)
|
|
164
|
+
[path, declaration.merge(
|
|
165
|
+
'path' => path,
|
|
166
|
+
'line_range' => line_range(source, declaration['text'] || declaration['source_fragment']),
|
|
167
|
+
'source_role' => source_role
|
|
168
|
+
)]
|
|
169
|
+
end
|
|
170
|
+
end
|
|
171
|
+
|
|
172
|
+
analysis.fetch('owners').to_h do |owner|
|
|
173
|
+
path = logical_owner_path(owner)
|
|
174
|
+
[path, owner.merge(
|
|
175
|
+
'path' => path,
|
|
176
|
+
'line_range' => line_range(source, owner['text'] || owner['source_fragment']),
|
|
177
|
+
'source_role' => source_role
|
|
178
|
+
)]
|
|
179
|
+
end
|
|
180
|
+
end
|
|
181
|
+
|
|
182
|
+
def same_owner?(before_owner, after_owner)
|
|
183
|
+
return true if before_owner.nil? && after_owner.nil?
|
|
184
|
+
return false if before_owner.nil? || after_owner.nil?
|
|
185
|
+
|
|
186
|
+
if before_owner.key?('text') || after_owner.key?('text') ||
|
|
187
|
+
before_owner.key?('source_fragment') || after_owner.key?('source_fragment')
|
|
188
|
+
(before_owner['text'] || before_owner['source_fragment']) ==
|
|
189
|
+
(after_owner['text'] || after_owner['source_fragment'])
|
|
190
|
+
else
|
|
191
|
+
before_owner == after_owner
|
|
192
|
+
end
|
|
193
|
+
end
|
|
194
|
+
|
|
195
|
+
def revision_record(owner, source_role)
|
|
196
|
+
return { present: false, source_role: source_role, line_range: [nil, nil] } unless owner
|
|
197
|
+
|
|
198
|
+
{ present: true, source_role: source_role, line_range: owner.fetch('line_range', [nil, nil]) }
|
|
199
|
+
end
|
|
200
|
+
|
|
201
|
+
def line_range(source, fragment)
|
|
202
|
+
return [nil, nil] if fragment.nil?
|
|
203
|
+
|
|
204
|
+
# Host declaration fragments include their terminating newline. The
|
|
205
|
+
# public provider contract reports the declaration's content lines,
|
|
206
|
+
# not the separator owned by the following region.
|
|
207
|
+
content = fragment.sub(/\r?\n+\z/, '')
|
|
208
|
+
start = source.index(fragment)
|
|
209
|
+
return [nil, nil] unless start
|
|
210
|
+
|
|
211
|
+
end_index = start + content.length
|
|
212
|
+
start_line = source[0...start].count("\n") + 1
|
|
213
|
+
end_line = source[0...end_index].count("\n") + 1
|
|
214
|
+
[start_line, end_line]
|
|
215
|
+
end
|
|
216
|
+
|
|
217
|
+
def logical_owner_path(owner)
|
|
218
|
+
match_key = owner['match_key']
|
|
219
|
+
return owner.fetch('path') if match_key.nil? || match_key.empty?
|
|
220
|
+
|
|
221
|
+
kind = if owner['declaration_kind']
|
|
222
|
+
owner['declaration_kind']
|
|
223
|
+
elsif owner['owner_kind'].nil? || owner['owner_kind'] == 'declaration'
|
|
224
|
+
'function'
|
|
225
|
+
else
|
|
226
|
+
owner.fetch('owner_kind')
|
|
227
|
+
end
|
|
228
|
+
"[:#{kind}, #{match_key.inspect}]"
|
|
229
|
+
end
|
|
230
|
+
|
|
231
|
+
def merge_result(operation, request, raw)
|
|
232
|
+
diagnostics = raw.fetch('diagnostics', []).map { |diagnostic| normalize_diagnostic(diagnostic) }
|
|
233
|
+
conflicts = raw.fetch('conflicts', [])
|
|
234
|
+
success = operation == :merge2 ? raw.fetch('ok') : raw.fetch('outcome') == 'clean'
|
|
235
|
+
envelope = {
|
|
236
|
+
provider: provider_metadata(request),
|
|
237
|
+
profile: { profile_id: request[:profile_id] || :source_preserving },
|
|
238
|
+
diagnostics: diagnostics,
|
|
239
|
+
conflicts: conflicts,
|
|
240
|
+
verification: {
|
|
241
|
+
rust_host: true,
|
|
242
|
+
source_preserving: true,
|
|
243
|
+
base_participated: operation == :merge3
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
ProviderResult.build(operation: operation, success: success, envelope: envelope, output: raw['output'])
|
|
247
|
+
end
|
|
248
|
+
|
|
249
|
+
def success(operation, request, envelope: {}, **payload)
|
|
250
|
+
ProviderResult.build(operation: operation, success: true, envelope: {
|
|
251
|
+
provider: provider_metadata(request),
|
|
252
|
+
profile: { profile_id: request[:profile_id] || :source_preserving },
|
|
253
|
+
verification: payload.delete(:verification) || {}
|
|
254
|
+
}.merge(envelope), **payload)
|
|
255
|
+
end
|
|
256
|
+
|
|
257
|
+
def failure(operation, request, raw)
|
|
258
|
+
diagnostics = Array(raw['diagnostics']).map { |diagnostic| normalize_diagnostic(diagnostic) }
|
|
259
|
+
diagnostics = [normalize_diagnostic(raw)] if diagnostics.empty?
|
|
260
|
+
ProviderResult.build(
|
|
261
|
+
operation: operation,
|
|
262
|
+
success: false,
|
|
263
|
+
envelope: {
|
|
264
|
+
provider: provider_metadata(request),
|
|
265
|
+
profile: { profile_id: request[:profile_id] || :source_preserving },
|
|
266
|
+
diagnostics: diagnostics,
|
|
267
|
+
verification: { rust_host: true }
|
|
268
|
+
}
|
|
269
|
+
)
|
|
270
|
+
end
|
|
271
|
+
|
|
272
|
+
def error_payload(category, message, source_role)
|
|
273
|
+
{
|
|
274
|
+
'diagnostics' => [{
|
|
275
|
+
'category' => category,
|
|
276
|
+
'message' => "#{source_role} parse error: #{message}",
|
|
277
|
+
'severity' => 'error'
|
|
278
|
+
}]
|
|
279
|
+
}
|
|
280
|
+
end
|
|
281
|
+
|
|
282
|
+
def normalize_diagnostic(diagnostic)
|
|
283
|
+
diagnostic.transform_keys(&:to_sym).merge(
|
|
284
|
+
category: diagnostic.fetch('category', 'parse_error').to_sym,
|
|
285
|
+
severity: diagnostic.fetch('severity', 'error').to_sym,
|
|
286
|
+
blocking: true
|
|
287
|
+
)
|
|
288
|
+
end
|
|
289
|
+
|
|
290
|
+
def provider_metadata(request)
|
|
291
|
+
{
|
|
292
|
+
provider_id: provider_id,
|
|
293
|
+
family: family,
|
|
294
|
+
dialect: request[:dialect] || @dialect,
|
|
295
|
+
backend: :rust_tslp,
|
|
296
|
+
package: @package,
|
|
297
|
+
package_version: @package_version
|
|
298
|
+
}
|
|
299
|
+
end
|
|
300
|
+
end
|
|
301
|
+
end
|
|
302
|
+
end
|
|
303
|
+
|
|
304
|
+
# rubocop:enable Metrics/AbcSize, Metrics/ClassLength, Metrics/CyclomaticComplexity, Metrics/MethodLength, Metrics/ParameterLists, Metrics/PerceivedComplexity
|
|
@@ -61,6 +61,17 @@ It keeps the source-preserving removal behavior passive while also recording:
|
|
|
61
61
|
- which adjacent owners survived
|
|
62
62
|
- which removed comment/layout attachments should be promoted rather than dropped
|
|
63
63
|
|
|
64
|
+
Blank lines at the junction are capped rather than preserved. Removal's
|
|
65
|
+
replacement is always empty, so nothing needs separating from the content that
|
|
66
|
+
follows, and the blank run that preceded the removed range already separates the
|
|
67
|
+
survivors. Re-emitting the removed owner's trailing separator would either
|
|
68
|
+
duplicate it, giving two blank lines at a junction where the source had one, or
|
|
69
|
+
invent a leading blank line when the range started the file. `SplicePlan`
|
|
70
|
+
therefore ignores `preserve_removed_trailing_blank_lines` for an empty
|
|
71
|
+
replacement, and caps any remaining junction run at the longest single
|
|
72
|
+
contributing run so a genuine multi-blank separator that lies outside the removed
|
|
73
|
+
range survives intact.
|
|
74
|
+
|
|
64
75
|
Minimal example:
|
|
65
76
|
|
|
66
77
|
```ruby
|
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Ast
|
|
4
|
+
module Merge
|
|
5
|
+
module StructuralEdit
|
|
6
|
+
# Blank-line invariants for structural splices and removals.
|
|
7
|
+
#
|
|
8
|
+
# This module owns the separator arithmetic a splice must respect so that
|
|
9
|
+
# editing a line range cannot invent blank lines the source never had. It
|
|
10
|
+
# stays parser-agnostic, working purely on source text the same way
|
|
11
|
+
# +BoundarySupport+ works purely on line ranges and attachments.
|
|
12
|
+
#
|
|
13
|
+
# The two rules here are complementary:
|
|
14
|
+
#
|
|
15
|
+
# - {#missing_trailing_blank_line_chunks} decides which trailing blanks a
|
|
16
|
+
# replacement should re-emit so it is not left butting against the
|
|
17
|
+
# following content.
|
|
18
|
+
# - {#cap_junction_blank_runs} then limits the blank run formed where the
|
|
19
|
+
# spliced head meets the untouched tail, so the two sides of a junction
|
|
20
|
+
# never stack into a longer run than either had.
|
|
21
|
+
#
|
|
22
|
+
# Removal is what makes both rules necessary. A replacement supplies its own
|
|
23
|
+
# text, so the head usually ends in content and no junction run forms.
|
|
24
|
+
# Deletion supplies none, which is exactly when separators on both sides of
|
|
25
|
+
# the removed range become adjacent.
|
|
26
|
+
module BlankLineSupport
|
|
27
|
+
module_function
|
|
28
|
+
|
|
29
|
+
# Trailing blank lines of +text+, as an array of line chunks.
|
|
30
|
+
#
|
|
31
|
+
# The chunks keep their trailing newlines so callers can rejoin them
|
|
32
|
+
# losslessly.
|
|
33
|
+
#
|
|
34
|
+
# @param text [String] source fragment
|
|
35
|
+
# @return [Array<String>] trailing blank lines, in source order
|
|
36
|
+
def trailing_blank_line_chunks(text)
|
|
37
|
+
text.lines.reverse.take_while { |line| line.strip.empty? }.reverse
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
# Leading blank lines of +text+.
|
|
41
|
+
#
|
|
42
|
+
# @param text [String] source fragment
|
|
43
|
+
# @return [Array<String>] leading blank lines, in source order
|
|
44
|
+
def leading_blank_line_chunks(text)
|
|
45
|
+
text.lines.take_while { |line| line.strip.empty? }
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
# Trailing blanks from a removed range that a replacement should re-emit,
|
|
49
|
+
# so the replacement is not left butting against the following content.
|
|
50
|
+
#
|
|
51
|
+
# Returns nothing when the replacement already ends in at least as many
|
|
52
|
+
# blanks as the removed range did, or when the tail opens with a blank and
|
|
53
|
+
# so needs no separator.
|
|
54
|
+
#
|
|
55
|
+
# @param removed_content [String] source removed by the splice
|
|
56
|
+
# @param replacement [String] text substituted for the removed range
|
|
57
|
+
# @param after_content [String] untouched source following the range
|
|
58
|
+
# @return [Array<String>] trailing blank chunks to append to the replacement
|
|
59
|
+
def missing_trailing_blank_line_chunks(removed_content:, replacement:, after_content:)
|
|
60
|
+
removed_chunks = trailing_blank_line_chunks(removed_content)
|
|
61
|
+
kept_chunks = trailing_blank_line_chunks(replacement)
|
|
62
|
+
return [] if removed_chunks.empty?
|
|
63
|
+
return [] if kept_chunks.length >= removed_chunks.length
|
|
64
|
+
return [] if after_content.start_with?("\n")
|
|
65
|
+
|
|
66
|
+
removed_chunks[kept_chunks.length..]
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
# Cap the blank-line run formed where a spliced head meets the untouched
|
|
70
|
+
# tail, so a splice never emits a longer run than any single contributor
|
|
71
|
+
# already had.
|
|
72
|
+
#
|
|
73
|
+
# Capping at the longest contributing run rather than at one is what keeps
|
|
74
|
+
# a source that genuinely separates sections with two blanks intact, and
|
|
75
|
+
# leaves in-place replacement unaffected, since its head ends with the
|
|
76
|
+
# replacement text and forms no junction run at all. This mirrors the
|
|
77
|
+
# excess-blank trimming the *-merge emitters already perform, keeping one
|
|
78
|
+
# rule across the stack instead of a second, weaker one for structural
|
|
79
|
+
# edits.
|
|
80
|
+
#
|
|
81
|
+
# This is a line-count invariant, not full blank-line ownership.
|
|
82
|
+
# Layout::Gap#effective_controller can attribute a shared gap to a
|
|
83
|
+
# surviving owner, but only where the analysis populates gaps. Psych-backed
|
|
84
|
+
# analyses report empty layout attachments for nested mapping entries, so
|
|
85
|
+
# ownership has no data to consult there. Wiring gap ownership into removal
|
|
86
|
+
# needs gap population for those owners first.
|
|
87
|
+
#
|
|
88
|
+
# @param head [String] spliced source up to the junction
|
|
89
|
+
# @param tail [String] untouched source after the junction
|
|
90
|
+
# @param preserved_count [Integer] blanks appended to the replacement
|
|
91
|
+
# @param before_content [String] untouched source before the range
|
|
92
|
+
# @param replacement [String] text substituted for the removed range
|
|
93
|
+
# @return [String] head and tail joined with the junction run capped
|
|
94
|
+
def cap_junction_blank_runs(head:, tail:, preserved_count:, before_content:, replacement:)
|
|
95
|
+
trailing_run = trailing_blank_line_chunks(head).length
|
|
96
|
+
return head + tail if tail.empty? || trailing_run.zero?
|
|
97
|
+
|
|
98
|
+
leading_run = leading_blank_line_chunks(tail).length
|
|
99
|
+
allowed = max_contributing_blank_run(before_content, replacement, preserved_count, leading_run)
|
|
100
|
+
excess = (trailing_run + leading_run) - allowed
|
|
101
|
+
return head + tail unless excess.positive?
|
|
102
|
+
|
|
103
|
+
# +allowed+ counts +leading_run+ among the contributors, so
|
|
104
|
+
# +excess+ is always at most +trailing_run+ and never pops past the
|
|
105
|
+
# head's own blanks. +trailing_run+ is nonzero because of the guard above.
|
|
106
|
+
pop_blank_lines(head, count: excess) + tail
|
|
107
|
+
end
|
|
108
|
+
|
|
109
|
+
# Longest blank run any single contributor brings to a junction.
|
|
110
|
+
#
|
|
111
|
+
# The contributors are the blanks before the range, the blanks the
|
|
112
|
+
# replacement itself ends with, the blanks preserved from the removed
|
|
113
|
+
# range, and the blanks the tail opens with.
|
|
114
|
+
#
|
|
115
|
+
# @return [Integer]
|
|
116
|
+
# @api private
|
|
117
|
+
def max_contributing_blank_run(before_content, replacement, preserved_count, leading_run)
|
|
118
|
+
[
|
|
119
|
+
trailing_blank_line_chunks(before_content).length,
|
|
120
|
+
trailing_blank_line_chunks(replacement).length,
|
|
121
|
+
preserved_count,
|
|
122
|
+
leading_run
|
|
123
|
+
].max
|
|
124
|
+
end
|
|
125
|
+
|
|
126
|
+
# Drop the last +count+ blank lines from +head+.
|
|
127
|
+
#
|
|
128
|
+
# @return [String]
|
|
129
|
+
# @api private
|
|
130
|
+
def pop_blank_lines(head, count:)
|
|
131
|
+
head_lines = head.lines
|
|
132
|
+
head_lines.pop(count)
|
|
133
|
+
head_lines.join
|
|
134
|
+
end
|
|
135
|
+
end
|
|
136
|
+
end
|
|
137
|
+
end
|
|
138
|
+
end
|
|
@@ -24,6 +24,10 @@ module Ast
|
|
|
24
24
|
# @param metadata [Hash] base metadata
|
|
25
25
|
# @param preserve_removed_trailing_blank_lines [Boolean] whether trailing blank lines should survive in the splice
|
|
26
26
|
# @param options [Hash] extra metadata merged into +metadata+
|
|
27
|
+
#
|
|
28
|
+
# @note Removal always passes an empty replacement, so trailing blank
|
|
29
|
+
# preservation is not applied; the junction blank run is capped instead.
|
|
30
|
+
# See BlankLineSupport.
|
|
27
31
|
def initialize(source:, remove_start_line:, remove_end_line:, leading_boundary: nil, trailing_boundary: nil,
|
|
28
32
|
removed_attachments: [], removed_owners: nil, retained_owners: nil, metadata: {}, preserve_removed_trailing_blank_lines: true, **options)
|
|
29
33
|
normalized_removed_attachments = Array(removed_attachments).compact.freeze
|
|
@@ -39,6 +39,12 @@ module Ast
|
|
|
39
39
|
# @param preserve_removed_trailing_blank_lines [Boolean] whether to preserve trailing blank lines from removed content
|
|
40
40
|
# @param metadata [Hash] base metadata
|
|
41
41
|
# @param options [Hash] extra metadata merged into +metadata+
|
|
42
|
+
#
|
|
43
|
+
# @note An empty replacement never preserves trailing blanks: a deletion
|
|
44
|
+
# contributes no text to separate from the following content, and the
|
|
45
|
+
# blank run preceding the range already separates the survivors. The
|
|
46
|
+
# junction blank run is also capped at the longest single contributing
|
|
47
|
+
# run. See BlankLineSupport for both rules.
|
|
42
48
|
def initialize(source:, replacement:, replace_start_line:, replace_end_line:, leading_boundary: nil,
|
|
43
49
|
trailing_boundary: nil, preserve_removed_trailing_blank_lines: true, metadata: {}, **options)
|
|
44
50
|
@source = source.to_s
|
|
@@ -92,7 +98,9 @@ module Ast
|
|
|
92
98
|
#
|
|
93
99
|
# @return [String]
|
|
94
100
|
def merged_content
|
|
95
|
-
|
|
101
|
+
preserved = preserved_trailing_blank_line_chunks
|
|
102
|
+
head = +before_content + replacement_with_preserved_boundary_layout(preserved)
|
|
103
|
+
capped_junction_content(head, after_content, preserved.length)
|
|
96
104
|
end
|
|
97
105
|
|
|
98
106
|
def changed?
|
|
@@ -134,33 +142,52 @@ module Ast
|
|
|
134
142
|
|
|
135
143
|
private
|
|
136
144
|
|
|
137
|
-
def replacement_with_preserved_boundary_layout
|
|
145
|
+
def replacement_with_preserved_boundary_layout(preserved_chunks)
|
|
138
146
|
result = +replacement
|
|
139
|
-
|
|
140
|
-
result << missing_trailing_blank_line_chunks.join if preserve_removed_trailing_blank_lines?
|
|
147
|
+
result << preserved_chunks.join unless preserved_chunks.empty?
|
|
141
148
|
|
|
142
149
|
result
|
|
143
150
|
end
|
|
144
151
|
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
return []
|
|
157
|
-
return [] if
|
|
158
|
-
|
|
159
|
-
|
|
152
|
+
# Trailing blank chunks from the removed range that the replacement should
|
|
153
|
+
# re-emit, so it is not left butting against the following content.
|
|
154
|
+
#
|
|
155
|
+
# An empty replacement preserves nothing: a deletion contributes no text of
|
|
156
|
+
# its own to separate, and the blank run that preceded the range already
|
|
157
|
+
# separates the survivors. Re-emitting the removed owner's trailing
|
|
158
|
+
# separator instead either duplicates it -- two blanks at a junction where
|
|
159
|
+
# the source had one -- or, when the range starts the file, invents a
|
|
160
|
+
# leading blank the source never had. Deleting a job from a GitHub Actions
|
|
161
|
+
# workflow produced the first; deleting a leading block produced the second.
|
|
162
|
+
def preserved_trailing_blank_line_chunks
|
|
163
|
+
return [] unless preserve_removed_trailing_blank_lines
|
|
164
|
+
return [] if replacement.empty?
|
|
165
|
+
return [] if after_content.empty?
|
|
166
|
+
|
|
167
|
+
BlankLineSupport.missing_trailing_blank_line_chunks(
|
|
168
|
+
removed_content: removed_content,
|
|
169
|
+
replacement: replacement,
|
|
170
|
+
after_content: after_content
|
|
171
|
+
)
|
|
160
172
|
end
|
|
161
173
|
|
|
162
|
-
|
|
163
|
-
|
|
174
|
+
# Cap the blank run where the spliced head meets the untouched tail.
|
|
175
|
+
#
|
|
176
|
+
# Removal is what exposes this: a blank on each side of the removed owner
|
|
177
|
+
# was separated by that owner's own text, and deleting it makes the two
|
|
178
|
+
# runs adjacent. Observed in both shapes -- removing a Ruby `appraise`
|
|
179
|
+
# block, whose owner range ends at the statement so the separator on either
|
|
180
|
+
# side survives, and removing a GitHub Actions job, whose range includes
|
|
181
|
+
# the job's trailing separator so it stacks on the blank that preceded it.
|
|
182
|
+
# In each case the source had a single blank and the splice emitted two.
|
|
183
|
+
def capped_junction_content(head, tail, preserved_count)
|
|
184
|
+
BlankLineSupport.cap_junction_blank_runs(
|
|
185
|
+
head: head,
|
|
186
|
+
tail: tail,
|
|
187
|
+
preserved_count: preserved_count,
|
|
188
|
+
before_content: before_content,
|
|
189
|
+
replacement: replacement
|
|
190
|
+
)
|
|
164
191
|
end
|
|
165
192
|
|
|
166
193
|
def validate_range!
|
|
@@ -8,6 +8,7 @@ module Ast
|
|
|
8
8
|
# boundaries and splice plans without taking ownership of parser-specific
|
|
9
9
|
# traversal or post-processing behavior.
|
|
10
10
|
module StructuralEdit
|
|
11
|
+
autoload :BlankLineSupport, 'ast/merge/structural_edit/blank_line_support'
|
|
11
12
|
autoload :BoundarySupport, 'ast/merge/structural_edit/boundary_support'
|
|
12
13
|
autoload :Boundary, 'ast/merge/structural_edit/boundary'
|
|
13
14
|
autoload :PlanSet, 'ast/merge/structural_edit/plan_set'
|
|
@@ -0,0 +1,214 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'digest'
|
|
4
|
+
require 'json'
|
|
5
|
+
|
|
6
|
+
module Ast
|
|
7
|
+
module Merge
|
|
8
|
+
# Shared typed transport adaptation. Subclasses select explicit Rust profiles.
|
|
9
|
+
class TypedCoreProvider
|
|
10
|
+
DIALECTS = %i[json jsonc json5].freeze
|
|
11
|
+
ROLES = { analyze: %i[source], diff2: %i[before after], merge2: %i[incoming current],
|
|
12
|
+
merge3: %i[base ours theirs] }.freeze
|
|
13
|
+
VERIFICATION_FIELDS = %i[consumed_source_roles directional_roles_preserved base_participated
|
|
14
|
+
classification_reached output_reparsed structural_equivalence preservation retained_source_regions].freeze
|
|
15
|
+
# Only generated read-only data records are projected. Registry objects,
|
|
16
|
+
# operation controls and executable handles must never be traversed.
|
|
17
|
+
DATA_RECORDS = %w[OperationResult ResultProvider ResultProfile ResultParserSelection
|
|
18
|
+
ResultVerification PreservationProperty ResultSourceRegion ResultRange ResultSpan ResultPoint
|
|
19
|
+
ResultAnalysis ResultDiff ResultChange ResultConflict ResultDiagnostic
|
|
20
|
+
DiagnosticRecord PortableDiagnostic DiagnosticOrigin DiagnosticSourceRef DiagnosticSubjectRef
|
|
21
|
+
ConflictRecord PortableConflict ConflictSubject ConflictSourceAlternative ConflictClassification
|
|
22
|
+
ConflictLocalization ConflictResolution ExactConflictRegion NativeExtension].freeze
|
|
23
|
+
|
|
24
|
+
def self.available?
|
|
25
|
+
require 'structuredmerge_core' unless defined?(::StructuredmergeCore)
|
|
26
|
+
::StructuredmergeCore.respond_to?(:execute_operation) && TreeHaver::Backends::RustTslp.available?
|
|
27
|
+
rescue LoadError
|
|
28
|
+
false
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
def provider_id = self.class::PROVIDER_ID
|
|
32
|
+
def family = self.class::FAMILY
|
|
33
|
+
|
|
34
|
+
def capabilities
|
|
35
|
+
{ operations: self.class::OPERATIONS, dialects: self.class::DIALECTS, backends: [:rust_tslp],
|
|
36
|
+
profiles: [:source_preserving], role: :workflow,
|
|
37
|
+
source_preservation: %i[exact_source byte_spans reparse] }.freeze
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
ROLES.each_key do |operation|
|
|
41
|
+
define_method(operation) { |request| execute(operation, request) }
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
private
|
|
45
|
+
|
|
46
|
+
def core = ::StructuredmergeCore
|
|
47
|
+
|
|
48
|
+
def execute(operation, request)
|
|
49
|
+
unless capabilities[:operations].include?(operation)
|
|
50
|
+
return Ast::Merge::ProviderResult.unsupported(operation: operation,
|
|
51
|
+
message: 'Typed profile does not implement this operation')
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
fields = ROLES.fetch(operation).map { |role| role == :source ? :source : :"#{role}_source" }
|
|
55
|
+
allowed = fields + %i[dialect provider_id family backend profile_id request_id] + additional_fields
|
|
56
|
+
unless (request.keys - allowed).empty? &&
|
|
57
|
+
self.class::DIALECTS.include?(request.fetch(:dialect, default_dialect).to_s.to_sym) &&
|
|
58
|
+
[nil, 'rust_tslp'].include?(request[:backend]&.to_s) &&
|
|
59
|
+
[nil, 'source_preserving'].include?(request[:profile_id]&.to_s) &&
|
|
60
|
+
[nil, provider_id].include?(request[:provider_id]) && [nil, family].include?(request[:family]&.to_s)
|
|
61
|
+
return Ast::Merge::ProviderResult.unsupported(operation: operation,
|
|
62
|
+
message: 'Typed provider cannot honor the requested fields or selectors')
|
|
63
|
+
end
|
|
64
|
+
unless self.class.available?
|
|
65
|
+
return Ast::Merge::ProviderResult.unsupported(operation: operation,
|
|
66
|
+
message: 'structuredmerge-core is unavailable')
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
dialect = request.fetch(:dialect, default_dialect).to_s
|
|
70
|
+
parser_id = TreeHaver::Backends::RustTslp.register_language_parser(parser_language(dialect))
|
|
71
|
+
sources = ROLES.fetch(operation).zip(fields).to_h do |role, field|
|
|
72
|
+
text = request.fetch(field)
|
|
73
|
+
unless text.is_a?(String) && [Encoding::UTF_8, Encoding::US_ASCII, Encoding::ASCII_8BIT].include?(text.encoding)
|
|
74
|
+
raise ArgumentError, 'Source must contain UTF-8 bytes'
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
text = text.dup.force_encoding(Encoding::UTF_8)
|
|
78
|
+
raise ArgumentError, 'Source must be valid UTF-8' unless text.valid_encoding?
|
|
79
|
+
|
|
80
|
+
text.freeze
|
|
81
|
+
[role, core::OperationSource.new(source_id: role.to_s, role: role, content: text,
|
|
82
|
+
encoding: 'utf-8', byte_length: text.bytesize, sha256: Digest::SHA256.hexdigest(text), extra: {})]
|
|
83
|
+
end
|
|
84
|
+
typed_request = core::OperationRequest.new(schema: 'structuredmerge.operation-request/v1',
|
|
85
|
+
request_id: request[:request_id] || "ruby-#{provider_id}-#{operation}", operation: policy(operation, request), sources: sources,
|
|
86
|
+
provider_selection: core::MergeProviderSelection.new(provider_id: self.class::CORE_PROVIDER, family: family,
|
|
87
|
+
dialect: dialect, profile_id: self.class::CORE_PROFILE, required_capabilities: [operation.to_s], extra: {}),
|
|
88
|
+
parser_selection: core::OperationParserSelection.new(backend: parser_id, preference: [], required_capabilities: [], extra: {}),
|
|
89
|
+
extensions: [], metadata: {}, extra: {})
|
|
90
|
+
result = core.execute_operation(typed_request,
|
|
91
|
+
core::ParseLimits.new(max_batch_items: 3, max_input_bytes: 64 * 1024 * 1024, max_nodes: 1_000_000,
|
|
92
|
+
max_diagnostics: 1000))
|
|
93
|
+
project(operation, request, result)
|
|
94
|
+
rescue KeyError, ArgumentError, TypeError, RangeError => e
|
|
95
|
+
Ast::Merge::ProviderResult.invalid_request(operation: operation, message: e.message)
|
|
96
|
+
rescue RuntimeError => e
|
|
97
|
+
Ast::Merge::ProviderResult.build(operation: operation, success: false,
|
|
98
|
+
envelope: { diagnostics: [{ category: :provider_failure, severity: :error, blocking: true, message: e.message }] })
|
|
99
|
+
end
|
|
100
|
+
|
|
101
|
+
def additional_fields = []
|
|
102
|
+
def default_dialect = self.class::DIALECTS.first
|
|
103
|
+
def parser_language(dialect) = dialect == 'json' ? 'json' : 'json5'
|
|
104
|
+
def owner_path(owner) = owner.fetch('path')
|
|
105
|
+
|
|
106
|
+
def policy(operation, _request)
|
|
107
|
+
case operation
|
|
108
|
+
when :analyze
|
|
109
|
+
core::OperationPolicy.from_analyze(core::AnalyzePolicy.new(extra: {}))
|
|
110
|
+
when :diff2
|
|
111
|
+
core::OperationPolicy.from_diff2(core::DiffPolicy.new(comparison_profile: 'exact-source-owners',
|
|
112
|
+
equivalence: ['exact-source'], extra: {}))
|
|
113
|
+
when :merge2
|
|
114
|
+
core::OperationPolicy.from_merge2(core::DirectionalMergePolicy.new(
|
|
115
|
+
directional_merge: 'template-into-current', render_policy: 'source-preserving', extra: {}
|
|
116
|
+
))
|
|
117
|
+
when :merge3
|
|
118
|
+
core::OperationPolicy.from_merge3(core::ThreeWayMergePolicy.new(render_policy: 'source-preserving',
|
|
119
|
+
fallback_policy: 'none', extra: {}))
|
|
120
|
+
end
|
|
121
|
+
end
|
|
122
|
+
|
|
123
|
+
# Only open metadata values are encoded by the generated binding; never
|
|
124
|
+
# send or decode a whole-operation JSON string.
|
|
125
|
+
def metadata(values)
|
|
126
|
+
values.to_h { |key, value| [key, JSON.parse(value)] }
|
|
127
|
+
end
|
|
128
|
+
|
|
129
|
+
def project(operation, request, result)
|
|
130
|
+
changes = result.changes.map do |change|
|
|
131
|
+
states = metadata(change.role_states)
|
|
132
|
+
{ id: change.id, path: change.path, subject_ref: change.subject_ref, change: change.classification.to_sym,
|
|
133
|
+
before: revision(states['before'], :before, change.source_spans), after: revision(states['after'], :after, change.source_spans),
|
|
134
|
+
source_spans: change.source_spans, metadata: metadata(change.metadata) }
|
|
135
|
+
end
|
|
136
|
+
diagnostics = result.diagnostics.map do |record|
|
|
137
|
+
diagnostic = record.canonical || record.migration
|
|
138
|
+
{ id: diagnostic.id, code: diagnostic.code, category: diagnostic.category.to_sym,
|
|
139
|
+
severity: diagnostic.severity.to_sym, message: diagnostic.message, blocking: diagnostic.blocking, typed_record: record }
|
|
140
|
+
end
|
|
141
|
+
conflicts = result.conflicts.map do |record|
|
|
142
|
+
canonical = record.canonical
|
|
143
|
+
native = canonical && metadata(canonical.classification.extra)['native_conflict']
|
|
144
|
+
native || { typed_record: record }
|
|
145
|
+
end
|
|
146
|
+
verification = VERIFICATION_FIELDS.to_h { |field| [field, result.verification.public_send(field)] }
|
|
147
|
+
verification[:rust_core] = true
|
|
148
|
+
payload = { output: result.output, conflicted_output: result.conflicted_output, typed_result: result }
|
|
149
|
+
if result.analysis
|
|
150
|
+
facts = metadata(result.analysis.extra)
|
|
151
|
+
payload[:analysis] = { backend: :rust_tslp, valid: true, facts: facts,
|
|
152
|
+
declarations: facts.fetch('owners').map do |owner|
|
|
153
|
+
{ path: owner_path(owner), signature: owner_path(owner), source_role: :source,
|
|
154
|
+
line_range: line_range(owner.fetch('span')), byte_range: owner.fetch('span').fetch('range') }
|
|
155
|
+
end }
|
|
156
|
+
end
|
|
157
|
+
payload[:diff] = { changes: changes } if result.diff
|
|
158
|
+
projected = Ast::Merge::ProviderResult.build(operation: operation, success: result.ok,
|
|
159
|
+
envelope: { provider: { provider_id: provider_id, family: family, dialect: request.fetch(:dialect, default_dialect),
|
|
160
|
+
backend: :rust_tslp, package: self.class::PACKAGE, package_version: package_version },
|
|
161
|
+
profile: { profile_id: :source_preserving, core_profile_id: result.profile.profile_id },
|
|
162
|
+
diagnostics: diagnostics, changes: changes, conflicts: conflicts,
|
|
163
|
+
render_report: metadata(result.render_report), verification: verification }, **payload)
|
|
164
|
+
portable(projected)
|
|
165
|
+
end
|
|
166
|
+
|
|
167
|
+
def portable(value)
|
|
168
|
+
case value
|
|
169
|
+
when Hash then value.sort_by { |key, _item| key.to_s }.to_h { |key, item| [key, portable(item)] }
|
|
170
|
+
when Array then value.map { |item| portable(item) }
|
|
171
|
+
when String, Symbol, Numeric, TrueClass, FalseClass, NilClass then value
|
|
172
|
+
else
|
|
173
|
+
name = value.class.name
|
|
174
|
+
unless name&.start_with?('StructuredmergeCore::') && DATA_RECORDS.include?(name.delete_prefix('StructuredmergeCore::'))
|
|
175
|
+
raise TypeError, 'Unsupported typed core data record'
|
|
176
|
+
end
|
|
177
|
+
|
|
178
|
+
value.class.public_instance_methods(false).sort.to_h do |field|
|
|
179
|
+
raise TypeError, 'Unexpected executable method on core data record' unless value.method(field).arity.zero?
|
|
180
|
+
|
|
181
|
+
item = value.public_send(field)
|
|
182
|
+
item = metadata(item) if %i[extra metadata data role_states render_report].include?(field)
|
|
183
|
+
item = item.map { |entry| metadata(entry) } if field == :fallbacks
|
|
184
|
+
item = JSON.parse(item) if field == :payload
|
|
185
|
+
[field, portable(item)]
|
|
186
|
+
end
|
|
187
|
+
end
|
|
188
|
+
end
|
|
189
|
+
|
|
190
|
+
def revision(state, role, source_spans)
|
|
191
|
+
return { present: false, source_role: role, line_range: [nil, nil] } unless state
|
|
192
|
+
|
|
193
|
+
span = state.fetch('owner', state)['span']
|
|
194
|
+
unless span
|
|
195
|
+
# Native-owner diffs carry regions rather than JSON owner records.
|
|
196
|
+
# Line points come from the kernel's typed span, never host text scans.
|
|
197
|
+
typed = source_spans.fetch(role)
|
|
198
|
+
span = { 'range' => { 'start_byte' => typed.range.start_byte, 'end_byte' => typed.range.end_byte },
|
|
199
|
+
'start_point' => { 'row' => typed.start_point.row, 'column' => typed.start_point.column },
|
|
200
|
+
'end_point' => { 'row' => typed.end_point.row, 'column' => typed.end_point.column } }
|
|
201
|
+
end
|
|
202
|
+
{ present: true, source_role: role, line_range: line_range(span), byte_range: span.fetch('range') }
|
|
203
|
+
end
|
|
204
|
+
|
|
205
|
+
def line_range(span)
|
|
206
|
+
start = span.fetch('start_point')
|
|
207
|
+
finish = span.fetch('end_point')
|
|
208
|
+
end_row = finish.fetch('row')
|
|
209
|
+
end_row -= 1 if finish.fetch('column').zero? && end_row > start.fetch('row')
|
|
210
|
+
[start.fetch('row') + 1, end_row + 1]
|
|
211
|
+
end
|
|
212
|
+
end
|
|
213
|
+
end
|
|
214
|
+
end
|
data/lib/ast/merge/version.rb
CHANGED
|
@@ -5,7 +5,7 @@ module Ast
|
|
|
5
5
|
# Version namespace for this gem.
|
|
6
6
|
module Version
|
|
7
7
|
# Current gem version.
|
|
8
|
-
VERSION = '7.1.
|
|
8
|
+
VERSION = '7.1.10'
|
|
9
9
|
end
|
|
10
10
|
# Current gem version exposed at the traditional constant location.
|
|
11
11
|
VERSION = Version::VERSION # Traditional Constant Location
|
data/lib/ast/merge.rb
CHANGED
|
@@ -86,6 +86,7 @@ module Ast
|
|
|
86
86
|
autoload :PortableBenchmarkContract, 'ast/merge/portable_benchmark_contract'
|
|
87
87
|
autoload :ProviderContract, 'ast/merge/provider_contract'
|
|
88
88
|
autoload :ProviderRegistry, 'ast/merge/provider_registry'
|
|
89
|
+
autoload :RustHostProvider, 'ast/merge/rust_host_provider'
|
|
89
90
|
autoload :ProviderResult, 'ast/merge/provider_result'
|
|
90
91
|
autoload :SectionTyping, 'ast/merge/section_typing'
|
|
91
92
|
autoload :SmartMergerBase, 'ast/merge/smart_merger_base'
|
data.tar.gz.sig
CHANGED
|
Binary file
|
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: ast-merge
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 7.1.
|
|
4
|
+
version: 7.1.10
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Peter H. Boling
|
|
@@ -77,14 +77,14 @@ dependencies:
|
|
|
77
77
|
requirements:
|
|
78
78
|
- - '='
|
|
79
79
|
- !ruby/object:Gem::Version
|
|
80
|
-
version: 7.1.
|
|
80
|
+
version: 7.1.10
|
|
81
81
|
type: :runtime
|
|
82
82
|
prerelease: false
|
|
83
83
|
version_requirements: !ruby/object:Gem::Requirement
|
|
84
84
|
requirements:
|
|
85
85
|
- - '='
|
|
86
86
|
- !ruby/object:Gem::Version
|
|
87
|
-
version: 7.1.
|
|
87
|
+
version: 7.1.10
|
|
88
88
|
- !ruby/object:Gem::Dependency
|
|
89
89
|
name: version_gem
|
|
90
90
|
requirement: !ruby/object:Gem::Requirement
|
|
@@ -111,20 +111,20 @@ dependencies:
|
|
|
111
111
|
requirements:
|
|
112
112
|
- - "~>"
|
|
113
113
|
- !ruby/object:Gem::Version
|
|
114
|
-
version: '3.
|
|
114
|
+
version: '3.1'
|
|
115
115
|
- - ">="
|
|
116
116
|
- !ruby/object:Gem::Version
|
|
117
|
-
version: 3.
|
|
117
|
+
version: 3.1.8
|
|
118
118
|
type: :development
|
|
119
119
|
prerelease: false
|
|
120
120
|
version_requirements: !ruby/object:Gem::Requirement
|
|
121
121
|
requirements:
|
|
122
122
|
- - "~>"
|
|
123
123
|
- !ruby/object:Gem::Version
|
|
124
|
-
version: '3.
|
|
124
|
+
version: '3.1'
|
|
125
125
|
- - ">="
|
|
126
126
|
- !ruby/object:Gem::Version
|
|
127
|
-
version: 3.
|
|
127
|
+
version: 3.1.8
|
|
128
128
|
- !ruby/object:Gem::Dependency
|
|
129
129
|
name: bundler-audit
|
|
130
130
|
requirement: !ruby/object:Gem::Requirement
|
|
@@ -202,7 +202,7 @@ dependencies:
|
|
|
202
202
|
version: '3.2'
|
|
203
203
|
- - ">="
|
|
204
204
|
- !ruby/object:Gem::Version
|
|
205
|
-
version: 3.2.
|
|
205
|
+
version: 3.2.5
|
|
206
206
|
type: :development
|
|
207
207
|
prerelease: false
|
|
208
208
|
version_requirements: !ruby/object:Gem::Requirement
|
|
@@ -212,7 +212,7 @@ dependencies:
|
|
|
212
212
|
version: '3.2'
|
|
213
213
|
- - ">="
|
|
214
214
|
- !ruby/object:Gem::Version
|
|
215
|
-
version: 3.2.
|
|
215
|
+
version: 3.2.5
|
|
216
216
|
- !ruby/object:Gem::Dependency
|
|
217
217
|
name: kettle-test
|
|
218
218
|
requirement: !ruby/object:Gem::Requirement
|
|
@@ -222,7 +222,7 @@ dependencies:
|
|
|
222
222
|
version: '2.0'
|
|
223
223
|
- - ">="
|
|
224
224
|
- !ruby/object:Gem::Version
|
|
225
|
-
version: 2.0.
|
|
225
|
+
version: 2.0.23
|
|
226
226
|
type: :development
|
|
227
227
|
prerelease: false
|
|
228
228
|
version_requirements: !ruby/object:Gem::Requirement
|
|
@@ -232,7 +232,7 @@ dependencies:
|
|
|
232
232
|
version: '2.0'
|
|
233
233
|
- - ">="
|
|
234
234
|
- !ruby/object:Gem::Version
|
|
235
|
-
version: 2.0.
|
|
235
|
+
version: 2.0.23
|
|
236
236
|
- !ruby/object:Gem::Dependency
|
|
237
237
|
name: turbo_tests2
|
|
238
238
|
requirement: !ruby/object:Gem::Requirement
|
|
@@ -242,7 +242,7 @@ dependencies:
|
|
|
242
242
|
version: '3.2'
|
|
243
243
|
- - ">="
|
|
244
244
|
- !ruby/object:Gem::Version
|
|
245
|
-
version: 3.2.
|
|
245
|
+
version: 3.2.14
|
|
246
246
|
type: :development
|
|
247
247
|
prerelease: false
|
|
248
248
|
version_requirements: !ruby/object:Gem::Requirement
|
|
@@ -252,7 +252,7 @@ dependencies:
|
|
|
252
252
|
version: '3.2'
|
|
253
253
|
- - ">="
|
|
254
254
|
- !ruby/object:Gem::Version
|
|
255
|
-
version: 3.2.
|
|
255
|
+
version: 3.2.14
|
|
256
256
|
- !ruby/object:Gem::Dependency
|
|
257
257
|
name: ruby-progressbar
|
|
258
258
|
requirement: !ruby/object:Gem::Requirement
|
|
@@ -276,7 +276,7 @@ dependencies:
|
|
|
276
276
|
version: '1.0'
|
|
277
277
|
- - ">="
|
|
278
278
|
- !ruby/object:Gem::Version
|
|
279
|
-
version: 1.0.
|
|
279
|
+
version: 1.0.9
|
|
280
280
|
type: :development
|
|
281
281
|
prerelease: false
|
|
282
282
|
version_requirements: !ruby/object:Gem::Requirement
|
|
@@ -286,7 +286,7 @@ dependencies:
|
|
|
286
286
|
version: '1.0'
|
|
287
287
|
- - ">="
|
|
288
288
|
- !ruby/object:Gem::Version
|
|
289
|
-
version: 1.0.
|
|
289
|
+
version: 1.0.9
|
|
290
290
|
- !ruby/object:Gem::Dependency
|
|
291
291
|
name: gitmoji-regex
|
|
292
292
|
requirement: !ruby/object:Gem::Requirement
|
|
@@ -296,7 +296,7 @@ dependencies:
|
|
|
296
296
|
version: '2.0'
|
|
297
297
|
- - ">="
|
|
298
298
|
- !ruby/object:Gem::Version
|
|
299
|
-
version: 2.0.
|
|
299
|
+
version: 2.0.15
|
|
300
300
|
type: :development
|
|
301
301
|
prerelease: false
|
|
302
302
|
version_requirements: !ruby/object:Gem::Requirement
|
|
@@ -306,7 +306,7 @@ dependencies:
|
|
|
306
306
|
version: '2.0'
|
|
307
307
|
- - ">="
|
|
308
308
|
- !ruby/object:Gem::Version
|
|
309
|
-
version: 2.0.
|
|
309
|
+
version: 2.0.15
|
|
310
310
|
description: "☯️ Core diagnostics, conformance, planning, reporting, and review-state
|
|
311
311
|
contracts for Structured Merge."
|
|
312
312
|
email:
|
|
@@ -452,6 +452,7 @@ files:
|
|
|
452
452
|
- lib/ast/merge/runtime/root_session_support.rb
|
|
453
453
|
- lib/ast/merge/runtime/session.rb
|
|
454
454
|
- lib/ast/merge/runtime/surface.rb
|
|
455
|
+
- lib/ast/merge/rust_host_provider.rb
|
|
455
456
|
- lib/ast/merge/section_typing.rb
|
|
456
457
|
- lib/ast/merge/smart_merger_base.rb
|
|
457
458
|
- lib/ast/merge/source_region_report_support.rb
|
|
@@ -462,6 +463,7 @@ files:
|
|
|
462
463
|
- lib/ast/merge/source_render/result.rb
|
|
463
464
|
- lib/ast/merge/structural_edit.rb
|
|
464
465
|
- lib/ast/merge/structural_edit/README.md
|
|
466
|
+
- lib/ast/merge/structural_edit/blank_line_support.rb
|
|
465
467
|
- lib/ast/merge/structural_edit/boundary.rb
|
|
466
468
|
- lib/ast/merge/structural_edit/boundary_support.rb
|
|
467
469
|
- lib/ast/merge/structural_edit/plan_set.rb
|
|
@@ -486,6 +488,7 @@ files:
|
|
|
486
488
|
- lib/ast/merge/trailing_groups/alignment_sort.rb
|
|
487
489
|
- lib/ast/merge/trailing_groups/core.rb
|
|
488
490
|
- lib/ast/merge/trailing_groups/dest_iterate.rb
|
|
491
|
+
- lib/ast/merge/typed_core_provider.rb
|
|
489
492
|
- lib/ast/merge/unresolved_policy.rb
|
|
490
493
|
- lib/ast/merge/unresolved_review_state.rb
|
|
491
494
|
- lib/ast/merge/unresolved_support.rb
|
|
@@ -497,10 +500,10 @@ licenses:
|
|
|
497
500
|
- PolyForm-Small-Business-1.0.0
|
|
498
501
|
metadata:
|
|
499
502
|
homepage_uri: https://structuredmerge.org
|
|
500
|
-
source_code_uri: https://github.com/structuredmerge/structuredmerge-ruby/tree/v7.1.
|
|
501
|
-
changelog_uri: https://github.com/structuredmerge/structuredmerge-ruby/blob/v7.1.
|
|
503
|
+
source_code_uri: https://github.com/structuredmerge/structuredmerge-ruby/tree/v7.1.10
|
|
504
|
+
changelog_uri: https://github.com/structuredmerge/structuredmerge-ruby/blob/v7.1.10/CHANGELOG.md
|
|
502
505
|
bug_tracker_uri: https://github.com/structuredmerge/structuredmerge-ruby/issues
|
|
503
|
-
documentation_uri: https://www.rubydoc.info/gems/ast-merge/7.1.
|
|
506
|
+
documentation_uri: https://www.rubydoc.info/gems/ast-merge/7.1.10
|
|
504
507
|
funding_uri: https://github.com/sponsors/pboling
|
|
505
508
|
wiki_uri: https://github.com/structuredmerge/structuredmerge-ruby/wiki
|
|
506
509
|
news_uri: https://www.railsbling.com/tags/ast-merge
|
|
@@ -530,7 +533,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
|
530
533
|
- !ruby/object:Gem::Version
|
|
531
534
|
version: '0'
|
|
532
535
|
requirements: []
|
|
533
|
-
rubygems_version: 4.0.
|
|
536
|
+
rubygems_version: 4.0.20
|
|
534
537
|
specification_version: 4
|
|
535
538
|
summary: "☯️ Structured Merge AST/core contracts for the Ruby monorepo"
|
|
536
539
|
test_files: []
|
metadata.gz.sig
CHANGED
|
Binary file
|