json-merge 7.0.0 → 7.1.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- checksums.yaml.gz.sig +0 -0
- data/LICENSE.md +13 -0
- data/README.md +600 -0
- data/lib/json/merge/comment_tracker.rb +27 -0
- data/lib/json/merge/conflict_resolver.rb +1379 -0
- data/lib/json/merge/debug_logger.rb +41 -0
- data/lib/json/merge/emitter.rb +227 -0
- data/lib/json/merge/file_analysis.rb +355 -0
- data/lib/json/merge/freeze_node.rb +62 -0
- data/lib/json/merge/merge_result.rb +156 -0
- data/lib/json/merge/node_wrapper.rb +258 -0
- data/lib/json/merge/object_match_refiner.rb +338 -0
- data/lib/json/merge/provider.rb +696 -0
- data/lib/json/merge/smart_merger.rb +276 -0
- data/lib/json/merge/source_locator.rb +68 -0
- data/lib/json/merge/three_way_decision.rb +168 -0
- data/lib/json/merge/version.rb +5 -3
- data/lib/json/merge.rb +393 -266
- data/lib/json-merge.rb +7 -1
- data/sig/json/merge.rbs +54 -0
- data.tar.gz.sig +0 -0
- metadata +264 -15
- metadata.gz.sig +0 -0
data/lib/json/merge.rb
CHANGED
|
@@ -1,131 +1,351 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
-
require
|
|
4
|
-
require
|
|
3
|
+
require 'json'
|
|
4
|
+
require 'tree_haver'
|
|
5
|
+
require 'ast/merge'
|
|
6
|
+
require_relative 'merge/version'
|
|
5
7
|
|
|
6
8
|
module Json
|
|
7
9
|
module Merge
|
|
8
|
-
PACKAGE_NAME =
|
|
10
|
+
PACKAGE_NAME = 'json-merge'
|
|
9
11
|
DESTINATION_WINS_ARRAY_POLICY = {
|
|
10
|
-
surface:
|
|
11
|
-
name:
|
|
12
|
-
}.freeze
|
|
13
|
-
TRAILING_COMMA_FALLBACK_POLICY = {
|
|
14
|
-
surface: "fallback",
|
|
15
|
-
name: "trailing_comma_destination_fallback"
|
|
12
|
+
surface: 'array',
|
|
13
|
+
name: 'destination_wins_array'
|
|
16
14
|
}.freeze
|
|
15
|
+
TREE_SITTER_BACKEND = TreeHaver::KREUZBERG_LANGUAGE_PACK_BACKEND
|
|
16
|
+
BACKEND_REGISTRY = Struct.new(:registered, :mutex).new(false, Mutex.new)
|
|
17
|
+
|
|
18
|
+
class Error < Ast::Merge::Error; end
|
|
19
|
+
|
|
20
|
+
class ParseError < Ast::Merge::ParseError
|
|
21
|
+
def initialize(message = nil, content: nil, errors: [])
|
|
22
|
+
super(message, errors: errors, content: content)
|
|
23
|
+
end
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
class TemplateParseError < ParseError; end
|
|
27
|
+
class DestinationParseError < ParseError; end
|
|
28
|
+
class JsoncDialectError < ParseError; end
|
|
29
|
+
class CorruptionDetectedError < Error; end
|
|
30
|
+
|
|
31
|
+
autoload :CommentTracker, 'json/merge/comment_tracker'
|
|
32
|
+
autoload :DebugLogger, 'json/merge/debug_logger'
|
|
33
|
+
autoload :Emitter, 'json/merge/emitter'
|
|
34
|
+
autoload :FileAnalysis, 'json/merge/file_analysis'
|
|
35
|
+
autoload :FreezeNode, 'json/merge/freeze_node'
|
|
36
|
+
autoload :MergeResult, 'json/merge/merge_result'
|
|
37
|
+
autoload :NodeWrapper, 'json/merge/node_wrapper'
|
|
38
|
+
autoload :ConflictResolver, 'json/merge/conflict_resolver'
|
|
39
|
+
autoload :SmartMerger, 'json/merge/smart_merger'
|
|
40
|
+
autoload :ObjectMatchRefiner, 'json/merge/object_match_refiner'
|
|
41
|
+
autoload :Provider, 'json/merge/provider'
|
|
42
|
+
autoload :SourceLocator, 'json/merge/source_locator'
|
|
43
|
+
autoload :ThreeWayDecision, 'json/merge/three_way_decision'
|
|
44
|
+
|
|
45
|
+
class << self
|
|
46
|
+
def register_backend!
|
|
47
|
+
BACKEND_REGISTRY.mutex.synchronize do
|
|
48
|
+
return if BACKEND_REGISTRY.registered
|
|
49
|
+
|
|
50
|
+
TreeHaver::BackendRegistry.register(TREE_SITTER_BACKEND)
|
|
51
|
+
|
|
52
|
+
grammar_finder = TreeHaver::GrammarFinder.new(:json)
|
|
53
|
+
grammar_finder.register! if grammar_finder.available?
|
|
54
|
+
|
|
55
|
+
json5_grammar_finder = TreeHaver::GrammarFinder.new(:json5)
|
|
56
|
+
json5_grammar_finder.register! if json5_grammar_finder.available?
|
|
57
|
+
|
|
58
|
+
BACKEND_REGISTRY.registered = true
|
|
59
|
+
end
|
|
60
|
+
end
|
|
61
|
+
end
|
|
17
62
|
|
|
18
63
|
module_function
|
|
19
64
|
|
|
20
65
|
def json_feature_profile
|
|
21
66
|
{
|
|
22
|
-
family:
|
|
23
|
-
supported_dialects: %w[json jsonc],
|
|
24
|
-
supported_policies: [DESTINATION_WINS_ARRAY_POLICY
|
|
67
|
+
family: 'json',
|
|
68
|
+
supported_dialects: %w[json jsonc json5],
|
|
69
|
+
supported_policies: [DESTINATION_WINS_ARRAY_POLICY]
|
|
25
70
|
}
|
|
26
71
|
end
|
|
27
72
|
|
|
28
|
-
def
|
|
29
|
-
|
|
73
|
+
def merge_provider
|
|
74
|
+
@merge_provider ||= Provider.new
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
def register_provider!(replace: false)
|
|
78
|
+
Ast::Merge.register_provider(merge_provider, replace: replace)
|
|
30
79
|
end
|
|
31
80
|
|
|
32
|
-
def
|
|
33
|
-
|
|
81
|
+
def available_json_backends
|
|
82
|
+
json_backend_available_for_analysis?(TREE_SITTER_BACKEND.id) ? [TREE_SITTER_BACKEND] : []
|
|
83
|
+
end
|
|
34
84
|
|
|
35
|
-
|
|
36
|
-
|
|
85
|
+
def json_backend_feature_profile(backend: nil)
|
|
86
|
+
requested = requested_json_backend_id(backend)
|
|
87
|
+
unless available_json_backends.any? { |backend_ref| backend_ref.id == requested }
|
|
88
|
+
return unsupported_feature_result("Unsupported JSON backend #{requested}.")
|
|
89
|
+
end
|
|
37
90
|
|
|
38
|
-
|
|
91
|
+
json_feature_profile.merge(
|
|
92
|
+
backend: requested,
|
|
93
|
+
backend_ref: TREE_SITTER_BACKEND.to_h
|
|
94
|
+
)
|
|
39
95
|
end
|
|
40
96
|
|
|
41
|
-
def
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
return parse_failure("Trailing commas are not supported for #{dialect}.") if detect_trailing_comma(normalized_source)
|
|
97
|
+
def json_plan_context(backend: nil)
|
|
98
|
+
profile = json_backend_feature_profile(backend: backend)
|
|
99
|
+
return profile if profile[:ok] == false
|
|
45
100
|
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
root_kind: json_root_kind(parsed),
|
|
54
|
-
owners: collect_json_owners(parsed)
|
|
101
|
+
{
|
|
102
|
+
family_profile: json_feature_profile,
|
|
103
|
+
feature_profile: {
|
|
104
|
+
backend: profile[:backend],
|
|
105
|
+
supports_dialects: true,
|
|
106
|
+
supported_policies: profile[:supported_policies]
|
|
107
|
+
}
|
|
55
108
|
}
|
|
109
|
+
end
|
|
110
|
+
|
|
111
|
+
def parse_json(source, dialect, backend: nil)
|
|
112
|
+
dialect = normalize_json_dialect(dialect)
|
|
113
|
+
requested = requested_json_backend_id(backend)
|
|
114
|
+
unless json_backend_available_for_analysis?(requested, dialect: dialect)
|
|
115
|
+
return unsupported_feature_result("Unsupported JSON backend #{requested}.")
|
|
116
|
+
end
|
|
117
|
+
|
|
118
|
+
if dialect == :json && detect_trailing_comma(source)
|
|
119
|
+
return parse_failure("Trailing commas are not supported for #{dialect}.")
|
|
120
|
+
end
|
|
121
|
+
if dialect == :json && detect_json_comments(source)
|
|
122
|
+
return parse_failure("Comments are not supported for #{dialect}.")
|
|
123
|
+
end
|
|
124
|
+
|
|
125
|
+
register_backend!
|
|
126
|
+
analysis = TreeHaver.with_backend(requested) { FileAnalysis.new(source, dialect: dialect) }
|
|
127
|
+
unless analysis.valid?
|
|
128
|
+
return parse_failure(analysis.errors.map do |error|
|
|
129
|
+
error.respond_to?(:message) ? error.message : error.inspect
|
|
130
|
+
end.join(', '))
|
|
131
|
+
end
|
|
132
|
+
|
|
56
133
|
{
|
|
57
134
|
ok: true,
|
|
58
135
|
diagnostics: [],
|
|
59
|
-
analysis:
|
|
136
|
+
analysis: {
|
|
137
|
+
dialect: dialect.to_s,
|
|
138
|
+
allows_comments: %i[jsonc json5].include?(dialect),
|
|
139
|
+
root_kind: json_analysis_root_kind(analysis),
|
|
140
|
+
owners: collect_file_analysis_owners(analysis)
|
|
141
|
+
},
|
|
142
|
+
policies: []
|
|
60
143
|
}
|
|
61
|
-
rescue
|
|
144
|
+
rescue TreeHaver::Error, StandardError => e
|
|
62
145
|
parse_failure(e.message)
|
|
63
146
|
end
|
|
64
147
|
|
|
65
148
|
def match_json_owners(template, destination)
|
|
66
|
-
|
|
67
|
-
template_paths = template[:owners].to_h { |owner| [owner[:path], true] }
|
|
68
|
-
|
|
69
|
-
{
|
|
70
|
-
matched: template[:owners].filter_map do |owner|
|
|
71
|
-
next unless destination_paths[owner[:path]]
|
|
72
|
-
|
|
73
|
-
{ template_path: owner[:path], destination_path: owner[:path] }
|
|
74
|
-
end,
|
|
75
|
-
unmatched_template: template[:owners].map { |owner| owner[:path] }.reject { |path| destination_paths[path] },
|
|
76
|
-
unmatched_destination: destination[:owners].map { |owner| owner[:path] }.reject { |path| template_paths[path] }
|
|
77
|
-
}
|
|
149
|
+
Ast::Merge::OwnerSelection.match_by_path(template, destination)
|
|
78
150
|
end
|
|
79
151
|
|
|
80
|
-
def merge_json(template_source, destination_source, dialect)
|
|
81
|
-
|
|
152
|
+
def merge_json(template_source, destination_source, dialect, backend: nil)
|
|
153
|
+
dialect = normalize_json_dialect(dialect)
|
|
154
|
+
requested = requested_json_backend_id(backend)
|
|
155
|
+
unless json_backend_available_for_analysis?(requested, dialect: dialect)
|
|
156
|
+
return unsupported_feature_result("Unsupported JSON backend #{requested}.")
|
|
157
|
+
end
|
|
158
|
+
|
|
159
|
+
template_result = parse_json(template_source, dialect, backend: requested)
|
|
82
160
|
return { ok: false, diagnostics: template_result[:diagnostics] } unless template_result[:ok]
|
|
83
161
|
|
|
84
|
-
destination_result = parse_json(destination_source, dialect)
|
|
162
|
+
destination_result = parse_json(destination_source, dialect, backend: requested)
|
|
85
163
|
if destination_result[:ok]
|
|
86
|
-
output = JSON.generate(
|
|
87
|
-
merge_json_values(
|
|
88
|
-
JSON.parse(template_result.dig(:analysis, :normalized_source)),
|
|
89
|
-
JSON.parse(destination_result.dig(:analysis, :normalized_source))
|
|
90
|
-
)
|
|
91
|
-
)
|
|
92
164
|
return {
|
|
93
165
|
ok: true,
|
|
94
166
|
diagnostics: [],
|
|
95
|
-
output:
|
|
167
|
+
output: TreeHaver.with_backend(requested) do
|
|
168
|
+
merge_json_sources(template_source, destination_source, dialect: dialect)
|
|
169
|
+
end,
|
|
96
170
|
policies: [DESTINATION_WINS_ARRAY_POLICY]
|
|
97
171
|
}
|
|
98
172
|
end
|
|
99
173
|
|
|
100
|
-
fallback_source = try_destination_trailing_comma_fallback(destination_source)
|
|
101
|
-
if fallback_source
|
|
102
|
-
retried = parse_json(fallback_source, dialect)
|
|
103
|
-
if retried[:ok]
|
|
104
|
-
output = JSON.generate(
|
|
105
|
-
merge_json_values(
|
|
106
|
-
JSON.parse(template_result.dig(:analysis, :normalized_source)),
|
|
107
|
-
JSON.parse(retried.dig(:analysis, :normalized_source))
|
|
108
|
-
)
|
|
109
|
-
)
|
|
110
|
-
return {
|
|
111
|
-
ok: true,
|
|
112
|
-
diagnostics: [
|
|
113
|
-
fallback_applied("stripped trailing commas from destination before retrying json merge.")
|
|
114
|
-
],
|
|
115
|
-
output: output,
|
|
116
|
-
policies: [DESTINATION_WINS_ARRAY_POLICY, TRAILING_COMMA_FALLBACK_POLICY]
|
|
117
|
-
}
|
|
118
|
-
end
|
|
119
|
-
end
|
|
120
|
-
|
|
121
174
|
{
|
|
122
175
|
ok: false,
|
|
123
176
|
diagnostics: destination_result[:diagnostics].map do |diagnostic|
|
|
124
|
-
diagnostic[:category] ==
|
|
177
|
+
diagnostic[:category] == 'parse_error' ? diagnostic.merge(category: 'destination_parse_error') : diagnostic
|
|
125
178
|
end
|
|
126
179
|
}
|
|
127
180
|
end
|
|
128
181
|
|
|
182
|
+
def merge_json_sources(template_source, destination_source, dialect:)
|
|
183
|
+
SmartMerger.new(
|
|
184
|
+
template_source,
|
|
185
|
+
destination_source,
|
|
186
|
+
dialect: dialect,
|
|
187
|
+
add_template_only_nodes: true,
|
|
188
|
+
merge_arrays: false,
|
|
189
|
+
preserve_atomic_formatting: true
|
|
190
|
+
).merge_result.to_json
|
|
191
|
+
end
|
|
192
|
+
private_class_method :merge_json_sources
|
|
193
|
+
|
|
194
|
+
def json_value_for_source(source, dialect: 'json', backend: nil)
|
|
195
|
+
dialect = normalize_json_dialect(dialect)
|
|
196
|
+
requested = requested_json_backend_id(backend)
|
|
197
|
+
unless json_backend_available_for_analysis?(requested, dialect: dialect)
|
|
198
|
+
raise ParseError, "Unsupported JSON backend #{requested}."
|
|
199
|
+
end
|
|
200
|
+
|
|
201
|
+
if dialect == :json && detect_trailing_comma(source)
|
|
202
|
+
raise ParseError,
|
|
203
|
+
"Trailing commas are not supported for #{dialect}."
|
|
204
|
+
end
|
|
205
|
+
raise ParseError, "Comments are not supported for #{dialect}." if dialect == :json && detect_json_comments(source)
|
|
206
|
+
|
|
207
|
+
register_backend!
|
|
208
|
+
analysis = TreeHaver.with_backend(requested) { FileAnalysis.new(source, dialect: dialect) }
|
|
209
|
+
unless analysis.valid?
|
|
210
|
+
message = analysis.errors.map { |error| error.respond_to?(:message) ? error.message : error.inspect }.join(', ')
|
|
211
|
+
raise ParseError, message
|
|
212
|
+
end
|
|
213
|
+
|
|
214
|
+
json_value_for_node(analysis.root_node, dialect: dialect)
|
|
215
|
+
end
|
|
216
|
+
|
|
217
|
+
def json_value_for_node(node, dialect: :json)
|
|
218
|
+
case node.type.to_s
|
|
219
|
+
when 'document'
|
|
220
|
+
child = node.semantic_children.first
|
|
221
|
+
if child
|
|
222
|
+
json_value_for_node(NodeWrapper.new(child, lines: node.lines, source: node.source, dialect: dialect),
|
|
223
|
+
dialect: dialect)
|
|
224
|
+
end
|
|
225
|
+
when 'object'
|
|
226
|
+
node.pairs.each_with_object({}) do |pair, object|
|
|
227
|
+
key = pair.key_name
|
|
228
|
+
next unless key
|
|
229
|
+
|
|
230
|
+
object[key] = json_value_for_node(pair.value_node, dialect: dialect)
|
|
231
|
+
end
|
|
232
|
+
when 'array'
|
|
233
|
+
node.elements.map { |element| json_value_for_node(element, dialect: dialect) }
|
|
234
|
+
when 'string'
|
|
235
|
+
decode_string_literal(node.text, dialect: dialect)
|
|
236
|
+
when 'number'
|
|
237
|
+
parse_number_literal(node.text, dialect: dialect)
|
|
238
|
+
when 'true'
|
|
239
|
+
true
|
|
240
|
+
when 'false'
|
|
241
|
+
false
|
|
242
|
+
when 'null'
|
|
243
|
+
nil
|
|
244
|
+
end
|
|
245
|
+
end
|
|
246
|
+
|
|
247
|
+
def object_key_for_literal(text, dialect: :json)
|
|
248
|
+
literal = text.to_s
|
|
249
|
+
return literal if dialect == :json5 && !%w[' "].include?(literal[0])
|
|
250
|
+
|
|
251
|
+
decode_string_literal(literal, dialect: dialect)
|
|
252
|
+
end
|
|
253
|
+
|
|
254
|
+
def decode_string_literal(text, dialect: :json)
|
|
255
|
+
return decode_json_string_literal(text) unless dialect == :json5
|
|
256
|
+
|
|
257
|
+
decode_json5_string_literal(text)
|
|
258
|
+
end
|
|
259
|
+
|
|
260
|
+
def decode_json_string_literal(text)
|
|
261
|
+
literal = text.to_s
|
|
262
|
+
literal = literal[1...-1] if literal.start_with?('"') && literal.end_with?('"')
|
|
263
|
+
literal.gsub(%r{\\(?:["\\/bfnrt]|u[0-9a-fA-F]{4})}) do |escape|
|
|
264
|
+
case escape
|
|
265
|
+
when '\\"' then '"'
|
|
266
|
+
when '\\\\' then '\\'
|
|
267
|
+
when '\\/' then '/'
|
|
268
|
+
when '\\b' then "\b"
|
|
269
|
+
when '\\f' then "\f"
|
|
270
|
+
when '\\n' then "\n"
|
|
271
|
+
when '\\r' then "\r"
|
|
272
|
+
when '\\t' then "\t"
|
|
273
|
+
else
|
|
274
|
+
[escape[2..].to_i(16)].pack('U')
|
|
275
|
+
end
|
|
276
|
+
end
|
|
277
|
+
end
|
|
278
|
+
private_class_method :decode_json_string_literal
|
|
279
|
+
|
|
280
|
+
def decode_json5_string_literal(text)
|
|
281
|
+
literal = text.to_s
|
|
282
|
+
return literal unless literal.length >= 2 && %w[' "].include?(literal[0]) && literal[-1] == literal[0]
|
|
283
|
+
|
|
284
|
+
body = literal[1...-1]
|
|
285
|
+
result = +''
|
|
286
|
+
index = 0
|
|
287
|
+
while index < body.length
|
|
288
|
+
char = body[index]
|
|
289
|
+
unless char == '\\'
|
|
290
|
+
result << char
|
|
291
|
+
index += 1
|
|
292
|
+
next
|
|
293
|
+
end
|
|
294
|
+
|
|
295
|
+
index += 1
|
|
296
|
+
escape = body[index]
|
|
297
|
+
break unless escape
|
|
298
|
+
|
|
299
|
+
case escape
|
|
300
|
+
when "\n"
|
|
301
|
+
# JSON5 line continuations contribute no character.
|
|
302
|
+
when "\r"
|
|
303
|
+
index += 1 if body[index + 1] == "\n"
|
|
304
|
+
when 'b' then result << "\b"
|
|
305
|
+
when 'f' then result << "\f"
|
|
306
|
+
when 'n' then result << "\n"
|
|
307
|
+
when 'r' then result << "\r"
|
|
308
|
+
when 't' then result << "\t"
|
|
309
|
+
when 'v' then result << "\v"
|
|
310
|
+
when '0' then result << "\0"
|
|
311
|
+
when 'x'
|
|
312
|
+
hex = body[(index + 1), 2]
|
|
313
|
+
result << hex.to_i(16).chr(Encoding::UTF_8) if hex&.match?(/\A[0-9a-fA-F]{2}\z/)
|
|
314
|
+
index += 2
|
|
315
|
+
when 'u'
|
|
316
|
+
hex = body[(index + 1), 4]
|
|
317
|
+
result << [hex.to_i(16)].pack('U') if hex&.match?(/\A[0-9a-fA-F]{4}\z/)
|
|
318
|
+
index += 4
|
|
319
|
+
else
|
|
320
|
+
result << escape
|
|
321
|
+
end
|
|
322
|
+
index += 1
|
|
323
|
+
end
|
|
324
|
+
result
|
|
325
|
+
end
|
|
326
|
+
private_class_method :decode_json5_string_literal
|
|
327
|
+
|
|
328
|
+
def parse_number_literal(text, dialect: :json)
|
|
329
|
+
return parse_json_number(text) unless dialect == :json5
|
|
330
|
+
|
|
331
|
+
literal = text.to_s.delete('_')
|
|
332
|
+
return Float::INFINITY if ['Infinity', '+Infinity'].include?(literal)
|
|
333
|
+
return -Float::INFINITY if literal == '-Infinity'
|
|
334
|
+
return Float::NAN if %w[NaN +NaN -NaN].include?(literal)
|
|
335
|
+
|
|
336
|
+
Integer(literal, 0)
|
|
337
|
+
rescue ArgumentError
|
|
338
|
+
Float(literal)
|
|
339
|
+
end
|
|
340
|
+
|
|
341
|
+
def parse_json_number(text)
|
|
342
|
+
number = text.to_s
|
|
343
|
+
return number.to_f if number.match?(/[.eE]/)
|
|
344
|
+
|
|
345
|
+
number.to_i
|
|
346
|
+
end
|
|
347
|
+
private_class_method :parse_json_number
|
|
348
|
+
|
|
129
349
|
def parse_failure(message)
|
|
130
350
|
{
|
|
131
351
|
ok: false,
|
|
@@ -134,77 +354,96 @@ module Json
|
|
|
134
354
|
end
|
|
135
355
|
private_class_method :parse_failure
|
|
136
356
|
|
|
137
|
-
def
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
357
|
+
def requested_json_backend_id(backend)
|
|
358
|
+
return backend.to_s unless backend.to_s.empty?
|
|
359
|
+
|
|
360
|
+
current = TreeHaver.current_backend_id
|
|
361
|
+
return current if json_backend_available_for_analysis?(current)
|
|
362
|
+
|
|
363
|
+
available_json_backends.find do |backend_ref|
|
|
364
|
+
json_backend_available_for_analysis?(backend_ref.id)
|
|
365
|
+
end&.id || TREE_SITTER_BACKEND.id
|
|
366
|
+
end
|
|
367
|
+
private_class_method :requested_json_backend_id
|
|
368
|
+
|
|
369
|
+
def json_backend_available_for_analysis?(backend_id, dialect: :json)
|
|
370
|
+
register_backend!
|
|
371
|
+
languages = %i[jsonc json5].include?(dialect) ? [:json5] : [:json]
|
|
372
|
+
case backend_id.to_s
|
|
373
|
+
when TREE_SITTER_BACKEND.id
|
|
374
|
+
languages.all? do |language|
|
|
375
|
+
registrations = TreeHaver.registered_languages(language)
|
|
376
|
+
registrations.key?(:tree_sitter) || registrations.key?(:tslp)
|
|
377
|
+
end
|
|
378
|
+
else
|
|
379
|
+
false
|
|
380
|
+
end
|
|
144
381
|
end
|
|
145
|
-
private_class_method :
|
|
382
|
+
private_class_method :json_backend_available_for_analysis?
|
|
383
|
+
|
|
384
|
+
def normalize_json_dialect(dialect)
|
|
385
|
+
normalized = dialect.to_s.downcase
|
|
386
|
+
return normalized.to_sym if %w[json jsonc json5].include?(normalized)
|
|
387
|
+
|
|
388
|
+
raise ArgumentError, "Unsupported JSON dialect #{dialect.inspect}. Expected json, jsonc, or json5."
|
|
389
|
+
end
|
|
390
|
+
private_class_method :normalize_json_dialect
|
|
146
391
|
|
|
147
392
|
def parse_error(message)
|
|
148
|
-
{ severity:
|
|
393
|
+
{ severity: 'error', category: 'parse_error', message: message }
|
|
149
394
|
end
|
|
150
395
|
private_class_method :parse_error
|
|
151
396
|
|
|
152
397
|
def unsupported_feature(message)
|
|
153
|
-
{ severity:
|
|
398
|
+
{ severity: 'error', category: 'unsupported_feature', message: message }
|
|
154
399
|
end
|
|
155
400
|
private_class_method :unsupported_feature
|
|
156
401
|
|
|
157
|
-
def
|
|
158
|
-
{
|
|
402
|
+
def unsupported_feature_result(message)
|
|
403
|
+
{ ok: false, diagnostics: [unsupported_feature(message)], policies: [] }
|
|
159
404
|
end
|
|
160
|
-
private_class_method :
|
|
405
|
+
private_class_method :unsupported_feature_result
|
|
161
406
|
|
|
162
|
-
def
|
|
163
|
-
|
|
164
|
-
return
|
|
407
|
+
def json_analysis_root_kind(analysis)
|
|
408
|
+
root = analysis.root_object || analysis.root_node
|
|
409
|
+
return 'object' if root&.object?
|
|
410
|
+
return 'array' if root&.array?
|
|
165
411
|
|
|
166
|
-
|
|
412
|
+
'scalar'
|
|
167
413
|
end
|
|
168
|
-
private_class_method :
|
|
414
|
+
private_class_method :json_analysis_root_kind
|
|
415
|
+
|
|
416
|
+
def collect_file_analysis_owners(analysis)
|
|
417
|
+
root = analysis.root_object || analysis.root_node
|
|
418
|
+
return [] unless root
|
|
169
419
|
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
420
|
+
collect_json_node_owners(root, '')
|
|
421
|
+
.sort_by { |owner| owner.fetch(:path) }
|
|
422
|
+
end
|
|
423
|
+
private_class_method :collect_file_analysis_owners
|
|
424
|
+
|
|
425
|
+
def collect_json_node_owners(node, path)
|
|
426
|
+
if node.object?
|
|
427
|
+
node.pairs.flat_map do |pair|
|
|
428
|
+
key = pair.key_name
|
|
429
|
+
next [] unless key
|
|
430
|
+
|
|
431
|
+
owner_path = "#{path}/#{key}"
|
|
432
|
+
value = pair.value_node
|
|
433
|
+
[{ path: owner_path, owner_kind: 'member', match_key: key }] +
|
|
434
|
+
(value ? collect_json_node_owners(value, owner_path) : [])
|
|
175
435
|
end
|
|
176
|
-
elsif
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
[{ path:
|
|
436
|
+
elsif node.array?
|
|
437
|
+
node.elements.each_with_index.flat_map do |element, index|
|
|
438
|
+
owner_path = "#{path}/#{index}"
|
|
439
|
+
[{ path: owner_path, owner_kind: 'element' }] +
|
|
440
|
+
collect_json_node_owners(element, owner_path)
|
|
180
441
|
end
|
|
181
442
|
else
|
|
182
443
|
[]
|
|
183
444
|
end
|
|
184
445
|
end
|
|
185
|
-
private_class_method :
|
|
186
|
-
|
|
187
|
-
def merge_json_values(template, destination)
|
|
188
|
-
if template.is_a?(Hash) && destination.is_a?(Hash)
|
|
189
|
-
ordered_merge_keys(template, destination).each_with_object({}) do |key, merged|
|
|
190
|
-
if !template.key?(key)
|
|
191
|
-
merged[key] = destination[key]
|
|
192
|
-
elsif !destination.key?(key)
|
|
193
|
-
merged[key] = template[key]
|
|
194
|
-
else
|
|
195
|
-
merged[key] = merge_json_values(template[key], destination[key])
|
|
196
|
-
end
|
|
197
|
-
end
|
|
198
|
-
else
|
|
199
|
-
destination
|
|
200
|
-
end
|
|
201
|
-
end
|
|
202
|
-
private_class_method :merge_json_values
|
|
203
|
-
|
|
204
|
-
def ordered_merge_keys(template, destination)
|
|
205
|
-
template.keys + destination.keys.reject { |key| template.key?(key) }
|
|
206
|
-
end
|
|
207
|
-
private_class_method :ordered_merge_keys
|
|
446
|
+
private_class_method :collect_json_node_owners
|
|
208
447
|
|
|
209
448
|
def detect_trailing_comma(source)
|
|
210
449
|
state = scanner_state
|
|
@@ -213,154 +452,29 @@ module Json
|
|
|
213
452
|
advance_scanner_state(state, char, next_char)
|
|
214
453
|
next if state[:in_line_comment] || state[:in_block_comment] || state[:in_string]
|
|
215
454
|
|
|
216
|
-
if char ==
|
|
455
|
+
if char == ','
|
|
217
456
|
lookahead = source[(index + 1)..]
|
|
218
457
|
next unless lookahead
|
|
219
458
|
|
|
220
459
|
trimmed = lookahead.lstrip
|
|
221
|
-
return true if trimmed.start_with?(
|
|
460
|
+
return true if trimmed.start_with?(']', '}')
|
|
222
461
|
end
|
|
223
462
|
end
|
|
224
463
|
false
|
|
225
464
|
end
|
|
226
465
|
private_class_method :detect_trailing_comma
|
|
227
466
|
|
|
228
|
-
def
|
|
229
|
-
result = +""
|
|
230
|
-
state = scanner_state
|
|
231
|
-
index = 0
|
|
232
|
-
while index < source.length
|
|
233
|
-
char = source[index]
|
|
234
|
-
next_char = source[index + 1]
|
|
235
|
-
|
|
236
|
-
if state[:in_line_comment]
|
|
237
|
-
if char == "\n"
|
|
238
|
-
state[:in_line_comment] = false
|
|
239
|
-
result << "\n"
|
|
240
|
-
end
|
|
241
|
-
index += 1
|
|
242
|
-
next
|
|
243
|
-
end
|
|
244
|
-
|
|
245
|
-
if state[:in_block_comment]
|
|
246
|
-
if char == "*" && next_char == "/"
|
|
247
|
-
state[:in_block_comment] = false
|
|
248
|
-
index += 2
|
|
249
|
-
next
|
|
250
|
-
end
|
|
251
|
-
index += 1
|
|
252
|
-
next
|
|
253
|
-
end
|
|
254
|
-
|
|
255
|
-
if state[:in_string]
|
|
256
|
-
result << char
|
|
257
|
-
if state[:escaped]
|
|
258
|
-
state[:escaped] = false
|
|
259
|
-
elsif char == "\\"
|
|
260
|
-
state[:escaped] = true
|
|
261
|
-
elsif char == "\""
|
|
262
|
-
state[:in_string] = false
|
|
263
|
-
end
|
|
264
|
-
index += 1
|
|
265
|
-
next
|
|
266
|
-
end
|
|
267
|
-
|
|
268
|
-
if char == "\""
|
|
269
|
-
state[:in_string] = true
|
|
270
|
-
result << char
|
|
271
|
-
index += 1
|
|
272
|
-
next
|
|
273
|
-
end
|
|
274
|
-
|
|
275
|
-
if char == "/" && next_char == "/"
|
|
276
|
-
state[:in_line_comment] = true
|
|
277
|
-
index += 2
|
|
278
|
-
next
|
|
279
|
-
end
|
|
280
|
-
|
|
281
|
-
if char == "/" && next_char == "*"
|
|
282
|
-
state[:in_block_comment] = true
|
|
283
|
-
index += 2
|
|
284
|
-
next
|
|
285
|
-
end
|
|
286
|
-
|
|
287
|
-
result << char
|
|
288
|
-
index += 1
|
|
289
|
-
end
|
|
290
|
-
result
|
|
291
|
-
end
|
|
292
|
-
private_class_method :strip_json_comments
|
|
293
|
-
|
|
294
|
-
def try_destination_trailing_comma_fallback(source)
|
|
295
|
-
stripped = strip_trailing_commas(source)
|
|
296
|
-
return nil if stripped == source
|
|
297
|
-
|
|
298
|
-
stripped
|
|
299
|
-
end
|
|
300
|
-
private_class_method :try_destination_trailing_comma_fallback
|
|
301
|
-
|
|
302
|
-
def strip_trailing_commas(source)
|
|
303
|
-
result = +""
|
|
467
|
+
def detect_json_comments(source)
|
|
304
468
|
state = scanner_state
|
|
305
469
|
source.each_char.with_index do |char, index|
|
|
306
470
|
next_char = source[index + 1]
|
|
471
|
+
return true if !state[:in_string] && char == '/' && %w[/ *].include?(next_char)
|
|
307
472
|
|
|
308
|
-
|
|
309
|
-
result << char
|
|
310
|
-
state[:in_line_comment] = false if char == "\n"
|
|
311
|
-
next
|
|
312
|
-
end
|
|
313
|
-
|
|
314
|
-
if state[:in_block_comment]
|
|
315
|
-
result << char
|
|
316
|
-
if char == "*" && next_char == "/"
|
|
317
|
-
result << next_char
|
|
318
|
-
state[:in_block_comment] = false
|
|
319
|
-
end
|
|
320
|
-
next
|
|
321
|
-
end
|
|
322
|
-
|
|
323
|
-
if state[:in_string]
|
|
324
|
-
result << char
|
|
325
|
-
if state[:escaped]
|
|
326
|
-
state[:escaped] = false
|
|
327
|
-
elsif char == "\\"
|
|
328
|
-
state[:escaped] = true
|
|
329
|
-
elsif char == "\""
|
|
330
|
-
state[:in_string] = false
|
|
331
|
-
end
|
|
332
|
-
next
|
|
333
|
-
end
|
|
334
|
-
|
|
335
|
-
if char == "\""
|
|
336
|
-
state[:in_string] = true
|
|
337
|
-
result << char
|
|
338
|
-
next
|
|
339
|
-
end
|
|
340
|
-
|
|
341
|
-
if char == "/" && next_char == "/"
|
|
342
|
-
state[:in_line_comment] = true
|
|
343
|
-
result << char
|
|
344
|
-
next
|
|
345
|
-
end
|
|
346
|
-
|
|
347
|
-
if char == "/" && next_char == "*"
|
|
348
|
-
state[:in_block_comment] = true
|
|
349
|
-
result << char
|
|
350
|
-
next
|
|
351
|
-
end
|
|
352
|
-
|
|
353
|
-
if char == ","
|
|
354
|
-
lookahead = source[(index + 1)..]
|
|
355
|
-
trimmed = lookahead&.lstrip
|
|
356
|
-
next if trimmed&.start_with?("]", "}")
|
|
357
|
-
end
|
|
358
|
-
|
|
359
|
-
result << char
|
|
473
|
+
advance_scanner_state(state, char, next_char)
|
|
360
474
|
end
|
|
361
|
-
|
|
475
|
+
false
|
|
362
476
|
end
|
|
363
|
-
private_class_method :
|
|
477
|
+
private_class_method :detect_json_comments
|
|
364
478
|
|
|
365
479
|
def scanner_state
|
|
366
480
|
{
|
|
@@ -379,29 +493,42 @@ module Json
|
|
|
379
493
|
end
|
|
380
494
|
|
|
381
495
|
if state[:in_block_comment]
|
|
382
|
-
state[:in_block_comment] = false if char ==
|
|
496
|
+
state[:in_block_comment] = false if char == '*' && next_char == '/'
|
|
383
497
|
return
|
|
384
498
|
end
|
|
385
499
|
|
|
386
500
|
if state[:in_string]
|
|
387
501
|
if state[:escaped]
|
|
388
502
|
state[:escaped] = false
|
|
389
|
-
elsif char ==
|
|
503
|
+
elsif char == '\\'
|
|
390
504
|
state[:escaped] = true
|
|
391
|
-
elsif char == "
|
|
505
|
+
elsif char == '"'
|
|
392
506
|
state[:in_string] = false
|
|
393
507
|
end
|
|
394
508
|
return
|
|
395
509
|
end
|
|
396
510
|
|
|
397
|
-
if char == "
|
|
511
|
+
if char == '"'
|
|
398
512
|
state[:in_string] = true
|
|
399
|
-
elsif char ==
|
|
513
|
+
elsif char == '/' && next_char == '/'
|
|
400
514
|
state[:in_line_comment] = true
|
|
401
|
-
elsif char ==
|
|
515
|
+
elsif char == '/' && next_char == '*'
|
|
402
516
|
state[:in_block_comment] = true
|
|
403
517
|
end
|
|
404
518
|
end
|
|
405
519
|
private_class_method :advance_scanner_state
|
|
406
520
|
end
|
|
407
521
|
end
|
|
522
|
+
|
|
523
|
+
Json::Merge.register_backend!
|
|
524
|
+
Json::Merge.register_provider!
|
|
525
|
+
|
|
526
|
+
if defined?(Ast::Merge::RSpec::MergeGemRegistry)
|
|
527
|
+
Ast::Merge::RSpec::MergeGemRegistry.register(
|
|
528
|
+
:json_merge,
|
|
529
|
+
require_path: 'json/merge',
|
|
530
|
+
merger_class: 'Json::Merge::SmartMerger',
|
|
531
|
+
test_source: '{"key": "value"}',
|
|
532
|
+
category: :data
|
|
533
|
+
)
|
|
534
|
+
end
|