ast-merge 3.1.0 → 4.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 +67 -1
- data/README.md +228 -179
- data/exe/ast-merge-recipe +20 -0
- data/lib/ast/merge/conflict_resolver_base.rb +47 -1
- data/lib/ast/merge/diff_mapper_base.rb +245 -0
- data/lib/ast/merge/navigable/injection_point.rb +132 -0
- data/lib/ast/merge/navigable/injection_point_finder.rb +98 -0
- data/lib/ast/merge/navigable/statement.rb +380 -0
- data/lib/ast/merge/navigable.rb +20 -0
- data/lib/ast/merge/partial_template_merger_base.rb +4 -2
- data/lib/ast/merge/recipe/preset.rb +18 -0
- data/lib/ast/merge/recipe/runner.rb +8 -1
- data/lib/ast/merge/version.rb +1 -1
- data/lib/ast/merge.rb +2 -3
- data.tar.gz.sig +0 -0
- metadata +33 -9
- metadata.gz.sig +0 -0
- data/lib/ast/merge/navigable_statement.rb +0 -625
|
@@ -0,0 +1,380 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Ast
|
|
4
|
+
module Merge
|
|
5
|
+
module Navigable
|
|
6
|
+
# Wraps any node (parser-backed or synthetic) with uniform navigation.
|
|
7
|
+
#
|
|
8
|
+
# Provides two levels of navigation:
|
|
9
|
+
# 1. **Flat list navigation**: prev_statement, next_statement, index
|
|
10
|
+
# - Works for ALL nodes (synthetic and parser-backed)
|
|
11
|
+
# - Represents position in the flattened statement list
|
|
12
|
+
#
|
|
13
|
+
# 2. **Tree navigation**: tree_parent, tree_next, tree_previous, tree_children
|
|
14
|
+
# - Only available for parser-backed nodes
|
|
15
|
+
# - Delegates to inner_node's tree methods
|
|
16
|
+
#
|
|
17
|
+
# This allows code to work with the flat list for simple merging,
|
|
18
|
+
# while still accessing tree structure for section-aware operations.
|
|
19
|
+
#
|
|
20
|
+
# @example Basic usage
|
|
21
|
+
# statements = Statement.build_list(raw_statements)
|
|
22
|
+
# stmt = statements[0]
|
|
23
|
+
#
|
|
24
|
+
# # Flat navigation (always works)
|
|
25
|
+
# stmt.next # => next statement in flat list
|
|
26
|
+
# stmt.previous # => previous statement in flat list
|
|
27
|
+
# stmt.index # => position in array
|
|
28
|
+
#
|
|
29
|
+
# # Tree navigation (when available)
|
|
30
|
+
# stmt.tree_parent # => parent in original AST (or nil)
|
|
31
|
+
# stmt.tree_next # => next sibling in original AST (or nil)
|
|
32
|
+
# stmt.tree_children # => children in original AST (or [])
|
|
33
|
+
#
|
|
34
|
+
# @example Section grouping
|
|
35
|
+
# # Group statements into sections by heading level
|
|
36
|
+
# sections = Statement.group_by_heading(statements, level: 3)
|
|
37
|
+
# sections.each do |section|
|
|
38
|
+
# puts "Section: #{section.heading.text}"
|
|
39
|
+
# section.statements.each { |s| puts " - #{s.type}" }
|
|
40
|
+
# end
|
|
41
|
+
#
|
|
42
|
+
class Statement
|
|
43
|
+
# @return [Object] The wrapped node (parser-backed or synthetic)
|
|
44
|
+
attr_reader :node
|
|
45
|
+
|
|
46
|
+
# @return [Integer] Index in the flattened statement list
|
|
47
|
+
attr_reader :index
|
|
48
|
+
|
|
49
|
+
# @return [Statement, nil] Previous statement in flat list
|
|
50
|
+
attr_accessor :prev_statement
|
|
51
|
+
|
|
52
|
+
# @return [Statement, nil] Next statement in flat list
|
|
53
|
+
attr_accessor :next_statement
|
|
54
|
+
|
|
55
|
+
# @return [Object, nil] Optional context/metadata for this statement
|
|
56
|
+
attr_accessor :context
|
|
57
|
+
|
|
58
|
+
# Initialize a Statement wrapper.
|
|
59
|
+
#
|
|
60
|
+
# @param node [Object] The node to wrap
|
|
61
|
+
# @param index [Integer] Position in the statement list
|
|
62
|
+
def initialize(node, index:)
|
|
63
|
+
@node = node
|
|
64
|
+
@index = index
|
|
65
|
+
@prev_statement = nil
|
|
66
|
+
@next_statement = nil
|
|
67
|
+
@context = nil
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
class << self
|
|
71
|
+
# Build a linked list of Statements from raw statements.
|
|
72
|
+
#
|
|
73
|
+
# @param raw_statements [Array<Object>] Raw statement nodes
|
|
74
|
+
# @return [Array<Statement>] Linked statement list
|
|
75
|
+
def build_list(raw_statements)
|
|
76
|
+
statements = raw_statements.each_with_index.map do |node, i|
|
|
77
|
+
new(node, index: i)
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
# Link siblings in flat list
|
|
81
|
+
statements.each_cons(2) do |prev_stmt, next_stmt|
|
|
82
|
+
prev_stmt.next_statement = next_stmt
|
|
83
|
+
next_stmt.prev_statement = prev_stmt
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
statements
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
# Find statements matching a query.
|
|
90
|
+
#
|
|
91
|
+
# @param statements [Array<Statement>] Statement list
|
|
92
|
+
# @param type [Symbol, String, nil] Node type to match (nil = any)
|
|
93
|
+
# @param text [String, Regexp, nil] Text pattern to match
|
|
94
|
+
# @yield [Statement] Optional block for custom matching
|
|
95
|
+
# @return [Array<Statement>] Matching statements
|
|
96
|
+
def find_matching(statements, type: nil, text: nil, &block)
|
|
97
|
+
# If no criteria specified, return empty array (nothing to match)
|
|
98
|
+
return [] if type.nil? && text.nil? && !block_given?
|
|
99
|
+
|
|
100
|
+
statements.select do |stmt|
|
|
101
|
+
matches = true
|
|
102
|
+
matches &&= stmt.type.to_s == type.to_s if type
|
|
103
|
+
matches &&= text.is_a?(Regexp) ? stmt.text.match?(text) : stmt.text.include?(text.to_s) if text
|
|
104
|
+
matches &&= yield(stmt) if block_given?
|
|
105
|
+
matches
|
|
106
|
+
end
|
|
107
|
+
end
|
|
108
|
+
|
|
109
|
+
# Find the first statement matching criteria.
|
|
110
|
+
#
|
|
111
|
+
# @param statements [Array<Statement>] Statement list
|
|
112
|
+
# @param type [Symbol, String, nil] Node type to match
|
|
113
|
+
# @param text [String, Regexp, nil] Text pattern to match
|
|
114
|
+
# @yield [Statement] Optional block for custom matching
|
|
115
|
+
# @return [Statement, nil] First matching statement
|
|
116
|
+
def find_first(statements, type: nil, text: nil, &block)
|
|
117
|
+
find_matching(statements, type: type, text: text, &block).first
|
|
118
|
+
end
|
|
119
|
+
end
|
|
120
|
+
|
|
121
|
+
# ============================================================
|
|
122
|
+
# Flat list navigation (always available)
|
|
123
|
+
# ============================================================
|
|
124
|
+
|
|
125
|
+
# @return [Statement, nil] Next statement in flat list
|
|
126
|
+
def next
|
|
127
|
+
next_statement
|
|
128
|
+
end
|
|
129
|
+
|
|
130
|
+
# @return [Statement, nil] Previous statement in flat list
|
|
131
|
+
def previous
|
|
132
|
+
prev_statement
|
|
133
|
+
end
|
|
134
|
+
|
|
135
|
+
# @return [Boolean] true if this is the first statement
|
|
136
|
+
def first?
|
|
137
|
+
prev_statement.nil?
|
|
138
|
+
end
|
|
139
|
+
|
|
140
|
+
# @return [Boolean] true if this is the last statement
|
|
141
|
+
def last?
|
|
142
|
+
next_statement.nil?
|
|
143
|
+
end
|
|
144
|
+
|
|
145
|
+
# Iterate from this statement to the end (or until block returns false).
|
|
146
|
+
#
|
|
147
|
+
# @yield [Statement] Each statement
|
|
148
|
+
# @return [Enumerator, nil]
|
|
149
|
+
def each_following(&block)
|
|
150
|
+
return to_enum(:each_following) unless block_given?
|
|
151
|
+
|
|
152
|
+
current = self.next
|
|
153
|
+
while current
|
|
154
|
+
break unless yield(current)
|
|
155
|
+
current = current.next
|
|
156
|
+
end
|
|
157
|
+
end
|
|
158
|
+
|
|
159
|
+
# Collect statements until a condition is met.
|
|
160
|
+
#
|
|
161
|
+
# @yield [Statement] Each statement
|
|
162
|
+
# @return [Array<Statement>] Statements until condition
|
|
163
|
+
def take_until(&block)
|
|
164
|
+
result = []
|
|
165
|
+
each_following do |stmt|
|
|
166
|
+
break if yield(stmt)
|
|
167
|
+
result << stmt
|
|
168
|
+
true
|
|
169
|
+
end
|
|
170
|
+
result
|
|
171
|
+
end
|
|
172
|
+
|
|
173
|
+
# ============================================================
|
|
174
|
+
# Tree navigation (delegates to inner_node when available)
|
|
175
|
+
# ============================================================
|
|
176
|
+
|
|
177
|
+
# @return [Object, nil] Parent node in original AST
|
|
178
|
+
def tree_parent
|
|
179
|
+
inner = unwrapped_node
|
|
180
|
+
inner.parent if inner.respond_to?(:parent)
|
|
181
|
+
end
|
|
182
|
+
|
|
183
|
+
# @return [Object, nil] Next sibling in original AST
|
|
184
|
+
def tree_next
|
|
185
|
+
inner = unwrapped_node
|
|
186
|
+
inner.next if inner.respond_to?(:next)
|
|
187
|
+
end
|
|
188
|
+
|
|
189
|
+
# @return [Object, nil] Previous sibling in original AST
|
|
190
|
+
def tree_previous
|
|
191
|
+
inner = unwrapped_node
|
|
192
|
+
inner.previous if inner.respond_to?(:previous)
|
|
193
|
+
end
|
|
194
|
+
|
|
195
|
+
# @return [Array<Object>] Children in original AST
|
|
196
|
+
def tree_children
|
|
197
|
+
inner = unwrapped_node
|
|
198
|
+
if inner.respond_to?(:each)
|
|
199
|
+
inner.to_a
|
|
200
|
+
elsif inner.respond_to?(:children)
|
|
201
|
+
inner.children
|
|
202
|
+
else
|
|
203
|
+
[]
|
|
204
|
+
end
|
|
205
|
+
end
|
|
206
|
+
|
|
207
|
+
# @return [Object, nil] First child in original AST
|
|
208
|
+
def tree_first_child
|
|
209
|
+
inner = unwrapped_node
|
|
210
|
+
inner.first_child if inner.respond_to?(:first_child)
|
|
211
|
+
end
|
|
212
|
+
|
|
213
|
+
# @return [Object, nil] Last child in original AST
|
|
214
|
+
def tree_last_child
|
|
215
|
+
inner = unwrapped_node
|
|
216
|
+
inner.last_child if inner.respond_to?(:last_child)
|
|
217
|
+
end
|
|
218
|
+
|
|
219
|
+
# @return [Boolean] true if tree navigation is available
|
|
220
|
+
def has_tree_navigation?
|
|
221
|
+
inner = unwrapped_node
|
|
222
|
+
inner.respond_to?(:parent) || inner.respond_to?(:next)
|
|
223
|
+
end
|
|
224
|
+
|
|
225
|
+
# @return [Boolean] true if this is a synthetic node (no tree navigation)
|
|
226
|
+
def synthetic?
|
|
227
|
+
!has_tree_navigation?
|
|
228
|
+
end
|
|
229
|
+
|
|
230
|
+
# Calculate the tree depth (distance from root).
|
|
231
|
+
#
|
|
232
|
+
# @return [Integer] Depth in tree (0 = root level)
|
|
233
|
+
def tree_depth
|
|
234
|
+
depth = 0
|
|
235
|
+
current = tree_parent
|
|
236
|
+
while current
|
|
237
|
+
depth += 1
|
|
238
|
+
# Navigate up through parents
|
|
239
|
+
if current.respond_to?(:parent)
|
|
240
|
+
current = current.parent
|
|
241
|
+
else
|
|
242
|
+
break
|
|
243
|
+
end
|
|
244
|
+
end
|
|
245
|
+
depth
|
|
246
|
+
end
|
|
247
|
+
|
|
248
|
+
# Check if this node is at same or shallower depth than another.
|
|
249
|
+
# Useful for determining section boundaries.
|
|
250
|
+
#
|
|
251
|
+
# @param other [Statement, Integer] Other statement or depth value
|
|
252
|
+
# @return [Boolean] true if this node is at same or shallower depth
|
|
253
|
+
def same_or_shallower_than?(other)
|
|
254
|
+
other_depth = other.is_a?(Integer) ? other : other.tree_depth
|
|
255
|
+
tree_depth <= other_depth
|
|
256
|
+
end
|
|
257
|
+
|
|
258
|
+
# ============================================================
|
|
259
|
+
# Node delegation
|
|
260
|
+
# ============================================================
|
|
261
|
+
|
|
262
|
+
# @return [Symbol, String] Node type
|
|
263
|
+
def type
|
|
264
|
+
return node.type if node.respond_to?(:type)
|
|
265
|
+
|
|
266
|
+
# Fallback: derive type from class name (handle anonymous classes)
|
|
267
|
+
class_name = node.class.name
|
|
268
|
+
class_name ? class_name.split("::").last : "Anonymous"
|
|
269
|
+
end
|
|
270
|
+
|
|
271
|
+
# @return [Array, Object, nil] Node signature for matching
|
|
272
|
+
def signature
|
|
273
|
+
node.signature if node.respond_to?(:signature)
|
|
274
|
+
end
|
|
275
|
+
|
|
276
|
+
# @return [String] Node text content
|
|
277
|
+
def text
|
|
278
|
+
# TreeHaver nodes (and any node conforming to the unified API) provide #text.
|
|
279
|
+
# No conditional fallbacks - nodes must conform to the API.
|
|
280
|
+
node.text.to_s
|
|
281
|
+
end
|
|
282
|
+
|
|
283
|
+
# @return [Hash, nil] Source position info
|
|
284
|
+
def source_position
|
|
285
|
+
node.source_position if node.respond_to?(:source_position)
|
|
286
|
+
end
|
|
287
|
+
|
|
288
|
+
# @return [Integer, nil] Start line number
|
|
289
|
+
def start_line
|
|
290
|
+
pos = source_position
|
|
291
|
+
pos[:start_line] if pos
|
|
292
|
+
end
|
|
293
|
+
|
|
294
|
+
# @return [Integer, nil] End line number
|
|
295
|
+
def end_line
|
|
296
|
+
pos = source_position
|
|
297
|
+
pos[:end_line] if pos
|
|
298
|
+
end
|
|
299
|
+
|
|
300
|
+
# ============================================================
|
|
301
|
+
# Node attribute helpers (language-agnostic)
|
|
302
|
+
# ============================================================
|
|
303
|
+
|
|
304
|
+
# Check if this node matches a type.
|
|
305
|
+
#
|
|
306
|
+
# @param expected_type [Symbol, String] Type to check
|
|
307
|
+
# @return [Boolean]
|
|
308
|
+
def type?(expected_type)
|
|
309
|
+
type.to_s == expected_type.to_s
|
|
310
|
+
end
|
|
311
|
+
|
|
312
|
+
# Check if this node's text matches a pattern.
|
|
313
|
+
#
|
|
314
|
+
# @param pattern [String, Regexp] Pattern to match
|
|
315
|
+
# @return [Boolean]
|
|
316
|
+
def text_matches?(pattern)
|
|
317
|
+
case pattern
|
|
318
|
+
when Regexp
|
|
319
|
+
text.match?(pattern)
|
|
320
|
+
else
|
|
321
|
+
text.include?(pattern.to_s)
|
|
322
|
+
end
|
|
323
|
+
end
|
|
324
|
+
|
|
325
|
+
# Get an attribute from the underlying node.
|
|
326
|
+
#
|
|
327
|
+
# Tries multiple method names to support different parser APIs.
|
|
328
|
+
#
|
|
329
|
+
# @param name [Symbol, String] Attribute name
|
|
330
|
+
# @param aliases [Array<Symbol>] Alternative method names
|
|
331
|
+
# @return [Object, nil] Attribute value
|
|
332
|
+
def node_attribute(name, *aliases)
|
|
333
|
+
inner = unwrapped_node
|
|
334
|
+
[name, *aliases].each do |method_name|
|
|
335
|
+
return inner.send(method_name) if inner.respond_to?(method_name)
|
|
336
|
+
end
|
|
337
|
+
nil
|
|
338
|
+
end
|
|
339
|
+
|
|
340
|
+
# ============================================================
|
|
341
|
+
# Utilities
|
|
342
|
+
# ============================================================
|
|
343
|
+
|
|
344
|
+
# Get the unwrapped inner node.
|
|
345
|
+
#
|
|
346
|
+
# @return [Object] The innermost node
|
|
347
|
+
def unwrapped_node
|
|
348
|
+
current = node
|
|
349
|
+
while current.respond_to?(:inner_node) && current.inner_node != current
|
|
350
|
+
current = current.inner_node
|
|
351
|
+
end
|
|
352
|
+
current
|
|
353
|
+
end
|
|
354
|
+
|
|
355
|
+
# @return [String] Human-readable representation
|
|
356
|
+
def inspect
|
|
357
|
+
"#<Navigable::Statement[#{index}] type=#{type} tree=#{has_tree_navigation?}>"
|
|
358
|
+
end
|
|
359
|
+
|
|
360
|
+
# @return [String] String representation
|
|
361
|
+
def to_s
|
|
362
|
+
text.to_s.strip[0, 50]
|
|
363
|
+
end
|
|
364
|
+
|
|
365
|
+
# Delegate unknown methods to the wrapped node.
|
|
366
|
+
def method_missing(method, *args, &block)
|
|
367
|
+
if node.respond_to?(method)
|
|
368
|
+
node.send(method, *args, &block)
|
|
369
|
+
else
|
|
370
|
+
super
|
|
371
|
+
end
|
|
372
|
+
end
|
|
373
|
+
|
|
374
|
+
def respond_to_missing?(method, include_private = false)
|
|
375
|
+
node.respond_to?(method, include_private) || super
|
|
376
|
+
end
|
|
377
|
+
end
|
|
378
|
+
end
|
|
379
|
+
end
|
|
380
|
+
end
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Ast
|
|
4
|
+
module Merge
|
|
5
|
+
# Namespace for navigation-related classes.
|
|
6
|
+
#
|
|
7
|
+
# Provides unified navigation over AST nodes regardless of the underlying parser.
|
|
8
|
+
# Classes in this namespace work together to enable finding and manipulating
|
|
9
|
+
# positions in document structures.
|
|
10
|
+
#
|
|
11
|
+
# @see Navigable::Statement Wraps nodes with navigation capabilities
|
|
12
|
+
# @see Navigable::InjectionPoint Represents a location for content injection
|
|
13
|
+
# @see Navigable::InjectionPointFinder Finds injection points by matching rules
|
|
14
|
+
module Navigable
|
|
15
|
+
autoload :Statement, "ast/merge/navigable/statement"
|
|
16
|
+
autoload :InjectionPoint, "ast/merge/navigable/injection_point"
|
|
17
|
+
autoload :InjectionPointFinder, "ast/merge/navigable/injection_point_finder"
|
|
18
|
+
end
|
|
19
|
+
end
|
|
20
|
+
end
|
|
@@ -129,15 +129,16 @@ module Ast
|
|
|
129
129
|
def merge
|
|
130
130
|
# Parse destination and find injection point
|
|
131
131
|
d_analysis = create_analysis(destination)
|
|
132
|
-
d_statements =
|
|
132
|
+
d_statements = Navigable::Statement.build_list(d_analysis.statements)
|
|
133
133
|
|
|
134
|
-
finder = InjectionPointFinder.new(d_statements)
|
|
134
|
+
finder = Navigable::InjectionPointFinder.new(d_statements)
|
|
135
135
|
injection_point = finder.find(
|
|
136
136
|
type: anchor[:type],
|
|
137
137
|
text: anchor[:text],
|
|
138
138
|
position: :replace,
|
|
139
139
|
boundary_type: boundary&.dig(:type),
|
|
140
140
|
boundary_text: boundary&.dig(:text),
|
|
141
|
+
boundary_same_or_shallower: boundary&.dig(:same_or_shallower) || false,
|
|
141
142
|
)
|
|
142
143
|
|
|
143
144
|
if injection_point.nil?
|
|
@@ -200,6 +201,7 @@ module Ast
|
|
|
200
201
|
result[:level] = matcher[:level] if matcher[:level]
|
|
201
202
|
result[:level_lte] = matcher[:level_lte] if matcher[:level_lte]
|
|
202
203
|
result[:level_gte] = matcher[:level_gte] if matcher[:level_gte]
|
|
204
|
+
result[:same_or_shallower] = matcher[:same_or_shallower] if matcher.key?(:same_or_shallower)
|
|
203
205
|
result.compact
|
|
204
206
|
end
|
|
205
207
|
|
|
@@ -135,6 +135,20 @@ module Ast
|
|
|
135
135
|
script_loader.load_callable(value)
|
|
136
136
|
end
|
|
137
137
|
|
|
138
|
+
# Get the normalize_whitespace setting.
|
|
139
|
+
#
|
|
140
|
+
# @return [Boolean] Whether to collapse excessive blank lines
|
|
141
|
+
def normalize_whitespace
|
|
142
|
+
merge_config[:normalize_whitespace] == true
|
|
143
|
+
end
|
|
144
|
+
|
|
145
|
+
# Get the rehydrate_link_references setting.
|
|
146
|
+
#
|
|
147
|
+
# @return [Boolean] Whether to convert inline links to reference style
|
|
148
|
+
def rehydrate_link_references
|
|
149
|
+
merge_config[:rehydrate_link_references] == true
|
|
150
|
+
end
|
|
151
|
+
|
|
138
152
|
# Convert preset to a hash suitable for SmartMerger options.
|
|
139
153
|
#
|
|
140
154
|
# @return [Hash]
|
|
@@ -146,6 +160,8 @@ module Ast
|
|
|
146
160
|
node_typing: node_typing,
|
|
147
161
|
match_refiner: match_refiner,
|
|
148
162
|
freeze_token: freeze_token,
|
|
163
|
+
normalize_whitespace: normalize_whitespace,
|
|
164
|
+
rehydrate_link_references: rehydrate_link_references,
|
|
149
165
|
}.compact
|
|
150
166
|
end
|
|
151
167
|
|
|
@@ -168,6 +184,8 @@ module Ast
|
|
|
168
184
|
signature_generator: config["signature_generator"],
|
|
169
185
|
node_typing: config["node_typing"],
|
|
170
186
|
match_refiner: config["match_refiner"],
|
|
187
|
+
normalize_whitespace: config["normalize_whitespace"] == true,
|
|
188
|
+
rehydrate_link_references: config["rehydrate_link_references"] == true,
|
|
171
189
|
}
|
|
172
190
|
end
|
|
173
191
|
|
|
@@ -26,7 +26,7 @@ module Ast
|
|
|
26
26
|
#
|
|
27
27
|
class Runner
|
|
28
28
|
# Result of processing a single file
|
|
29
|
-
Result = Struct.new(:path, :relative_path, :status, :changed, :has_anchor, :message, :stats, :error, keyword_init: true)
|
|
29
|
+
Result = Struct.new(:path, :relative_path, :status, :changed, :has_anchor, :message, :stats, :problems, :error, keyword_init: true)
|
|
30
30
|
|
|
31
31
|
# @return [Config] The recipe being executed
|
|
32
32
|
attr_reader :recipe
|
|
@@ -157,6 +157,8 @@ module Ast
|
|
|
157
157
|
signature_generator: recipe.signature_generator,
|
|
158
158
|
node_typing: recipe.node_typing,
|
|
159
159
|
match_refiner: recipe.match_refiner,
|
|
160
|
+
normalize_whitespace: recipe.normalize_whitespace,
|
|
161
|
+
rehydrate_link_references: recipe.rehydrate_link_references,
|
|
160
162
|
)
|
|
161
163
|
|
|
162
164
|
result = merger.merge
|
|
@@ -202,6 +204,9 @@ module Ast
|
|
|
202
204
|
def create_result_from_merge(target_path, relative_path, _destination_content, merge_result)
|
|
203
205
|
changed = merge_result.changed
|
|
204
206
|
|
|
207
|
+
# Extract problems from stats if present (PartialTemplateMerger stores them there)
|
|
208
|
+
problems = merge_result.stats[:problems] if merge_result.stats.is_a?(Hash)
|
|
209
|
+
|
|
205
210
|
if changed
|
|
206
211
|
unless dry_run
|
|
207
212
|
File.write(target_path, merge_result.content)
|
|
@@ -215,6 +220,7 @@ module Ast
|
|
|
215
220
|
has_anchor: true,
|
|
216
221
|
message: dry_run ? "Would update" : "Updated",
|
|
217
222
|
stats: merge_result.stats,
|
|
223
|
+
problems: problems,
|
|
218
224
|
)
|
|
219
225
|
else
|
|
220
226
|
Result.new(
|
|
@@ -225,6 +231,7 @@ module Ast
|
|
|
225
231
|
has_anchor: true,
|
|
226
232
|
message: "No changes needed",
|
|
227
233
|
stats: merge_result.stats,
|
|
234
|
+
problems: problems,
|
|
228
235
|
)
|
|
229
236
|
end
|
|
230
237
|
end
|
data/lib/ast/merge/version.rb
CHANGED
data/lib/ast/merge.rb
CHANGED
|
@@ -142,17 +142,16 @@ module Ast
|
|
|
142
142
|
autoload :ConflictResolverBase, "ast/merge/conflict_resolver_base"
|
|
143
143
|
autoload :ContentMatchRefiner, "ast/merge/content_match_refiner"
|
|
144
144
|
autoload :DebugLogger, "ast/merge/debug_logger"
|
|
145
|
+
autoload :DiffMapperBase, "ast/merge/diff_mapper_base"
|
|
145
146
|
autoload :EmitterBase, "ast/merge/emitter_base"
|
|
146
147
|
autoload :FileAnalyzable, "ast/merge/file_analyzable"
|
|
147
148
|
autoload :Freezable, "ast/merge/freezable"
|
|
148
149
|
autoload :FreezeNodeBase, "ast/merge/freeze_node_base"
|
|
149
|
-
autoload :InjectionPoint, "ast/merge/navigable_statement"
|
|
150
|
-
autoload :InjectionPointFinder, "ast/merge/navigable_statement"
|
|
151
150
|
autoload :MatchRefinerBase, "ast/merge/match_refiner_base"
|
|
152
151
|
autoload :MatchScoreBase, "ast/merge/match_score_base"
|
|
153
152
|
autoload :MergeResultBase, "ast/merge/merge_result_base"
|
|
154
153
|
autoload :MergerConfig, "ast/merge/merger_config"
|
|
155
|
-
autoload :
|
|
154
|
+
autoload :Navigable, "ast/merge/navigable"
|
|
156
155
|
autoload :NodeTyping, "ast/merge/node_typing"
|
|
157
156
|
autoload :NodeWrapperBase, "ast/merge/node_wrapper_base"
|
|
158
157
|
autoload :PartialTemplateMergerBase, "ast/merge/partial_template_merger_base"
|
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:
|
|
4
|
+
version: 4.0.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Peter H. Boling
|
|
@@ -63,20 +63,20 @@ dependencies:
|
|
|
63
63
|
requirements:
|
|
64
64
|
- - "~>"
|
|
65
65
|
- !ruby/object:Gem::Version
|
|
66
|
-
version: '
|
|
66
|
+
version: '5.0'
|
|
67
67
|
- - ">="
|
|
68
68
|
- !ruby/object:Gem::Version
|
|
69
|
-
version:
|
|
69
|
+
version: 5.0.0
|
|
70
70
|
type: :runtime
|
|
71
71
|
prerelease: false
|
|
72
72
|
version_requirements: !ruby/object:Gem::Requirement
|
|
73
73
|
requirements:
|
|
74
74
|
- - "~>"
|
|
75
75
|
- !ruby/object:Gem::Version
|
|
76
|
-
version: '
|
|
76
|
+
version: '5.0'
|
|
77
77
|
- - ">="
|
|
78
78
|
- !ruby/object:Gem::Version
|
|
79
|
-
version:
|
|
79
|
+
version: 5.0.0
|
|
80
80
|
- !ruby/object:Gem::Dependency
|
|
81
81
|
name: kettle-dev
|
|
82
82
|
requirement: !ruby/object:Gem::Requirement
|
|
@@ -261,6 +261,26 @@ dependencies:
|
|
|
261
261
|
- - ">="
|
|
262
262
|
- !ruby/object:Gem::Version
|
|
263
263
|
version: 1.0.3
|
|
264
|
+
- !ruby/object:Gem::Dependency
|
|
265
|
+
name: ostruct
|
|
266
|
+
requirement: !ruby/object:Gem::Requirement
|
|
267
|
+
requirements:
|
|
268
|
+
- - "~>"
|
|
269
|
+
- !ruby/object:Gem::Version
|
|
270
|
+
version: '0.6'
|
|
271
|
+
- - ">="
|
|
272
|
+
- !ruby/object:Gem::Version
|
|
273
|
+
version: 0.6.3
|
|
274
|
+
type: :development
|
|
275
|
+
prerelease: false
|
|
276
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
277
|
+
requirements:
|
|
278
|
+
- - "~>"
|
|
279
|
+
- !ruby/object:Gem::Version
|
|
280
|
+
version: '0.6'
|
|
281
|
+
- - ">="
|
|
282
|
+
- !ruby/object:Gem::Version
|
|
283
|
+
version: 0.6.3
|
|
264
284
|
description: "☯️ Ast::Merge provides base classes, modules, and RSpec shared examples
|
|
265
285
|
for building intelligent file mergers using AST analysis. It powers prism-merge,
|
|
266
286
|
psych-merge, json-merge, and other format-specific merge gems."
|
|
@@ -309,6 +329,7 @@ files:
|
|
|
309
329
|
- lib/ast/merge/detector/mergeable.rb
|
|
310
330
|
- lib/ast/merge/detector/toml_frontmatter.rb
|
|
311
331
|
- lib/ast/merge/detector/yaml_frontmatter.rb
|
|
332
|
+
- lib/ast/merge/diff_mapper_base.rb
|
|
312
333
|
- lib/ast/merge/emitter_base.rb
|
|
313
334
|
- lib/ast/merge/file_analyzable.rb
|
|
314
335
|
- lib/ast/merge/freezable.rb
|
|
@@ -317,7 +338,10 @@ files:
|
|
|
317
338
|
- lib/ast/merge/match_score_base.rb
|
|
318
339
|
- lib/ast/merge/merge_result_base.rb
|
|
319
340
|
- lib/ast/merge/merger_config.rb
|
|
320
|
-
- lib/ast/merge/
|
|
341
|
+
- lib/ast/merge/navigable.rb
|
|
342
|
+
- lib/ast/merge/navigable/injection_point.rb
|
|
343
|
+
- lib/ast/merge/navigable/injection_point_finder.rb
|
|
344
|
+
- lib/ast/merge/navigable/statement.rb
|
|
321
345
|
- lib/ast/merge/node_typing.rb
|
|
322
346
|
- lib/ast/merge/node_typing/frozen_wrapper.rb
|
|
323
347
|
- lib/ast/merge/node_typing/normalizer.rb
|
|
@@ -357,10 +381,10 @@ licenses:
|
|
|
357
381
|
- MIT
|
|
358
382
|
metadata:
|
|
359
383
|
homepage_uri: https://ast-merge.galtzo.com/
|
|
360
|
-
source_code_uri: https://github.com/kettle-rb/ast-merge/tree/
|
|
361
|
-
changelog_uri: https://github.com/kettle-rb/ast-merge/blob/
|
|
384
|
+
source_code_uri: https://github.com/kettle-rb/ast-merge/tree/v4.0.0
|
|
385
|
+
changelog_uri: https://github.com/kettle-rb/ast-merge/blob/v4.0.0/CHANGELOG.md
|
|
362
386
|
bug_tracker_uri: https://github.com/kettle-rb/ast-merge/issues
|
|
363
|
-
documentation_uri: https://www.rubydoc.info/gems/ast-merge/
|
|
387
|
+
documentation_uri: https://www.rubydoc.info/gems/ast-merge/4.0.0
|
|
364
388
|
funding_uri: https://github.com/sponsors/pboling
|
|
365
389
|
wiki_uri: https://github.com/kettle-rb/ast-merge/wiki
|
|
366
390
|
news_uri: https://www.railsbling.com/tags/ast-merge
|
metadata.gz.sig
CHANGED
|
Binary file
|