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