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.
data/lib/json/merge.rb CHANGED
@@ -1,131 +1,351 @@
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
+ 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: "json",
23
- supported_dialects: %w[json jsonc],
24
- supported_policies: [DESTINATION_WINS_ARRAY_POLICY, TRAILING_COMMA_FALLBACK_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 json_parse_request(source, dialect)
29
- TreeHaver::ParserRequest.new(source: source, language: "json", dialect: dialect)
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 parse_json_with_language_pack(source, dialect)
33
- return unsupported_jsonc_language_pack_result if dialect != "json"
81
+ def available_json_backends
82
+ json_backend_available_for_analysis?(TREE_SITTER_BACKEND.id) ? [TREE_SITTER_BACKEND] : []
83
+ end
34
84
 
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]
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
- parse_json(source, dialect)
91
+ json_feature_profile.merge(
92
+ backend: requested,
93
+ backend_ref: TREE_SITTER_BACKEND.to_h
94
+ )
39
95
  end
40
96
 
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)
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
- 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)
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: 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 JSON::ParserError => e
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
- 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
- }
149
+ Ast::Merge::OwnerSelection.match_by_path(template, destination)
78
150
  end
79
151
 
80
- def merge_json(template_source, destination_source, dialect)
81
- template_result = parse_json(template_source, dialect)
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: 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] == "parse_error" ? diagnostic.merge(category: "destination_parse_error") : diagnostic
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 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
- }
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 :unsupported_jsonc_language_pack_result
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: "error", category: "parse_error", message: message }
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: "error", category: "unsupported_feature", message: message }
398
+ { severity: 'error', category: 'unsupported_feature', message: message }
154
399
  end
155
400
  private_class_method :unsupported_feature
156
401
 
157
- def fallback_applied(message)
158
- { severity: "warning", category: "fallback_applied", message: message }
402
+ def unsupported_feature_result(message)
403
+ { ok: false, diagnostics: [unsupported_feature(message)], policies: [] }
159
404
  end
160
- private_class_method :fallback_applied
405
+ private_class_method :unsupported_feature_result
161
406
 
162
- def json_root_kind(value)
163
- return "object" if value.is_a?(Hash)
164
- return "array" if value.is_a?(Array)
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
- "scalar"
412
+ 'scalar'
167
413
  end
168
- private_class_method :json_root_kind
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
- 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)
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 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)
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 :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
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 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 = +""
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
- 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
473
+ advance_scanner_state(state, char, next_char)
360
474
  end
361
- result
475
+ false
362
476
  end
363
- private_class_method :strip_trailing_commas
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 == "*" && next_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 == "/" && next_char == "/"
513
+ elsif char == '/' && next_char == '/'
400
514
  state[:in_line_comment] = true
401
- elsif char == "/" && next_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