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.
data/lib/json/merge.rb CHANGED
@@ -1,131 +1,340 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require "json"
4
- require "tree_haver"
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 = "json-merge"
10
+ PACKAGE_NAME = 'json-merge'
9
11
  DESTINATION_WINS_ARRAY_POLICY = {
10
- surface: "array",
11
- name: "destination_wins_array"
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: "json",
23
- supported_dialects: %w[json jsonc],
24
- supported_policies: [DESTINATION_WINS_ARRAY_POLICY, TRAILING_COMMA_FALLBACK_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 json_parse_request(source, dialect)
29
- TreeHaver::ParserRequest.new(source: source, language: "json", dialect: dialect)
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 parse_json_with_language_pack(source, dialect)
33
- return unsupported_jsonc_language_pack_result if dialect != "json"
34
-
35
- backend_result = TreeHaver.parse_with_language_pack(json_parse_request(source, dialect))
36
- return { ok: false, diagnostics: backend_result[:diagnostics] } unless backend_result[:ok]
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
- parse_json(source, dialect)
80
+ json_feature_profile.merge(
81
+ backend: requested,
82
+ backend_ref: TREE_SITTER_BACKEND.to_h
83
+ )
39
84
  end
40
85
 
41
- def parse_json(source, dialect)
42
- normalized_source = dialect == "jsonc" ? strip_json_comments(source) : source
43
- allows_comments = dialect == "jsonc"
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
- parsed = JSON.parse(normalized_source)
47
- canonical = JSON.generate(parsed)
48
- analysis = {
49
- kind: "json",
50
- dialect: dialect,
51
- allows_comments: allows_comments,
52
- normalized_source: canonical,
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: 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 JSON::ParserError => e
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
- destination_paths = destination[:owners].to_h { |owner| [owner[:path], true] }
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
- template_result = parse_json(template_source, dialect)
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: 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] == "parse_error" ? diagnostic.merge(category: "destination_parse_error") : diagnostic
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 unsupported_jsonc_language_pack_result
138
- {
139
- ok: false,
140
- diagnostics: [
141
- unsupported_feature("tree-sitter-language-pack json parsing currently supports only the json dialect.")
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 :unsupported_jsonc_language_pack_result
379
+ private_class_method :normalize_json_dialect
146
380
 
147
381
  def parse_error(message)
148
- { severity: "error", category: "parse_error", message: message }
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: "error", category: "unsupported_feature", message: message }
387
+ { severity: 'error', category: 'unsupported_feature', message: message }
154
388
  end
155
389
  private_class_method :unsupported_feature
156
390
 
157
- def fallback_applied(message)
158
- { severity: "warning", category: "fallback_applied", message: message }
391
+ def unsupported_feature_result(message)
392
+ { ok: false, diagnostics: [unsupported_feature(message)], policies: [] }
159
393
  end
160
- private_class_method :fallback_applied
394
+ private_class_method :unsupported_feature_result
161
395
 
162
- def json_root_kind(value)
163
- return "object" if value.is_a?(Hash)
164
- return "array" if value.is_a?(Array)
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
- "scalar"
401
+ 'scalar'
167
402
  end
168
- private_class_method :json_root_kind
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
- def collect_json_owners(value, path = "")
171
- if value.is_a?(Hash)
172
- value.keys.sort.flat_map do |key|
173
- next_path = "#{path}/#{key}"
174
- [{ path: next_path, owner_kind: "member", match_key: key }] + collect_json_owners(value[key], next_path)
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 value.is_a?(Array)
177
- value.each_with_index.flat_map do |item, index|
178
- next_path = "#{path}/#{index}"
179
- [{ path: next_path, owner_kind: "element" }] + collect_json_owners(item, next_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 :collect_json_owners
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 strip_json_comments(source)
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
- if state[:in_line_comment]
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
- result
464
+ false
362
465
  end
363
- private_class_method :strip_trailing_commas
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 == "*" && next_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 == "/" && next_char == "/"
502
+ elsif char == '/' && next_char == '/'
400
503
  state[:in_line_comment] = true
401
- elsif char == "/" && next_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