ast-merge 1.1.0 → 2.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 +4 -4
- checksums.yaml.gz.sig +0 -0
- data/CHANGELOG.md +165 -7
- data/README.md +208 -39
- data/exe/ast-merge-recipe +366 -0
- data/lib/ast/merge/conflict_resolver_base.rb +8 -1
- data/lib/ast/merge/content_match_refiner.rb +278 -0
- data/lib/ast/merge/debug_logger.rb +2 -1
- data/lib/ast/merge/detector/base.rb +193 -0
- data/lib/ast/merge/detector/fenced_code_block.rb +227 -0
- data/lib/ast/merge/detector/mergeable.rb +369 -0
- data/lib/ast/merge/detector/toml_frontmatter.rb +82 -0
- data/lib/ast/merge/detector/yaml_frontmatter.rb +82 -0
- data/lib/ast/merge/merge_result_base.rb +4 -1
- data/lib/ast/merge/navigable_statement.rb +630 -0
- data/lib/ast/merge/partial_template_merger.rb +432 -0
- data/lib/ast/merge/recipe/config.rb +198 -0
- data/lib/ast/merge/recipe/preset.rb +171 -0
- data/lib/ast/merge/recipe/runner.rb +254 -0
- data/lib/ast/merge/recipe/script_loader.rb +181 -0
- data/lib/ast/merge/recipe.rb +26 -0
- data/lib/ast/merge/rspec/dependency_tags.rb +252 -0
- data/lib/ast/merge/rspec/shared_examples/reproducible_merge.rb +3 -2
- data/lib/ast/merge/rspec.rb +33 -2
- data/lib/ast/merge/smart_merger_base.rb +86 -3
- data/lib/ast/merge/version.rb +1 -1
- data/lib/ast/merge.rb +10 -6
- data/sig/ast/merge.rbs +389 -2
- data.tar.gz.sig +0 -0
- metadata +58 -14
- metadata.gz.sig +0 -0
- data/lib/ast/merge/fenced_code_block_detector.rb +0 -313
- data/lib/ast/merge/region.rb +0 -124
- data/lib/ast/merge/region_detector_base.rb +0 -114
- data/lib/ast/merge/region_mergeable.rb +0 -364
- data/lib/ast/merge/toml_frontmatter_detector.rb +0 -88
- data/lib/ast/merge/yaml_frontmatter_detector.rb +0 -88
|
@@ -0,0 +1,252 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# Ast::Merge RSpec Dependency Tags
|
|
4
|
+
#
|
|
5
|
+
# This module provides dependency detection helpers for conditional test execution
|
|
6
|
+
# in the ast-merge gem family. It extends tree_haver's dependency tags with
|
|
7
|
+
# merge-gem-specific checks.
|
|
8
|
+
#
|
|
9
|
+
# @example Loading in spec_helper.rb
|
|
10
|
+
# require "ast/merge/rspec/dependency_tags"
|
|
11
|
+
#
|
|
12
|
+
# @example Usage in specs
|
|
13
|
+
# it "requires markly-merge", :markly_merge do
|
|
14
|
+
# # This test only runs when markly-merge is available
|
|
15
|
+
# end
|
|
16
|
+
#
|
|
17
|
+
# it "requires prism-merge", :prism_merge do
|
|
18
|
+
# # This test only runs when prism-merge is available
|
|
19
|
+
# end
|
|
20
|
+
#
|
|
21
|
+
# == Available Tags
|
|
22
|
+
#
|
|
23
|
+
# === Merge Gem Tags (run when dependency IS available)
|
|
24
|
+
#
|
|
25
|
+
# [:markly_merge]
|
|
26
|
+
# markly-merge gem is available and functional.
|
|
27
|
+
#
|
|
28
|
+
# [:commonmarker_merge]
|
|
29
|
+
# commonmarker-merge gem is available and functional.
|
|
30
|
+
#
|
|
31
|
+
# [:markdown_merge]
|
|
32
|
+
# markdown-merge gem is available and functional.
|
|
33
|
+
#
|
|
34
|
+
# [:prism_merge]
|
|
35
|
+
# prism-merge gem is available and functional.
|
|
36
|
+
#
|
|
37
|
+
# [:json_merge]
|
|
38
|
+
# json-merge gem is available and functional.
|
|
39
|
+
#
|
|
40
|
+
# [:jsonc_merge]
|
|
41
|
+
# jsonc-merge gem is available and functional.
|
|
42
|
+
#
|
|
43
|
+
# [:toml_merge]
|
|
44
|
+
# toml-merge gem is available and functional.
|
|
45
|
+
#
|
|
46
|
+
# [:bash_merge]
|
|
47
|
+
# bash-merge gem is available and functional.
|
|
48
|
+
#
|
|
49
|
+
# [:psych_merge]
|
|
50
|
+
# psych-merge gem is available and functional.
|
|
51
|
+
#
|
|
52
|
+
# [:any_markdown_merge]
|
|
53
|
+
# At least one markdown merge gem (markly-merge or commonmarker-merge) is available.
|
|
54
|
+
#
|
|
55
|
+
# === Negated Tags (run when dependency is NOT available)
|
|
56
|
+
#
|
|
57
|
+
# All positive tags have negated versions prefixed with `not_`:
|
|
58
|
+
# - :not_markly_merge, :not_commonmarker_merge, :not_markdown_merge
|
|
59
|
+
# - :not_prism_merge, :not_json_merge, :not_jsonc_merge
|
|
60
|
+
# - :not_toml_merge, :not_bash_merge, :not_psych_merge
|
|
61
|
+
# - :not_any_markdown_merge
|
|
62
|
+
|
|
63
|
+
module Ast
|
|
64
|
+
module Merge
|
|
65
|
+
module RSpec
|
|
66
|
+
# Dependency detection helpers for conditional test execution
|
|
67
|
+
module DependencyTags
|
|
68
|
+
class << self
|
|
69
|
+
# ============================================================
|
|
70
|
+
# Merge Gem Availability
|
|
71
|
+
# ============================================================
|
|
72
|
+
|
|
73
|
+
# rubocop:disable ThreadSafety/ClassInstanceVariable
|
|
74
|
+
# Check if markly-merge is available and functional
|
|
75
|
+
#
|
|
76
|
+
# @return [Boolean] true if markly-merge works
|
|
77
|
+
def markly_merge_available?
|
|
78
|
+
return @markly_merge_available if defined?(@markly_merge_available)
|
|
79
|
+
@markly_merge_available = merge_gem_works?("markly/merge", "Markly::Merge::SmartMerger", "# Test\n\nParagraph")
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
# Check if commonmarker-merge is available and functional
|
|
83
|
+
#
|
|
84
|
+
# @return [Boolean] true if commonmarker-merge works
|
|
85
|
+
def commonmarker_merge_available?
|
|
86
|
+
return @commonmarker_merge_available if defined?(@commonmarker_merge_available)
|
|
87
|
+
@commonmarker_merge_available = merge_gem_works?("commonmarker/merge", "Commonmarker::Merge::SmartMerger", "# Test\n\nParagraph")
|
|
88
|
+
end
|
|
89
|
+
|
|
90
|
+
# Check if markdown-merge is available and functional
|
|
91
|
+
#
|
|
92
|
+
# @return [Boolean] true if markdown-merge works
|
|
93
|
+
def markdown_merge_available?
|
|
94
|
+
return @markdown_merge_available if defined?(@markdown_merge_available)
|
|
95
|
+
@markdown_merge_available = merge_gem_works?("markdown/merge", "Markdown::Merge::SmartMerger", "# Test\n\nParagraph")
|
|
96
|
+
end
|
|
97
|
+
|
|
98
|
+
# Check if prism-merge is available and functional
|
|
99
|
+
#
|
|
100
|
+
# @return [Boolean] true if prism-merge works
|
|
101
|
+
def prism_merge_available?
|
|
102
|
+
return @prism_merge_available if defined?(@prism_merge_available)
|
|
103
|
+
@prism_merge_available = merge_gem_works?("prism/merge", "Prism::Merge::SmartMerger", "puts 1")
|
|
104
|
+
end
|
|
105
|
+
|
|
106
|
+
# Check if json-merge is available and functional
|
|
107
|
+
#
|
|
108
|
+
# @return [Boolean] true if json-merge works
|
|
109
|
+
def json_merge_available?
|
|
110
|
+
return @json_merge_available if defined?(@json_merge_available)
|
|
111
|
+
@json_merge_available = merge_gem_works?("json/merge", "Json::Merge::SmartMerger", '{"key": "value"}')
|
|
112
|
+
end
|
|
113
|
+
|
|
114
|
+
# Check if jsonc-merge is available and functional
|
|
115
|
+
#
|
|
116
|
+
# @return [Boolean] true if jsonc-merge works
|
|
117
|
+
def jsonc_merge_available?
|
|
118
|
+
return @jsonc_merge_available if defined?(@jsonc_merge_available)
|
|
119
|
+
@jsonc_merge_available = merge_gem_works?("jsonc/merge", "Jsonc::Merge::SmartMerger", '{"key": "value" /* comment */}')
|
|
120
|
+
end
|
|
121
|
+
|
|
122
|
+
# Check if toml-merge is available and functional
|
|
123
|
+
#
|
|
124
|
+
# @return [Boolean] true if toml-merge works
|
|
125
|
+
def toml_merge_available?
|
|
126
|
+
return @toml_merge_available if defined?(@toml_merge_available)
|
|
127
|
+
@toml_merge_available = merge_gem_works?("toml/merge", "Toml::Merge::SmartMerger", 'key = "value"')
|
|
128
|
+
end
|
|
129
|
+
|
|
130
|
+
# Check if bash-merge is available and functional
|
|
131
|
+
#
|
|
132
|
+
# @return [Boolean] true if bash-merge works
|
|
133
|
+
def bash_merge_available?
|
|
134
|
+
return @bash_merge_available if defined?(@bash_merge_available)
|
|
135
|
+
@bash_merge_available = merge_gem_works?("bash/merge", "Bash::Merge::SmartMerger", "echo hello")
|
|
136
|
+
end
|
|
137
|
+
|
|
138
|
+
# Check if psych-merge is available and functional
|
|
139
|
+
#
|
|
140
|
+
# @return [Boolean] true if psych-merge works
|
|
141
|
+
def psych_merge_available?
|
|
142
|
+
return @psych_merge_available if defined?(@psych_merge_available)
|
|
143
|
+
@psych_merge_available = merge_gem_works?("psych/merge", "Psych::Merge::SmartMerger", "key: value")
|
|
144
|
+
end
|
|
145
|
+
# rubocop:enable ThreadSafety/ClassInstanceVariable
|
|
146
|
+
|
|
147
|
+
# Check if at least one markdown merge gem is available
|
|
148
|
+
#
|
|
149
|
+
# @return [Boolean] true if any markdown merge gem works
|
|
150
|
+
def any_markdown_merge_available?
|
|
151
|
+
markly_merge_available? || commonmarker_merge_available? || markdown_merge_available?
|
|
152
|
+
end
|
|
153
|
+
|
|
154
|
+
# ============================================================
|
|
155
|
+
# Summary and Reset
|
|
156
|
+
# ============================================================
|
|
157
|
+
|
|
158
|
+
# Get a summary of available dependencies (for debugging)
|
|
159
|
+
#
|
|
160
|
+
# @return [Hash{Symbol => Boolean}] map of dependency name to availability
|
|
161
|
+
def summary
|
|
162
|
+
{
|
|
163
|
+
markly_merge: markly_merge_available?,
|
|
164
|
+
commonmarker_merge: commonmarker_merge_available?,
|
|
165
|
+
markdown_merge: markdown_merge_available?,
|
|
166
|
+
prism_merge: prism_merge_available?,
|
|
167
|
+
json_merge: json_merge_available?,
|
|
168
|
+
jsonc_merge: jsonc_merge_available?,
|
|
169
|
+
toml_merge: toml_merge_available?,
|
|
170
|
+
bash_merge: bash_merge_available?,
|
|
171
|
+
psych_merge: psych_merge_available?,
|
|
172
|
+
any_markdown_merge: any_markdown_merge_available?,
|
|
173
|
+
}
|
|
174
|
+
end
|
|
175
|
+
|
|
176
|
+
# Reset all memoized availability checks
|
|
177
|
+
#
|
|
178
|
+
# @return [void]
|
|
179
|
+
def reset!
|
|
180
|
+
instance_variables.each do |ivar|
|
|
181
|
+
remove_instance_variable(ivar) if ivar.to_s.end_with?("_available")
|
|
182
|
+
end
|
|
183
|
+
end
|
|
184
|
+
|
|
185
|
+
private
|
|
186
|
+
|
|
187
|
+
# Generic helper to check if a merge gem is available and functional
|
|
188
|
+
#
|
|
189
|
+
# @param require_path [String] the require path for the gem
|
|
190
|
+
# @param merger_class [String] the full class name of the SmartMerger
|
|
191
|
+
# @param test_source [String] sample source code to test merging
|
|
192
|
+
# @return [Boolean] true if the merger can be instantiated
|
|
193
|
+
def merge_gem_works?(require_path, merger_class, test_source)
|
|
194
|
+
require require_path
|
|
195
|
+
klass = Object.const_get(merger_class)
|
|
196
|
+
klass.new(test_source, test_source)
|
|
197
|
+
true
|
|
198
|
+
rescue LoadError, StandardError
|
|
199
|
+
false
|
|
200
|
+
end
|
|
201
|
+
end
|
|
202
|
+
end
|
|
203
|
+
end
|
|
204
|
+
end
|
|
205
|
+
end
|
|
206
|
+
|
|
207
|
+
# Configure RSpec with dependency-based exclusion filters
|
|
208
|
+
RSpec.configure do |config|
|
|
209
|
+
deps = Ast::Merge::RSpec::DependencyTags
|
|
210
|
+
|
|
211
|
+
config.before(:suite) do
|
|
212
|
+
# Print dependency summary if AST_MERGE_DEBUG is set
|
|
213
|
+
if ENV["AST_MERGE_DEBUG"]
|
|
214
|
+
puts "\n=== Ast::Merge Test Dependencies ==="
|
|
215
|
+
deps.summary.each do |dep, available|
|
|
216
|
+
status = available ? "✓ available" : "✗ not available"
|
|
217
|
+
puts " #{dep}: #{status}"
|
|
218
|
+
end
|
|
219
|
+
puts "=====================================\n"
|
|
220
|
+
end
|
|
221
|
+
end
|
|
222
|
+
|
|
223
|
+
# ============================================================
|
|
224
|
+
# Merge Gem Tags
|
|
225
|
+
# ============================================================
|
|
226
|
+
|
|
227
|
+
config.filter_run_excluding(markly_merge: true) unless deps.markly_merge_available?
|
|
228
|
+
config.filter_run_excluding(commonmarker_merge: true) unless deps.commonmarker_merge_available?
|
|
229
|
+
config.filter_run_excluding(markdown_merge: true) unless deps.markdown_merge_available?
|
|
230
|
+
config.filter_run_excluding(prism_merge: true) unless deps.prism_merge_available?
|
|
231
|
+
config.filter_run_excluding(json_merge: true) unless deps.json_merge_available?
|
|
232
|
+
config.filter_run_excluding(jsonc_merge: true) unless deps.jsonc_merge_available?
|
|
233
|
+
config.filter_run_excluding(toml_merge: true) unless deps.toml_merge_available?
|
|
234
|
+
config.filter_run_excluding(bash_merge: true) unless deps.bash_merge_available?
|
|
235
|
+
config.filter_run_excluding(psych_merge: true) unless deps.psych_merge_available?
|
|
236
|
+
config.filter_run_excluding(any_markdown_merge: true) unless deps.any_markdown_merge_available?
|
|
237
|
+
|
|
238
|
+
# ============================================================
|
|
239
|
+
# Negated Tags (run when dependency is NOT available)
|
|
240
|
+
# ============================================================
|
|
241
|
+
|
|
242
|
+
config.filter_run_excluding(not_markly_merge: true) if deps.markly_merge_available?
|
|
243
|
+
config.filter_run_excluding(not_commonmarker_merge: true) if deps.commonmarker_merge_available?
|
|
244
|
+
config.filter_run_excluding(not_markdown_merge: true) if deps.markdown_merge_available?
|
|
245
|
+
config.filter_run_excluding(not_prism_merge: true) if deps.prism_merge_available?
|
|
246
|
+
config.filter_run_excluding(not_json_merge: true) if deps.json_merge_available?
|
|
247
|
+
config.filter_run_excluding(not_jsonc_merge: true) if deps.jsonc_merge_available?
|
|
248
|
+
config.filter_run_excluding(not_toml_merge: true) if deps.toml_merge_available?
|
|
249
|
+
config.filter_run_excluding(not_bash_merge: true) if deps.bash_merge_available?
|
|
250
|
+
config.filter_run_excluding(not_psych_merge: true) if deps.psych_merge_available?
|
|
251
|
+
config.filter_run_excluding(not_any_markdown_merge: true) if deps.any_markdown_merge_available?
|
|
252
|
+
end
|
|
@@ -77,8 +77,9 @@ RSpec.shared_examples("a reproducible merge") do |scenario, options = {}|
|
|
|
77
77
|
result = merger.merge
|
|
78
78
|
|
|
79
79
|
# Normalize trailing newlines for comparison
|
|
80
|
-
|
|
81
|
-
|
|
80
|
+
# Different backends may or may not add a trailing newline
|
|
81
|
+
expected = fixture[:expected].chomp
|
|
82
|
+
actual = result.to_s.chomp
|
|
82
83
|
|
|
83
84
|
expect(actual).to(
|
|
84
85
|
eq(expected),
|
data/lib/ast/merge/rspec.rb
CHANGED
|
@@ -1,4 +1,35 @@
|
|
|
1
|
-
#
|
|
2
|
-
require "ast/merge"
|
|
1
|
+
# frozen_string_literal: true
|
|
3
2
|
|
|
3
|
+
# Ast::Merge RSpec Support
|
|
4
|
+
#
|
|
5
|
+
# This file provides a single entry point for all RSpec helpers in the ast-merge family.
|
|
6
|
+
# It loads:
|
|
7
|
+
# - TreeHaver dependency tags (parser backend availability)
|
|
8
|
+
# - Ast::Merge dependency tags (merge gem availability)
|
|
9
|
+
# - Ast::Merge shared examples (for testing *-merge implementations)
|
|
10
|
+
#
|
|
11
|
+
# @example Loading in spec_helper.rb
|
|
12
|
+
# require "ast/merge/rspec"
|
|
13
|
+
#
|
|
14
|
+
# @example Usage in specs
|
|
15
|
+
# # Dependency tags for conditional execution
|
|
16
|
+
# it "requires markly-merge", :markly_merge do
|
|
17
|
+
# # Skipped if markly-merge not available
|
|
18
|
+
# end
|
|
19
|
+
#
|
|
20
|
+
# # Shared examples for implementation validation
|
|
21
|
+
# RSpec.describe MyMerge::ConflictResolver do
|
|
22
|
+
# it_behaves_like "Ast::Merge::ConflictResolverBase"
|
|
23
|
+
# end
|
|
24
|
+
#
|
|
25
|
+
# @see https://github.com/kettle-rb/tree_haver/blob/main/lib/tree_haver/rspec/README.md
|
|
26
|
+
# TreeHaver RSpec documentation for parser backend tags
|
|
27
|
+
|
|
28
|
+
# Load TreeHaver dependency tags first (provides parser backend tags like :markly, :prism_backend, etc.)
|
|
29
|
+
require "tree_haver/rspec/dependency_tags"
|
|
30
|
+
|
|
31
|
+
# Load Ast::Merge dependency tags (provides merge gem tags like :markly_merge, :prism_merge, etc.)
|
|
32
|
+
require_relative "rspec/dependency_tags"
|
|
33
|
+
|
|
34
|
+
# Load Ast::Merge shared examples (for testing *-merge implementations)
|
|
4
35
|
require_relative "rspec/shared_examples"
|
|
@@ -12,13 +12,14 @@ module Ast
|
|
|
12
12
|
#
|
|
13
13
|
# All SmartMerger implementations support these common options:
|
|
14
14
|
#
|
|
15
|
-
# - `preference` - `:destination` (default) or `:template
|
|
15
|
+
# - `preference` - `:destination` (default) or `:template`, or Hash for per-type
|
|
16
16
|
# - `add_template_only_nodes` - `false` (default) or `true`
|
|
17
17
|
# - `signature_generator` - Custom signature proc or `nil`
|
|
18
18
|
# - `freeze_token` - Token for freeze block markers
|
|
19
19
|
# - `match_refiner` - Fuzzy match refiner or `nil`
|
|
20
20
|
# - `regions` - Region configurations for nested merging
|
|
21
21
|
# - `region_placeholder` - Custom placeholder for regions
|
|
22
|
+
# - `node_typing` - Hash mapping node types to callables for per-type preferences
|
|
22
23
|
#
|
|
23
24
|
# ## Implementing a SmartMerger
|
|
24
25
|
#
|
|
@@ -35,6 +36,53 @@ module Ast
|
|
|
35
36
|
# - `build_analysis_options` - Additional analysis options
|
|
36
37
|
# - `build_resolver_options` - Additional resolver options
|
|
37
38
|
#
|
|
39
|
+
# ## FileAnalysis Error Handling Pattern
|
|
40
|
+
#
|
|
41
|
+
# All FileAnalysis classes must follow this consistent error handling pattern:
|
|
42
|
+
#
|
|
43
|
+
# 1. **Catch backend errors internally** - Handle `TreeHaver::NotAvailable` and
|
|
44
|
+
# similar backend errors inside the FileAnalysis class, storing them in `@errors`
|
|
45
|
+
# and setting `@ast = nil`. Do NOT re-raise these errors.
|
|
46
|
+
#
|
|
47
|
+
# 2. **Collect parse errors without raising** - When the parser detects syntax errors
|
|
48
|
+
# (e.g., `has_error?` returns true), collect them in `@errors` but do NOT raise.
|
|
49
|
+
#
|
|
50
|
+
# 3. **Implement `valid?`** - Return `false` when there are errors or no AST:
|
|
51
|
+
# ```ruby
|
|
52
|
+
# def valid?
|
|
53
|
+
# @errors.empty? && !@ast.nil?
|
|
54
|
+
# end
|
|
55
|
+
# ```
|
|
56
|
+
#
|
|
57
|
+
# 4. **SmartMergerBase handles the rest** - After FileAnalysis creation,
|
|
58
|
+
# `parse_and_analyze` checks `valid?` and raises the appropriate parse error
|
|
59
|
+
# (TemplateParseError or DestinationParseError) if the analysis is invalid.
|
|
60
|
+
#
|
|
61
|
+
# This pattern ensures:
|
|
62
|
+
# - Consistent error handling across all *-merge gems
|
|
63
|
+
# - TreeHaver::NotAvailable (which inherits from Exception) is handled safely
|
|
64
|
+
# - Parse errors are properly wrapped in format-specific error classes
|
|
65
|
+
# - No need to rescue Exception in SmartMergerBase
|
|
66
|
+
#
|
|
67
|
+
# @example FileAnalysis error handling
|
|
68
|
+
# def parse_content
|
|
69
|
+
# parser = TreeHaver.parser_for(:myformat, library_path: @parser_path)
|
|
70
|
+
# @ast = parser.parse(@source)
|
|
71
|
+
#
|
|
72
|
+
# if @ast&.root_node&.has_error?
|
|
73
|
+
# collect_parse_errors(@ast.root_node)
|
|
74
|
+
# # Do NOT raise here - SmartMergerBase will check valid?
|
|
75
|
+
# end
|
|
76
|
+
# rescue TreeHaver::NotAvailable => e
|
|
77
|
+
# @errors << e.message
|
|
78
|
+
# @ast = nil
|
|
79
|
+
# # Do NOT re-raise - SmartMergerBase will check valid?
|
|
80
|
+
# rescue StandardError => e
|
|
81
|
+
# @errors << e
|
|
82
|
+
# @ast = nil
|
|
83
|
+
# # Do NOT re-raise - SmartMergerBase will check valid?
|
|
84
|
+
# end
|
|
85
|
+
#
|
|
38
86
|
# @example Implementing a custom SmartMerger
|
|
39
87
|
# class MyFormat::SmartMerger < Ast::Merge::SmartMergerBase
|
|
40
88
|
# def analysis_class
|
|
@@ -57,7 +105,7 @@ module Ast
|
|
|
57
105
|
# @abstract Subclass and implement {#analysis_class} and {#perform_merge}
|
|
58
106
|
# @api public
|
|
59
107
|
class SmartMergerBase
|
|
60
|
-
include
|
|
108
|
+
include Detector::Mergeable
|
|
61
109
|
|
|
62
110
|
# @return [String] Template source content
|
|
63
111
|
attr_reader :template_content
|
|
@@ -95,6 +143,9 @@ module Ast
|
|
|
95
143
|
# @return [Object, nil] Match refiner for fuzzy matching
|
|
96
144
|
attr_reader :match_refiner
|
|
97
145
|
|
|
146
|
+
# @return [Hash{Symbol,String => #call}, nil] Node typing configuration
|
|
147
|
+
attr_reader :node_typing
|
|
148
|
+
|
|
98
149
|
# Creates a new SmartMerger for intelligent file merging.
|
|
99
150
|
#
|
|
100
151
|
# @param template_content [String] Template source content
|
|
@@ -140,6 +191,16 @@ module Ast
|
|
|
140
191
|
# - Commonmarker: `options: { parse: { smart: true } }`
|
|
141
192
|
# - Prism: (no additional parser options needed)
|
|
142
193
|
#
|
|
194
|
+
# @param node_typing [Hash{Symbol,String => #call}, nil] Node typing configuration
|
|
195
|
+
# for per-node-type merge preferences. Maps node type names to callables that
|
|
196
|
+
# can wrap nodes with custom merge_types for use with Hash-based preference.
|
|
197
|
+
# @example
|
|
198
|
+
# node_typing = {
|
|
199
|
+
# CallNode: ->(node) {
|
|
200
|
+
# NodeTyping.with_merge_type(node, :special) if special_node?(node)
|
|
201
|
+
# }
|
|
202
|
+
# }
|
|
203
|
+
#
|
|
143
204
|
# @raise [Ast::Merge::TemplateParseError] If template has syntax errors
|
|
144
205
|
# @raise [Ast::Merge::DestinationParseError] If destination has syntax errors
|
|
145
206
|
def initialize(
|
|
@@ -152,6 +213,7 @@ module Ast
|
|
|
152
213
|
match_refiner: nil,
|
|
153
214
|
regions: nil,
|
|
154
215
|
region_placeholder: nil,
|
|
216
|
+
node_typing: nil,
|
|
155
217
|
**format_options
|
|
156
218
|
)
|
|
157
219
|
@template_content = template_content
|
|
@@ -161,8 +223,12 @@ module Ast
|
|
|
161
223
|
@add_template_only_nodes = add_template_only_nodes
|
|
162
224
|
@freeze_token = freeze_token || default_freeze_token
|
|
163
225
|
@match_refiner = match_refiner
|
|
226
|
+
@node_typing = node_typing
|
|
164
227
|
@format_options = format_options
|
|
165
228
|
|
|
229
|
+
# Validate node_typing if provided
|
|
230
|
+
NodeTyping.validate!(node_typing) if node_typing
|
|
231
|
+
|
|
166
232
|
# Set up region support
|
|
167
233
|
setup_regions(regions: regions || [], region_placeholder: region_placeholder)
|
|
168
234
|
|
|
@@ -321,19 +387,36 @@ module Ast
|
|
|
321
387
|
|
|
322
388
|
# Parse and analyze content, raising appropriate errors.
|
|
323
389
|
#
|
|
390
|
+
# Error handling:
|
|
391
|
+
# - All FileAnalysis classes handle TreeHaver::NotAvailable internally,
|
|
392
|
+
# storing the error and setting valid? to false
|
|
393
|
+
# - The validity check catches silent failures (grammar not available, parse errors)
|
|
394
|
+
# - StandardError from FileAnalysis initialization is wrapped in parse error
|
|
395
|
+
#
|
|
324
396
|
# @param content [String] Content to parse
|
|
325
397
|
# @param source [Symbol] :template or :destination
|
|
326
398
|
# @return [Object] The analysis result
|
|
327
399
|
def parse_and_analyze(content, source)
|
|
328
400
|
options = build_full_analysis_options
|
|
329
401
|
|
|
330
|
-
DebugLogger.time("#{self.class.name}#analyze_#{source}") do
|
|
402
|
+
analysis = DebugLogger.time("#{self.class.name}#analyze_#{source}") do
|
|
331
403
|
analysis_class.new(content, **options)
|
|
332
404
|
end
|
|
405
|
+
|
|
406
|
+
# Check if the analysis is valid - if not, raise a parse error
|
|
407
|
+
# This catches cases where parsing fails silently (e.g., grammar not available)
|
|
408
|
+
if analysis.respond_to?(:valid?) && !analysis.valid?
|
|
409
|
+
error_class = (source == :template) ? template_parse_error_class : destination_parse_error_class
|
|
410
|
+
errors = analysis.respond_to?(:errors) ? analysis.errors : []
|
|
411
|
+
raise error_class.new(errors: errors, content: content)
|
|
412
|
+
end
|
|
413
|
+
|
|
414
|
+
analysis
|
|
333
415
|
rescue StandardError => e
|
|
334
416
|
# Don't re-wrap our own parse errors
|
|
335
417
|
raise if e.is_a?(template_parse_error_class) || e.is_a?(destination_parse_error_class)
|
|
336
418
|
|
|
419
|
+
# Wrap the error in our parse error class
|
|
337
420
|
error_class = (source == :template) ? template_parse_error_class : destination_parse_error_class
|
|
338
421
|
raise error_class.new(errors: [e], content: content)
|
|
339
422
|
end
|
data/lib/ast/merge/version.rb
CHANGED
data/lib/ast/merge.rb
CHANGED
|
@@ -136,27 +136,31 @@ module Ast
|
|
|
136
136
|
end
|
|
137
137
|
end
|
|
138
138
|
|
|
139
|
+
# Core classes
|
|
139
140
|
autoload :AstNode, "ast/merge/ast_node"
|
|
140
141
|
autoload :Comment, "ast/merge/comment"
|
|
141
142
|
autoload :ConflictResolverBase, "ast/merge/conflict_resolver_base"
|
|
143
|
+
autoload :ContentMatchRefiner, "ast/merge/content_match_refiner"
|
|
142
144
|
autoload :DebugLogger, "ast/merge/debug_logger"
|
|
143
|
-
autoload :FencedCodeBlockDetector, "ast/merge/fenced_code_block_detector"
|
|
144
145
|
autoload :FileAnalyzable, "ast/merge/file_analyzable"
|
|
145
146
|
autoload :Freezable, "ast/merge/freezable"
|
|
146
147
|
autoload :FreezeNodeBase, "ast/merge/freeze_node_base"
|
|
148
|
+
autoload :InjectionPoint, "ast/merge/navigable_statement"
|
|
149
|
+
autoload :InjectionPointFinder, "ast/merge/navigable_statement"
|
|
147
150
|
autoload :MatchRefinerBase, "ast/merge/match_refiner_base"
|
|
148
151
|
autoload :MatchScoreBase, "ast/merge/match_score_base"
|
|
149
152
|
autoload :MergeResultBase, "ast/merge/merge_result_base"
|
|
150
153
|
autoload :MergerConfig, "ast/merge/merger_config"
|
|
154
|
+
autoload :NavigableStatement, "ast/merge/navigable_statement"
|
|
151
155
|
autoload :NodeTyping, "ast/merge/node_typing"
|
|
152
|
-
autoload :
|
|
153
|
-
autoload :RegionDetectorBase, "ast/merge/region_detector_base"
|
|
154
|
-
autoload :RegionMergeable, "ast/merge/region_mergeable"
|
|
156
|
+
autoload :PartialTemplateMerger, "ast/merge/partial_template_merger"
|
|
155
157
|
autoload :SectionTyping, "ast/merge/section_typing"
|
|
156
158
|
autoload :SmartMergerBase, "ast/merge/smart_merger_base"
|
|
157
159
|
autoload :Text, "ast/merge/text"
|
|
158
|
-
|
|
159
|
-
|
|
160
|
+
|
|
161
|
+
# Namespaces
|
|
162
|
+
autoload :Detector, "ast/merge/detector/base" # Detector::Region, Detector::Base, Detector::Mergeable, etc.
|
|
163
|
+
autoload :Recipe, "ast/merge/recipe"
|
|
160
164
|
end
|
|
161
165
|
end
|
|
162
166
|
|