ast-merge-git 7.1.1 → 7.1.4

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.
@@ -6,7 +6,7 @@ module Ast
6
6
  # Version namespace for this gem.
7
7
  module Version
8
8
  # Current gem version.
9
- VERSION = '7.1.1'
9
+ VERSION = '7.1.4'
10
10
  end
11
11
  # Current gem version exposed at the traditional constant location.
12
12
  VERSION = Version::VERSION # Traditional Constant Location
data/lib/ast/merge/git.rb CHANGED
@@ -1,412 +1,265 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require 'ast/merge'
4
- require 'json'
5
- require 'json/merge'
4
+ require_relative 'git/corpus'
5
+ require_relative 'git/local_benchmark'
6
6
 
7
7
  module Ast
8
8
  module Merge
9
+ # Adapts Git merge-driver roles and outcomes to the portable provider API.
10
+ # rubocop:disable Metrics/AbcSize, Metrics/MethodLength, Metrics/ModuleLength -- Git protocol translation is one cohesive adapter boundary
9
11
  module Git
10
12
  PACKAGE_NAME = 'ast-merge-git'
11
- MERGE_CONFLICT_CATEGORY = 'merge_conflict'
12
- MISSING = Object.new.freeze
13
+ EXIT_SUCCESS = 0
14
+ EXIT_CONFLICT = 1
15
+ EXIT_ERROR = 2
16
+ CONFLICT_POLICIES = %i[write leave_ours].freeze
17
+ ADAPTER_ERROR_CATEGORIES = %i[
18
+ file_error
19
+ invalid_provider_output
20
+ invalid_provider_result
21
+ ].freeze
13
22
 
14
23
  module_function
15
24
 
16
25
  def merge3(request)
17
26
  normalized = normalize_request(request)
18
- case normalize_language(normalized)
19
- when 'json'
20
- merge3_json(normalized)
21
- else
22
- response(
23
- ok: false,
24
- request: normalized,
25
- diagnostics: [{
26
- severity: 'error',
27
- category: 'unsupported_feature',
28
- message: 'ast-merge-git currently supports only json merge3.'
29
- }]
30
- )
31
- end
27
+ provider_result = Ast::Merge.dispatch_provider(:merge3, provider_request(normalized))
28
+ adapt_provider_result(provider_result)
29
+ rescue Ast::Merge::ProviderContract::Error, ArgumentError, KeyError => e
30
+ adapt_provider_result(adapter_failure(:invalid_provider_result, e.message))
32
31
  end
33
32
 
34
- def merge3_json(request)
35
- parsed = parse_json_merge3_inputs(request)
36
- return parsed unless parsed.is_a?(Hash) && parsed[:ok]
37
-
38
- merge = merge_json_values(
39
- parsed.fetch(:base),
40
- parsed.fetch(:ours),
41
- parsed.fetch(:theirs),
42
- path: ''
43
- )
44
-
45
- if merge.fetch(:conflicts).empty?
46
- merged_source = "#{JSON.pretty_generate(merge.fetch(:value))}\n"
47
- return response(
48
- ok: true,
49
- request: request,
50
- merged_source: merged_source,
51
- change_classifications: merge.fetch(:change_classifications),
52
- reparse_after_render: reparse_json_source(merged_source),
53
- formatting_preservation: {
54
- line_diff_score: 1.0,
55
- character_diff_score: 1.0
56
- }
33
+ def merge_files(base_path:, ours_path:, theirs_path:, conflict_policy: :write, **request)
34
+ policy = normalize_conflict_policy(conflict_policy)
35
+ result = merge3(
36
+ request.merge(
37
+ base_source: File.binread(base_path),
38
+ ours_source: File.binread(ours_path),
39
+ theirs_source: File.binread(theirs_path),
40
+ path_name: request[:path_name] || ours_path
57
41
  )
58
- end
59
-
60
- render_conflicted_json(request, merge)
61
- end
62
-
63
- def parse_json_merge3_inputs(request)
64
- dialect = request[:dialect].to_s.empty? ? 'json' : request[:dialect].to_s
65
- {
66
- ok: true,
67
- base: Json::Merge.json_value_for_source(request.fetch(:base_source), dialect: dialect),
68
- ours: Json::Merge.json_value_for_source(request.fetch(:ours_source), dialect: dialect),
69
- theirs: Json::Merge.json_value_for_source(request.fetch(:theirs_source), dialect: dialect)
70
- }
71
- rescue Json::Merge::ParseError => e
72
- source_role = parse_error_role(request, dialect)
73
- response(
74
- ok: false,
75
- request: request,
76
- diagnostics: [{
77
- severity: 'error',
78
- category: 'parse_error',
79
- message: "#{source_role} parse error: #{e.message}"
80
- }]
81
42
  )
43
+ write_result(result, ours_path, policy)
44
+ rescue SystemCallError => e
45
+ file_failure = adapt_provider_result(adapter_failure(:file_error, e.message))
46
+ file_failure.merge(git: git_report(file_failure, ours_path, policy, output_written: false))
82
47
  end
83
48
 
84
- def parse_error_role(request, dialect)
85
- %i[base ours theirs].find do |role|
86
- Json::Merge.json_value_for_source(request.fetch(:"#{role}_source"), dialect: dialect)
87
- false
88
- rescue Json::Merge::ParseError
89
- true
90
- end || :unknown
49
+ def run(argv, env: ENV, stderr: $stderr)
50
+ load_provider_requirements(env['AST_MERGE_REQUIRE'])
51
+ request = command_request(argv, env)
52
+ result = merge_files(**request)
53
+ emit_diagnostics(result, stderr)
54
+ result.dig(:git, :exit_code)
55
+ rescue ArgumentError, LoadError => e
56
+ stderr.puts("#{PACKAGE_NAME}: #{e.message}")
57
+ EXIT_ERROR
91
58
  end
92
59
 
93
- def merge_json_values(base, ours, theirs, path:)
94
- if ours == theirs
95
- return merge_success(ours)
96
- elsif base == ours
97
- return merge_success(theirs, change: classify_json_change(path, base, ours, theirs))
98
- elsif base == theirs
99
- return merge_success(ours, change: classify_json_change(path, base, ours, theirs))
100
- end
60
+ def git_exit_code(result)
61
+ return EXIT_SUCCESS if result[:ok]
62
+ return EXIT_ERROR if adapter_error?(result)
63
+ return EXIT_CONFLICT unless result.fetch(:conflicts, []).empty?
101
64
 
102
- if base.is_a?(Hash) && ours.is_a?(Hash) && theirs.is_a?(Hash)
103
- return merge_json_objects(base, ours, theirs, path: path)
104
- end
105
-
106
- conflict = json_conflict(path, base, ours, theirs)
107
- {
108
- value: ours.equal?(MISSING) ? theirs : ours,
109
- conflicts: [conflict],
110
- change_classifications: [conflict.fetch(:change_classification)]
111
- }
65
+ EXIT_ERROR
112
66
  end
113
67
 
114
- def merge_json_objects(base, ours, theirs, path:)
115
- merged = {}
116
- conflicts = []
117
- change_classifications = []
118
- ordered_keys(base, ours, theirs).each do |key|
119
- child_path = "#{path}/#{key}"
120
- child = merge_json_values(
121
- base.fetch(key, MISSING),
122
- ours.fetch(key, MISSING),
123
- theirs.fetch(key, MISSING),
124
- path: child_path
125
- )
126
- merged[key] = child.fetch(:value) unless child.fetch(:value).equal?(MISSING)
127
- conflicts.concat(child.fetch(:conflicts))
128
- change_classifications.concat(child.fetch(:change_classifications))
68
+ def adapter_error?(result)
69
+ result.fetch(:diagnostics, []).any? do |item|
70
+ ADAPTER_ERROR_CATEGORIES.include?(item[:category].to_s.to_sym)
129
71
  end
130
-
131
- {
132
- value: merged,
133
- conflicts: conflicts,
134
- change_classifications: change_classifications
135
- }
136
72
  end
73
+ private_class_method :adapter_error?
74
+
75
+ def provider_request(request)
76
+ request.slice(
77
+ :provider_id,
78
+ :family,
79
+ :dialect,
80
+ :backend,
81
+ :profile_id,
82
+ :base_source,
83
+ :ours_source,
84
+ :theirs_source,
85
+ :path_name,
86
+ :labels,
87
+ :conflict_marker_size
88
+ ).compact
89
+ end
90
+ private_class_method :provider_request
137
91
 
138
- def ordered_keys(*objects)
139
- objects.each_with_object([]) do |object, keys|
140
- next unless object.is_a?(Hash)
92
+ def adapt_provider_result(result)
93
+ if result[:ok]
94
+ output = result[:output]
95
+ unless output.is_a?(String)
96
+ return invalid_output_result(result, 'Successful provider result is missing String output.')
97
+ end
141
98
 
142
- object.each_key { |key| keys << key unless keys.include?(key) }
143
- end
144
- end
99
+ result.merge(
100
+ merged_source: output,
101
+ conflicted_source: nil,
102
+ change_classifications: result.fetch(:changes)
103
+ )
104
+ else
105
+ conflicted = result[:conflicted_output]
106
+ if conflicted && !conflicted.is_a?(String)
107
+ return invalid_output_result(result, 'Provider conflicted_output must be a String when present.')
108
+ end
145
109
 
146
- def merge_success(value, change: nil)
147
- {
148
- value: value,
149
- conflicts: [],
150
- change_classifications: change ? [change] : []
151
- }
110
+ result.merge(
111
+ merged_source: nil,
112
+ conflicted_source: conflicted,
113
+ change_classifications: result.fetch(:changes)
114
+ )
115
+ end
152
116
  end
117
+ private_class_method :adapt_provider_result
153
118
 
154
- def classify_json_change(path, base, ours, theirs)
155
- {
156
- path: path,
157
- ours: json_change_state(base, ours),
158
- theirs: json_change_state(base, theirs)
159
- }
119
+ def invalid_output_result(result, message)
120
+ result.merge(
121
+ ok: false,
122
+ output: nil,
123
+ merged_source: nil,
124
+ conflicted_source: nil,
125
+ diagnostics: result.fetch(:diagnostics) + [diagnostic(:invalid_provider_output, message)]
126
+ )
160
127
  end
161
-
162
- def json_change_state(base, value)
163
- return 'unchanged' if base == value
164
- return 'added' if base.equal?(MISSING)
165
- return 'deleted' if value.equal?(MISSING)
166
-
167
- 'edited'
128
+ private_class_method :invalid_output_result
129
+
130
+ def adapter_failure(category, message)
131
+ Ast::Merge::ProviderResult.build(
132
+ operation: :merge3,
133
+ success: false,
134
+ envelope: {
135
+ provider: { adapter: PACKAGE_NAME },
136
+ diagnostics: [diagnostic(category, message)],
137
+ verification: { base_participated: false }
138
+ }
139
+ )
168
140
  end
141
+ private_class_method :adapter_failure
169
142
 
170
- def json_conflict(path, base, ours, theirs)
171
- category =
172
- if ours.equal?(MISSING) || theirs.equal?(MISSING)
173
- 'delete_edit'
174
- else
175
- 'edit_edit'
176
- end
143
+ def diagnostic(category, message)
177
144
  {
178
- conflict_id: "json-conflict-#{path.delete_prefix('/').tr('/', '-')}",
145
+ severity: :error,
179
146
  category: category,
180
- path: path,
181
- message: "JSON value changed incompatibly at #{path}",
182
- base: base,
183
- ours: ours,
184
- theirs: theirs,
185
- change_classification: classify_json_change(path, base, ours, theirs)
147
+ message: message,
148
+ blocking: true
186
149
  }
187
150
  end
151
+ private_class_method :diagnostic
188
152
 
189
- def render_conflicted_json(request, merge)
190
- conflicts = merge.fetch(:conflicts)
191
- full_file = conflicts.any? { |conflict| conflict.fetch(:category) == 'delete_edit' }
192
- conflicted_source = if full_file
193
- render_conflict_source(request, conflicts)
194
- else
195
- render_owned_json_conflict_source(request, conflicts.first)
196
- end
197
- response(
153
+ def write_result(result, ours_path, policy)
154
+ if missing_required_conflict_output?(result, policy)
155
+ result = invalid_output_result(result, 'Conflict write policy requires String conflicted_output.')
156
+ end
157
+ output = writable_output(result, policy)
158
+ File.binwrite(ours_path, output) if output
159
+ result.merge(git: git_report(result, ours_path, policy, output_written: !output.nil?))
160
+ rescue SystemCallError => e
161
+ failed = result.merge(
198
162
  ok: false,
199
- request: request,
200
- conflicted_source: conflicted_source,
201
- conflicts: conflicts.map { |conflict| conflict.slice(:conflict_id, :category, :path, :message) },
202
- change_classifications: merge.fetch(:change_classifications),
203
- diagnostics: [{
204
- severity: 'error',
205
- category: MERGE_CONFLICT_CATEGORY,
206
- message: "#{conflicts.length} unresolved JSON merge conflict(s)."
207
- }],
208
- owned_regions: full_file ? [] : [owned_json_region(conflicts.first)],
209
- reparse_after_render: nil,
210
- render_strategy: full_file ? 'full_file_conflict_markers' : 'owned_region_conflict_markers'
163
+ diagnostics: result.fetch(:diagnostics) + [diagnostic(:file_error, e.message)]
211
164
  )
165
+ failed.merge(git: git_report(failed, ours_path, policy, output_written: false))
212
166
  end
167
+ private_class_method :write_result
213
168
 
214
- def render_owned_json_conflict_source(request, conflict)
215
- marker_size = request[:conflict_marker_size].to_i
216
- marker_size = 7 unless marker_size.positive?
217
- key = conflict.fetch(:path).split('/').last
218
- [
219
- '{',
220
- "#{'<' * marker_size} ours",
221
- "#{JSON.generate(key)}:#{JSON.generate(conflict.fetch(:ours))}",
222
- "#{'|' * marker_size} base",
223
- "#{JSON.generate(key)}:#{JSON.generate(conflict.fetch(:base))}",
224
- '=' * marker_size,
225
- "#{JSON.generate(key)}:#{JSON.generate(conflict.fetch(:theirs))}",
226
- "#{'>' * marker_size} theirs",
227
- '}',
228
- ''
229
- ].join("\n")
230
- end
231
-
232
- def owned_json_region(conflict)
233
- {
234
- owner_path: conflict.fetch(:path),
235
- node_id: "json:key:#{conflict.fetch(:path).split('/').last}",
236
- region_kind: 'node',
237
- line_range: { start: 1, end: 1 },
238
- attached_spans: [],
239
- backend_id: 'tree-haver',
240
- parser_identity: 'tree_sitter_language_pack',
241
- can_replace: true,
242
- can_line_merge: false,
243
- requires_reparse: true
244
- }
169
+ def missing_required_conflict_output?(result, policy)
170
+ policy == :write &&
171
+ !result[:ok] &&
172
+ !result.fetch(:conflicts, []).empty? &&
173
+ !result[:conflicted_source].is_a?(String)
245
174
  end
175
+ private_class_method :missing_required_conflict_output?
246
176
 
247
- def reparse_json_source(source)
248
- Json::Merge.json_value_for_source(source)
249
- true
250
- rescue Json::Merge::ParseError
251
- false
252
- end
177
+ def writable_output(result, policy)
178
+ return result.fetch(:merged_source) if result[:ok]
179
+ return unless policy == :write
180
+ return if result.fetch(:conflicts, []).empty?
253
181
 
254
- def normalize_request(request)
255
- request.transform_keys(&:to_sym)
182
+ result[:conflicted_source]
256
183
  end
184
+ private_class_method :writable_output
257
185
 
258
- def merge_comment_delta(base_comment:, ours_comment:, theirs_comment:, owner_path: '/')
259
- conflicts = []
260
- merged_comment =
261
- if ours_comment == theirs_comment
262
- ours_comment
263
- elsif base_comment == ours_comment
264
- theirs_comment
265
- elsif base_comment == theirs_comment
266
- ours_comment
267
- elsif ours_comment.nil?
268
- conflicts << comment_conflict('delete_edit', owner_path, 'ours deleted a comment that theirs edited')
269
- theirs_comment
270
- elsif theirs_comment.nil?
271
- conflicts << comment_conflict('delete_edit', owner_path, 'theirs deleted a comment that ours edited')
272
- ours_comment
273
- else
274
- conflicts << comment_conflict('edit_edit', owner_path, 'comment changed differently in ours and theirs')
275
- ours_comment
276
- end
277
-
186
+ def git_report(result, ours_path, policy, output_written:)
278
187
  {
279
- ok: conflicts.empty?,
280
- merged_comment: conflicts.empty? ? merged_comment : nil,
281
- conflicts: conflicts
188
+ exit_code: git_exit_code(result),
189
+ output_written: output_written,
190
+ output_path: ours_path.to_s,
191
+ conflict_policy: policy
282
192
  }
283
193
  end
194
+ private_class_method :git_report
284
195
 
285
- def response(ok:, request:, merged_source: nil, conflicted_source: nil, conflicts: [],
286
- change_classifications: [], diagnostics: [], fallbacks: [], owned_regions: [], reparse_after_render: nil, formatting_preservation: {}, secondary_formatting_metrics: nil, render_strategy: nil)
287
- {
288
- ok: ok,
289
- merged_source: merged_source,
290
- conflicted_source: conflicted_source,
291
- conflicts: conflicts,
292
- change_classifications: change_classifications,
293
- diagnostics: diagnostics,
294
- fallbacks: fallbacks,
295
- owned_regions: owned_regions,
296
- profile: {
297
- profile_id: request[:profile_id].to_s,
298
- language: normalize_language(request),
299
- dialect: request[:dialect].to_s
300
- },
301
- render_report: {
302
- strategy: render_strategy || (request[:render_policy].to_s.empty? ? 'canonical' : request[:render_policy].to_s),
303
- **render_identity(request)
304
- },
305
- formatting_preservation: {
306
- line_diff_score: 0.0,
307
- character_diff_score: 0.0
308
- }.merge(formatting_preservation),
309
- secondary_formatting_metrics: secondary_formatting_metrics || secondary_formatting_metrics_for(ok && merged_source),
310
- default_driver_evaluation: default_driver_evaluation(
311
- formatting_preservation: {
312
- line_diff_score: 0.0,
313
- character_diff_score: 0.0
314
- }.merge(formatting_preservation),
315
- reparse_after_render: reparse_after_render,
316
- render_strategy: render_strategy || (request[:render_policy].to_s.empty? ? 'canonical' : request[:render_policy].to_s)
317
- ),
318
- reparse_after_render: reparse_after_render
319
- }
196
+ def normalize_request(request)
197
+ request.transform_keys(&:to_sym).then do |normalized|
198
+ normalized[:family] ||= normalized.delete(:language)
199
+ normalized[:labels] = normalize_labels(normalized)
200
+ normalized
201
+ end
202
+ end
203
+ private_class_method :normalize_request
204
+
205
+ def normalize_labels(request)
206
+ labels = request.fetch(:labels, {}).transform_keys(&:to_sym)
207
+ labels[:base] ||= request[:base_label]
208
+ labels[:ours] ||= request[:ours_label]
209
+ labels[:theirs] ||= request[:theirs_label]
210
+ labels.compact
320
211
  end
212
+ private_class_method :normalize_labels
321
213
 
322
- def default_driver_evaluation(formatting_preservation:, reparse_after_render:, render_strategy:)
323
- threshold = 0.95
324
- score = (formatting_preservation.fetch(:line_diff_score) + formatting_preservation.fetch(:character_diff_score)) / 2.0
325
- reparse_passed = reparse_after_render == true
326
- no_full_file_rewrite = render_strategy != 'full_file_conflict_markers'
327
- coherent_conflict_markers = render_strategy != 'full_file_conflict_markers'
328
- blocking_reasons = []
329
- blocking_reasons << 'rendered output did not reparse' unless reparse_passed
330
- blocking_reasons << 'formatting score is below threshold' if score < threshold
331
- blocking_reasons << 'full-file rewrite or conflict markers were used' unless no_full_file_rewrite
332
- blocking_reasons << 'conflict marker placement is not syntactically coherent' unless coherent_conflict_markers
214
+ def normalize_conflict_policy(policy)
215
+ normalized = policy.to_s.strip.to_sym
216
+ return normalized if CONFLICT_POLICIES.include?(normalized)
333
217
 
334
- {
335
- status: blocking_reasons.empty? ? 'recommended' : 'not_recommended',
336
- formatting_threshold: threshold,
337
- formatting_score: score,
338
- hard_gates: [
339
- { name: 'reparse_after_render', passed: reparse_passed, weighted: false },
340
- { name: 'no_full_file_rewrite', passed: no_full_file_rewrite, weighted: false },
341
- { name: 'coherent_conflict_marker_placement', passed: coherent_conflict_markers, weighted: false }
342
- ],
343
- blocking_reasons: blocking_reasons,
344
- diagnostics: ['default-driver evaluation is advisory unless explicitly required']
345
- }
218
+ raise ArgumentError, "Unknown conflict policy: #{policy.inspect}"
346
219
  end
220
+ private_class_method :normalize_conflict_policy
347
221
 
348
- def secondary_formatting_metrics_for(merged)
349
- if merged
350
- {
351
- unchanged_line_churn: 0,
352
- output_diff_size: 0,
353
- source_fragment_retention: 1.0,
354
- weighted: false,
355
- diagnostics: ['canonical JSON has no trivia-preserving source fragments yet']
356
- }
357
- else
358
- {
359
- unchanged_line_churn: 0,
360
- output_diff_size: 0,
361
- source_fragment_retention: 0.0,
362
- weighted: false,
363
- diagnostics: ['unresolved conflict did not produce a merged source-fragment retention measurement']
364
- }
222
+ def command_request(argv, env)
223
+ unless argv.length.between?(3, 8)
224
+ raise ArgumentError,
225
+ 'usage: ast-merge-git BASE OURS THEIRS [PATH [MARKER_SIZE [BASE_LABEL OURS_LABEL THEIRS_LABEL]]]'
365
226
  end
366
- end
367
227
 
368
- def render_identity(request)
369
- case normalize_language(request)
370
- when 'json'
371
- { backend_id: 'tree-haver', parser_identity: 'tree_sitter_language_pack' }
372
- else
373
- {}
228
+ family = env['AST_MERGE_FAMILY']
229
+ provider_id = env['AST_MERGE_PROVIDER']
230
+ if family.to_s.empty? && provider_id.to_s.empty?
231
+ raise ArgumentError, 'AST_MERGE_FAMILY or AST_MERGE_PROVIDER is required'
374
232
  end
375
- end
376
-
377
- def render_conflict_source(request, conflicts)
378
- marker_size = request[:conflict_marker_size].to_i
379
- marker_size = 7 unless marker_size.positive?
380
- header = "/* smorg structured conflicts: #{conflicts.length} unresolved */"
381
- [
382
- header,
383
- "#{'<' * marker_size} ours",
384
- request.fetch(:ours_source),
385
- "#{'|' * marker_size} base",
386
- request.fetch(:base_source),
387
- '=' * marker_size,
388
- request.fetch(:theirs_source),
389
- "#{'>' * marker_size} theirs",
390
- ''
391
- ].join("\n")
392
- end
393
233
 
394
- def comment_conflict(category, path, message)
395
234
  {
396
- conflict_id: 'comment-conflict-1',
397
- category: category,
398
- path: path.to_s.empty? ? '/' : path,
399
- message: message
400
- }
235
+ base_path: argv.fetch(0),
236
+ ours_path: argv.fetch(1),
237
+ theirs_path: argv.fetch(2),
238
+ path_name: argv[3],
239
+ conflict_marker_size: argv[4],
240
+ labels: { base: argv[5], ours: argv[6], theirs: argv[7] }.compact,
241
+ family: family,
242
+ provider_id: provider_id,
243
+ dialect: env['AST_MERGE_DIALECT'],
244
+ backend: env['AST_MERGE_BACKEND'],
245
+ profile_id: env['AST_MERGE_PROFILE'],
246
+ conflict_policy: env.fetch('AST_MERGE_CONFLICT_POLICY', :write)
247
+ }.compact
401
248
  end
249
+ private_class_method :command_request
402
250
 
403
- def normalize_language(request)
404
- language = request[:language].to_s.strip.downcase
405
- return 'json' if language == 'json'
406
- return 'json' if request[:path_name].to_s.downcase.end_with?('.json')
251
+ def load_provider_requirements(value)
252
+ value.to_s.split(',').map(&:strip).reject(&:empty?).each { |require_path| require require_path }
253
+ end
254
+ private_class_method :load_provider_requirements
407
255
 
408
- language
256
+ def emit_diagnostics(result, stderr)
257
+ result.fetch(:diagnostics).each do |item|
258
+ stderr.puts("#{PACKAGE_NAME}: #{item[:category]}: #{item[:message]}")
259
+ end
409
260
  end
261
+ private_class_method :emit_diagnostics
410
262
  end
263
+ # rubocop:enable Metrics/AbcSize, Metrics/MethodLength, Metrics/ModuleLength
411
264
  end
412
265
  end