bash-merge 2.0.6 → 7.1.3
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/LICENSE.md +13 -0
- data/README.md +98 -645
- data/lib/bash/merge/comment_tracker.rb +36 -100
- data/lib/bash/merge/debug_logger.rb +10 -10
- data/lib/bash/merge/emitter.rb +45 -33
- data/lib/bash/merge/file_analysis.rb +133 -33
- data/lib/bash/merge/freeze_node.rb +9 -9
- data/lib/bash/merge/merge_result.rb +11 -5
- data/lib/bash/merge/node_wrapper.rb +89 -36
- data/lib/bash/merge/provider.rb +983 -0
- data/lib/bash/merge/smart_merger.rb +641 -32
- data/lib/bash/merge/version.rb +5 -4
- data/lib/bash/merge.rb +120 -25
- data/lib/bash-merge.rb +7 -1
- data/sig/bash/merge.rbs +18 -248
- data.tar.gz.sig +3 -1
- metadata +84 -88
- metadata.gz.sig +0 -0
- data/CHANGELOG.md +0 -211
- data/CITATION.cff +0 -20
- data/CODE_OF_CONDUCT.md +0 -134
- data/CONTRIBUTING.md +0 -227
- data/FUNDING.md +0 -74
- data/LICENSE.txt +0 -21
- data/REEK +0 -0
- data/RUBOCOP.md +0 -71
- data/SECURITY.md +0 -21
- data/lib/bash/merge/conflict_resolver.rb +0 -199
|
@@ -5,6 +5,10 @@ module Bash
|
|
|
5
5
|
# Extracts and tracks comments with their line numbers from Bash source.
|
|
6
6
|
# Bash comments use the # syntax, making freeze block detection straightforward.
|
|
7
7
|
#
|
|
8
|
+
# Inherits shared lookup, query, region-building, and attachment API from
|
|
9
|
+
# +Ast::Merge::Comment::HashTrackerBase+. Only format-specific comment
|
|
10
|
+
# extraction, shebang detection, and owner resolution are overridden here.
|
|
11
|
+
#
|
|
8
12
|
# @example Basic usage
|
|
9
13
|
# tracker = CommentTracker.new(bash_source)
|
|
10
14
|
# tracker.comments # => [{line: 1, indent: 0, text: "This is a comment"}]
|
|
@@ -13,91 +17,14 @@ module Bash
|
|
|
13
17
|
# @example Comment types
|
|
14
18
|
# # Full-line comment
|
|
15
19
|
# command # Inline comment
|
|
16
|
-
class CommentTracker
|
|
17
|
-
# Regex to match full-line comments (line is only whitespace + comment)
|
|
18
|
-
FULL_LINE_COMMENT_REGEX = /\A(\s*)#\s?(.*)\z/
|
|
19
|
-
|
|
20
|
-
# Regex to match inline comments (comment after Bash content)
|
|
21
|
-
# Note: This is simplified and doesn't handle all edge cases like comments in strings
|
|
22
|
-
INLINE_COMMENT_REGEX = /\s+#\s?(.*)$/
|
|
23
|
-
|
|
24
|
-
# @return [Array<Hash>] All extracted comments with metadata
|
|
25
|
-
attr_reader :comments
|
|
26
|
-
|
|
27
|
-
# @return [Array<String>] Source lines
|
|
28
|
-
attr_reader :lines
|
|
29
|
-
|
|
20
|
+
class CommentTracker < Ast::Merge::Comment::HashTrackerBase
|
|
30
21
|
# Initialize comment tracker by scanning the source
|
|
31
22
|
#
|
|
32
23
|
# @param source [String] Bash source code
|
|
33
24
|
def initialize(source)
|
|
34
25
|
@source = source
|
|
35
|
-
@
|
|
36
|
-
|
|
37
|
-
@comments_by_line = @comments.group_by { |c| c[:line] }
|
|
38
|
-
end
|
|
39
|
-
|
|
40
|
-
# Get comment at a specific line
|
|
41
|
-
#
|
|
42
|
-
# @param line_num [Integer] 1-based line number
|
|
43
|
-
# @return [Hash, nil] Comment info or nil
|
|
44
|
-
def comment_at(line_num)
|
|
45
|
-
@comments_by_line[line_num]&.first
|
|
46
|
-
end
|
|
47
|
-
|
|
48
|
-
# Get all comments in a line range
|
|
49
|
-
#
|
|
50
|
-
# @param range [Range] Range of 1-based line numbers
|
|
51
|
-
# @return [Array<Hash>] Comments in the range
|
|
52
|
-
def comments_in_range(range)
|
|
53
|
-
@comments.select { |c| range.cover?(c[:line]) }
|
|
54
|
-
end
|
|
55
|
-
|
|
56
|
-
# Get leading comments before a line (consecutive comment lines immediately above)
|
|
57
|
-
#
|
|
58
|
-
# @param line_num [Integer] 1-based line number
|
|
59
|
-
# @return [Array<Hash>] Leading comments
|
|
60
|
-
def leading_comments_before(line_num)
|
|
61
|
-
leading = []
|
|
62
|
-
current = line_num - 1
|
|
63
|
-
|
|
64
|
-
while current >= 1
|
|
65
|
-
comment = comment_at(current)
|
|
66
|
-
break unless comment && comment[:full_line]
|
|
67
|
-
|
|
68
|
-
leading.unshift(comment)
|
|
69
|
-
current -= 1
|
|
70
|
-
end
|
|
71
|
-
|
|
72
|
-
leading
|
|
73
|
-
end
|
|
74
|
-
|
|
75
|
-
# Get trailing comment on the same line (inline comment)
|
|
76
|
-
#
|
|
77
|
-
# @param line_num [Integer] 1-based line number
|
|
78
|
-
# @return [Hash, nil] Inline comment or nil
|
|
79
|
-
def inline_comment_at(line_num)
|
|
80
|
-
comment = comment_at(line_num)
|
|
81
|
-
comment if comment && !comment[:full_line]
|
|
82
|
-
end
|
|
83
|
-
|
|
84
|
-
# Check if a line is a full-line comment
|
|
85
|
-
#
|
|
86
|
-
# @param line_num [Integer] 1-based line number
|
|
87
|
-
# @return [Boolean]
|
|
88
|
-
def full_line_comment?(line_num)
|
|
89
|
-
comment = comment_at(line_num)
|
|
90
|
-
comment&.dig(:full_line) || false
|
|
91
|
-
end
|
|
92
|
-
|
|
93
|
-
# Check if a line is blank
|
|
94
|
-
#
|
|
95
|
-
# @param line_num [Integer] 1-based line number
|
|
96
|
-
# @return [Boolean]
|
|
97
|
-
def blank_line?(line_num)
|
|
98
|
-
return false if line_num < 1 || line_num > @lines.length
|
|
99
|
-
|
|
100
|
-
@lines[line_num - 1].strip.empty?
|
|
26
|
+
@line_parser = Ast::Merge::Comment::QuotedHashLineParser.new
|
|
27
|
+
super(source.lines.map(&:chomp))
|
|
101
28
|
end
|
|
102
29
|
|
|
103
30
|
# Check if a line is a shebang
|
|
@@ -107,7 +34,19 @@ module Bash
|
|
|
107
34
|
def shebang?(line_num)
|
|
108
35
|
return false if line_num < 1 || line_num > @lines.length
|
|
109
36
|
|
|
110
|
-
@lines[line_num - 1].start_with?(
|
|
37
|
+
@lines[line_num - 1].start_with?('#!')
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
def augment(owners: [], **options)
|
|
41
|
+
Ast::Merge::Comment::Augmenter.new(
|
|
42
|
+
lines: @lines,
|
|
43
|
+
comments: @comments,
|
|
44
|
+
owners: owners,
|
|
45
|
+
style: :hash_comment,
|
|
46
|
+
total_comment_count: @comments.size,
|
|
47
|
+
inline_comment_count: @comments.count { |comment| !comment[:full_line] },
|
|
48
|
+
**options
|
|
49
|
+
)
|
|
111
50
|
end
|
|
112
51
|
|
|
113
52
|
private
|
|
@@ -119,30 +58,27 @@ module Bash
|
|
|
119
58
|
line_num = idx + 1
|
|
120
59
|
|
|
121
60
|
# Skip shebang lines
|
|
122
|
-
next if line.start_with?(
|
|
61
|
+
next if line.start_with?('#!')
|
|
62
|
+
|
|
63
|
+
parsed = @line_parser.parse(line)
|
|
64
|
+
next unless parsed
|
|
123
65
|
|
|
124
|
-
|
|
125
|
-
if (match = line.match(FULL_LINE_COMMENT_REGEX))
|
|
66
|
+
if parsed.full_line?
|
|
126
67
|
comments << {
|
|
127
68
|
line: line_num,
|
|
128
|
-
indent:
|
|
129
|
-
text:
|
|
69
|
+
indent: parsed.indent,
|
|
70
|
+
text: parsed.text,
|
|
130
71
|
full_line: true,
|
|
131
|
-
raw:
|
|
72
|
+
raw: parsed.raw
|
|
73
|
+
}
|
|
74
|
+
elsif parsed.inline?
|
|
75
|
+
comments << {
|
|
76
|
+
line: line_num,
|
|
77
|
+
indent: 0,
|
|
78
|
+
text: parsed.text,
|
|
79
|
+
full_line: false,
|
|
80
|
+
raw: parsed.raw
|
|
132
81
|
}
|
|
133
|
-
# Check for inline comment (simplified - doesn't handle quotes)
|
|
134
|
-
elsif line.include?(" #") && !line.strip.start_with?("#")
|
|
135
|
-
# Try to extract inline comment, but be careful with strings
|
|
136
|
-
# This is a simplified approach
|
|
137
|
-
if (inline_match = line.match(INLINE_COMMENT_REGEX))
|
|
138
|
-
comments << {
|
|
139
|
-
line: line_num,
|
|
140
|
-
indent: 0,
|
|
141
|
-
text: inline_match[1],
|
|
142
|
-
full_line: false,
|
|
143
|
-
raw: "# #{inline_match[1]}",
|
|
144
|
-
}
|
|
145
|
-
end
|
|
146
82
|
end
|
|
147
83
|
end
|
|
148
84
|
|
|
@@ -15,25 +15,25 @@ module Bash
|
|
|
15
15
|
extend Ast::Merge::DebugLogger
|
|
16
16
|
|
|
17
17
|
# Bash-specific configuration
|
|
18
|
-
self.env_var_name =
|
|
19
|
-
self.log_prefix =
|
|
18
|
+
self.env_var_name = 'BASH_MERGE_DEBUG'
|
|
19
|
+
self.log_prefix = '[Bash::Merge]'
|
|
20
20
|
|
|
21
21
|
class << self
|
|
22
22
|
# Override log_node to handle Bash-specific node types.
|
|
23
23
|
#
|
|
24
24
|
# @param node [Object] Node to log information about
|
|
25
25
|
# @param label [String] Label for the node
|
|
26
|
-
def log_node(node, label:
|
|
26
|
+
def log_node(node, label: 'Node')
|
|
27
27
|
return unless enabled?
|
|
28
28
|
|
|
29
29
|
info = case node
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
30
|
+
when Bash::Merge::FreezeNode
|
|
31
|
+
{ type: 'FreezeNode', lines: "#{node.start_line}..#{node.end_line}" }
|
|
32
|
+
when Bash::Merge::NodeWrapper
|
|
33
|
+
{ type: node.type.to_s, lines: "#{node.start_line}..#{node.end_line}" }
|
|
34
|
+
else
|
|
35
|
+
extract_node_info(node)
|
|
36
|
+
end
|
|
37
37
|
|
|
38
38
|
debug(label, info)
|
|
39
39
|
end
|
data/lib/bash/merge/emitter.rb
CHANGED
|
@@ -13,21 +13,27 @@ module Bash
|
|
|
13
13
|
# emitter.emit_comment("This is a comment")
|
|
14
14
|
# emitter.emit_line("echo 'hello'")
|
|
15
15
|
class Emitter < Ast::Merge::EmitterBase
|
|
16
|
+
include Ast::Merge::EmitterLineMetadataSupport
|
|
17
|
+
|
|
16
18
|
# Initialize subclass-specific state
|
|
17
|
-
def initialize_subclass_state(**
|
|
18
|
-
|
|
19
|
+
def initialize_subclass_state(**_options)
|
|
20
|
+
initialize_line_metadata_state
|
|
19
21
|
end
|
|
20
22
|
|
|
21
23
|
# Clear subclass-specific state
|
|
22
24
|
def clear_subclass_state
|
|
23
|
-
|
|
25
|
+
clear_line_metadata_state
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
def emit_blank_line
|
|
29
|
+
append_line('')
|
|
24
30
|
end
|
|
25
31
|
|
|
26
32
|
# Emit a tracked comment from CommentTracker
|
|
27
33
|
# @param comment [Hash] Comment with :text, :indent
|
|
28
34
|
def emit_tracked_comment(comment)
|
|
29
|
-
indent =
|
|
30
|
-
|
|
35
|
+
indent = ' ' * (comment[:indent] || 0)
|
|
36
|
+
append_line("#{indent}# #{comment[:text]}")
|
|
31
37
|
end
|
|
32
38
|
|
|
33
39
|
# Emit a comment line
|
|
@@ -41,15 +47,15 @@ module Bash
|
|
|
41
47
|
|
|
42
48
|
@lines[-1] = "#{@lines[-1]} # #{text}"
|
|
43
49
|
else
|
|
44
|
-
|
|
50
|
+
append_line("#{current_indent}# #{text}")
|
|
45
51
|
end
|
|
46
52
|
end
|
|
47
53
|
|
|
48
54
|
# Emit a shebang line
|
|
49
55
|
#
|
|
50
56
|
# @param interpreter [String] Interpreter path (e.g., "/bin/bash")
|
|
51
|
-
def emit_shebang(interpreter =
|
|
52
|
-
|
|
57
|
+
def emit_shebang(interpreter = '/bin/bash', metadata: nil)
|
|
58
|
+
append_line("#!#{interpreter}", metadata)
|
|
53
59
|
end
|
|
54
60
|
|
|
55
61
|
# Emit a variable assignment
|
|
@@ -58,113 +64,119 @@ module Bash
|
|
|
58
64
|
# @param value [String] Variable value
|
|
59
65
|
# @param export [Boolean] Whether to export the variable
|
|
60
66
|
# @param inline_comment [String, nil] Optional inline comment
|
|
61
|
-
def emit_variable_assignment(name, value, export: false, inline_comment: nil)
|
|
62
|
-
prefix = export ?
|
|
67
|
+
def emit_variable_assignment(name, value, export: false, inline_comment: nil, metadata: nil)
|
|
68
|
+
prefix = export ? 'export ' : ''
|
|
63
69
|
line = "#{current_indent}#{prefix}#{name}=#{value}"
|
|
64
70
|
line += " # #{inline_comment}" if inline_comment
|
|
65
|
-
|
|
71
|
+
append_line(line, metadata)
|
|
66
72
|
end
|
|
67
73
|
|
|
68
74
|
# Emit a function definition start
|
|
69
75
|
#
|
|
70
76
|
# @param name [String] Function name
|
|
71
|
-
def emit_function_start(name)
|
|
72
|
-
|
|
77
|
+
def emit_function_start(name, metadata: nil)
|
|
78
|
+
append_line("#{current_indent}#{name}() {", metadata)
|
|
73
79
|
indent
|
|
74
80
|
end
|
|
75
81
|
|
|
76
82
|
# Emit a function definition end
|
|
77
83
|
def emit_function_end
|
|
78
84
|
dedent
|
|
79
|
-
|
|
85
|
+
append_line("#{current_indent}}")
|
|
80
86
|
end
|
|
81
87
|
|
|
82
88
|
# Emit an if statement start
|
|
83
89
|
#
|
|
84
90
|
# @param condition [String] Condition expression
|
|
85
|
-
def emit_if_start(condition)
|
|
86
|
-
|
|
91
|
+
def emit_if_start(condition, metadata: nil)
|
|
92
|
+
append_line("#{current_indent}if #{condition}; then", metadata)
|
|
87
93
|
indent
|
|
88
94
|
end
|
|
89
95
|
|
|
90
96
|
# Emit an elif clause
|
|
91
97
|
#
|
|
92
98
|
# @param condition [String] Condition expression
|
|
93
|
-
def emit_elif(condition)
|
|
99
|
+
def emit_elif(condition, metadata: nil)
|
|
94
100
|
dedent
|
|
95
|
-
|
|
101
|
+
append_line("#{current_indent}elif #{condition}; then", metadata)
|
|
96
102
|
indent
|
|
97
103
|
end
|
|
98
104
|
|
|
99
105
|
# Emit an else clause
|
|
100
106
|
def emit_else
|
|
101
107
|
dedent
|
|
102
|
-
|
|
108
|
+
append_line("#{current_indent}else")
|
|
103
109
|
indent
|
|
104
110
|
end
|
|
105
111
|
|
|
106
112
|
# Emit an if statement end
|
|
107
113
|
def emit_fi
|
|
108
114
|
dedent
|
|
109
|
-
|
|
115
|
+
append_line("#{current_indent}fi")
|
|
110
116
|
end
|
|
111
117
|
|
|
112
118
|
# Emit a for loop start
|
|
113
119
|
#
|
|
114
120
|
# @param var [String] Loop variable name
|
|
115
121
|
# @param items [String] Items to iterate over
|
|
116
|
-
def emit_for_start(var, items)
|
|
117
|
-
|
|
122
|
+
def emit_for_start(var, items, metadata: nil)
|
|
123
|
+
append_line("#{current_indent}for #{var} in #{items}; do", metadata)
|
|
118
124
|
indent
|
|
119
125
|
end
|
|
120
126
|
|
|
121
127
|
# Emit a for/while loop end
|
|
122
128
|
def emit_done
|
|
123
129
|
dedent
|
|
124
|
-
|
|
130
|
+
append_line("#{current_indent}done")
|
|
125
131
|
end
|
|
126
132
|
|
|
127
133
|
# Emit a while loop start
|
|
128
134
|
#
|
|
129
135
|
# @param condition [String] Condition expression
|
|
130
|
-
def emit_while_start(condition)
|
|
131
|
-
|
|
136
|
+
def emit_while_start(condition, metadata: nil)
|
|
137
|
+
append_line("#{current_indent}while #{condition}; do", metadata)
|
|
132
138
|
indent
|
|
133
139
|
end
|
|
134
140
|
|
|
135
141
|
# Emit a case statement start
|
|
136
142
|
#
|
|
137
143
|
# @param expression [String] Expression to match
|
|
138
|
-
def emit_case_start(expression)
|
|
139
|
-
|
|
144
|
+
def emit_case_start(expression, metadata: nil)
|
|
145
|
+
append_line("#{current_indent}case #{expression} in", metadata)
|
|
140
146
|
indent
|
|
141
147
|
end
|
|
142
148
|
|
|
143
149
|
# Emit a case pattern
|
|
144
150
|
#
|
|
145
151
|
# @param pattern [String] Pattern to match
|
|
146
|
-
def emit_case_pattern(pattern)
|
|
147
|
-
|
|
152
|
+
def emit_case_pattern(pattern, metadata: nil)
|
|
153
|
+
append_line("#{current_indent}#{pattern})", metadata)
|
|
148
154
|
indent
|
|
149
155
|
end
|
|
150
156
|
|
|
151
157
|
# Emit a case pattern terminator
|
|
152
158
|
def emit_case_pattern_end
|
|
153
159
|
dedent
|
|
154
|
-
|
|
160
|
+
append_line("#{current_indent};;")
|
|
155
161
|
end
|
|
156
162
|
|
|
157
163
|
# Emit a case statement end
|
|
158
164
|
def emit_esac
|
|
159
165
|
dedent
|
|
160
|
-
|
|
166
|
+
append_line("#{current_indent}esac")
|
|
161
167
|
end
|
|
162
168
|
|
|
163
169
|
# Emit a raw line of code
|
|
164
170
|
#
|
|
165
171
|
# @param line [String] Line to emit
|
|
166
|
-
def emit_line(line)
|
|
167
|
-
|
|
172
|
+
def emit_line(line, metadata: nil)
|
|
173
|
+
append_line("#{current_indent}#{line}", metadata)
|
|
174
|
+
end
|
|
175
|
+
|
|
176
|
+
def emit_raw_lines(raw_lines, metadata: nil)
|
|
177
|
+
raw_lines.each_with_index do |line, idx|
|
|
178
|
+
append_line(line.chomp, expanded_line_metadata(metadata, idx))
|
|
179
|
+
end
|
|
168
180
|
end
|
|
169
181
|
|
|
170
182
|
# Get the output as a Bash string
|
|
@@ -14,7 +14,7 @@ module Bash
|
|
|
14
14
|
include Ast::Merge::FileAnalyzable
|
|
15
15
|
|
|
16
16
|
# Default freeze token for identifying freeze blocks
|
|
17
|
-
DEFAULT_FREEZE_TOKEN =
|
|
17
|
+
DEFAULT_FREEZE_TOKEN = 'bash-merge'
|
|
18
18
|
|
|
19
19
|
# @return [CommentTracker] Comment tracker for this file
|
|
20
20
|
attr_reader :comment_tracker
|
|
@@ -42,12 +42,12 @@ module Bash
|
|
|
42
42
|
# @param signature_generator [Proc, nil] Custom signature generator
|
|
43
43
|
# @param parser_path [String, nil] Path to tree-sitter-bash parser library
|
|
44
44
|
# @param options [Hash] Additional options (forward compatibility - ignored by FileAnalysis)
|
|
45
|
-
def initialize(source, freeze_token: DEFAULT_FREEZE_TOKEN, signature_generator: nil, parser_path: nil, **
|
|
45
|
+
def initialize(source, freeze_token: DEFAULT_FREEZE_TOKEN, signature_generator: nil, parser_path: nil, **_options)
|
|
46
46
|
@source = source
|
|
47
47
|
@lines = source.lines.map(&:chomp)
|
|
48
48
|
@freeze_token = freeze_token
|
|
49
49
|
@signature_generator = signature_generator
|
|
50
|
-
@parser_path = parser_path
|
|
50
|
+
@parser_path = parser_path
|
|
51
51
|
@errors = []
|
|
52
52
|
# **options captures any additional parameters (e.g., node_typing) for forward compatibility
|
|
53
53
|
|
|
@@ -55,18 +55,18 @@ module Bash
|
|
|
55
55
|
@comment_tracker = CommentTracker.new(source)
|
|
56
56
|
|
|
57
57
|
# Parse the Bash script
|
|
58
|
-
DebugLogger.time(
|
|
58
|
+
DebugLogger.time('FileAnalysis#parse_bash') { parse_bash }
|
|
59
59
|
|
|
60
60
|
# Extract freeze blocks and integrate with nodes
|
|
61
61
|
@freeze_blocks = extract_freeze_blocks
|
|
62
62
|
@nodes = integrate_nodes_and_freeze_blocks
|
|
63
63
|
|
|
64
|
-
DebugLogger.debug(
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
64
|
+
DebugLogger.debug('FileAnalysis initialized', {
|
|
65
|
+
signature_generator: signature_generator ? 'custom' : 'default',
|
|
66
|
+
nodes_count: @nodes.size,
|
|
67
|
+
freeze_blocks: @freeze_blocks.size,
|
|
68
|
+
valid: valid?
|
|
69
|
+
})
|
|
70
70
|
end
|
|
71
71
|
|
|
72
72
|
# Check if parse was successful
|
|
@@ -75,14 +75,102 @@ module Bash
|
|
|
75
75
|
@errors.empty? && !@ast.nil?
|
|
76
76
|
end
|
|
77
77
|
|
|
78
|
+
# Get shared comment capability information for this analysis.
|
|
79
|
+
#
|
|
80
|
+
# @return [Ast::Merge::Comment::Capability]
|
|
81
|
+
def comment_capability
|
|
82
|
+
@comment_capability ||= comment_tracker.augment(owners: []).capability
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
# Describe how Bash merges currently own and emit comments.
|
|
86
|
+
#
|
|
87
|
+
# Bash comment handling is fully source-augmented and emitted through the
|
|
88
|
+
# synthetic merge layer.
|
|
89
|
+
#
|
|
90
|
+
# @return [Ast::Merge::Comment::SupportStyle]
|
|
91
|
+
def comment_support_style
|
|
92
|
+
@comment_support_style ||= shared_comment_support_style(
|
|
93
|
+
source: :bash_source,
|
|
94
|
+
style: :hash_comment,
|
|
95
|
+
read_strategy: :source_augmented_portable_write
|
|
96
|
+
)
|
|
97
|
+
end
|
|
98
|
+
|
|
99
|
+
# Get all tracked comments converted to shared Ast::Merge comment nodes.
|
|
100
|
+
#
|
|
101
|
+
# @return [Array<Ast::Merge::Comment::Line>]
|
|
102
|
+
def comment_nodes
|
|
103
|
+
comment_tracker.comment_nodes
|
|
104
|
+
end
|
|
105
|
+
|
|
106
|
+
# Get a shared Ast::Merge comment node at a specific line.
|
|
107
|
+
#
|
|
108
|
+
# @param line_num [Integer] 1-based line number
|
|
109
|
+
# @return [Ast::Merge::Comment::Line, nil]
|
|
110
|
+
def comment_node_at(line_num)
|
|
111
|
+
comment_tracker.comment_node_at(line_num)
|
|
112
|
+
end
|
|
113
|
+
|
|
114
|
+
# Get comments in a line range converted to a shared comment region.
|
|
115
|
+
#
|
|
116
|
+
# @param range [Range] Range of 1-based line numbers
|
|
117
|
+
# @param kind [Symbol] Region kind (:leading, :inline, :orphan, etc.)
|
|
118
|
+
# @param full_line_only [Boolean] Whether to keep only full-line comments
|
|
119
|
+
# @return [Ast::Merge::Comment::Region]
|
|
120
|
+
def comment_region_for_range(range, kind:, full_line_only: false)
|
|
121
|
+
comment_tracker.comment_region_for_range(
|
|
122
|
+
range,
|
|
123
|
+
kind: kind,
|
|
124
|
+
full_line_only: full_line_only
|
|
125
|
+
)
|
|
126
|
+
end
|
|
127
|
+
|
|
128
|
+
# Build a passive shared comment attachment for an owner.
|
|
129
|
+
#
|
|
130
|
+
# @param owner [Object] Structural owner for the attachment
|
|
131
|
+
# @param options [Hash] Additional metadata / lookup overrides
|
|
132
|
+
# @return [Ast::Merge::Comment::Attachment]
|
|
133
|
+
def comment_attachment_for(owner, **options)
|
|
134
|
+
shared_comment_attachment_for(
|
|
135
|
+
owner,
|
|
136
|
+
tracker_attachment: comment_tracker.comment_attachment_for(owner, **options),
|
|
137
|
+
**options
|
|
138
|
+
)
|
|
139
|
+
end
|
|
140
|
+
|
|
141
|
+
# @return [Symbol]
|
|
142
|
+
def comment_attachment_strategy
|
|
143
|
+
:augmenter_preferred_tracker_layout
|
|
144
|
+
end
|
|
145
|
+
|
|
146
|
+
def ruleset_owner_selector
|
|
147
|
+
:line_bound_statements
|
|
148
|
+
end
|
|
149
|
+
|
|
150
|
+
def ruleset_render_family
|
|
151
|
+
:bash_script_statements
|
|
152
|
+
end
|
|
153
|
+
|
|
154
|
+
# Build a passive shared comment augmenter for this analysis.
|
|
155
|
+
#
|
|
156
|
+
# @param owners [Array<#start_line,#end_line>, nil] Owners used for attachment inference
|
|
157
|
+
# @param options [Hash] Additional augmenter options
|
|
158
|
+
# @return [Ast::Merge::Comment::Augmenter]
|
|
159
|
+
def comment_augmenter(owners: nil, **options)
|
|
160
|
+
comment_tracker.augment(
|
|
161
|
+
owners: owners || comment_augmenter_default_owners,
|
|
162
|
+
**options
|
|
163
|
+
)
|
|
164
|
+
end
|
|
165
|
+
|
|
78
166
|
# The base module uses 'statements' - provide both names for compatibility
|
|
79
167
|
# @return [Array<NodeWrapper, FreezeNodeBase>]
|
|
80
168
|
def statements
|
|
81
|
-
@nodes
|
|
169
|
+
@nodes || []
|
|
82
170
|
end
|
|
83
171
|
|
|
84
172
|
# Alias for convenience - bash-merge prefers "nodes" terminology
|
|
85
|
-
|
|
173
|
+
alias nodes statements
|
|
86
174
|
|
|
87
175
|
# Check if a line is within a freeze block.
|
|
88
176
|
#
|
|
@@ -120,30 +208,42 @@ module Bash
|
|
|
120
208
|
def top_level_statements
|
|
121
209
|
return [] unless valid?
|
|
122
210
|
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
211
|
+
@top_level_statements ||= begin
|
|
212
|
+
root = @ast.root_node
|
|
213
|
+
if root
|
|
214
|
+
statements = []
|
|
215
|
+
root.each do |child|
|
|
216
|
+
next if child.type.to_s == 'comment' # Comments handled separately
|
|
217
|
+
|
|
218
|
+
statements << NodeWrapper.new(child, lines: @lines, source: @source)
|
|
219
|
+
end
|
|
220
|
+
statements
|
|
221
|
+
else
|
|
222
|
+
[]
|
|
223
|
+
end
|
|
131
224
|
end
|
|
132
|
-
statements
|
|
133
225
|
end
|
|
134
226
|
|
|
135
227
|
private
|
|
136
228
|
|
|
137
229
|
def parse_bash
|
|
138
|
-
# TreeHaver handles
|
|
230
|
+
# TreeHaver handles backend selection against the grammars Bash::Merge
|
|
231
|
+
# has already registered during bootstrap.
|
|
139
232
|
# Set TREE_HAVER_BACKEND=ffi for bash (MRI/Rust have compatibility issues)
|
|
140
|
-
parser =
|
|
233
|
+
parser = if @parser_path
|
|
234
|
+
TreeHaver.with_language_registration(
|
|
235
|
+
:bash,
|
|
236
|
+
:tree_sitter,
|
|
237
|
+
path: @parser_path,
|
|
238
|
+
symbol: TreeHaver::GrammarFinder.new(:bash).symbol_name
|
|
239
|
+
) { TreeHaver.parser_for(:bash, backend_type: :tree_sitter) }
|
|
240
|
+
else
|
|
241
|
+
TreeHaver.parser_for(:bash, backend_type: :tree_sitter)
|
|
242
|
+
end
|
|
141
243
|
@ast = parser.parse(@source)
|
|
142
244
|
|
|
143
245
|
# Check for parse errors in the tree
|
|
144
|
-
if @ast&.root_node&.has_error?
|
|
145
|
-
collect_parse_errors(@ast.root_node)
|
|
146
|
-
end
|
|
246
|
+
collect_parse_errors(@ast.root_node) if @ast&.root_node&.has_error?
|
|
147
247
|
rescue TreeHaver::Error => e
|
|
148
248
|
# TreeHaver::Error inherits from Exception, not StandardError.
|
|
149
249
|
# This also catches TreeHaver::NotAvailable (subclass of Error).
|
|
@@ -156,12 +256,12 @@ module Bash
|
|
|
156
256
|
|
|
157
257
|
def collect_parse_errors(node)
|
|
158
258
|
# Collect ERROR and MISSING nodes from the tree
|
|
159
|
-
if node.type.to_s ==
|
|
259
|
+
if node.type.to_s == 'ERROR' || node.missing?
|
|
160
260
|
@errors << {
|
|
161
261
|
type: node.type.to_s,
|
|
162
262
|
start_point: node.start_point,
|
|
163
263
|
end_point: node.end_point,
|
|
164
|
-
text: node.to_s
|
|
264
|
+
text: node.to_s
|
|
165
265
|
}
|
|
166
266
|
end
|
|
167
267
|
|
|
@@ -180,10 +280,10 @@ module Bash
|
|
|
180
280
|
next unless (match = line.match(freeze_pattern))
|
|
181
281
|
|
|
182
282
|
marker_type = match[1]&.downcase # 'freeze' or 'unfreeze'
|
|
183
|
-
if marker_type ==
|
|
184
|
-
freeze_starts << {line: line_num, marker: line}
|
|
185
|
-
elsif marker_type ==
|
|
186
|
-
freeze_ends << {line: line_num, marker: line}
|
|
283
|
+
if marker_type == 'freeze'
|
|
284
|
+
freeze_starts << { line: line_num, marker: line }
|
|
285
|
+
elsif marker_type == 'unfreeze'
|
|
286
|
+
freeze_ends << { line: line_num, marker: line }
|
|
187
287
|
end
|
|
188
288
|
end
|
|
189
289
|
|
|
@@ -202,7 +302,7 @@ module Bash
|
|
|
202
302
|
end_line: matching_end[:line],
|
|
203
303
|
lines: @lines,
|
|
204
304
|
start_marker: start_info[:marker],
|
|
205
|
-
end_marker: matching_end[:marker]
|
|
305
|
+
end_marker: matching_end[:marker]
|
|
206
306
|
)
|
|
207
307
|
end
|
|
208
308
|
|