ast-merge 7.1.7 → 7.1.8

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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: f1af004153456efec0bb60b9fba798b42f0de4ef6e1db37a77425710fc8d19de
4
- data.tar.gz: 6210d3c3b11243effad45183c086d32407b978423e640ee123f7ba8113a82bc0
3
+ metadata.gz: 205697b391355f6dfc618a12f26c0d567e186fe148981baa5f38f16b0bf0653f
4
+ data.tar.gz: 304d5c93be7f48d8f98c68229b182ae9d831baf43c3987d41ce22b2c6fcdc655
5
5
  SHA512:
6
- metadata.gz: 329b0d280fa31e851f2ffb15d64a36501deed7075178cca26cae049b626bcf54d42dbfdda0547759e7aa3d4a8f43245d597c812555af1963f937097d2aef05f1
7
- data.tar.gz: c91310f88ccf4d3a4a7a935a7f101138b7174af7c8dc5992a69b98163670dc4bf5af28414eaae1cd96c04ef924b22af3792a86356fa6d40fc661c520a0052058
6
+ metadata.gz: 38c22b55d61033a0c77f2257cc65204f87ae041bcd658e00a8d5bc87b5cfb839de897679ce6e7134008972b5f19239b050d2f40d4cb6b93f0642384e30d75d2c
7
+ data.tar.gz: ff57c5b383ae4634dedbc89a3028a5fbe7ba44a9668a3c50c14e953323f86bd1f206177f2642d47d630d52fb2ca4e0835b11d1c649ba149b7550b4321e9edb79
checksums.yaml.gz.sig CHANGED
Binary file
@@ -18,7 +18,8 @@ module Ast
18
18
  elsif object.respond_to?(:source_position)
19
19
  object.source_position&.dig(:start_line)
20
20
  elsif object.respond_to?(:start_point) && object.start_point
21
- object.start_point.row + 1
21
+ row = line_point_row(object.start_point)
22
+ row + 1 if row
22
23
  end
23
24
  end
24
25
 
@@ -30,10 +31,18 @@ module Ast
30
31
  elsif object.respond_to?(:source_position)
31
32
  object.source_position&.dig(:end_line)
32
33
  elsif object.respond_to?(:end_point) && object.end_point
33
- object.end_point.row + 1
34
+ row = line_point_row(object.end_point)
35
+ row + 1 if row
34
36
  end
35
37
  end
36
38
 
39
+ def line_point_row(point)
40
+ return point.row if point.respond_to?(:row)
41
+ return point[:row] if point.respond_to?(:[])
42
+
43
+ nil
44
+ end
45
+
37
46
  def source_line_at(analysis, line_number)
38
47
  line = analysis.line_at(line_number)
39
48
  line.respond_to?(:raw) ? line.raw : line
@@ -17,6 +17,7 @@ That loads:
17
17
  - TreeHaver backend dependency tags
18
18
  - Ast::Merge merge-gem dependency tags
19
19
  - Shared conformance fixture helpers
20
+ - Portable provider snapshot and differential replay helpers
20
21
  - Ast::Merge shared examples
21
22
 
22
23
  ### Split loading for `ast-merge` itself or registration-heavy suites
@@ -249,3 +250,47 @@ Use this namespace for test support shared across the merge-gem family:
249
250
  - shared examples for common contracts
250
251
 
251
252
  Format-specific parser fixtures and merge behavior specs belong in the corresponding `*-merge` gem.
253
+
254
+ ## Provider snapshots
255
+
256
+ `Ast::Merge::RSpec::ProviderSnapshot` captures the versioned parse and analysis
257
+ projections used by the cross-runtime fixture repository. Parsing always enters
258
+ through an explicitly selected TreeHaver backend. The capture harness only
259
+ reads the normalized TreeHaver node contract; a provider suite supplies a
260
+ callback for parser-native evidence:
261
+
262
+ ```ruby
263
+ snapshot = Ast::Merge::RSpec::ProviderSnapshot.new(
264
+ snapshot_id: "snapshot.prism.ruby",
265
+ provider: Prism::Merge.merge_provider,
266
+ source: source,
267
+ language: :ruby,
268
+ dialect: :ruby,
269
+ backend_id: :prism,
270
+ parser_contract: :prism,
271
+ backend_identity: {
272
+ id: :prism,
273
+ family: :native,
274
+ host_runtime: :ruby,
275
+ package: :prism,
276
+ parser: :Prism,
277
+ capabilities: %i[comments directives exact_source_spans normalized_tree],
278
+ },
279
+ extension_builder: lambda do |tree:, **|
280
+ {
281
+ schema: "structuredmerge.extension/ruby-prism/v1",
282
+ namespace: "ruby-prism",
283
+ capabilities: %w[magic-comments],
284
+ payload: {magic_comments: tree.magic_comments.map(&:to_s)},
285
+ }
286
+ end,
287
+ )
288
+
289
+ artifacts = snapshot.capture
290
+ ```
291
+
292
+ The extension callback is intentionally provider-owned. The common harness
293
+ does not reflect over native parser objects or flatten their capabilities.
294
+ `#differential_replay` serializes a provider request through canonical JSON,
295
+ replays it, and reports whether the provider result and output bytes remain
296
+ identical.
@@ -0,0 +1,497 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'digest'
4
+ require 'json'
5
+ require 'ast/merge'
6
+ require 'tree_haver'
7
+
8
+ module Ast
9
+ module Merge
10
+ module RSpec
11
+ # Captures the portable TreeHaver and provider projections used by the
12
+ # cross-runtime contract fixtures. Native facts enter only through an
13
+ # explicit, namespaced extension builder supplied by the provider suite.
14
+ # rubocop:disable Metrics/AbcSize, Metrics/ClassLength, Metrics/MethodLength, Metrics/ParameterLists -- the capture boundary mirrors the complete versioned contract
15
+ class ProviderSnapshot
16
+ PARSE_REQUEST_SCHEMA = 'structuredmerge.parse-request/v1'
17
+ PARSE_RESULT_SCHEMA = 'structuredmerge.parse-result/v1'
18
+ ANALYSIS_RESULT_SCHEMA = 'structuredmerge.analysis-result/v1'
19
+ EXTENSION_SCHEMA_PATTERN = %r{\Astructuredmerge\.extension/[a-z0-9-]+/v\d+\z}
20
+ NORMALIZED_CONTRACT = 'structuredmerge.normalized-node/v1'
21
+
22
+ class Error < StandardError; end
23
+
24
+ attr_reader :snapshot_id, :source_id, :provider, :source, :language, :dialect, :backend_id,
25
+ :backend_identity, :parser_contract, :profile_id, :extension_builder
26
+
27
+ def initialize(snapshot_id:, provider:, source:, language:, dialect:, backend_id:, backend_identity:,
28
+ source_id: nil, parser_contract: nil, profile_id: :source_preserving, extension_builder: nil)
29
+ @snapshot_id = required_string(snapshot_id, :snapshot_id)
30
+ @source_id = required_string(source_id || "#{@snapshot_id}:source", :source_id)
31
+ @provider = provider
32
+ @source = source.to_s
33
+ @language = required_string(language, :language)
34
+ @dialect = required_string(dialect, :dialect)
35
+ @backend_id = required_string(backend_id, :backend_id)
36
+ @backend_identity = normalize_backend_identity(backend_identity)
37
+ @parser_contract = parser_contract&.to_sym
38
+ @profile_id = profile_id.to_sym
39
+ @extension_builder = extension_builder
40
+ end
41
+
42
+ def capture
43
+ tree = parse_tree
44
+ projection = TreeProjection.new(tree, source).call
45
+ provider_result = analyze
46
+ extensions = build_extensions(tree, projection, provider_result)
47
+ parse_request = build_parse_request
48
+ parse_result = build_parse_result(parse_request, projection, extensions)
49
+
50
+ {
51
+ parse_request: parse_request,
52
+ parse_result: parse_result,
53
+ analysis_result: build_analysis_result(parse_request, parse_result, provider_result, extensions)
54
+ }
55
+ end
56
+
57
+ def differential_replay(operation:, request:)
58
+ operation = ProviderContract.normalize_operation(operation)
59
+ original_request = ProviderContract.validate_request!(operation, request)
60
+ replay_request = ProviderContract.validate_request!(operation, self.class.round_trip(original_request))
61
+ original_result = ProviderContract.validate_result!(
62
+ operation,
63
+ provider.public_send(operation, original_request)
64
+ )
65
+ replay_result = ProviderContract.validate_result!(operation, provider.public_send(operation, replay_request))
66
+ original_json = self.class.canonical_json(original_result)
67
+ replay_json = self.class.canonical_json(replay_result)
68
+
69
+ {
70
+ operation: operation.to_s,
71
+ equivalent: original_json == replay_json,
72
+ original_sha256: Digest::SHA256.hexdigest(original_json),
73
+ replay_sha256: Digest::SHA256.hexdigest(replay_json),
74
+ output_bytes_equal: output_bytes(original_result) == output_bytes(replay_result),
75
+ original_request: original_request,
76
+ transported_request: replay_request,
77
+ original_result: original_result,
78
+ transported_result: replay_result
79
+ }
80
+ end
81
+
82
+ class << self
83
+ def tree_sitter_extension(nodes:, **)
84
+ {
85
+ schema: 'structuredmerge.extension/tree-sitter/v1',
86
+ namespace: 'tree-sitter',
87
+ capabilities: ['grammar-node-types'],
88
+ payload: {
89
+ grammar_node_types: nodes.map { |node| node.fetch(:native_type).to_s }.uniq.sort
90
+ }
91
+ }
92
+ end
93
+
94
+ def canonical_json(value)
95
+ JSON.generate(canonical_value(Ast::Merge.json_ready(value)))
96
+ end
97
+
98
+ def round_trip(value)
99
+ JSON.parse(canonical_json(value), symbolize_names: true)
100
+ end
101
+
102
+ def extension_forwarding_digest(extension)
103
+ forwarded = round_trip(extension.merge('x-unknown-forwarding-probe' => { 'retained' => true }))
104
+ Digest::SHA256.hexdigest(canonical_json(forwarded))
105
+ end
106
+
107
+ private
108
+
109
+ def canonical_value(value)
110
+ case value
111
+ when Array
112
+ value.map { |item| canonical_value(item) }
113
+ when Hash
114
+ value.keys.map(&:to_s).sort.to_h do |key|
115
+ item = value.key?(key) ? value[key] : value[key.to_sym]
116
+ [key, canonical_value(item)]
117
+ end
118
+ else
119
+ value
120
+ end
121
+ end
122
+ end
123
+
124
+ private
125
+
126
+ def parse_tree
127
+ TreeHaver.with_backend(backend_id) do
128
+ options = {}
129
+ options[:contract] = parser_contract if parser_contract
130
+ TreeHaver.parser_for(language.to_sym, **options).parse(source)
131
+ end
132
+ end
133
+
134
+ def analyze
135
+ request = {
136
+ provider_id: provider.provider_id,
137
+ family: provider.family,
138
+ source: source,
139
+ dialect: dialect,
140
+ backend: backend_id,
141
+ profile_id: profile_id
142
+ }
143
+ ProviderContract.validate_result!(:analyze, provider.analyze(request))
144
+ end
145
+
146
+ def build_parse_request
147
+ {
148
+ schema: PARSE_REQUEST_SCHEMA,
149
+ request_id: request_id,
150
+ source: source_descriptor(include_content: true),
151
+ language: language,
152
+ dialect: dialect,
153
+ selection: {
154
+ backend: backend_id,
155
+ preference: [],
156
+ required_capabilities: backend_identity.fetch(:capabilities)
157
+ },
158
+ options: {
159
+ comments: true,
160
+ diagnostics: true,
161
+ native_extensions: true,
162
+ tokens: false
163
+ },
164
+ metadata: { snapshot_id: snapshot_id }
165
+ }
166
+ end
167
+
168
+ def build_parse_result(parse_request, projection, extensions)
169
+ {
170
+ schema: PARSE_RESULT_SCHEMA,
171
+ request_id: request_id,
172
+ ok: projection.fetch(:diagnostics).none? { |diagnostic| diagnostic.fetch(:blocking) },
173
+ source: source_descriptor,
174
+ selection: selection_report,
175
+ backend: backend_identity.merge(
176
+ language: language,
177
+ dialect: dialect,
178
+ normalized_contract: NORMALIZED_CONTRACT
179
+ ),
180
+ root_id: projection.fetch(:root_id),
181
+ nodes: projection.fetch(:nodes),
182
+ comments: projection.fetch(:comments),
183
+ diagnostics: projection.fetch(:diagnostics),
184
+ extensions: extensions,
185
+ metadata: {
186
+ parse_request_sha256: Digest::SHA256.hexdigest(self.class.canonical_json(parse_request))
187
+ }
188
+ }
189
+ end
190
+
191
+ def build_analysis_result(parse_request, parse_result, provider_result, extensions)
192
+ analysis = provider_result.fetch(:analysis, {})
193
+ {
194
+ schema: ANALYSIS_RESULT_SCHEMA,
195
+ request_id: request_id,
196
+ ok: provider_result.fetch(:ok),
197
+ source: source_descriptor,
198
+ provider: provider_result.fetch(:provider),
199
+ profile: provider_result.fetch(:profile),
200
+ parse_result_sha256: Digest::SHA256.hexdigest(self.class.canonical_json(parse_result)),
201
+ analysis: analysis,
202
+ owners: extract_owners(analysis),
203
+ comment_regions: Array(analysis[:comment_regions] || analysis['comment_regions']),
204
+ layout_gaps: Array(analysis[:layout_gaps] || analysis['layout_gaps']),
205
+ attachments: Array(analysis[:attachments] || analysis['attachments']),
206
+ ownership: Array(analysis[:ownership] || analysis['ownership']),
207
+ diagnostics: provider_result.fetch(:diagnostics),
208
+ extensions: extensions,
209
+ metadata: {
210
+ parse_request_sha256: Digest::SHA256.hexdigest(self.class.canonical_json(parse_request)),
211
+ provider_result_schema: provider_result.fetch(:schema)
212
+ }
213
+ }
214
+ end
215
+
216
+ def extract_owners(analysis)
217
+ %i[owners declarations entries].each do |key|
218
+ value = analysis[key] || analysis[key.to_s]
219
+ return value if value.is_a?(Array)
220
+ end
221
+ []
222
+ end
223
+
224
+ def build_extensions(tree, projection, provider_result)
225
+ return [] unless extension_builder
226
+
227
+ extension_value = extension_builder.call(
228
+ tree: tree,
229
+ nodes: projection.fetch(:nodes),
230
+ provider_result: provider_result,
231
+ source: source
232
+ )
233
+ extensions = extension_value.is_a?(Array) ? extension_value : [extension_value]
234
+ extensions.map { |extension| normalize_extension(extension) }
235
+ end
236
+
237
+ def normalize_extension(extension)
238
+ normalized = Ast::Merge.json_ready(extension)
239
+ schema = normalized.fetch('schema', '').to_s
240
+ namespace = normalized.fetch('namespace', '').to_s
241
+ capabilities = Array(normalized['capabilities']).map(&:to_s).sort.uniq
242
+ raise Error, "Invalid extension schema: #{schema.inspect}" unless schema.match?(EXTENSION_SCHEMA_PATTERN)
243
+ raise Error, 'Extension namespace must not be empty' if namespace.empty?
244
+ raise Error, 'Extension payload is required' unless normalized.key?('payload')
245
+
246
+ normalized.merge(
247
+ 'capabilities' => capabilities,
248
+ 'opaque_forwarding_replay_sha256' => self.class.extension_forwarding_digest(normalized)
249
+ )
250
+ end
251
+
252
+ def selection_report
253
+ {
254
+ mode: 'explicit',
255
+ requested_backend: backend_id,
256
+ preference: [],
257
+ required_capabilities: backend_identity.fetch(:capabilities),
258
+ candidates: [
259
+ {
260
+ backend_id: backend_id,
261
+ available: true,
262
+ compatible: true,
263
+ selected: true,
264
+ reasons: []
265
+ }
266
+ ],
267
+ selected_backend: backend_id,
268
+ selection_reason: 'explicit-compatible-backend',
269
+ registry_digest: registry_digest
270
+ }
271
+ end
272
+
273
+ def registry_digest
274
+ registrations = TreeHaver.registered_languages(language.to_sym)
275
+ portable = registrations.keys.map(&:to_s).sort.to_h do |key|
276
+ config = registrations[key.to_sym] || {}
277
+ [
278
+ key,
279
+ config.slice(:gem_name, :contract, :symbol).transform_values do |value|
280
+ value.respond_to?(:name) && value.name ? value.name : value.to_s
281
+ end
282
+ ]
283
+ end
284
+ Digest::SHA256.hexdigest(self.class.canonical_json(portable))
285
+ end
286
+
287
+ def source_descriptor(include_content: false)
288
+ descriptor = {
289
+ source_id: source_id,
290
+ role: 'source',
291
+ byte_length: source.bytesize,
292
+ sha256: Digest::SHA256.hexdigest(source.b),
293
+ encoding: source.encoding.name.downcase,
294
+ bom: source.b.start_with?("\xEF\xBB\xBF".b),
295
+ line_endings: line_ending_profile,
296
+ final_newline: source.end_with?("\n", "\r")
297
+ }
298
+ descriptor[:content] = source if include_content
299
+ descriptor
300
+ end
301
+
302
+ def line_ending_profile
303
+ endings = source.scan(/\r\n|\r|\n/).uniq
304
+ return 'none' if endings.empty?
305
+ return 'mixed' if endings.length > 1
306
+
307
+ { "\n" => 'lf', "\r\n" => 'crlf', "\r" => 'cr' }.fetch(endings.first)
308
+ end
309
+
310
+ def normalize_backend_identity(identity)
311
+ normalized = Ast::Merge::ProviderContract.normalize_hash(identity)
312
+ required = %i[id family host_runtime package parser capabilities]
313
+ missing = required.reject { |key| normalized.key?(key) }
314
+ raise Error, "Backend identity is missing: #{missing.join(', ')}" unless missing.empty?
315
+ unless normalized.fetch(:id).to_s == backend_id
316
+ raise Error, 'Backend identity does not match selected backend'
317
+ end
318
+
319
+ normalized.merge(capabilities: Array(normalized.fetch(:capabilities)).map(&:to_s).sort.uniq)
320
+ end
321
+
322
+ def required_string(value, name)
323
+ normalized = value.to_s.strip
324
+ raise Error, "#{name} must not be empty" if normalized.empty?
325
+
326
+ normalized
327
+ end
328
+
329
+ def request_id
330
+ "#{snapshot_id}:parse:#{Digest::SHA256.hexdigest(source.b)[0, 12]}"
331
+ end
332
+
333
+ def output_bytes(result)
334
+ output = result[:output] || result['output']
335
+ output&.b
336
+ end
337
+
338
+ # Projects only the normalized TreeHaver contract. It never introspects
339
+ # backend-native objects; providers retain those facts in extensions.
340
+ class TreeProjection
341
+ def initialize(tree, source)
342
+ @tree = tree
343
+ @source = source
344
+ @nodes = []
345
+ @comments = []
346
+ @seen_comments = {}
347
+ end
348
+
349
+ def call
350
+ root_id = project_node(@tree.root_node, nil)
351
+ project_tree_comments
352
+ {
353
+ root_id: root_id,
354
+ nodes: @nodes,
355
+ comments: ordered_comments,
356
+ diagnostics: diagnostics
357
+ }
358
+ end
359
+
360
+ private
361
+
362
+ def project_node(node, parent_id)
363
+ id = "node:#{@nodes.length}"
364
+ record = node_record(node, id, parent_id)
365
+ @nodes << record
366
+ children = node.children
367
+ record[:children] = children.each_with_index.map do |child, index|
368
+ { node_id: project_node(child, id), index: index }
369
+ end
370
+ record_comment(node, id) if record.fetch(:role) == 'comment'
371
+ id
372
+ end
373
+
374
+ def project_tree_comments
375
+ @tree.comments.each do |comment|
376
+ key = comment_key(comment)
377
+ next if @seen_comments[key]
378
+
379
+ id = "node:#{@nodes.length}"
380
+ @nodes << comment_record(comment, id)
381
+ record_comment(comment, id)
382
+ end
383
+ end
384
+
385
+ def node_record(node, id, parent_id)
386
+ type = node.type.to_s
387
+ {
388
+ id: id,
389
+ type: type,
390
+ native_type: node.native_type.to_s,
391
+ role: node_role(node, type),
392
+ named: node.named?,
393
+ missing: node.missing?,
394
+ has_error: node.has_error?,
395
+ span: span(node),
396
+ parent_id: parent_id,
397
+ children: [],
398
+ semantic_roles: [],
399
+ unsupported_features: [],
400
+ extensions: [],
401
+ metadata: {}
402
+ }
403
+ end
404
+
405
+ def comment_record(comment, id)
406
+ {
407
+ id: id,
408
+ type: comment.type.to_s,
409
+ native_type: comment.type.to_s,
410
+ role: 'comment',
411
+ named: false,
412
+ missing: false,
413
+ has_error: false,
414
+ span: span(comment),
415
+ parent_id: nil,
416
+ children: [],
417
+ semantic_roles: ['comment'],
418
+ unsupported_features: [],
419
+ extensions: [],
420
+ metadata: {}
421
+ }
422
+ end
423
+
424
+ def node_role(node, type)
425
+ return 'error' if node.has_error? || node.missing?
426
+ return 'comment' if type.include?('comment')
427
+
428
+ node.named? ? 'structural' : 'token'
429
+ end
430
+
431
+ def record_comment(comment, node_id)
432
+ key = comment_key(comment)
433
+ @seen_comments[key] = true
434
+ @comments << {
435
+ node_id: node_id,
436
+ style: optional_value(comment, :style),
437
+ native_kind: comment.type.to_s,
438
+ attachment_hint: optional_value(comment, :attachment_hint) || 'unknown'
439
+ }
440
+ end
441
+
442
+ def comment_key(comment)
443
+ [comment.start_byte, comment.end_byte]
444
+ end
445
+
446
+ def ordered_comments
447
+ nodes_by_id = @nodes.to_h { |node| [node.fetch(:id), node] }
448
+ @comments.sort_by do |comment|
449
+ range = nodes_by_id.fetch(comment.fetch(:node_id)).dig(:span, :range)
450
+ [range.fetch(:start_byte), range.fetch(:end_byte), comment.fetch(:node_id)]
451
+ end
452
+ end
453
+
454
+ def optional_value(object, method_name)
455
+ object.respond_to?(method_name) && object.public_send(method_name)&.to_s
456
+ end
457
+
458
+ def span(node)
459
+ {
460
+ range: { start_byte: node.start_byte, end_byte: node.end_byte },
461
+ start_point: point(node.start_point),
462
+ end_point: point(node.end_point)
463
+ }
464
+ end
465
+
466
+ def point(value)
467
+ {
468
+ row: value.respond_to?(:row) ? value.row : value[:row],
469
+ column: value.respond_to?(:column) ? value.column : value[:column]
470
+ }
471
+ end
472
+
473
+ def diagnostics
474
+ [
475
+ *@tree.errors.map { |error| diagnostic(error, 'error', true) },
476
+ *@tree.warnings.map { |warning| diagnostic(warning, 'warning', false) }
477
+ ]
478
+ end
479
+
480
+ def diagnostic(value, severity, blocking)
481
+ {
482
+ id: "diagnostic:#{severity}:#{value.class.name}:#{value}",
483
+ severity: severity,
484
+ category: 'parser_diagnostic',
485
+ code: value.respond_to?(:type) ? value.type.to_s : nil,
486
+ message: value.to_s,
487
+ source_role: 'source',
488
+ blocking: blocking,
489
+ metadata: {}
490
+ }
491
+ end
492
+ end
493
+ end
494
+ # rubocop:enable Metrics/AbcSize, Metrics/ClassLength, Metrics/MethodLength, Metrics/ParameterLists
495
+ end
496
+ end
497
+ end
@@ -34,6 +34,9 @@ require_relative 'rspec/dependency_tags'
34
34
  # Load conformance fixture helpers for shared fixture harnesses
35
35
  require_relative 'rspec/conformance_fixtures'
36
36
 
37
+ # Load portable provider snapshot and differential replay support
38
+ require_relative 'rspec/provider_snapshot'
39
+
37
40
  # Load Ast::Merge shared examples (for testing *-merge implementations)
38
41
  require_relative 'rspec/shared_examples'
39
42
 
@@ -5,7 +5,7 @@ module Ast
5
5
  # Version namespace for this gem.
6
6
  module Version
7
7
  # Current gem version.
8
- VERSION = '7.1.7'
8
+ VERSION = '7.1.8'
9
9
  end
10
10
  # Current gem version exposed at the traditional constant location.
11
11
  VERSION = Version::VERSION # Traditional Constant Location
data/lib/ast/merge.rb CHANGED
@@ -87,7 +87,6 @@ module Ast
87
87
  autoload :ProviderContract, 'ast/merge/provider_contract'
88
88
  autoload :ProviderRegistry, 'ast/merge/provider_registry'
89
89
  autoload :ProviderResult, 'ast/merge/provider_result'
90
- autoload :YamlDocument, 'ast/merge/yaml_document'
91
90
  autoload :SectionTyping, 'ast/merge/section_typing'
92
91
  autoload :SmartMergerBase, 'ast/merge/smart_merger_base'
93
92
  autoload :SourceRegionReportSupport, 'ast/merge/source_region_report_support'
data.tar.gz.sig CHANGED
Binary file
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: ast-merge
3
3
  version: !ruby/object:Gem::Version
4
- version: 7.1.7
4
+ version: 7.1.8
5
5
  platform: ruby
6
6
  authors:
7
7
  - Peter H. Boling
@@ -77,14 +77,14 @@ dependencies:
77
77
  requirements:
78
78
  - - '='
79
79
  - !ruby/object:Gem::Version
80
- version: 7.1.7
80
+ version: 7.1.8
81
81
  type: :runtime
82
82
  prerelease: false
83
83
  version_requirements: !ruby/object:Gem::Requirement
84
84
  requirements:
85
85
  - - '='
86
86
  - !ruby/object:Gem::Version
87
- version: 7.1.7
87
+ version: 7.1.8
88
88
  - !ruby/object:Gem::Dependency
89
89
  name: version_gem
90
90
  requirement: !ruby/object:Gem::Requirement
@@ -114,7 +114,7 @@ dependencies:
114
114
  version: '3.0'
115
115
  - - ">="
116
116
  - !ruby/object:Gem::Version
117
- version: 3.0.17
117
+ version: 3.0.31
118
118
  type: :development
119
119
  prerelease: false
120
120
  version_requirements: !ruby/object:Gem::Requirement
@@ -124,7 +124,7 @@ dependencies:
124
124
  version: '3.0'
125
125
  - - ">="
126
126
  - !ruby/object:Gem::Version
127
- version: 3.0.17
127
+ version: 3.0.31
128
128
  - !ruby/object:Gem::Dependency
129
129
  name: bundler-audit
130
130
  requirement: !ruby/object:Gem::Requirement
@@ -202,7 +202,7 @@ dependencies:
202
202
  version: '3.2'
203
203
  - - ">="
204
204
  - !ruby/object:Gem::Version
205
- version: 3.2.2
205
+ version: 3.2.3
206
206
  type: :development
207
207
  prerelease: false
208
208
  version_requirements: !ruby/object:Gem::Requirement
@@ -212,7 +212,7 @@ dependencies:
212
212
  version: '3.2'
213
213
  - - ">="
214
214
  - !ruby/object:Gem::Version
215
- version: 3.2.2
215
+ version: 3.2.3
216
216
  - !ruby/object:Gem::Dependency
217
217
  name: kettle-test
218
218
  requirement: !ruby/object:Gem::Requirement
@@ -242,7 +242,7 @@ dependencies:
242
242
  version: '3.2'
243
243
  - - ">="
244
244
  - !ruby/object:Gem::Version
245
- version: 3.2.6
245
+ version: 3.2.7
246
246
  type: :development
247
247
  prerelease: false
248
248
  version_requirements: !ruby/object:Gem::Requirement
@@ -252,7 +252,7 @@ dependencies:
252
252
  version: '3.2'
253
253
  - - ">="
254
254
  - !ruby/object:Gem::Version
255
- version: 3.2.6
255
+ version: 3.2.7
256
256
  - !ruby/object:Gem::Dependency
257
257
  name: ruby-progressbar
258
258
  requirement: !ruby/object:Gem::Requirement
@@ -400,6 +400,7 @@ files:
400
400
  - lib/ast/merge/rspec/dependency_tags_helpers.rb
401
401
  - lib/ast/merge/rspec/merge_gem_registry.rb
402
402
  - lib/ast/merge/rspec/provider_conformance.rb
403
+ - lib/ast/merge/rspec/provider_snapshot.rb
403
404
  - lib/ast/merge/rspec/setup.rb
404
405
  - lib/ast/merge/rspec/shared_examples.rb
405
406
  - lib/ast/merge/rspec/shared_examples/comment_attachment.rb
@@ -489,7 +490,6 @@ files:
489
490
  - lib/ast/merge/unresolved_review_state.rb
490
491
  - lib/ast/merge/unresolved_support.rb
491
492
  - lib/ast/merge/version.rb
492
- - lib/ast/merge/yaml_document.rb
493
493
  - sig/ast/merge.rbs
494
494
  homepage: https://github.com/structuredmerge/structuredmerge-ruby
495
495
  licenses:
@@ -497,10 +497,10 @@ licenses:
497
497
  - PolyForm-Small-Business-1.0.0
498
498
  metadata:
499
499
  homepage_uri: https://structuredmerge.org
500
- source_code_uri: https://github.com/structuredmerge/structuredmerge-ruby/tree/v7.1.7
501
- changelog_uri: https://github.com/structuredmerge/structuredmerge-ruby/blob/v7.1.7/CHANGELOG.md
500
+ source_code_uri: https://github.com/structuredmerge/structuredmerge-ruby/tree/v7.1.8
501
+ changelog_uri: https://github.com/structuredmerge/structuredmerge-ruby/blob/v7.1.8/CHANGELOG.md
502
502
  bug_tracker_uri: https://github.com/structuredmerge/structuredmerge-ruby/issues
503
- documentation_uri: https://www.rubydoc.info/gems/ast-merge/7.1.7
503
+ documentation_uri: https://www.rubydoc.info/gems/ast-merge/7.1.8
504
504
  funding_uri: https://github.com/sponsors/pboling
505
505
  wiki_uri: https://github.com/structuredmerge/structuredmerge-ruby/wiki
506
506
  news_uri: https://www.railsbling.com/tags/ast-merge
metadata.gz.sig CHANGED
Binary file
@@ -1,159 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- require 'json'
4
-
5
- module Ast
6
- module Merge
7
- # Parser-neutral semantic analysis shared by YAML backend providers.
8
- # rubocop:disable Metrics/AbcSize, Metrics/CyclomaticComplexity, Metrics/MethodLength, Metrics/ModuleLength, Metrics/PerceivedComplexity -- recursive YAML validation, rendering, and ownership are one parser-neutral semantic boundary
9
- module YamlDocument
10
- module_function
11
-
12
- def analyze(parsed, dialect)
13
- return unsupported("Unsupported YAML dialect #{dialect}.") unless dialect == 'yaml'
14
- return parse_error('YAML documents must parse to a mapping root.') unless parsed.is_a?(Hash)
15
-
16
- validated = validate_node(parsed, '')
17
- return { ok: false, diagnostics: [validated[:diagnostic]], policies: [] } unless validated[:ok]
18
-
19
- {
20
- ok: true,
21
- diagnostics: [],
22
- analysis: {
23
- kind: 'yaml',
24
- dialect: 'yaml',
25
- normalized_source: canonical_yaml(validated[:value]),
26
- document: validated[:value],
27
- root_kind: 'mapping',
28
- owners: collect_owners(validated[:value])
29
- },
30
- policies: []
31
- }
32
- end
33
-
34
- def match_owners(template, destination)
35
- destination_paths = destination[:owners].to_h { |owner| [owner[:path], true] }
36
- template_paths = template[:owners].to_h { |owner| [owner[:path], true] }
37
-
38
- {
39
- matched: template[:owners]
40
- .filter { |owner| destination_paths[owner[:path]] }
41
- .map { |owner| { template_path: owner[:path], destination_path: owner[:path] } },
42
- unmatched_template: template[:owners].map { |owner| owner[:path] }.reject { |path| destination_paths[path] },
43
- unmatched_destination: destination[:owners]
44
- .map { |owner| owner[:path] }
45
- .reject { |path| template_paths[path] }
46
- }
47
- end
48
-
49
- def validate_node(value, path)
50
- if scalar?(value)
51
- { ok: true, value: value }
52
- elsif value.is_a?(Array)
53
- value.each_with_index.each_with_object({ ok: true, value: [] }) do |(item, index), memo|
54
- validated = validate_node(item, "#{path}/#{index}")
55
- return validated unless validated[:ok]
56
-
57
- memo[:value] << validated[:value]
58
- end
59
- elsif value.is_a?(Hash)
60
- value.each_with_object({ ok: true, value: {} }) do |(key, item), memo|
61
- validated = validate_node(item, "#{path}/#{key}")
62
- return validated unless validated[:ok]
63
-
64
- memo[:value][key] = validated[:value]
65
- end
66
- else
67
- unsupported("Unsupported YAML value at #{path.empty? ? '/' : path}. " \
68
- 'Only mappings, scalar values, and sequences are supported.')
69
- end
70
- end
71
- private_class_method :validate_node
72
-
73
- def scalar?(value)
74
- value.nil? || value.is_a?(String) || value.is_a?(Numeric) || value == true || value == false
75
- end
76
- private_class_method :scalar?
77
-
78
- def canonical_yaml(mapping)
79
- "#{render_mapping(mapping).join("\n")}\n"
80
- end
81
- private_class_method :canonical_yaml
82
-
83
- def render_mapping(mapping, indent = 0)
84
- mapping.flat_map do |key, value|
85
- prefix = ' ' * indent
86
- if value.is_a?(Array)
87
- ["#{prefix}#{key}:"] + render_sequence(value, indent + 2)
88
- elsif value.is_a?(Hash)
89
- ["#{prefix}#{key}:"] + render_mapping(value, indent + 2)
90
- elsif value.nil?
91
- ["#{prefix}#{key}:"]
92
- else
93
- ["#{prefix}#{key}: #{render_scalar(value)}"]
94
- end
95
- end
96
- end
97
- private_class_method :render_mapping
98
-
99
- def render_sequence(sequence, indent)
100
- prefix = ' ' * indent
101
- sequence.flat_map do |item|
102
- if scalar?(item)
103
- ["#{prefix}- #{render_scalar(item)}"]
104
- elsif item.is_a?(Hash)
105
- ["#{prefix}-"] + render_mapping(item, indent + 2)
106
- elsif item.is_a?(Array)
107
- ["#{prefix}-"] + render_sequence(item, indent + 2)
108
- end
109
- end
110
- end
111
- private_class_method :render_sequence
112
-
113
- def render_scalar(value)
114
- return '' if value.nil?
115
- return value ? 'true' : 'false' if [true, false].include?(value)
116
- return value.to_s unless value.is_a?(String)
117
-
118
- value.match?(/\A[A-Za-z0-9_.-]+\z/) ? value : JSON.generate(value)
119
- end
120
- private_class_method :render_scalar
121
-
122
- def collect_owners(mapping, prefix = '')
123
- mapping.keys.sort.flat_map do |key|
124
- path = "#{prefix}/#{key}"
125
- value = mapping[key]
126
- if value.is_a?(Array)
127
- [{ path: path, owner_kind: 'key_value', match_key: key }] +
128
- value.each_with_index.flat_map do |item, index|
129
- item_path = "#{path}/#{index}"
130
- nested = item.is_a?(Hash) ? collect_owners(item, item_path) : []
131
- [{ path: item_path, owner_kind: 'sequence_item' }] + nested
132
- end
133
- elsif value.is_a?(Hash)
134
- [{ path: path, owner_kind: 'mapping', match_key: key }] + collect_owners(value, path)
135
- else
136
- [{ path: path, owner_kind: 'key_value', match_key: key }]
137
- end
138
- end
139
- end
140
- private_class_method :collect_owners
141
-
142
- def parse_error(message)
143
- { ok: false, diagnostics: [{ severity: 'error', category: 'parse_error', message: message }], policies: [] }
144
- end
145
- private_class_method :parse_error
146
-
147
- def unsupported(message)
148
- {
149
- ok: false,
150
- diagnostic: { severity: 'error', category: 'unsupported_feature', message: message },
151
- diagnostics: [{ severity: 'error', category: 'unsupported_feature', message: message }],
152
- policies: []
153
- }
154
- end
155
- private_class_method :unsupported
156
- end
157
- # rubocop:enable Metrics/AbcSize, Metrics/CyclomaticComplexity, Metrics/MethodLength, Metrics/ModuleLength, Metrics/PerceivedComplexity
158
- end
159
- end