ast-merge 1.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 +7 -0
- checksums.yaml.gz.sig +0 -0
- data/CHANGELOG.md +46 -0
- data/CITATION.cff +20 -0
- data/CODE_OF_CONDUCT.md +134 -0
- data/CONTRIBUTING.md +227 -0
- data/FUNDING.md +74 -0
- data/LICENSE.txt +21 -0
- data/README.md +852 -0
- data/REEK +0 -0
- data/RUBOCOP.md +71 -0
- data/SECURITY.md +21 -0
- data/lib/ast/merge/ast_node.rb +87 -0
- data/lib/ast/merge/comment/block.rb +195 -0
- data/lib/ast/merge/comment/empty.rb +78 -0
- data/lib/ast/merge/comment/line.rb +138 -0
- data/lib/ast/merge/comment/parser.rb +278 -0
- data/lib/ast/merge/comment/style.rb +282 -0
- data/lib/ast/merge/comment.rb +36 -0
- data/lib/ast/merge/conflict_resolver_base.rb +399 -0
- data/lib/ast/merge/debug_logger.rb +271 -0
- data/lib/ast/merge/fenced_code_block_detector.rb +211 -0
- data/lib/ast/merge/file_analyzable.rb +307 -0
- data/lib/ast/merge/freezable.rb +82 -0
- data/lib/ast/merge/freeze_node_base.rb +434 -0
- data/lib/ast/merge/match_refiner_base.rb +312 -0
- data/lib/ast/merge/match_score_base.rb +135 -0
- data/lib/ast/merge/merge_result_base.rb +169 -0
- data/lib/ast/merge/merger_config.rb +258 -0
- data/lib/ast/merge/node_typing.rb +373 -0
- data/lib/ast/merge/region.rb +124 -0
- data/lib/ast/merge/region_detector_base.rb +114 -0
- data/lib/ast/merge/region_mergeable.rb +364 -0
- data/lib/ast/merge/rspec/shared_examples/conflict_resolver_base.rb +416 -0
- data/lib/ast/merge/rspec/shared_examples/debug_logger.rb +174 -0
- data/lib/ast/merge/rspec/shared_examples/file_analyzable.rb +193 -0
- data/lib/ast/merge/rspec/shared_examples/freeze_node_base.rb +219 -0
- data/lib/ast/merge/rspec/shared_examples/merge_result_base.rb +106 -0
- data/lib/ast/merge/rspec/shared_examples/merger_config.rb +202 -0
- data/lib/ast/merge/rspec/shared_examples/reproducible_merge.rb +115 -0
- data/lib/ast/merge/rspec/shared_examples.rb +26 -0
- data/lib/ast/merge/rspec.rb +4 -0
- data/lib/ast/merge/section_typing.rb +303 -0
- data/lib/ast/merge/smart_merger_base.rb +417 -0
- data/lib/ast/merge/text/conflict_resolver.rb +161 -0
- data/lib/ast/merge/text/file_analysis.rb +168 -0
- data/lib/ast/merge/text/line_node.rb +142 -0
- data/lib/ast/merge/text/merge_result.rb +42 -0
- data/lib/ast/merge/text/section.rb +93 -0
- data/lib/ast/merge/text/section_splitter.rb +397 -0
- data/lib/ast/merge/text/smart_merger.rb +141 -0
- data/lib/ast/merge/text/word_node.rb +86 -0
- data/lib/ast/merge/text.rb +35 -0
- data/lib/ast/merge/toml_frontmatter_detector.rb +88 -0
- data/lib/ast/merge/version.rb +12 -0
- data/lib/ast/merge/yaml_frontmatter_detector.rb +108 -0
- data/lib/ast/merge.rb +165 -0
- data/lib/ast-merge.rb +4 -0
- data/sig/ast/merge.rbs +195 -0
- data.tar.gz.sig +0 -0
- metadata +326 -0
- metadata.gz.sig +0 -0
|
@@ -0,0 +1,434 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "freezable"
|
|
4
|
+
|
|
5
|
+
module Ast
|
|
6
|
+
module Merge
|
|
7
|
+
# Base class for freeze block nodes in AST merge libraries.
|
|
8
|
+
#
|
|
9
|
+
# A freeze block is a section marked with freeze/unfreeze comment markers that
|
|
10
|
+
# should be preserved from the destination during merges. The entire content
|
|
11
|
+
# between the markers is treated as opaque and matched by content identity.
|
|
12
|
+
#
|
|
13
|
+
# ## Key Distinction from FrozenWrapper
|
|
14
|
+
#
|
|
15
|
+
# FreezeNodeBase represents **explicit freeze blocks** with clear boundaries:
|
|
16
|
+
# - Starts with `# token:freeze` (or equivalent in other comment styles)
|
|
17
|
+
# - Ends with `# token:unfreeze`
|
|
18
|
+
# - The content between markers is opaque and preserved verbatim
|
|
19
|
+
# - Matched by CONTENT identity via `freeze_signature`
|
|
20
|
+
#
|
|
21
|
+
# In contrast, NodeTyping::FrozenWrapper represents **AST nodes with freeze markers
|
|
22
|
+
# in their leading comments**:
|
|
23
|
+
# - The marker appears in the node's leading comments, not as a block boundary
|
|
24
|
+
# - The node is still a structural AST element (e.g., a `gem` call)
|
|
25
|
+
# - Matched by the underlying node's STRUCTURAL identity
|
|
26
|
+
#
|
|
27
|
+
# ## Signature Generation Behavior
|
|
28
|
+
#
|
|
29
|
+
# When FileAnalyzable#generate_signature encounters a FreezeNodeBase, it uses
|
|
30
|
+
# the `freeze_signature` method directly, which returns `[:FreezeNode, content]`.
|
|
31
|
+
# This ensures that explicit freeze blocks are matched by their exact content.
|
|
32
|
+
#
|
|
33
|
+
# This class provides shared functionality for file-type-specific implementations
|
|
34
|
+
# (e.g., Prism::Merge::FreezeNode, Psych::Merge::FreezeNode).
|
|
35
|
+
#
|
|
36
|
+
# Supports multiple comment syntax styles via configurable marker patterns:
|
|
37
|
+
# - `:hash_comment` - Ruby/Python/YAML style (`# freeze-begin` / `# freeze-end`)
|
|
38
|
+
# - `:html_comment` - HTML/Markdown style (`<!-- freeze-begin -->` / `<!-- freeze-end -->`)
|
|
39
|
+
# - `:c_style_line` - C/JavaScript line comments (`// freeze-begin` / `// freeze-end`)
|
|
40
|
+
# - `:c_style_block` - C/JavaScript block comments (`/* freeze-begin */` / `/* freeze-end */`)
|
|
41
|
+
#
|
|
42
|
+
# @example Freeze block with hash comments (Ruby/YAML)
|
|
43
|
+
# # <token>:freeze
|
|
44
|
+
# content to preserve...
|
|
45
|
+
# # <token>:unfreeze
|
|
46
|
+
#
|
|
47
|
+
# @example Freeze block with HTML comments (Markdown)
|
|
48
|
+
# <!-- <token>:freeze -->
|
|
49
|
+
# content to preserve...
|
|
50
|
+
# <!-- <token>:unfreeze -->
|
|
51
|
+
#
|
|
52
|
+
# @example Creating a custom pattern
|
|
53
|
+
# FreezeNodeBase.register_pattern(:custom,
|
|
54
|
+
# start: /^--\s*freeze-begin/i,
|
|
55
|
+
# end_pattern: /^--\s*freeze-end/i
|
|
56
|
+
# )
|
|
57
|
+
#
|
|
58
|
+
# @see Freezable#freeze_signature - Content-based signature for matching
|
|
59
|
+
# @see NodeTyping::FrozenWrapper - Structural matching alternative
|
|
60
|
+
# @see FileAnalyzable#generate_signature - Routing logic for signature generation
|
|
61
|
+
class FreezeNodeBase
|
|
62
|
+
include Freezable
|
|
63
|
+
|
|
64
|
+
# Error raised when a freeze block has invalid structure
|
|
65
|
+
class InvalidStructureError < StandardError
|
|
66
|
+
# @return [Integer, nil] Starting line of the freeze block
|
|
67
|
+
attr_reader :start_line
|
|
68
|
+
|
|
69
|
+
# @return [Integer, nil] Ending line of the freeze block
|
|
70
|
+
attr_reader :end_line
|
|
71
|
+
|
|
72
|
+
# @return [Array] Nodes that caused the structure error (optional)
|
|
73
|
+
attr_reader :unclosed_nodes
|
|
74
|
+
|
|
75
|
+
# @param message [String] Error message
|
|
76
|
+
# @param start_line [Integer, nil] Start line number
|
|
77
|
+
# @param end_line [Integer, nil] End line number
|
|
78
|
+
# @param unclosed_nodes [Array] Nodes causing the error
|
|
79
|
+
def initialize(message, start_line: nil, end_line: nil, unclosed_nodes: [])
|
|
80
|
+
super(message)
|
|
81
|
+
@start_line = start_line
|
|
82
|
+
@end_line = end_line
|
|
83
|
+
@unclosed_nodes = unclosed_nodes
|
|
84
|
+
end
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
# Simple location struct for compatibility with AST nodes
|
|
88
|
+
Location = Struct.new(:start_line, :end_line) do
|
|
89
|
+
# Check if a line number is within this location
|
|
90
|
+
# @param line [Integer] Line number to check
|
|
91
|
+
# @return [Boolean]
|
|
92
|
+
def cover?(line)
|
|
93
|
+
(start_line..end_line).cover?(line)
|
|
94
|
+
end
|
|
95
|
+
end
|
|
96
|
+
|
|
97
|
+
# Pattern configuration for freeze block markers.
|
|
98
|
+
# Mutable to allow runtime registration of custom patterns.
|
|
99
|
+
# @return [Hash{Symbol => Hash{Symbol => Regexp}}] Registered marker patterns
|
|
100
|
+
MARKER_PATTERNS = {
|
|
101
|
+
hash_comment: {
|
|
102
|
+
start: /^\s*#\s*[\w-]+:freeze\b/i,
|
|
103
|
+
end: /^\s*#\s*[\w-]+:unfreeze\b/i,
|
|
104
|
+
},
|
|
105
|
+
html_comment: {
|
|
106
|
+
start: /^\s*<!--\s*[\w-]+:freeze\b.*-->/i,
|
|
107
|
+
end: /^\s*<!--\s*[\w-]+:unfreeze\b.*-->/i,
|
|
108
|
+
},
|
|
109
|
+
c_style_line: {
|
|
110
|
+
start: %r{^\s*//\s*[\w-]+:freeze\b}i,
|
|
111
|
+
end: %r{^\s*//\s*[\w-]+:unfreeze\b}i,
|
|
112
|
+
},
|
|
113
|
+
c_style_block: {
|
|
114
|
+
start: %r{^\s*/\*\s*[\w-]+:freeze\b.*\*/}i,
|
|
115
|
+
end: %r{^\s*/\*\s*[\w-]+:unfreeze\b.*\*/}i,
|
|
116
|
+
},
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
# Default pattern when none specified
|
|
120
|
+
# @return [Symbol]
|
|
121
|
+
DEFAULT_PATTERN = :hash_comment
|
|
122
|
+
|
|
123
|
+
class << self
|
|
124
|
+
# Register a custom marker pattern
|
|
125
|
+
# @param name [Symbol] Pattern name
|
|
126
|
+
# @param start [Regexp] Regex to match freeze start marker
|
|
127
|
+
# @param end_pattern [Regexp] Regex to match freeze end marker
|
|
128
|
+
# @return [Hash{Symbol => Regexp}] The registered pattern
|
|
129
|
+
# @raise [ArgumentError] if name already exists or patterns invalid
|
|
130
|
+
def register_pattern(name, start:, end_pattern:)
|
|
131
|
+
raise ArgumentError, "Pattern :#{name} already registered" if MARKER_PATTERNS.key?(name)
|
|
132
|
+
raise ArgumentError, "Start pattern must be a Regexp" unless start.is_a?(Regexp)
|
|
133
|
+
raise ArgumentError, "End pattern must be a Regexp" unless end_pattern.is_a?(Regexp)
|
|
134
|
+
|
|
135
|
+
MARKER_PATTERNS[name] = {start: start, end: end_pattern}
|
|
136
|
+
end
|
|
137
|
+
|
|
138
|
+
# Get start marker pattern for a given pattern type
|
|
139
|
+
# @param pattern_type [Symbol] Pattern type name (defaults to DEFAULT_PATTERN)
|
|
140
|
+
# @return [Regexp] Start marker regex
|
|
141
|
+
# @raise [ArgumentError] if pattern type not found
|
|
142
|
+
def start_pattern(pattern_type = DEFAULT_PATTERN)
|
|
143
|
+
patterns = MARKER_PATTERNS[pattern_type]
|
|
144
|
+
raise ArgumentError, "Unknown pattern type: #{pattern_type}" unless patterns
|
|
145
|
+
|
|
146
|
+
patterns[:start]
|
|
147
|
+
end
|
|
148
|
+
|
|
149
|
+
# Get end marker pattern for a given pattern type
|
|
150
|
+
# @param pattern_type [Symbol] Pattern type name (defaults to DEFAULT_PATTERN)
|
|
151
|
+
# @return [Regexp] End marker regex
|
|
152
|
+
# @raise [ArgumentError] if pattern type not found
|
|
153
|
+
def end_pattern(pattern_type = DEFAULT_PATTERN)
|
|
154
|
+
patterns = MARKER_PATTERNS[pattern_type]
|
|
155
|
+
raise ArgumentError, "Unknown pattern type: #{pattern_type}" unless patterns
|
|
156
|
+
|
|
157
|
+
patterns[:end]
|
|
158
|
+
end
|
|
159
|
+
|
|
160
|
+
# Get both start and end patterns for a given pattern type
|
|
161
|
+
# When token is provided, returns a combined pattern with capture groups
|
|
162
|
+
# for marker type (freeze/unfreeze) and optional reason.
|
|
163
|
+
#
|
|
164
|
+
# @param pattern_type [Symbol] Pattern type name (defaults to DEFAULT_PATTERN)
|
|
165
|
+
# @param token [String, nil] Optional freeze token to build specific pattern
|
|
166
|
+
# @return [Hash{Symbol => Regexp}, Regexp] Hash with :start/:end keys, or combined Regexp if token provided
|
|
167
|
+
# @raise [ArgumentError] if pattern type not found
|
|
168
|
+
#
|
|
169
|
+
# @example Without token (returns hash of patterns)
|
|
170
|
+
# FreezeNode.pattern_for(:hash_comment)
|
|
171
|
+
# # => { start: /.../, end: /.../ }
|
|
172
|
+
#
|
|
173
|
+
# @example With token (returns combined pattern with capture groups)
|
|
174
|
+
# FreezeNode.pattern_for(:hash_comment, "my-merge")
|
|
175
|
+
# # => /^\s*#\s*my-merge:(freeze|unfreeze)\b\s*(.*)?$/i
|
|
176
|
+
# # Capture group 1: "freeze" or "unfreeze"
|
|
177
|
+
# # Capture group 2: optional reason text
|
|
178
|
+
def pattern_for(pattern_type = DEFAULT_PATTERN, token = nil)
|
|
179
|
+
raise ArgumentError, "Unknown pattern type: #{pattern_type}" unless MARKER_PATTERNS.key?(pattern_type)
|
|
180
|
+
|
|
181
|
+
# If no token provided, return the static patterns hash
|
|
182
|
+
return MARKER_PATTERNS[pattern_type] unless token
|
|
183
|
+
|
|
184
|
+
# Build a combined pattern with capture groups for the specific token
|
|
185
|
+
escaped_token = Regexp.escape(token)
|
|
186
|
+
|
|
187
|
+
case pattern_type
|
|
188
|
+
when :hash_comment
|
|
189
|
+
/^\s*#\s*#{escaped_token}:(freeze|unfreeze)\b\s*(.*)?$/i
|
|
190
|
+
when :html_comment
|
|
191
|
+
/^\s*<!--\s*#{escaped_token}:(freeze|unfreeze)(?:\s+(.+?))?\s*-->/i
|
|
192
|
+
when :c_style_line
|
|
193
|
+
%r{^\s*//\s*#{escaped_token}:(freeze|unfreeze)\b\s*(.*)?$}i
|
|
194
|
+
when :c_style_block
|
|
195
|
+
%r{^\s*/\*\s*#{escaped_token}:(freeze|unfreeze)\b\s*(.*)? *\*/}i
|
|
196
|
+
else
|
|
197
|
+
# Fallback for custom registered patterns - can't build token-specific
|
|
198
|
+
raise ArgumentError, "Cannot build token-specific pattern for custom type: #{pattern_type}"
|
|
199
|
+
end
|
|
200
|
+
end
|
|
201
|
+
|
|
202
|
+
# Check if a line matches a freeze start marker
|
|
203
|
+
# @param line [String] Line content to check
|
|
204
|
+
# @param pattern_type [Symbol] Pattern type to use (defaults to DEFAULT_PATTERN)
|
|
205
|
+
# @return [Boolean]
|
|
206
|
+
def freeze_start?(line, pattern_type = DEFAULT_PATTERN)
|
|
207
|
+
return false if line.nil?
|
|
208
|
+
|
|
209
|
+
start_pattern(pattern_type).match?(line)
|
|
210
|
+
end
|
|
211
|
+
|
|
212
|
+
# Check if a line matches a freeze end marker
|
|
213
|
+
# @param line [String] Line content to check
|
|
214
|
+
# @param pattern_type [Symbol] Pattern type to use (defaults to DEFAULT_PATTERN)
|
|
215
|
+
# @return [Boolean]
|
|
216
|
+
def freeze_end?(line, pattern_type = DEFAULT_PATTERN)
|
|
217
|
+
return false if line.nil?
|
|
218
|
+
|
|
219
|
+
end_pattern(pattern_type).match?(line)
|
|
220
|
+
end
|
|
221
|
+
|
|
222
|
+
# Available pattern types
|
|
223
|
+
# @return [Array<Symbol>]
|
|
224
|
+
def pattern_types
|
|
225
|
+
MARKER_PATTERNS.keys
|
|
226
|
+
end
|
|
227
|
+
end
|
|
228
|
+
|
|
229
|
+
# @return [Integer] Line number of freeze marker (1-based)
|
|
230
|
+
attr_reader :start_line
|
|
231
|
+
|
|
232
|
+
# @return [Integer] Line number of unfreeze marker (1-based)
|
|
233
|
+
attr_reader :end_line
|
|
234
|
+
|
|
235
|
+
# @return [String] Content of the freeze block
|
|
236
|
+
attr_reader :content
|
|
237
|
+
|
|
238
|
+
# @return [String, nil] The freeze start marker text
|
|
239
|
+
attr_reader :start_marker
|
|
240
|
+
|
|
241
|
+
# @return [String, nil] The freeze end marker text
|
|
242
|
+
attr_reader :end_marker
|
|
243
|
+
|
|
244
|
+
# @return [Symbol] The pattern type used for this freeze node
|
|
245
|
+
attr_reader :pattern_type
|
|
246
|
+
|
|
247
|
+
# @return [Array<String>, nil] Lines within the freeze block
|
|
248
|
+
attr_reader :lines
|
|
249
|
+
|
|
250
|
+
# @return [Object, nil] Reference to FileAnalysis (for subclasses that need it)
|
|
251
|
+
attr_reader :analysis
|
|
252
|
+
|
|
253
|
+
# @return [Array] AST nodes contained within the freeze block
|
|
254
|
+
attr_reader :nodes
|
|
255
|
+
|
|
256
|
+
# @return [Array, nil] Nodes that overlap with the freeze block boundaries
|
|
257
|
+
attr_reader :overlapping_nodes
|
|
258
|
+
|
|
259
|
+
# Initialize a freeze node.
|
|
260
|
+
#
|
|
261
|
+
# This unified constructor accepts all parameters that any *-merge gem might need.
|
|
262
|
+
# Subclasses should call super with the parameters they use.
|
|
263
|
+
#
|
|
264
|
+
# Content can be provided via:
|
|
265
|
+
# - `lines:` - Direct array of line strings
|
|
266
|
+
# - `analysis:` - FileAnalysis reference (lines extracted via analysis.lines)
|
|
267
|
+
# - `content:` - Direct content string (will be split into lines)
|
|
268
|
+
#
|
|
269
|
+
# @param start_line [Integer] Line number of freeze marker (1-based)
|
|
270
|
+
# @param end_line [Integer] Line number of unfreeze marker (1-based)
|
|
271
|
+
# @param lines [Array<String>, nil] Direct array of source lines
|
|
272
|
+
# @param analysis [Object, nil] FileAnalysis reference for content access
|
|
273
|
+
# @param content [String, nil] Direct content string
|
|
274
|
+
# @param nodes [Array] AST nodes contained within the freeze block
|
|
275
|
+
# @param overlapping_nodes [Array, nil] Nodes that overlap block boundaries
|
|
276
|
+
# @param start_marker [String, nil] The freeze start marker text
|
|
277
|
+
# @param end_marker [String, nil] The freeze end marker text
|
|
278
|
+
# @param pattern_type [Symbol] Pattern type for marker matching
|
|
279
|
+
# @param reason [String, nil] Optional reason extracted from freeze marker
|
|
280
|
+
def initialize(
|
|
281
|
+
start_line:,
|
|
282
|
+
end_line:,
|
|
283
|
+
lines: nil,
|
|
284
|
+
analysis: nil,
|
|
285
|
+
content: nil,
|
|
286
|
+
nodes: [],
|
|
287
|
+
overlapping_nodes: nil,
|
|
288
|
+
start_marker: nil,
|
|
289
|
+
end_marker: nil,
|
|
290
|
+
pattern_type: DEFAULT_PATTERN,
|
|
291
|
+
reason: nil
|
|
292
|
+
)
|
|
293
|
+
@start_line = start_line
|
|
294
|
+
@end_line = end_line
|
|
295
|
+
@start_marker = start_marker
|
|
296
|
+
@end_marker = end_marker
|
|
297
|
+
@pattern_type = pattern_type
|
|
298
|
+
@explicit_reason = reason
|
|
299
|
+
@nodes = nodes
|
|
300
|
+
@overlapping_nodes = overlapping_nodes
|
|
301
|
+
@analysis = analysis
|
|
302
|
+
|
|
303
|
+
# Handle content from various sources
|
|
304
|
+
@lines = resolve_lines(lines, analysis, content)
|
|
305
|
+
@content = resolve_content(@lines, content)
|
|
306
|
+
end
|
|
307
|
+
|
|
308
|
+
# Returns a location-like object for compatibility with AST nodes
|
|
309
|
+
# @return [Location]
|
|
310
|
+
def location
|
|
311
|
+
@location ||= Location.new(@start_line, @end_line)
|
|
312
|
+
end
|
|
313
|
+
|
|
314
|
+
# Extract the reason/comment from the freeze start marker.
|
|
315
|
+
# The reason is any text after the freeze directive.
|
|
316
|
+
# If an explicit reason was provided at initialization, that takes precedence.
|
|
317
|
+
#
|
|
318
|
+
# @example With reason
|
|
319
|
+
# # rbs-merge:freeze Custom reason here
|
|
320
|
+
# => "Custom reason here"
|
|
321
|
+
#
|
|
322
|
+
# @example Without reason
|
|
323
|
+
# # rbs-merge:freeze
|
|
324
|
+
# => nil
|
|
325
|
+
#
|
|
326
|
+
# @return [String, nil] The reason text, or nil if not present
|
|
327
|
+
def reason
|
|
328
|
+
# Return explicit reason if provided at initialization
|
|
329
|
+
return @explicit_reason if @explicit_reason
|
|
330
|
+
|
|
331
|
+
return unless @start_marker
|
|
332
|
+
|
|
333
|
+
# Use the canonical pattern which has capture group 2 for reason
|
|
334
|
+
# We need to extract the token from the marker first
|
|
335
|
+
token = extract_token_from_marker
|
|
336
|
+
return unless token
|
|
337
|
+
|
|
338
|
+
pattern = self.class.pattern_for(@pattern_type, token)
|
|
339
|
+
match = @start_marker.match(pattern)
|
|
340
|
+
return unless match
|
|
341
|
+
|
|
342
|
+
# Capture group 2 is the reason text
|
|
343
|
+
reason_text = match[2]&.strip
|
|
344
|
+
reason_text&.empty? ? nil : reason_text
|
|
345
|
+
end
|
|
346
|
+
|
|
347
|
+
# Returns the freeze block content
|
|
348
|
+
# @return [String]
|
|
349
|
+
def slice
|
|
350
|
+
@content
|
|
351
|
+
end
|
|
352
|
+
|
|
353
|
+
# Check if this is a freeze node (always true for FreezeNode)
|
|
354
|
+
# @return [Boolean]
|
|
355
|
+
def freeze_node?
|
|
356
|
+
true
|
|
357
|
+
end
|
|
358
|
+
|
|
359
|
+
# Returns a stable signature for this freeze block.
|
|
360
|
+
# Override in subclasses for file-type-specific normalization.
|
|
361
|
+
# @return [Array] Signature array
|
|
362
|
+
def signature
|
|
363
|
+
[:FreezeNode, @content&.strip]
|
|
364
|
+
end
|
|
365
|
+
|
|
366
|
+
# String representation for debugging
|
|
367
|
+
# @return [String]
|
|
368
|
+
def inspect
|
|
369
|
+
"#<#{self.class.name} lines=#{start_line}..#{end_line} pattern=#{pattern_type}>"
|
|
370
|
+
end
|
|
371
|
+
|
|
372
|
+
# @return [String]
|
|
373
|
+
def to_s
|
|
374
|
+
inspect
|
|
375
|
+
end
|
|
376
|
+
|
|
377
|
+
protected
|
|
378
|
+
|
|
379
|
+
# Validate that end_line is not before start_line
|
|
380
|
+
# @raise [InvalidStructureError] if structure is invalid
|
|
381
|
+
def validate_line_order!
|
|
382
|
+
return unless @end_line < @start_line
|
|
383
|
+
|
|
384
|
+
raise InvalidStructureError.new(
|
|
385
|
+
"Freeze block end line (#{@end_line}) is before start line (#{@start_line})",
|
|
386
|
+
start_line: @start_line,
|
|
387
|
+
end_line: @end_line,
|
|
388
|
+
)
|
|
389
|
+
end
|
|
390
|
+
|
|
391
|
+
private
|
|
392
|
+
|
|
393
|
+
# Resolve lines from various sources
|
|
394
|
+
# @param lines [Array<String>, nil] Direct lines array
|
|
395
|
+
# @param analysis [Object, nil] FileAnalysis with lines method
|
|
396
|
+
# @param content [String, nil] Direct content string
|
|
397
|
+
# @return [Array<String>, nil] Resolved lines
|
|
398
|
+
def resolve_lines(lines, analysis, content)
|
|
399
|
+
return lines if lines
|
|
400
|
+
|
|
401
|
+
if analysis&.respond_to?(:lines)
|
|
402
|
+
# Extract lines from analysis using line numbers (1-based to 0-based)
|
|
403
|
+
all_lines = analysis.lines
|
|
404
|
+
return all_lines[(@start_line - 1)..(@end_line - 1)] if all_lines
|
|
405
|
+
end
|
|
406
|
+
|
|
407
|
+
content&.split("\n", -1)
|
|
408
|
+
end
|
|
409
|
+
|
|
410
|
+
# Resolve content from various sources
|
|
411
|
+
# @param lines [Array<String>, nil] Resolved lines
|
|
412
|
+
# @param content [String, nil] Direct content string
|
|
413
|
+
# @return [String, nil] Resolved content
|
|
414
|
+
def resolve_content(lines, content)
|
|
415
|
+
return content if content
|
|
416
|
+
|
|
417
|
+
lines&.join("\n")
|
|
418
|
+
end
|
|
419
|
+
|
|
420
|
+
# Extract the token from the start marker
|
|
421
|
+
# @return [String, nil] The token (e.g., "rbs-merge" from "# rbs-merge:freeze")
|
|
422
|
+
def extract_token_from_marker
|
|
423
|
+
# :nocov:
|
|
424
|
+
# Defensive: @start_marker is always set in normal usage, nil check is for safety
|
|
425
|
+
return unless @start_marker
|
|
426
|
+
# :nocov:
|
|
427
|
+
|
|
428
|
+
# Match the token before :freeze
|
|
429
|
+
match = @start_marker.match(/([\w-]+):freeze/i)
|
|
430
|
+
match&.[](1)
|
|
431
|
+
end
|
|
432
|
+
end
|
|
433
|
+
end
|
|
434
|
+
end
|