bash-merge 2.0.5 → 7.1.1

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.
@@ -75,11 +75,11 @@ module Bash
75
75
  # String representation for debugging
76
76
  # @return [String]
77
77
  def inspect
78
- # :nocov:
78
+ # simplecov:disable
79
79
  # Defensive: slice is always set via resolve_content in FreezeNodeBase#initialize
80
80
  # The || 0 branch is normally unreachable because validate_structure! ensures non-empty content
81
81
  content_length = slice&.length || 0
82
- # :nocov:
82
+ # simplecov:enable
83
83
  "#<#{self.class.name} lines=#{start_line}..#{end_line} content_length=#{content_length}>"
84
84
  end
85
85
 
@@ -88,13 +88,13 @@ module Bash
88
88
  def validate_structure!
89
89
  validate_line_order!
90
90
 
91
- if @lines.empty? || @lines.all?(&:nil?)
92
- raise InvalidStructureError.new(
93
- "Freeze block is empty",
94
- start_line: @start_line,
95
- end_line: @end_line,
96
- )
97
- end
91
+ return unless @lines.empty? || @lines.all?(&:nil?)
92
+
93
+ raise InvalidStructureError.new(
94
+ 'Freeze block is empty',
95
+ start_line: @start_line,
96
+ end_line: @end_line
97
+ )
98
98
  end
99
99
  end
100
100
  end
@@ -12,6 +12,8 @@ module Bash
12
12
  # result.add_line("echo 'hello'", decision: :kept_template, source: :template)
13
13
  # result.to_bash # => "echo 'hello'\n"
14
14
  class MergeResult < Ast::Merge::MergeResultBase
15
+ include Ast::Merge::StructuredReviewApplySupport
16
+
15
17
  # Inherit decision constants from base class
16
18
  DECISION_KEPT_TEMPLATE = Ast::Merge::MergeResultBase::DECISION_KEPT_TEMPLATE
17
19
  DECISION_KEPT_DEST = Ast::Merge::MergeResultBase::DECISION_KEPT_DEST
@@ -31,7 +33,7 @@ module Bash
31
33
  dest_lines: 0,
32
34
  merged_lines: 0,
33
35
  freeze_preserved_lines: 0,
34
- total_decisions: 0,
36
+ total_decisions: 0
35
37
  }
36
38
  end
37
39
 
@@ -46,7 +48,7 @@ module Bash
46
48
  content: line,
47
49
  decision: decision,
48
50
  source: source,
49
- original_line: original_line,
51
+ original_line: original_line
50
52
  }
51
53
 
52
54
  track_statistics(decision, source)
@@ -71,7 +73,7 @@ module Bash
71
73
  # @param decision [Symbol] Decision for the blank line
72
74
  # @param source [Symbol] Source
73
75
  def add_blank_line(decision: DECISION_MERGED, source: :merged)
74
- add_line("", decision: decision, source: source)
76
+ add_line('', decision: decision, source: source)
75
77
  end
76
78
 
77
79
  # Add content from a freeze block
@@ -83,7 +85,7 @@ module Bash
83
85
  line.chomp,
84
86
  decision: DECISION_FREEZE_BLOCK,
85
87
  source: :destination,
86
- original_line: freeze_node.start_line + idx,
88
+ original_line: freeze_node.start_line + idx
87
89
  )
88
90
  end
89
91
  end
@@ -121,9 +123,13 @@ module Bash
121
123
  to_bash
122
124
  end
123
125
 
126
+ def to_s
127
+ to_bash
128
+ end
129
+
124
130
  private
125
131
 
126
- def track_statistics(decision, source)
132
+ def track_statistics(decision, _source)
127
133
  @statistics[:total_decisions] += 1
128
134
 
129
135
  case decision
@@ -22,19 +22,19 @@ module Bash
22
22
  # Check if this is a function definition
23
23
  # @return [Boolean]
24
24
  def function_definition?
25
- @node.type.to_s == "function_definition"
25
+ @node.type.to_s == 'function_definition'
26
26
  end
27
27
 
28
28
  # Check if this is a variable assignment
29
29
  # @return [Boolean]
30
30
  def variable_assignment?
31
- @node.type.to_s == "variable_assignment"
31
+ @node.type.to_s == 'variable_assignment'
32
32
  end
33
33
 
34
34
  # Check if this is an if statement
35
35
  # @return [Boolean]
36
36
  def if_statement?
37
- @node.type.to_s == "if_statement"
37
+ @node.type.to_s == 'if_statement'
38
38
  end
39
39
 
40
40
  # Check if this is a for loop
@@ -46,31 +46,31 @@ module Bash
46
46
  # Check if this is a while loop
47
47
  # @return [Boolean]
48
48
  def while_statement?
49
- @node.type.to_s == "while_statement"
49
+ @node.type.to_s == 'while_statement'
50
50
  end
51
51
 
52
52
  # Check if this is a case statement
53
53
  # @return [Boolean]
54
54
  def case_statement?
55
- @node.type.to_s == "case_statement"
55
+ @node.type.to_s == 'case_statement'
56
56
  end
57
57
 
58
58
  # Check if this is a command
59
59
  # @return [Boolean]
60
60
  def command?
61
- @node.type.to_s == "command"
61
+ @node.type.to_s == 'command'
62
62
  end
63
63
 
64
64
  # Check if this is a pipeline
65
65
  # @return [Boolean]
66
66
  def pipeline?
67
- @node.type.to_s == "pipeline"
67
+ @node.type.to_s == 'pipeline'
68
68
  end
69
69
 
70
70
  # Check if this is a comment
71
71
  # @return [Boolean]
72
72
  def comment?
73
- @node.type.to_s == "comment"
73
+ @node.type.to_s == 'comment'
74
74
  end
75
75
 
76
76
  # Get the function name if this is a function definition
@@ -79,7 +79,7 @@ module Bash
79
79
  return unless function_definition?
80
80
 
81
81
  # In bash tree-sitter, function name is in a 'name' or 'word' child
82
- name_node = find_child_by_type("word") || find_child_by_field("name")
82
+ name_node = find_child_by_type('word') || find_child_by_field('name')
83
83
  node_text(name_node) if name_node
84
84
  end
85
85
 
@@ -89,7 +89,7 @@ module Bash
89
89
  return unless variable_assignment?
90
90
 
91
91
  # In bash tree-sitter, variable name is a child of type 'variable_name'
92
- name_node = find_child_by_type("variable_name")
92
+ name_node = find_child_by_type('variable_name')
93
93
  node_text(name_node) if name_node
94
94
  end
95
95
 
@@ -139,44 +139,49 @@ module Bash
139
139
  node_type = node.type.to_s
140
140
 
141
141
  case node_type
142
- when "program"
142
+ when 'program'
143
143
  # Root node - signature based on direct children structure
144
144
  child_types = []
145
- node.each { |child| child_types << child.type.to_s unless child.type.to_s == "comment" }
145
+ node.each { |child| child_types << child.type.to_s unless child.type.to_s == 'comment' }
146
146
  [:program, child_types.length]
147
- when "function_definition"
147
+ when 'function_definition'
148
148
  # Functions are identified by their name
149
149
  name = function_name
150
150
  [:function_definition, name]
151
- when "variable_assignment"
151
+ when 'variable_assignment'
152
152
  # Variable assignments are identified by variable name
153
153
  name = variable_name
154
154
  [:variable_assignment, name]
155
- when "command"
156
- # Commands identified by their command name
155
+ when 'command'
156
+ # Commands identified by their command name and arguments.
157
+ # Arguments are included so that `PATH_add exe` and `PATH_add bin`
158
+ # get distinct signatures, while `echo "hello"` repeated twice gets
159
+ # the same signature — the resolver handles positional matching for
160
+ # nodes with identical signatures.
157
161
  name = command_name
158
- [:command, name, extract_command_signature_context(node)]
159
- when "if_statement"
162
+ args = extract_command_arguments(node)
163
+ [:command, name, args, extract_command_signature_context(node)]
164
+ when 'if_statement'
160
165
  # If statements identified by their condition pattern
161
166
  condition = extract_condition_pattern(node)
162
167
  [:if_statement, condition]
163
- when "for_statement", "c_style_for_statement"
168
+ when 'for_statement', 'c_style_for_statement'
164
169
  # For loops identified by their loop variable
165
170
  var = extract_loop_variable(node)
166
171
  [:for_statement, var]
167
- when "while_statement"
172
+ when 'while_statement'
168
173
  # While loops identified by condition
169
174
  condition = extract_condition_pattern(node)
170
175
  [:while_statement, condition]
171
- when "case_statement"
176
+ when 'case_statement'
172
177
  # Case statements identified by the expression being matched
173
178
  expr = extract_case_expression(node)
174
179
  [:case_statement, expr]
175
- when "pipeline"
180
+ when 'pipeline'
176
181
  # Pipelines identified by command names in order
177
182
  commands = extract_pipeline_commands(node)
178
183
  [:pipeline, commands]
179
- when "comment"
184
+ when 'comment'
180
185
  # Comments identified by their content
181
186
  [:comment, node_text(node).strip]
182
187
  else
@@ -192,34 +197,55 @@ module Bash
192
197
  # Extract additional context like redirections
193
198
  redirections = []
194
199
  node.each do |child|
195
- if child.type.to_s.include?("redirect")
196
- redirections << child.type.to_s
197
- end
200
+ redirections << child.type.to_s if child.type.to_s.include?('redirect')
198
201
  end
199
202
  redirections.empty? ? nil : redirections.sort
200
203
  end
201
204
 
205
+ # Extract argument words from a command node.
206
+ # Returns the argument text values (everything after the command name).
207
+ #
208
+ # @param node [Object] A tree-sitter command node
209
+ # @return [Array<String>, nil] Argument values, or nil if none
210
+ def extract_command_arguments(node)
211
+ args = []
212
+ found_command_name = false
213
+ node.each do |child|
214
+ type_s = child.type.to_s
215
+ # Skip comments and redirections
216
+ next if %w[comment file_redirect heredoc_redirect].include?(type_s)
217
+
218
+ if !found_command_name && %w[word command_name].include?(type_s)
219
+ # First word/command_name is the command itself, skip it
220
+ found_command_name = true
221
+ next
222
+ end
223
+
224
+ # Everything after the command name is an argument
225
+ args << node_text(child) if found_command_name
226
+ end
227
+ args.empty? ? nil : args
228
+ end
229
+
202
230
  def extract_condition_pattern(node)
203
231
  # Try to extract the test/condition from if/while statements
204
232
  # Look for test_command, compound_statement, etc.
205
233
  node.each do |child|
206
- if %w[test_command bracket_command].include?(child.type.to_s)
207
- return node_text(child).slice(0, 100).strip
208
- end
234
+ return node_text(child).slice(0, 100).strip if %w[test_command bracket_command].include?(child.type.to_s)
209
235
  end
210
236
  nil
211
237
  end
212
238
 
213
239
  def extract_loop_variable(node)
214
240
  # Extract the loop variable from for statements
215
- var_node = node.each.find { |child| child.type.to_s == "variable_name" }
241
+ var_node = node.each.find { |child| child.type.to_s == 'variable_name' }
216
242
  node_text(var_node) if var_node
217
243
  end
218
244
 
219
245
  def extract_case_expression(node)
220
246
  # Extract the expression being matched in a case statement
221
247
  node.each do |child|
222
- return node_text(child).slice(0, 50).strip if child.type.to_s == "word" || child.type.to_s == "variable_name"
248
+ return node_text(child).slice(0, 50).strip if %w[word variable_name].include?(child.type.to_s)
223
249
  end
224
250
  nil
225
251
  end
@@ -228,11 +254,11 @@ module Bash
228
254
  # Extract command names from a pipeline
229
255
  commands = []
230
256
  node.each do |child|
231
- if child.type.to_s == "command"
232
- wrapper = NodeWrapper.new(child, lines: @lines, source: @source)
233
- cmd_name = wrapper.command_name
234
- commands << cmd_name if cmd_name
235
- end
257
+ next unless child.type.to_s == 'command'
258
+
259
+ wrapper = NodeWrapper.new(child, lines: @lines, source: @source)
260
+ cmd_name = wrapper.command_name
261
+ commands << cmd_name if cmd_name
236
262
  end
237
263
  commands
238
264
  end