ruby-merge 7.0.0

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 ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: 3d9fb197110a8e0a388d9daf71e144696f7db240ff5f59bf9d6cecf10518950b
4
+ data.tar.gz: 01ea7a23152ec998ea17b1e4463a1ce7115e5b4463202c5ecd73ab1829a4a224
5
+ SHA512:
6
+ metadata.gz: ab2fe88791d7ec23b4b9e1525a0726638e3afa01af7136e92e67b89eaa821f290187b11d96885caf44fb51adffdb12e9f6da01c25bc333e1f2358b8f84b756f2
7
+ data.tar.gz: 301910a3b44cf21892fb23b1c25106ca1ac82f53481121bb921d7978bab9efa9ba574bc8c6ba574fdb51cf7166d8a550abde90e4b9efae76f0c22219e3af8531
checksums.yaml.gz.sig ADDED
Binary file
@@ -0,0 +1,11 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Ruby
4
+ module Merge
5
+ module Version
6
+ VERSION = "7.0.0"
7
+ end
8
+
9
+ VERSION = Version::VERSION
10
+ end
11
+ end
data/lib/ruby/merge.rb ADDED
@@ -0,0 +1,779 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "tree_haver"
4
+ require "ast/merge"
5
+
6
+ module Ruby
7
+ module Merge
8
+ extend self
9
+
10
+ PACKAGE_NAME = "ruby-merge"
11
+ TREE_SITTER_BACKEND = TreeHaver::KREUZBERG_LANGUAGE_PACK_BACKEND
12
+ DESTINATION_WINS_ARRAY_POLICY = { surface: "array", name: "destination_wins_array" }.freeze
13
+ DIRECTIVE_LINE = /\A(?::nocov:|[\w-]+:(?:freeze|unfreeze))\z/
14
+ MAGIC_COMMENT_PREFIXES = %w[coding encoding frozen_string_literal shareable_constant_value typed warn_indent].freeze
15
+ REQUIRE_PATTERN = /^\s*require(?:_relative)?\s+["']([^"']+)["']/.freeze
16
+ DSL_CALL_PATTERN = /^(?<name>source|gemspec|git_source|gem|eval_gemfile|platform|group|desc|task)\b/.freeze
17
+ RAKEFILE_DEFAULT_TASK_COMMENT = "# Define a base default task early so other files can enhance it."
18
+ RAKEFILE_DEFAULT_TASK_DESC = 'desc "Default tasks aggregator"'
19
+ CLASS_PATTERN = /^\s*class\s+([A-Z]\w*(?:::\w+)*)/.freeze
20
+ MODULE_PATTERN = /^\s*module\s+([A-Z]\w*(?:::\w+)*)/.freeze
21
+ DEF_PATTERN = /^\s*def\s+(?:self\.)?([a-zA-Z_]\w*[!?=]?)/.freeze
22
+ EXAMPLE_TAG = /\A@example\b(?<rest>.*)\z/.freeze
23
+ TAG_PREFIX = /\A@[a-z_]+\b/.freeze
24
+
25
+ def ruby_feature_profile
26
+ {
27
+ family: "ruby",
28
+ supported_dialects: ["ruby"],
29
+ supported_policies: [DESTINATION_WINS_ARRAY_POLICY]
30
+ }
31
+ end
32
+
33
+ def available_ruby_backends
34
+ [TREE_SITTER_BACKEND]
35
+ end
36
+
37
+ def ruby_backend_feature_profile(backend: nil)
38
+ requested = backend.to_s.empty? ? TREE_SITTER_BACKEND.id : backend.to_s
39
+ return unsupported_feature_result("Unsupported Ruby backend #{requested}.") unless requested == TREE_SITTER_BACKEND.id
40
+
41
+ ruby_feature_profile.merge(
42
+ backend: requested,
43
+ backend_ref: TREE_SITTER_BACKEND.to_h,
44
+ supports_dialects: true
45
+ )
46
+ end
47
+
48
+ def ruby_plan_context(backend: nil)
49
+ profile = ruby_backend_feature_profile(backend: backend)
50
+ return profile if profile[:ok] == false
51
+
52
+ {
53
+ family_profile: ruby_feature_profile,
54
+ feature_profile: {
55
+ backend: profile[:backend],
56
+ supports_dialects: true,
57
+ supported_policies: profile[:supported_policies]
58
+ }
59
+ }
60
+ end
61
+
62
+ def parse_ruby(source, dialect, backend: nil)
63
+ requested = backend.to_s.empty? ? TREE_SITTER_BACKEND.id : backend.to_s
64
+ return unsupported_feature_result("Unsupported Ruby dialect #{dialect}.") unless dialect == "ruby"
65
+ return unsupported_feature_result("Unsupported Ruby backend #{requested}.") unless requested == TREE_SITTER_BACKEND.id
66
+
67
+ syntax = TreeHaver.parse_with_language_pack(
68
+ TreeHaver::ParserRequest.new(source: source, language: "ruby", dialect: dialect)
69
+ )
70
+ return { ok: false, diagnostics: syntax[:diagnostics], policies: [] } unless syntax[:ok]
71
+
72
+ {
73
+ ok: true,
74
+ diagnostics: [],
75
+ analysis: analyze_ruby_document(source),
76
+ policies: []
77
+ }
78
+ end
79
+
80
+ def match_ruby_owners(template, destination)
81
+ destination_paths = destination[:owners].to_h { |owner| [owner[:path], true] }
82
+ template_paths = template[:owners].to_h { |owner| [owner[:path], true] }
83
+ {
84
+ matched: template[:owners]
85
+ .filter { |owner| destination_paths[owner[:path]] }
86
+ .map { |owner| { template_path: owner[:path], destination_path: owner[:path] } },
87
+ unmatched_template: template[:owners].map { |owner| owner[:path] }.reject { |path| destination_paths[path] },
88
+ unmatched_destination: destination[:owners].map { |owner| owner[:path] }.reject { |path| template_paths[path] }
89
+ }
90
+ end
91
+
92
+ def merge_ruby(template_source, destination_source, dialect, merge_template_requires: false)
93
+ template = parse_ruby(template_source, dialect)
94
+ return template unless template[:ok]
95
+
96
+ destination = parse_ruby(destination_source, dialect)
97
+ unless destination[:ok]
98
+ return {
99
+ ok: false,
100
+ diagnostics: destination[:diagnostics].map do |diagnostic|
101
+ diagnostic[:category] == "parse_error" ? diagnostic.merge(category: "destination_parse_error") : diagnostic
102
+ end,
103
+ policies: []
104
+ }
105
+ end
106
+
107
+ destination_requires = collect_ruby_require_entries(destination.dig(:analysis, :source))
108
+ template_requires = collect_ruby_require_entries(template.dig(:analysis, :source))
109
+ destination_declarations = collect_ruby_declaration_entries(destination.dig(:analysis, :source))
110
+ template_declarations = collect_ruby_declaration_entries(template.dig(:analysis, :source))
111
+ destination_paths = destination_declarations.to_h { |entry| [entry[:path], true] }
112
+ destination_dsl = collect_top_level_dsl_entries(destination.dig(:analysis, :source))
113
+ template_dsl = collect_top_level_dsl_entries(template.dig(:analysis, :source))
114
+ sections = []
115
+ preamble = collect_ruby_preamble(destination.dig(:analysis, :source))
116
+ sections << preamble unless preamble.empty?
117
+ requires = merge_template_requires ? merge_ruby_requires(destination_requires, template_requires) : destination_requires
118
+ require_block = requires.map { |entry| entry[:text] }.join("\n").strip
119
+ sections << require_block unless require_block.empty?
120
+ sections.concat(merge_top_level_dsl_entries(destination_dsl, template_dsl).map { |entry| entry[:text] })
121
+ sections.concat(destination_declarations.map { |entry| entry[:text] })
122
+ sections.concat(
123
+ template_declarations.reject { |entry| destination_paths[entry[:path]] }.map { |entry| entry[:text] }
124
+ )
125
+
126
+ output = "#{sections.join("\n\n").strip}\n"
127
+
128
+ {
129
+ ok: true,
130
+ diagnostics: [],
131
+ output: normalize_rakefile_default_task_scaffold(output),
132
+ policies: [DESTINATION_WINS_ARRAY_POLICY]
133
+ }
134
+ end
135
+
136
+ def ruby_discovered_surfaces(analysis)
137
+ analysis[:discovered_surfaces] || []
138
+ end
139
+
140
+ def ruby_delegated_child_operations(analysis, parent_operation_id: "ruby-document-0")
141
+ surfaces = ruby_discovered_surfaces(analysis)
142
+ doc_operation_ids = {}
143
+ operations = []
144
+
145
+ surfaces.each_with_index do |surface, index|
146
+ next unless surface[:surface_kind] == "ruby_doc_comment"
147
+
148
+ operation_id = "ruby-doc-comment-#{index}"
149
+ doc_operation_ids[surface[:address]] = operation_id
150
+ operations << Ast::Merge.delegated_child_operation(
151
+ operation_id: operation_id,
152
+ parent_operation_id: parent_operation_id,
153
+ requested_strategy: "delegate_child_surface",
154
+ language_chain: ["ruby", surface[:effective_language]],
155
+ surface: surface
156
+ )
157
+ end
158
+
159
+ example_index = 0
160
+ surfaces.each do |surface|
161
+ next unless surface[:surface_kind] == "yard_example_block"
162
+
163
+ operations << Ast::Merge.delegated_child_operation(
164
+ operation_id: "yard-example-#{example_index}",
165
+ parent_operation_id: doc_operation_ids.fetch(surface[:parent_address], parent_operation_id),
166
+ requested_strategy: "delegate_child_surface",
167
+ language_chain: ["ruby", "yard", surface[:effective_language]],
168
+ surface: surface
169
+ )
170
+ example_index += 1
171
+ end
172
+
173
+ operations
174
+ end
175
+
176
+ def apply_ruby_delegated_child_outputs(source, delegated_operations, apply_plan, applied_children)
177
+ lines = normalize_source(source).split("\n")
178
+ operations_by_id = delegated_operations.to_h { |operation| [operation[:operation_id], operation] }
179
+ outputs_by_id = applied_children.to_h { |entry| [entry[:operation_id], entry[:output]] }
180
+
181
+ replacements = apply_plan[:entries].filter_map do |entry|
182
+ operation = operations_by_id[entry.dig(:delegated_group, :child_operation_id)]
183
+ output = outputs_by_id[entry.dig(:delegated_group, :child_operation_id)]
184
+ span = operation&.dig(:surface, :span)
185
+ next if operation.nil? || output.nil? || span.nil?
186
+
187
+ { start: span[:start_line] - 1, finish: span[:end_line] - 1, output: output }
188
+ end
189
+
190
+ replacements.sort_by { |entry| -entry[:start] }.each do |entry|
191
+ prefix = comment_prefix_for(lines[entry[:start]])
192
+ replacement_lines = entry[:output].empty? ? [] : entry[:output].sub(/\n\z/, "").split("\n").map { |line| "#{prefix}#{line}" }
193
+ lines[entry[:start]..entry[:finish]] = replacement_lines
194
+ end
195
+
196
+ {
197
+ ok: true,
198
+ diagnostics: [],
199
+ output: "#{lines.join("\n").sub(/\n+\z/, "")}\n",
200
+ policies: [DESTINATION_WINS_ARRAY_POLICY]
201
+ }
202
+ end
203
+
204
+ def merge_ruby_with_nested_outputs(template_source, destination_source, dialect, nested_outputs)
205
+ Ast::Merge.execute_nested_merge(
206
+ nested_outputs,
207
+ default_family: "ruby",
208
+ request_id_prefix: "nested_ruby_child",
209
+ merge_parent: -> { merge_ruby(template_source, destination_source, dialect) },
210
+ discover_operations: lambda { |merged_output|
211
+ analysis = parse_ruby(merged_output, dialect)
212
+ next { ok: false, diagnostics: analysis[:diagnostics] || [] } unless analysis[:ok]
213
+
214
+ {
215
+ ok: true,
216
+ diagnostics: [],
217
+ operations: ruby_delegated_child_operations(analysis[:analysis])
218
+ }
219
+ },
220
+ apply_resolved_outputs: lambda { |merged_output, operations, apply_plan, applied_children|
221
+ apply_ruby_delegated_child_outputs(
222
+ merged_output,
223
+ operations,
224
+ apply_plan,
225
+ applied_children
226
+ )
227
+ }
228
+ )
229
+ end
230
+
231
+ def merge_ruby_with_reviewed_nested_outputs(template_source, destination_source, dialect, review_state, applied_children)
232
+ Ast::Merge.execute_reviewed_nested_merge(
233
+ review_state,
234
+ "ruby",
235
+ applied_children,
236
+ merge_parent: -> { merge_ruby(template_source, destination_source, dialect) },
237
+ discover_operations: lambda { |merged_output|
238
+ analysis = parse_ruby(merged_output, dialect)
239
+ next({ ok: false, diagnostics: analysis[:diagnostics] || [] }) unless analysis[:ok]
240
+
241
+ {
242
+ ok: true,
243
+ diagnostics: [],
244
+ operations: ruby_delegated_child_operations(analysis[:analysis])
245
+ }
246
+ },
247
+ apply_resolved_outputs: lambda { |merged_output, operations, apply_plan, resolved_children|
248
+ apply_ruby_delegated_child_outputs(
249
+ merged_output,
250
+ operations,
251
+ apply_plan,
252
+ resolved_children
253
+ )
254
+ }
255
+ )
256
+ end
257
+
258
+ def merge_ruby_with_reviewed_nested_outputs_from_replay_bundle(template_source, destination_source, dialect, replay_bundle)
259
+ execution = Array(replay_bundle[:reviewed_nested_executions]).find { |entry| entry[:family] == "ruby" }
260
+ return { ok: false, diagnostics: [{ severity: "error", category: "configuration_error", message: "review replay bundle does not include a reviewed nested execution for ruby." }], policies: [] } unless execution
261
+
262
+ merge_ruby_with_reviewed_nested_outputs(
263
+ template_source,
264
+ destination_source,
265
+ dialect,
266
+ execution[:review_state],
267
+ execution[:applied_children]
268
+ )
269
+ end
270
+
271
+ def merge_ruby_with_reviewed_nested_outputs_from_review_state(template_source, destination_source, dialect, review_state)
272
+ execution = Array(review_state[:reviewed_nested_executions]).find { |entry| entry[:family] == "ruby" }
273
+ return { ok: false, diagnostics: [{ severity: "error", category: "configuration_error", message: "review state does not include a reviewed nested execution for ruby." }], policies: [] } unless execution
274
+
275
+ merge_ruby_with_reviewed_nested_outputs(
276
+ template_source,
277
+ destination_source,
278
+ dialect,
279
+ execution[:review_state],
280
+ execution[:applied_children]
281
+ )
282
+ end
283
+
284
+ def merge_ruby_with_reviewed_nested_outputs_from_replay_bundle_envelope(template_source, destination_source, dialect, envelope)
285
+ replay_bundle, import_error = Ast::Merge.import_review_replay_bundle_envelope(envelope)
286
+ return { ok: false, diagnostics: [{ severity: "error", category: import_error[:category], message: import_error[:message] }], policies: [] } if import_error
287
+
288
+ merge_ruby_with_reviewed_nested_outputs_from_replay_bundle(
289
+ template_source,
290
+ destination_source,
291
+ dialect,
292
+ replay_bundle
293
+ )
294
+ end
295
+
296
+ def merge_ruby_with_reviewed_nested_outputs_from_review_state_envelope(template_source, destination_source, dialect, envelope)
297
+ review_state, import_error = Ast::Merge.import_conformance_manifest_review_state_envelope(envelope)
298
+ return { ok: false, diagnostics: [{ severity: "error", category: import_error[:category], message: import_error[:message] }], policies: [] } if import_error
299
+
300
+ merge_ruby_with_reviewed_nested_outputs_from_review_state(
301
+ template_source,
302
+ destination_source,
303
+ dialect,
304
+ review_state
305
+ )
306
+ end
307
+
308
+ def analyze_ruby_document(source)
309
+ lines = normalize_source(source).split("\n", -1)
310
+ requires = []
311
+ declarations = []
312
+ discovered_surfaces = []
313
+ pending_comments = []
314
+
315
+ lines.each_with_index do |line, index|
316
+ line_number = index + 1
317
+ stripped = line.strip
318
+
319
+ if comment_line?(line)
320
+ pending_comments << { line: line_number, raw: line }
321
+ next
322
+ end
323
+
324
+ if stripped.empty?
325
+ pending_comments = []
326
+ next
327
+ end
328
+
329
+ if (match = REQUIRE_PATTERN.match(line))
330
+ requires << {
331
+ path: "/requires/#{requires.length}",
332
+ owner_kind: "require",
333
+ match_key: match[1]
334
+ }
335
+ pending_comments = []
336
+ next
337
+ end
338
+
339
+ declaration = declaration_for_line(line)
340
+ if declaration
341
+ declarations << {
342
+ path: "/declarations/#{declaration[:name]}",
343
+ owner_kind: "declaration",
344
+ match_key: declaration[:name]
345
+ }
346
+ surfaces = surfaces_for_owner(
347
+ owner_name: declaration[:name],
348
+ comment_entries: pending_comments
349
+ )
350
+ discovered_surfaces.concat(surfaces)
351
+ pending_comments = []
352
+ next
353
+ end
354
+
355
+ pending_comments = []
356
+ end
357
+
358
+ {
359
+ kind: "ruby",
360
+ dialect: "ruby",
361
+ root_kind: "document",
362
+ source: normalize_source(source),
363
+ owners: (requires + declarations).sort_by { |owner| owner[:path] },
364
+ discovered_surfaces: discovered_surfaces
365
+ }
366
+ end
367
+
368
+ def collect_ruby_require_entries(source)
369
+ normalize_source(source).split("\n").filter_map do |line|
370
+ match = REQUIRE_PATTERN.match(line)
371
+ next unless match
372
+
373
+ { path: "/requires/#{match[1]}", text: line.rstrip }
374
+ end
375
+ end
376
+
377
+ def collect_ruby_preamble(source)
378
+ lines = normalize_source(source).split("\n")
379
+ preamble = []
380
+ lines.each do |line|
381
+ break unless line.strip.empty? || comment_line?(line)
382
+
383
+ preamble << line.rstrip
384
+ end
385
+ preamble.join("\n").strip
386
+ end
387
+
388
+ def collect_top_level_dsl_entries(source)
389
+ lines = normalize_source(source).split("\n")
390
+ entries = []
391
+ pending_comments = []
392
+ index = 0
393
+
394
+ while index < lines.length
395
+ line = lines[index]
396
+ stripped = line.strip
397
+ if comment_line?(line)
398
+ pending_comments << index
399
+ index += 1
400
+ next
401
+ end
402
+ if stripped.empty?
403
+ pending_comments = []
404
+ index += 1
405
+ next
406
+ end
407
+ if REQUIRE_PATTERN.match?(line) || declaration_for_line(line)
408
+ pending_comments = []
409
+ index += 1
410
+ next
411
+ end
412
+
413
+ if line.match?(/\Abegin\b/)
414
+ start_index = pending_comments.first || index
415
+ finish_index = ruby_block_finish_index(lines, index)
416
+ text = lines[start_index..finish_index].join("\n").strip
417
+ signature = begin_block_signature(text)
418
+ entries << { path: "/dsl/#{signature}", name: "begin", signature: signature, text: text }
419
+ pending_comments = []
420
+ index = finish_index + 1
421
+ next
422
+ end
423
+
424
+ match = DSL_CALL_PATTERN.match(line)
425
+ unless match
426
+ pending_comments = []
427
+ index += 1
428
+ next
429
+ end
430
+
431
+ name = match[:name]
432
+ if name == "desc" && next_code_line_is_task?(lines, index + 1)
433
+ pending_comments << index
434
+ index += 1
435
+ next
436
+ end
437
+
438
+ start_index = pending_comments.first || index
439
+ finish_index = dsl_entry_finish_index(lines, index)
440
+ text = lines[start_index..finish_index].join("\n").strip
441
+ signature = dsl_entry_signature(name, line)
442
+ entries << { path: "/dsl/#{signature}", name: name, signature: signature, text: text } if signature
443
+ pending_comments = []
444
+ index = finish_index + 1
445
+ end
446
+
447
+ entries
448
+ end
449
+
450
+ def merge_top_level_dsl_entries(destination_entries, template_entries)
451
+ destination_by_signature = destination_entries.to_h { |entry| [entry[:signature], entry] }
452
+ template_singletons = template_entries.select { |entry| dsl_singleton_entry?(entry) }
453
+ template_singleton_signatures = template_singletons.map { |entry| entry[:signature] }.to_h { |signature| [signature, true] }
454
+ result = []
455
+ result.concat(template_singletons)
456
+ result.concat(destination_entries.reject { |entry| template_singleton_signatures[entry[:signature]] })
457
+ result.concat(
458
+ template_entries.reject do |entry|
459
+ dsl_singleton_entry?(entry) || destination_by_signature[entry[:signature]]
460
+ end
461
+ )
462
+ result
463
+ end
464
+
465
+ def merge_ruby_requires(destination_requires, template_requires)
466
+ destination_paths = destination_requires.to_h { |entry| [entry[:path], true] }
467
+ destination_requires + template_requires.reject { |entry| destination_paths[entry[:path]] }
468
+ end
469
+
470
+ def collect_ruby_declaration_entries(source)
471
+ lines = normalize_source(source).split("\n")
472
+ entries = []
473
+ pending_comments = []
474
+ index = 0
475
+
476
+ while index < lines.length
477
+ line = lines[index]
478
+ stripped = line.strip
479
+
480
+ if comment_line?(line)
481
+ pending_comments << index
482
+ index += 1
483
+ next
484
+ end
485
+
486
+ if stripped.empty?
487
+ pending_comments = []
488
+ index += 1
489
+ next
490
+ end
491
+
492
+ if REQUIRE_PATTERN.match?(line)
493
+ pending_comments = []
494
+ index += 1
495
+ next
496
+ end
497
+
498
+ declaration = declaration_for_line(line)
499
+ unless declaration
500
+ pending_comments = []
501
+ index += 1
502
+ next
503
+ end
504
+
505
+ start_index = pending_comments.first || index
506
+ depth = 1
507
+ cursor = index + 1
508
+ while cursor < lines.length
509
+ candidate = lines[cursor].strip
510
+ depth += 1 if declaration_for_line(candidate)
511
+ if candidate == "end"
512
+ depth -= 1
513
+ if depth.zero?
514
+ cursor += 1
515
+ break
516
+ end
517
+ end
518
+ cursor += 1
519
+ end
520
+
521
+ entries << {
522
+ path: "/declarations/#{declaration[:name]}",
523
+ text: lines[start_index...cursor].join("\n").strip
524
+ }
525
+ pending_comments = []
526
+ index = cursor
527
+ end
528
+
529
+ entries
530
+ end
531
+
532
+ def unsupported_feature_result(message)
533
+ {
534
+ ok: false,
535
+ diagnostics: [{ severity: "error", category: "unsupported_feature", message: message }],
536
+ policies: []
537
+ }
538
+ end
539
+
540
+ private
541
+
542
+ def comment_line?(line)
543
+ line.lstrip.start_with?("#")
544
+ end
545
+
546
+ def declaration_for_line(line)
547
+ if (match = CLASS_PATTERN.match(line))
548
+ { kind: "class", name: match[1] }
549
+ elsif (match = MODULE_PATTERN.match(line))
550
+ { kind: "module", name: match[1] }
551
+ elsif (match = DEF_PATTERN.match(line))
552
+ { kind: "def", name: match[1] }
553
+ end
554
+ end
555
+
556
+ def next_code_line_is_task?(lines, start_index)
557
+ lines[start_index..].to_a.each do |line|
558
+ next if line.strip.empty? || comment_line?(line)
559
+
560
+ match = DSL_CALL_PATTERN.match(line)
561
+ return match && match[:name] == "task"
562
+ end
563
+ false
564
+ end
565
+
566
+ def dsl_entry_finish_index(lines, start_index)
567
+ return start_index unless lines[start_index].match?(/\bdo\b/)
568
+
569
+ ruby_block_finish_index(lines, start_index)
570
+ end
571
+
572
+ def ruby_block_finish_index(lines, start_index)
573
+ depth = 0
574
+ cursor = start_index
575
+ while cursor < lines.length
576
+ stripped = lines[cursor].strip
577
+ depth += stripped.scan(/\bdo\b/).length
578
+ depth += 1 if declaration_for_line(stripped) || stripped.match?(/\A(begin|if|unless|case|while|until|for)\b/)
579
+ depth -= 1 if stripped == "end"
580
+ return cursor if depth <= 0 && cursor > start_index
581
+
582
+ cursor += 1
583
+ end
584
+ lines.length - 1
585
+ end
586
+
587
+ def begin_block_signature(text)
588
+ require_path = text[/^\s*require(?:_relative)?\s+["']([^"']+)["']/, 1]
589
+ return "begin:require:#{require_path}" if require_path
590
+
591
+ "begin:#{text.lines.first.to_s.strip}"
592
+ end
593
+
594
+ def dsl_entry_signature(name, line)
595
+ case name
596
+ when "source", "gemspec"
597
+ name
598
+ when "git_source", "gem", "eval_gemfile", "platform", "group", "task"
599
+ first_argument = line[/\b#{Regexp.escape(name)}\s*(?:\(|\s)\s*["']([^"']+)["']/, 1] ||
600
+ line[/\b#{Regexp.escape(name)}\s*(?:\(|\s)\s*:([a-zA-Z_]\w*[!?=]?)/, 1]
601
+ first_argument ? "#{name}:#{normalize_dsl_argument(name, first_argument)}" : "#{name}:#{line.strip}"
602
+ when "desc"
603
+ "desc:#{line.strip}"
604
+ end
605
+ end
606
+
607
+ def normalize_dsl_argument(name, argument)
608
+ return argument.gsub(%r{/r\d+/}, "/") if name == "eval_gemfile"
609
+
610
+ argument
611
+ end
612
+
613
+ def dsl_singleton_entry?(entry)
614
+ %w[source gemspec].include?(entry[:name])
615
+ end
616
+
617
+ def normalize_rakefile_default_task_scaffold(content)
618
+ lines = normalize_source(content).split("\n")
619
+ desc_index = lines.find_index { |line| line.strip == RAKEFILE_DEFAULT_TASK_DESC }
620
+ return content unless desc_index
621
+
622
+ comment_index = preceding_code_line_index(lines, desc_index - 1)
623
+ return content unless comment_index && lines[comment_index].strip == RAKEFILE_DEFAULT_TASK_COMMENT
624
+
625
+ next_code_index = next_code_line_index(lines, desc_index + 1)
626
+ return content if next_code_index && lines[next_code_index].match?(/\Atask\s+:default\b/)
627
+
628
+ task_index = lines.each_index.find { |index| lines[index].match?(/\Atask\s+:default\b/) }
629
+ return content unless task_index
630
+
631
+ finish_index = dsl_entry_finish_index(lines, task_index)
632
+ task_block = lines[task_index..finish_index]
633
+ lines[task_index..finish_index] = []
634
+ insertion_index = lines.find_index { |line| line.strip == RAKEFILE_DEFAULT_TASK_DESC } + 1
635
+ insertion = task_block.dup
636
+ insertion << "" unless lines[insertion_index].to_s.strip.empty?
637
+ lines.insert(insertion_index, *insertion)
638
+ "#{lines.join("\n").sub(/\n+\z/, "")}\n"
639
+ end
640
+
641
+ def preceding_code_line_index(lines, start_index)
642
+ start_index.downto(0) do |index|
643
+ next if lines[index].strip.empty?
644
+
645
+ return index
646
+ end
647
+ nil
648
+ end
649
+
650
+ def next_code_line_index(lines, start_index)
651
+ start_index.upto(lines.length - 1) do |index|
652
+ next if lines[index].strip.empty?
653
+
654
+ return index
655
+ end
656
+ nil
657
+ end
658
+
659
+ def surfaces_for_owner(owner_name:, comment_entries:)
660
+ filtered_entries = comment_entries.filter { |entry| doc_comment_content?(entry[:raw]) }
661
+ return [] if filtered_entries.empty?
662
+
663
+ start_line = filtered_entries.first[:line]
664
+ end_line = filtered_entries.last[:line]
665
+ doc_surface = Ast::Merge.discovered_surface(
666
+ surface_kind: "ruby_doc_comment",
667
+ declared_language: "yard",
668
+ effective_language: "yard",
669
+ address: "document[0] > ruby_doc_comment[#{owner_name}]",
670
+ parent_address: "document[0]",
671
+ owner: Ast::Merge.surface_owner_ref(kind: "owned_region", address: "/declarations/#{owner_name}"),
672
+ span: Ast::Merge.surface_span(start_line: start_line, end_line: end_line),
673
+ reconstruction_strategy: "rewrite_with_prefix_preservation",
674
+ metadata: {
675
+ owner_signature: owner_name,
676
+ comment_prefix: comment_prefix_for(filtered_entries.first[:raw]),
677
+ entries: filtered_entries.map { |entry| { line: entry[:line], raw: entry[:raw] } }
678
+ }
679
+ )
680
+
681
+ [doc_surface] + example_surfaces_for(doc_surface)
682
+ end
683
+
684
+ def example_surfaces_for(surface)
685
+ entries = Array(surface.dig(:metadata, :entries))
686
+ normalized = entries.map { |entry| normalize_comment_content(entry[:raw]) }
687
+
688
+ normalized.each_with_index.filter_map do |content, tag_index|
689
+ match = EXAMPLE_TAG.match(content)
690
+ next unless match
691
+
692
+ body_start = tag_index + 1
693
+ body_end = next_tag_index(normalized, body_start) || normalized.length
694
+ next if body_start >= body_end
695
+
696
+ body_entries = entries[body_start...body_end]
697
+ next if body_entries.nil? || body_entries.empty?
698
+
699
+ declared_language = declared_example_language(match[:rest]) || "ruby"
700
+ Ast::Merge.discovered_surface(
701
+ surface_kind: "yard_example_block",
702
+ declared_language: declared_language,
703
+ effective_language: declared_language,
704
+ address: "#{surface[:address]} > yard_example[#{tag_index}]",
705
+ parent_address: surface[:address],
706
+ owner: Ast::Merge.surface_owner_ref(kind: "owned_region", address: surface[:address]),
707
+ span: Ast::Merge.surface_span(start_line: body_entries.first[:line], end_line: body_entries.last[:line]),
708
+ reconstruction_strategy: "rewrite_with_prefix_preservation",
709
+ metadata: {
710
+ tag_kind: "example",
711
+ tag_index: tag_index,
712
+ tag_text: normalized[tag_index],
713
+ comment_prefix: surface.dig(:metadata, :comment_prefix)
714
+ }
715
+ )
716
+ end
717
+ end
718
+
719
+ def next_tag_index(normalized_lines, start_index)
720
+ normalized_lines.each_with_index do |content, index|
721
+ next if index < start_index
722
+ return index if TAG_PREFIX.match?(content)
723
+ end
724
+ nil
725
+ end
726
+
727
+ def normalize_source(source)
728
+ source.gsub(/\r\n?/, "\n")
729
+ end
730
+
731
+ def normalize_comment_content(raw)
732
+ raw.to_s.sub(/\A\s*#\s?/, "").strip
733
+ end
734
+
735
+ def doc_comment_content?(raw)
736
+ content = normalize_comment_content(raw)
737
+ return false if content.empty?
738
+ return false if DIRECTIVE_LINE.match?(content)
739
+ return false if MAGIC_COMMENT_PREFIXES.any? { |prefix| content.start_with?("#{prefix}:") }
740
+
741
+ true
742
+ end
743
+
744
+ def comment_prefix_for(raw)
745
+ raw.to_s[/\A\s*#\s*/] || "# "
746
+ end
747
+
748
+ def declared_example_language(rest)
749
+ match = rest.to_s.strip.match(/\A\[(?<language>[^\]]+)\]/)
750
+ language = match && match[:language]
751
+ return if language.nil? || language.empty?
752
+
753
+ language.downcase.tr("-", "_")
754
+ end
755
+
756
+ module_function(
757
+ :ruby_feature_profile,
758
+ :available_ruby_backends,
759
+ :ruby_backend_feature_profile,
760
+ :ruby_plan_context,
761
+ :parse_ruby,
762
+ :match_ruby_owners,
763
+ :merge_ruby,
764
+ :ruby_discovered_surfaces,
765
+ :ruby_delegated_child_operations,
766
+ :apply_ruby_delegated_child_outputs,
767
+ :merge_ruby_with_reviewed_nested_outputs,
768
+ :merge_ruby_with_reviewed_nested_outputs_from_replay_bundle,
769
+ :merge_ruby_with_reviewed_nested_outputs_from_replay_bundle_envelope,
770
+ :merge_ruby_with_reviewed_nested_outputs_from_review_state,
771
+ :merge_ruby_with_reviewed_nested_outputs_from_review_state_envelope,
772
+ :merge_ruby_with_nested_outputs,
773
+ :analyze_ruby_document,
774
+ :collect_ruby_require_entries,
775
+ :collect_ruby_declaration_entries,
776
+ :unsupported_feature_result
777
+ )
778
+ end
779
+ end
data/lib/ruby-merge.rb ADDED
@@ -0,0 +1,3 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "ruby/merge"
data.tar.gz.sig ADDED
Binary file
metadata ADDED
@@ -0,0 +1,109 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: ruby-merge
3
+ version: !ruby/object:Gem::Version
4
+ version: 7.0.0
5
+ platform: ruby
6
+ authors:
7
+ - Peter H. Boling
8
+ bindir: bin
9
+ cert_chain:
10
+ - |
11
+ -----BEGIN CERTIFICATE-----
12
+ MIIEgDCCAuigAwIBAgIBATANBgkqhkiG9w0BAQsFADBDMRUwEwYDVQQDDAxwZXRl
13
+ ci5ib2xpbmcxFTATBgoJkiaJk/IsZAEZFgVnbWFpbDETMBEGCgmSJomT8ixkARkW
14
+ A2NvbTAeFw0yNTA1MDQxNTMzMDlaFw00NTA0MjkxNTMzMDlaMEMxFTATBgNVBAMM
15
+ DHBldGVyLmJvbGluZzEVMBMGCgmSJomT8ixkARkWBWdtYWlsMRMwEQYKCZImiZPy
16
+ LGQBGRYDY29tMIIBojANBgkqhkiG9w0BAQEFAAOCAY8AMIIBigKCAYEAruUoo0WA
17
+ uoNuq6puKWYeRYiZekz/nsDeK5x/0IEirzcCEvaHr3Bmz7rjo1I6On3gGKmiZs61
18
+ LRmQ3oxy77ydmkGTXBjruJB+pQEn7UfLSgQ0xa1/X3kdBZt6RmabFlBxnHkoaGY5
19
+ mZuZ5+Z7walmv6sFD9ajhzj+oIgwWfnEHkXYTR8I6VLN7MRRKGMPoZ/yvOmxb2DN
20
+ coEEHWKO9CvgYpW7asIihl/9GMpKiRkcYPm9dGQzZc6uTwom1COfW0+ZOFrDVBuV
21
+ FMQRPswZcY4Wlq0uEBLPU7hxnCL9nKK6Y9IhdDcz1mY6HZ91WImNslOSI0S8hRpj
22
+ yGOWxQIhBT3fqCBlRIqFQBudrnD9jSNpSGsFvbEijd5ns7Z9ZMehXkXDycpGAUj1
23
+ to/5cuTWWw1JqUWrKJYoifnVhtE1o1DZ+LkPtWxHtz5kjDG/zR3MG0Ula0UOavlD
24
+ qbnbcXPBnwXtTFeZ3C+yrWpE4pGnl3yGkZj9SMTlo9qnTMiPmuWKQDatAgMBAAGj
25
+ fzB9MAkGA1UdEwQCMAAwCwYDVR0PBAQDAgSwMB0GA1UdDgQWBBQE8uWvNbPVNRXZ
26
+ HlgPbc2PCzC4bjAhBgNVHREEGjAYgRZwZXRlci5ib2xpbmdAZ21haWwuY29tMCEG
27
+ A1UdEgQaMBiBFnBldGVyLmJvbGluZ0BnbWFpbC5jb20wDQYJKoZIhvcNAQELBQAD
28
+ ggGBAJbnUwfJQFPkBgH9cL7hoBfRtmWiCvdqdjeTmi04u8zVNCUox0A4gT982DE9
29
+ wmuN12LpdajxZONqbXuzZvc+nb0StFwmFYZG6iDwaf4BPywm2e/Vmq0YG45vZXGR
30
+ L8yMDSK1cQXjmA+ZBKOHKWavxP6Vp7lWvjAhz8RFwqF9GuNIdhv9NpnCAWcMZtpm
31
+ GUPyIWw/Cw/2wZp74QzZj6Npx+LdXoLTF1HMSJXZ7/pkxLCsB8m4EFVdb/IrW/0k
32
+ kNSfjtAfBHO8nLGuqQZVH9IBD1i9K6aSs7pT6TW8itXUIlkIUI2tg5YzW6OFfPzq
33
+ QekSkX3lZfY+HTSp/o+YvKkqWLUV7PQ7xh1ZYDtocpaHwgxe/j3bBqHE+CUPH2vA
34
+ 0V/FwdTRWcwsjVoOJTrYcff8pBZ8r2MvtAc54xfnnhGFzeRHfcltobgFxkAXdE6p
35
+ DVjBtqT23eugOqQ73umLcYDZkc36vnqGxUBSsXrzY9pzV5gGr2I8YUxMqf6ATrZt
36
+ L9nRqA==
37
+ -----END CERTIFICATE-----
38
+ date: 1980-01-02 00:00:00.000000000 Z
39
+ dependencies:
40
+ - !ruby/object:Gem::Dependency
41
+ name: ast-merge
42
+ requirement: !ruby/object:Gem::Requirement
43
+ requirements:
44
+ - - '='
45
+ - !ruby/object:Gem::Version
46
+ version: 7.0.0
47
+ type: :runtime
48
+ prerelease: false
49
+ version_requirements: !ruby/object:Gem::Requirement
50
+ requirements:
51
+ - - '='
52
+ - !ruby/object:Gem::Version
53
+ version: 7.0.0
54
+ - !ruby/object:Gem::Dependency
55
+ name: tree_haver
56
+ requirement: !ruby/object:Gem::Requirement
57
+ requirements:
58
+ - - '='
59
+ - !ruby/object:Gem::Version
60
+ version: 7.0.0
61
+ type: :runtime
62
+ prerelease: false
63
+ version_requirements: !ruby/object:Gem::Requirement
64
+ requirements:
65
+ - - '='
66
+ - !ruby/object:Gem::Version
67
+ version: 7.0.0
68
+ description: Tree-sitter-backed Ruby family substrate for Structured Merge.
69
+ email:
70
+ - info@structuredmerge.org
71
+ executables: []
72
+ extensions: []
73
+ extra_rdoc_files: []
74
+ files:
75
+ - lib/ruby-merge.rb
76
+ - lib/ruby/merge.rb
77
+ - lib/ruby/merge/version.rb
78
+ homepage: https://github.com/structuredmerge/structuredmerge-ruby
79
+ licenses:
80
+ - AGPL-3.0-only
81
+ - PolyForm-Small-Business-1.0.0
82
+ metadata:
83
+ homepage_uri: https://structuredmerge.org
84
+ source_code_uri: https://github.com/structuredmerge/structuredmerge-ruby/tree/v7.0.0
85
+ changelog_uri: https://github.com/structuredmerge/structuredmerge-ruby/blob/v7.0.0/CHANGELOG.md
86
+ bug_tracker_uri: https://github.com/structuredmerge/structuredmerge-ruby/issues
87
+ documentation_uri: https://www.rubydoc.info/gems/ruby-merge/7.0.0
88
+ funding_uri: https://github.com/sponsors/pboling
89
+ wiki_uri: https://github.com/structuredmerge/structuredmerge-ruby/wiki
90
+ discord_uri: https://discord.gg/3qme4XHNKN
91
+ rubygems_mfa_required: 'true'
92
+ rdoc_options: []
93
+ require_paths:
94
+ - lib
95
+ required_ruby_version: !ruby/object:Gem::Requirement
96
+ requirements:
97
+ - - ">="
98
+ - !ruby/object:Gem::Version
99
+ version: 4.0.0
100
+ required_rubygems_version: !ruby/object:Gem::Requirement
101
+ requirements:
102
+ - - ">="
103
+ - !ruby/object:Gem::Version
104
+ version: '0'
105
+ requirements: []
106
+ rubygems_version: 4.0.10
107
+ specification_version: 4
108
+ summary: Structured Merge Ruby substrate analysis for Ruby
109
+ test_files: []
metadata.gz.sig ADDED
Binary file