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,278 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "style"
|
|
4
|
+
require_relative "line"
|
|
5
|
+
require_relative "empty"
|
|
6
|
+
require_relative "block"
|
|
7
|
+
|
|
8
|
+
module Ast
|
|
9
|
+
module Merge
|
|
10
|
+
module Comment
|
|
11
|
+
# Parser for building comment AST from source lines.
|
|
12
|
+
#
|
|
13
|
+
# This parser takes an array of source lines and produces an array of
|
|
14
|
+
# AstNode objects (Block, Line, Empty) that represent the structure
|
|
15
|
+
# of a comment-only file or section.
|
|
16
|
+
#
|
|
17
|
+
# The parser is style-aware and can handle:
|
|
18
|
+
# - Line comments (`#`, `//`, `--`, `;`)
|
|
19
|
+
# - HTML-style comments (`<!-- ... -->`)
|
|
20
|
+
# - C-style block comments (`/* ... */`)
|
|
21
|
+
#
|
|
22
|
+
# @example Parsing Ruby-style comments
|
|
23
|
+
# lines = ["# frozen_string_literal: true", "", "# A comment block"]
|
|
24
|
+
# parser = Parser.new(lines)
|
|
25
|
+
# nodes = parser.parse
|
|
26
|
+
# # => [Block(...), Empty(...), Block(...)]
|
|
27
|
+
#
|
|
28
|
+
# @example Parsing C-style block comments
|
|
29
|
+
# lines = ["/* Header comment", " * with multiple lines", " */"]
|
|
30
|
+
# parser = Parser.new(lines, style: :c_style_block)
|
|
31
|
+
# nodes = parser.parse
|
|
32
|
+
# # => [Block(raw_content: "/* Header comment\n * with multiple lines\n */")]
|
|
33
|
+
#
|
|
34
|
+
# @example Auto-detecting comment style
|
|
35
|
+
# lines = ["// JavaScript comment", "// continues here"]
|
|
36
|
+
# parser = Parser.new(lines, style: :auto)
|
|
37
|
+
# nodes = parser.parse
|
|
38
|
+
#
|
|
39
|
+
class Parser
|
|
40
|
+
# @return [Array<String>] The source lines
|
|
41
|
+
attr_reader :lines
|
|
42
|
+
|
|
43
|
+
# @return [Style] The comment style configuration
|
|
44
|
+
attr_reader :style
|
|
45
|
+
|
|
46
|
+
# Initialize a new Parser.
|
|
47
|
+
#
|
|
48
|
+
# @param lines [Array<String>] Source lines (without trailing newlines)
|
|
49
|
+
# @param style [Style, Symbol, nil] The comment style (:hash_comment, :c_style_line, etc.)
|
|
50
|
+
# Pass :auto to attempt auto-detection.
|
|
51
|
+
def initialize(lines, style: nil)
|
|
52
|
+
@lines = lines || []
|
|
53
|
+
@style = resolve_style(style)
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
# Parse the lines into an AST.
|
|
57
|
+
#
|
|
58
|
+
# Groups contiguous comment lines into Block nodes,
|
|
59
|
+
# and represents blank lines as Empty nodes.
|
|
60
|
+
#
|
|
61
|
+
# @return [Array<AstNode>] Array of parsed nodes
|
|
62
|
+
def parse
|
|
63
|
+
return [] if lines.empty?
|
|
64
|
+
|
|
65
|
+
if style.supports_block_comments?
|
|
66
|
+
parse_with_block_comments
|
|
67
|
+
else
|
|
68
|
+
parse_line_comments
|
|
69
|
+
end
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
# Class method for convenient one-shot parsing.
|
|
73
|
+
#
|
|
74
|
+
# @param lines [Array<String>] Source lines
|
|
75
|
+
# @param style [Style, Symbol, nil] Comment style
|
|
76
|
+
# @return [Array<AstNode>] Parsed nodes
|
|
77
|
+
def self.parse(lines, style: nil)
|
|
78
|
+
new(lines, style: style).parse
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
private
|
|
82
|
+
|
|
83
|
+
# Resolve the style parameter to a Style instance.
|
|
84
|
+
#
|
|
85
|
+
# @param style [Style, Symbol, nil] Style configuration
|
|
86
|
+
# @return [Style] Resolved style instance
|
|
87
|
+
def resolve_style(style)
|
|
88
|
+
case style
|
|
89
|
+
when Style
|
|
90
|
+
style
|
|
91
|
+
when :auto
|
|
92
|
+
auto_detect_style
|
|
93
|
+
when Symbol
|
|
94
|
+
Style.for(style)
|
|
95
|
+
when nil
|
|
96
|
+
Style.for(Style::DEFAULT_STYLE)
|
|
97
|
+
else
|
|
98
|
+
raise ArgumentError, "Invalid style: #{style.inspect}"
|
|
99
|
+
end
|
|
100
|
+
end
|
|
101
|
+
|
|
102
|
+
# Auto-detect the comment style from the source lines.
|
|
103
|
+
#
|
|
104
|
+
# Looks at the first non-empty line to determine the style.
|
|
105
|
+
#
|
|
106
|
+
# @return [Style] Detected style (defaults to hash_comment)
|
|
107
|
+
def auto_detect_style
|
|
108
|
+
first_content = lines.find { |l| !l.to_s.strip.empty? }
|
|
109
|
+
return Style.for(:hash_comment) unless first_content
|
|
110
|
+
|
|
111
|
+
stripped = first_content.to_s.strip
|
|
112
|
+
|
|
113
|
+
# Check each style's pattern
|
|
114
|
+
Style::STYLES.each do |name, config|
|
|
115
|
+
if config[:line_pattern]&.match?(stripped)
|
|
116
|
+
return Style.for(name)
|
|
117
|
+
end
|
|
118
|
+
if config[:block_start_pattern]&.match?(stripped)
|
|
119
|
+
return Style.for(name)
|
|
120
|
+
end
|
|
121
|
+
end
|
|
122
|
+
|
|
123
|
+
# Default to hash_comment
|
|
124
|
+
Style.for(:hash_comment)
|
|
125
|
+
end
|
|
126
|
+
|
|
127
|
+
# Parse lines using line-comment style (e.g., #, //, --, ;).
|
|
128
|
+
#
|
|
129
|
+
# Groups contiguous comment lines into Block nodes.
|
|
130
|
+
#
|
|
131
|
+
# @return [Array<AstNode>] Parsed nodes
|
|
132
|
+
def parse_line_comments
|
|
133
|
+
nodes = []
|
|
134
|
+
current_block = []
|
|
135
|
+
|
|
136
|
+
lines.each_with_index do |line, idx|
|
|
137
|
+
line_number = idx + 1
|
|
138
|
+
stripped = line.to_s.rstrip
|
|
139
|
+
|
|
140
|
+
if stripped.empty?
|
|
141
|
+
# Blank line - flush current block and add Empty
|
|
142
|
+
if current_block.any?
|
|
143
|
+
nodes << build_block(current_block)
|
|
144
|
+
current_block = []
|
|
145
|
+
end
|
|
146
|
+
nodes << Empty.new(line_number: line_number, text: line.to_s)
|
|
147
|
+
elsif style.match_line?(stripped)
|
|
148
|
+
# Comment line - add to current block
|
|
149
|
+
current_block << Line.new(
|
|
150
|
+
text: stripped,
|
|
151
|
+
line_number: line_number,
|
|
152
|
+
style: style,
|
|
153
|
+
)
|
|
154
|
+
else
|
|
155
|
+
# Non-comment, non-empty line
|
|
156
|
+
# Flush current block and treat this as content
|
|
157
|
+
if current_block.any?
|
|
158
|
+
nodes << build_block(current_block)
|
|
159
|
+
current_block = []
|
|
160
|
+
end
|
|
161
|
+
# Add as a single line (non-comment content in a comment-only context)
|
|
162
|
+
nodes << Line.new(
|
|
163
|
+
text: stripped,
|
|
164
|
+
line_number: line_number,
|
|
165
|
+
style: Style.for(:hash_comment), # Fallback style for non-comment lines
|
|
166
|
+
)
|
|
167
|
+
end
|
|
168
|
+
end
|
|
169
|
+
|
|
170
|
+
# Flush remaining block
|
|
171
|
+
if current_block.any?
|
|
172
|
+
nodes << build_block(current_block)
|
|
173
|
+
end
|
|
174
|
+
|
|
175
|
+
nodes
|
|
176
|
+
end
|
|
177
|
+
|
|
178
|
+
# Parse lines that may contain block comments (e.g., /* ... */, <!-- ... -->).
|
|
179
|
+
#
|
|
180
|
+
# Handles both single-line and multi-line block comments.
|
|
181
|
+
#
|
|
182
|
+
# @return [Array<AstNode>] Parsed nodes
|
|
183
|
+
def parse_with_block_comments
|
|
184
|
+
nodes = []
|
|
185
|
+
current_block_lines = []
|
|
186
|
+
in_block_comment = false
|
|
187
|
+
|
|
188
|
+
lines.each_with_index do |line, idx|
|
|
189
|
+
line_number = idx + 1
|
|
190
|
+
stripped = line.to_s.rstrip
|
|
191
|
+
|
|
192
|
+
if stripped.empty? && !in_block_comment
|
|
193
|
+
# Blank line outside block comment
|
|
194
|
+
if current_block_lines.any?
|
|
195
|
+
nodes << build_raw_block(current_block_lines)
|
|
196
|
+
current_block_lines = []
|
|
197
|
+
end
|
|
198
|
+
nodes << Empty.new(line_number: line_number, text: line.to_s)
|
|
199
|
+
elsif style.match_block_start?(stripped)
|
|
200
|
+
# Starting a block comment
|
|
201
|
+
# Flush any pending content first
|
|
202
|
+
if current_block_lines.any? && !in_block_comment
|
|
203
|
+
nodes << build_raw_block(current_block_lines)
|
|
204
|
+
current_block_lines = []
|
|
205
|
+
end
|
|
206
|
+
|
|
207
|
+
current_block_lines << {line: stripped, line_number: line_number}
|
|
208
|
+
in_block_comment = true
|
|
209
|
+
|
|
210
|
+
# Check if block ends on same line
|
|
211
|
+
if style.match_block_end?(stripped)
|
|
212
|
+
nodes << build_raw_block(current_block_lines)
|
|
213
|
+
current_block_lines = []
|
|
214
|
+
in_block_comment = false
|
|
215
|
+
end
|
|
216
|
+
elsif in_block_comment
|
|
217
|
+
# Inside a block comment
|
|
218
|
+
current_block_lines << {line: stripped.empty? ? line.to_s : stripped, line_number: line_number}
|
|
219
|
+
|
|
220
|
+
# Check if block ends
|
|
221
|
+
if style.match_block_end?(stripped)
|
|
222
|
+
nodes << build_raw_block(current_block_lines)
|
|
223
|
+
current_block_lines = []
|
|
224
|
+
in_block_comment = false
|
|
225
|
+
end
|
|
226
|
+
elsif style.supports_line_comments? && style.match_line?(stripped)
|
|
227
|
+
# Line comment (in a style that supports both line and block)
|
|
228
|
+
current_block_lines << {line: stripped, line_number: line_number}
|
|
229
|
+
else
|
|
230
|
+
# Other content - flush and add as-is
|
|
231
|
+
if current_block_lines.any?
|
|
232
|
+
nodes << build_raw_block(current_block_lines)
|
|
233
|
+
current_block_lines = []
|
|
234
|
+
end
|
|
235
|
+
nodes << Line.new(
|
|
236
|
+
text: stripped,
|
|
237
|
+
line_number: line_number,
|
|
238
|
+
style: style,
|
|
239
|
+
)
|
|
240
|
+
end
|
|
241
|
+
end
|
|
242
|
+
|
|
243
|
+
# Flush remaining block
|
|
244
|
+
if current_block_lines.any?
|
|
245
|
+
nodes << build_raw_block(current_block_lines)
|
|
246
|
+
end
|
|
247
|
+
|
|
248
|
+
nodes
|
|
249
|
+
end
|
|
250
|
+
|
|
251
|
+
# Build a Block from accumulated line comment nodes.
|
|
252
|
+
#
|
|
253
|
+
# @param comment_lines [Array<Line>] The comment lines
|
|
254
|
+
# @return [Block] Block containing the lines
|
|
255
|
+
def build_block(comment_lines)
|
|
256
|
+
Block.new(children: comment_lines, style: style)
|
|
257
|
+
end
|
|
258
|
+
|
|
259
|
+
# Build a Block from accumulated raw lines (for block-style comments).
|
|
260
|
+
#
|
|
261
|
+
# @param line_data [Array<Hash>] Array of { line:, line_number: } hashes
|
|
262
|
+
# @return [Block] Block with raw content
|
|
263
|
+
def build_raw_block(line_data)
|
|
264
|
+
raw_content = line_data.map { |d| d[:line] }.join("\n")
|
|
265
|
+
start_line = line_data.first[:line_number]
|
|
266
|
+
end_line = line_data.last[:line_number]
|
|
267
|
+
|
|
268
|
+
Block.new(
|
|
269
|
+
raw_content: raw_content,
|
|
270
|
+
start_line: start_line,
|
|
271
|
+
end_line: end_line,
|
|
272
|
+
style: style,
|
|
273
|
+
)
|
|
274
|
+
end
|
|
275
|
+
end
|
|
276
|
+
end
|
|
277
|
+
end
|
|
278
|
+
end
|
|
@@ -0,0 +1,282 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Ast
|
|
4
|
+
module Merge
|
|
5
|
+
module Comment
|
|
6
|
+
# Configuration for different comment syntax styles.
|
|
7
|
+
#
|
|
8
|
+
# Supports multiple comment syntax patterns used across programming languages:
|
|
9
|
+
# - `:hash_comment` - Ruby/Python/YAML/Shell style (`# comment`)
|
|
10
|
+
# - `:html_comment` - HTML/XML/Markdown style (`<!-- comment -->`)
|
|
11
|
+
# - `:c_style_line` - C/JavaScript/Go line comments (`// comment`)
|
|
12
|
+
# - `:c_style_block` - C/JavaScript/CSS block comments (`/* comment */`)
|
|
13
|
+
# - `:semicolon_comment` - Lisp/Clojure/Assembly style (`; comment`)
|
|
14
|
+
# - `:double_dash_comment` - SQL/Haskell/Lua style (`-- comment`)
|
|
15
|
+
#
|
|
16
|
+
# @example Using a predefined style
|
|
17
|
+
# style = Style.for(:hash_comment)
|
|
18
|
+
# style.line_start #=> "#"
|
|
19
|
+
# style.match_line?("# hello") #=> true
|
|
20
|
+
#
|
|
21
|
+
# @example Registering a custom style
|
|
22
|
+
# Style.register(:percent_comment,
|
|
23
|
+
# line_start: "%",
|
|
24
|
+
# line_pattern: /^\s*%/
|
|
25
|
+
# )
|
|
26
|
+
#
|
|
27
|
+
class Style
|
|
28
|
+
# @return [Symbol] The style identifier
|
|
29
|
+
attr_reader :name
|
|
30
|
+
|
|
31
|
+
# @return [String, nil] Line comment start delimiter (e.g., "#", "//")
|
|
32
|
+
attr_reader :line_start
|
|
33
|
+
|
|
34
|
+
# @return [String, nil] Line comment end delimiter (for HTML-style: "-->")
|
|
35
|
+
attr_reader :line_end
|
|
36
|
+
|
|
37
|
+
# @return [String, nil] Block comment start delimiter (e.g., "/*")
|
|
38
|
+
attr_reader :block_start
|
|
39
|
+
|
|
40
|
+
# @return [String, nil] Block comment end delimiter (e.g., "*/")
|
|
41
|
+
attr_reader :block_end
|
|
42
|
+
|
|
43
|
+
# @return [Regexp] Pattern to match a single comment line
|
|
44
|
+
attr_reader :line_pattern
|
|
45
|
+
|
|
46
|
+
# @return [Regexp, nil] Pattern to match block comment start
|
|
47
|
+
attr_reader :block_start_pattern
|
|
48
|
+
|
|
49
|
+
# @return [Regexp, nil] Pattern to match block comment end
|
|
50
|
+
attr_reader :block_end_pattern
|
|
51
|
+
|
|
52
|
+
# Predefined comment styles.
|
|
53
|
+
# Mutable to allow runtime registration of custom styles.
|
|
54
|
+
# @return [Hash{Symbol => Hash}] Registered comment styles
|
|
55
|
+
STYLES = {
|
|
56
|
+
hash_comment: {
|
|
57
|
+
line_start: "#",
|
|
58
|
+
line_end: nil,
|
|
59
|
+
block_start: nil,
|
|
60
|
+
block_end: nil,
|
|
61
|
+
line_pattern: /^\s*#/,
|
|
62
|
+
block_start_pattern: nil,
|
|
63
|
+
block_end_pattern: nil,
|
|
64
|
+
},
|
|
65
|
+
html_comment: {
|
|
66
|
+
line_start: "<!--",
|
|
67
|
+
line_end: "-->",
|
|
68
|
+
block_start: "<!--",
|
|
69
|
+
block_end: "-->",
|
|
70
|
+
line_pattern: /^\s*<!--.*-->\s*$/,
|
|
71
|
+
block_start_pattern: /^\s*<!--/,
|
|
72
|
+
block_end_pattern: /-->\s*$/,
|
|
73
|
+
},
|
|
74
|
+
c_style_line: {
|
|
75
|
+
line_start: "//",
|
|
76
|
+
line_end: nil,
|
|
77
|
+
block_start: nil,
|
|
78
|
+
block_end: nil,
|
|
79
|
+
line_pattern: %r{^\s*//},
|
|
80
|
+
block_start_pattern: nil,
|
|
81
|
+
block_end_pattern: nil,
|
|
82
|
+
},
|
|
83
|
+
c_style_block: {
|
|
84
|
+
line_start: nil,
|
|
85
|
+
line_end: nil,
|
|
86
|
+
block_start: "/*",
|
|
87
|
+
block_end: "*/",
|
|
88
|
+
line_pattern: nil,
|
|
89
|
+
block_start_pattern: %r{^\s*/\*},
|
|
90
|
+
block_end_pattern: %r{\*/\s*$},
|
|
91
|
+
},
|
|
92
|
+
semicolon_comment: {
|
|
93
|
+
line_start: ";",
|
|
94
|
+
line_end: nil,
|
|
95
|
+
block_start: nil,
|
|
96
|
+
block_end: nil,
|
|
97
|
+
line_pattern: /^\s*;/,
|
|
98
|
+
block_start_pattern: nil,
|
|
99
|
+
block_end_pattern: nil,
|
|
100
|
+
},
|
|
101
|
+
double_dash_comment: {
|
|
102
|
+
line_start: "--",
|
|
103
|
+
line_end: nil,
|
|
104
|
+
block_start: nil,
|
|
105
|
+
block_end: nil,
|
|
106
|
+
line_pattern: /^\s*--/,
|
|
107
|
+
block_start_pattern: nil,
|
|
108
|
+
block_end_pattern: nil,
|
|
109
|
+
},
|
|
110
|
+
}.freeze
|
|
111
|
+
|
|
112
|
+
# Default style when none specified
|
|
113
|
+
# @return [Symbol]
|
|
114
|
+
DEFAULT_STYLE = :hash_comment
|
|
115
|
+
|
|
116
|
+
class << self
|
|
117
|
+
# Get a Style instance for a given style name.
|
|
118
|
+
#
|
|
119
|
+
# @param name [Symbol] Style name (e.g., :hash_comment, :c_style_line)
|
|
120
|
+
# @return [Style] The style configuration
|
|
121
|
+
# @raise [ArgumentError] if style name is not registered
|
|
122
|
+
def for(name)
|
|
123
|
+
name = name&.to_sym || DEFAULT_STYLE
|
|
124
|
+
config = STYLES[name]
|
|
125
|
+
raise ArgumentError, "Unknown comment style: #{name}" unless config
|
|
126
|
+
|
|
127
|
+
new(name, **config)
|
|
128
|
+
end
|
|
129
|
+
|
|
130
|
+
# Register a custom comment style.
|
|
131
|
+
#
|
|
132
|
+
# @param name [Symbol] Style identifier
|
|
133
|
+
# @param line_start [String, nil] Line comment start delimiter
|
|
134
|
+
# @param line_end [String, nil] Line comment end delimiter
|
|
135
|
+
# @param block_start [String, nil] Block comment start delimiter
|
|
136
|
+
# @param block_end [String, nil] Block comment end delimiter
|
|
137
|
+
# @param line_pattern [Regexp, nil] Pattern to match comment lines
|
|
138
|
+
# @param block_start_pattern [Regexp, nil] Pattern to match block start
|
|
139
|
+
# @param block_end_pattern [Regexp, nil] Pattern to match block end
|
|
140
|
+
# @return [Hash] The registered style configuration
|
|
141
|
+
# @raise [ArgumentError] if name already exists
|
|
142
|
+
def register(name, line_start: nil, line_end: nil, block_start: nil, block_end: nil,
|
|
143
|
+
line_pattern: nil, block_start_pattern: nil, block_end_pattern: nil)
|
|
144
|
+
name = name.to_sym
|
|
145
|
+
if STYLES.key?(name)
|
|
146
|
+
raise ArgumentError, "Style :#{name} already registered"
|
|
147
|
+
end
|
|
148
|
+
|
|
149
|
+
config = {
|
|
150
|
+
line_start: line_start,
|
|
151
|
+
line_end: line_end,
|
|
152
|
+
block_start: block_start,
|
|
153
|
+
block_end: block_end,
|
|
154
|
+
line_pattern: line_pattern,
|
|
155
|
+
block_start_pattern: block_start_pattern,
|
|
156
|
+
block_end_pattern: block_end_pattern,
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
# Modify STYLES (it's frozen, so we need to work around)
|
|
160
|
+
STYLES.dup.tap do |styles|
|
|
161
|
+
styles[name] = config
|
|
162
|
+
remove_const(:STYLES)
|
|
163
|
+
const_set(:STYLES, styles.freeze)
|
|
164
|
+
end
|
|
165
|
+
|
|
166
|
+
config
|
|
167
|
+
end
|
|
168
|
+
|
|
169
|
+
# List all registered style names.
|
|
170
|
+
#
|
|
171
|
+
# @return [Array<Symbol>] Available style names
|
|
172
|
+
def available_styles
|
|
173
|
+
STYLES.keys
|
|
174
|
+
end
|
|
175
|
+
|
|
176
|
+
# Check if a style supports line comments.
|
|
177
|
+
#
|
|
178
|
+
# @param name [Symbol] Style name
|
|
179
|
+
# @return [Boolean] true if style has line comment support
|
|
180
|
+
def supports_line_comments?(name)
|
|
181
|
+
config = STYLES[name.to_sym]
|
|
182
|
+
config && config[:line_pattern]
|
|
183
|
+
end
|
|
184
|
+
|
|
185
|
+
# Check if a style supports block comments.
|
|
186
|
+
#
|
|
187
|
+
# @param name [Symbol] Style name
|
|
188
|
+
# @return [Boolean] true if style has block comment support
|
|
189
|
+
def supports_block_comments?(name)
|
|
190
|
+
config = STYLES[name.to_sym]
|
|
191
|
+
config && config[:block_start_pattern]
|
|
192
|
+
end
|
|
193
|
+
end
|
|
194
|
+
|
|
195
|
+
# Initialize a new Style.
|
|
196
|
+
#
|
|
197
|
+
# @param name [Symbol] Style identifier
|
|
198
|
+
# @param line_start [String, nil] Line comment start delimiter
|
|
199
|
+
# @param line_end [String, nil] Line comment end delimiter
|
|
200
|
+
# @param block_start [String, nil] Block comment start delimiter
|
|
201
|
+
# @param block_end [String, nil] Block comment end delimiter
|
|
202
|
+
# @param line_pattern [Regexp, nil] Pattern to match comment lines
|
|
203
|
+
# @param block_start_pattern [Regexp, nil] Pattern to match block start
|
|
204
|
+
# @param block_end_pattern [Regexp, nil] Pattern to match block end
|
|
205
|
+
def initialize(name, line_start: nil, line_end: nil, block_start: nil, block_end: nil,
|
|
206
|
+
line_pattern: nil, block_start_pattern: nil, block_end_pattern: nil)
|
|
207
|
+
@name = name
|
|
208
|
+
@line_start = line_start
|
|
209
|
+
@line_end = line_end
|
|
210
|
+
@block_start = block_start
|
|
211
|
+
@block_end = block_end
|
|
212
|
+
@line_pattern = line_pattern
|
|
213
|
+
@block_start_pattern = block_start_pattern
|
|
214
|
+
@block_end_pattern = block_end_pattern
|
|
215
|
+
end
|
|
216
|
+
|
|
217
|
+
# Check if a line matches this style's line comment pattern.
|
|
218
|
+
#
|
|
219
|
+
# @param line [String] The line to check
|
|
220
|
+
# @return [Boolean] true if line is a comment in this style
|
|
221
|
+
def match_line?(line)
|
|
222
|
+
return false unless line_pattern
|
|
223
|
+
|
|
224
|
+
line_pattern.match?(line.to_s)
|
|
225
|
+
end
|
|
226
|
+
|
|
227
|
+
# Check if a line starts a block comment.
|
|
228
|
+
#
|
|
229
|
+
# @param line [String] The line to check
|
|
230
|
+
# @return [Boolean] true if line starts a block comment
|
|
231
|
+
def match_block_start?(line)
|
|
232
|
+
return false unless block_start_pattern
|
|
233
|
+
|
|
234
|
+
block_start_pattern.match?(line.to_s)
|
|
235
|
+
end
|
|
236
|
+
|
|
237
|
+
# Check if a line ends a block comment.
|
|
238
|
+
#
|
|
239
|
+
# @param line [String] The line to check
|
|
240
|
+
# @return [Boolean] true if line ends a block comment
|
|
241
|
+
def match_block_end?(line)
|
|
242
|
+
return false unless block_end_pattern
|
|
243
|
+
|
|
244
|
+
block_end_pattern.match?(line.to_s)
|
|
245
|
+
end
|
|
246
|
+
|
|
247
|
+
# Extract content from a line comment, removing the delimiter.
|
|
248
|
+
#
|
|
249
|
+
# @param line [String] The comment line
|
|
250
|
+
# @return [String] The comment content without delimiters
|
|
251
|
+
def extract_line_content(line)
|
|
252
|
+
return line.to_s unless line_start
|
|
253
|
+
|
|
254
|
+
content = line.to_s.sub(/^\s*#{Regexp.escape(line_start)}\s?/, "")
|
|
255
|
+
if line_end
|
|
256
|
+
content = content.sub(/\s*#{Regexp.escape(line_end)}\s*$/, "")
|
|
257
|
+
end
|
|
258
|
+
content.rstrip
|
|
259
|
+
end
|
|
260
|
+
|
|
261
|
+
# Check if this style supports line comments.
|
|
262
|
+
#
|
|
263
|
+
# @return [Boolean] true if line comments are supported
|
|
264
|
+
def supports_line_comments?
|
|
265
|
+
!line_pattern.nil?
|
|
266
|
+
end
|
|
267
|
+
|
|
268
|
+
# Check if this style supports block comments.
|
|
269
|
+
#
|
|
270
|
+
# @return [Boolean] true if block comments are supported
|
|
271
|
+
def supports_block_comments?
|
|
272
|
+
!block_start_pattern.nil?
|
|
273
|
+
end
|
|
274
|
+
|
|
275
|
+
# @return [String] Human-readable representation
|
|
276
|
+
def inspect
|
|
277
|
+
"#<Comment::Style:#{name}>"
|
|
278
|
+
end
|
|
279
|
+
end
|
|
280
|
+
end
|
|
281
|
+
end
|
|
282
|
+
end
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Ast
|
|
4
|
+
module Merge
|
|
5
|
+
# Comment AST nodes for representing comment-only content.
|
|
6
|
+
#
|
|
7
|
+
# This module provides generic, language-agnostic comment representation
|
|
8
|
+
# that supports multiple comment syntax styles:
|
|
9
|
+
# - `:hash_comment` - Ruby/Python/YAML/Shell style (`# comment`)
|
|
10
|
+
# - `:html_comment` - HTML/XML/Markdown style (`<!-- comment -->`)
|
|
11
|
+
# - `:c_style_line` - C/JavaScript/Go line comments (`// comment`)
|
|
12
|
+
# - `:c_style_block` - C/JavaScript/CSS block comments (`/* comment */`)
|
|
13
|
+
# - `:semicolon_comment` - Lisp/Clojure/Assembly style (`; comment`)
|
|
14
|
+
# - `:double_dash_comment` - SQL/Haskell/Lua style (`-- comment`)
|
|
15
|
+
#
|
|
16
|
+
# @example Parsing Ruby-style comments
|
|
17
|
+
# lines = ["# frozen_string_literal: true", "", "# Main comment"]
|
|
18
|
+
# nodes = Comment::Parser.parse(lines, style: :hash_comment)
|
|
19
|
+
#
|
|
20
|
+
# @example Parsing JavaScript-style comments
|
|
21
|
+
# lines = ["// Header comment", "// continues here"]
|
|
22
|
+
# nodes = Comment::Parser.parse(lines, style: :c_style_line)
|
|
23
|
+
#
|
|
24
|
+
# @example Auto-detecting style
|
|
25
|
+
# lines = ["<!-- HTML comment -->"]
|
|
26
|
+
# nodes = Comment::Parser.parse(lines, style: :auto)
|
|
27
|
+
#
|
|
28
|
+
module Comment
|
|
29
|
+
autoload :Style, "ast/merge/comment/style"
|
|
30
|
+
autoload :Line, "ast/merge/comment/line"
|
|
31
|
+
autoload :Empty, "ast/merge/comment/empty"
|
|
32
|
+
autoload :Block, "ast/merge/comment/block"
|
|
33
|
+
autoload :Parser, "ast/merge/comment/parser"
|
|
34
|
+
end
|
|
35
|
+
end
|
|
36
|
+
end
|