• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

kettle-rb / bash-merge / 22648367198

03 Mar 2026 11:56PM UTC coverage: 88.382% (-2.4%) from 90.795%
22648367198

push

github

web-flow
Merge pull request #18 from kettle-rb/dependabot/bundler/addressable-2.8.9

130 of 185 branches covered (70.27%)

Branch coverage included in aggregate %.

509 of 538 relevant lines covered (94.61%)

56.61 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

85.54
/lib/bash/merge/node_wrapper.rb
1
# frozen_string_literal: true
2

3
module Bash
2✔
4
  module Merge
2✔
5
    # Wraps TreeHaver nodes with comment associations, line information, and signatures.
6
    # This provides a unified interface for working with Bash AST nodes during merging.
7
    #
8
    # Inherits common functionality from Ast::Merge::NodeWrapperBase:
9
    # - Source context (lines, source, comments)
10
    # - Line info extraction
11
    # - Basic methods: #type, #type?, #text, #content, #signature
12
    #
13
    # @example Basic usage
14
    #   parser = TreeHaver::Parser.new
15
    #   parser.language = TreeHaver::Language.bash
16
    #   tree = parser.parse(source)
17
    #   wrapper = NodeWrapper.new(tree.root_node, lines: source.lines, source: source)
18
    #   wrapper.signature # => [:program, ...]
19
    #
20
    # @see Ast::Merge::NodeWrapperBase
21
    class NodeWrapper < Ast::Merge::NodeWrapperBase
2✔
22
      # Check if this is a function definition
23
      # @return [Boolean]
24
      def function_definition?
2✔
25
        @node.type.to_s == "function_definition"
26✔
26
      end
27

28
      # Check if this is a variable assignment
29
      # @return [Boolean]
30
      def variable_assignment?
2✔
31
        @node.type.to_s == "variable_assignment"
75✔
32
      end
33

34
      # Check if this is an if statement
35
      # @return [Boolean]
36
      def if_statement?
2✔
37
        @node.type.to_s == "if_statement"
5✔
38
      end
39

40
      # Check if this is a for loop
41
      # @return [Boolean]
42
      def for_statement?
2✔
43
        %w[for_statement c_style_for_statement].include?(@node.type.to_s)
3✔
44
      end
45

46
      # Check if this is a while loop
47
      # @return [Boolean]
48
      def while_statement?
2✔
49
        @node.type.to_s == "while_statement"
3✔
50
      end
51

52
      # Check if this is a case statement
53
      # @return [Boolean]
54
      def case_statement?
2✔
55
        @node.type.to_s == "case_statement"
3✔
56
      end
57

58
      # Check if this is a command
59
      # @return [Boolean]
60
      def command?
2✔
61
        @node.type.to_s == "command"
129✔
62
      end
63

64
      # Check if this is a pipeline
65
      # @return [Boolean]
66
      def pipeline?
2✔
67
        @node.type.to_s == "pipeline"
3✔
68
      end
69

70
      # Check if this is a comment
71
      # @return [Boolean]
72
      def comment?
2✔
73
        @node.type.to_s == "comment"
4✔
74
      end
75

76
      # Get the function name if this is a function definition
77
      # @return [String, nil]
78
      def function_name
2✔
79
        return unless function_definition?
21✔
80

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")
20✔
83
        node_text(name_node) if name_node
20!
84
      end
85

86
      # Get the variable name if this is a variable assignment
87
      # @return [String, nil]
88
      def variable_name
2✔
89
        return unless variable_assignment?
65✔
90

91
        # In bash tree-sitter, variable name is a child of type 'variable_name'
92
        name_node = find_child_by_type("variable_name")
64✔
93
        node_text(name_node) if name_node
64!
94
      end
95

96
      # Get the command name if this is a command
97
      # @return [String, nil]
98
      def command_name
2✔
99
        return unless command?
119✔
100

101
        # First child that is a word or simple_expansion
102
        @node.each do |child|
118✔
103
          next if %w[comment file_redirect heredoc_redirect].include?(child.type.to_s)
118!
104

105
          return node_text(child) if %w[word command_name].include?(child.type.to_s)
118!
106
        end
107
        nil
×
108
      end
109

110
      # Find a child by field name
111
      # @param field_name [String] Field name to look for
112
      # @return [TreeSitter::Node, nil]
113
      def find_child_by_field(field_name)
2✔
114
        return unless @node.respond_to?(:child_by_field_name)
×
115

116
        @node.child_by_field_name(field_name)
×
117
      end
118

119
      # Find a child by type
120
      # @param type_name [String] Type name to look for
121
      # @return [TreeSitter::Node, nil]
122
      def find_child_by_type(type_name)
2✔
123
        return unless @node.respond_to?(:each)
84!
124

125
        @node.each do |child|
84✔
126
          return child if child.type.to_s == type_name
84!
127
        end
128
        nil
×
129
      end
130

131
      protected
2✔
132

133
      # Override wrap_child to use Bash::Merge::NodeWrapper
134
      def wrap_child(child)
2✔
135
        NodeWrapper.new(child, lines: @lines, source: @source)
9✔
136
      end
137

138
      def compute_signature(node)
2✔
139
        node_type = node.type.to_s
208✔
140

141
        case node_type
208✔
142
        when "program"
143
          # Root node - signature based on direct children structure
1✔
144
          child_types = []
1✔
145
          node.each { |child| child_types << child.type.to_s unless child.type.to_s == "comment" }
2!
146
          [:program, child_types.length]
1✔
147
        when "function_definition"
148
          # Functions are identified by their name
19✔
149
          name = function_name
19✔
150
          [:function_definition, name]
19✔
151
        when "variable_assignment"
152
          # Variable assignments are identified by variable name
63✔
153
          name = variable_name
63✔
154
          [:variable_assignment, name]
63✔
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.
115✔
161
          name = command_name
115✔
162
          args = extract_command_arguments(node)
115✔
163
          [:command, name, args, extract_command_signature_context(node)]
115✔
164
        when "if_statement"
165
          # If statements identified by their condition pattern
1✔
166
          condition = extract_condition_pattern(node)
1✔
167
          [:if_statement, condition]
1✔
168
        when "for_statement", "c_style_for_statement"
169
          # For loops identified by their loop variable
1✔
170
          var = extract_loop_variable(node)
1✔
171
          [:for_statement, var]
1✔
172
        when "while_statement"
173
          # While loops identified by condition
1✔
174
          condition = extract_condition_pattern(node)
1✔
175
          [:while_statement, condition]
1✔
176
        when "case_statement"
177
          # Case statements identified by the expression being matched
1✔
178
          expr = extract_case_expression(node)
1✔
179
          [:case_statement, expr]
1✔
180
        when "pipeline"
181
          # Pipelines identified by command names in order
1✔
182
          commands = extract_pipeline_commands(node)
1✔
183
          [:pipeline, commands]
1✔
184
        when "comment"
185
          # Comments identified by their content
×
186
          [:comment, node_text(node).strip]
×
187
        else
188
          # Generic fallback - type and first few chars of content
5✔
189
          content_preview = node_text(node).slice(0, 50).strip
5✔
190
          [node_type.to_sym, content_preview]
5✔
191
        end
192
      end
193

194
      private
2✔
195

196
      def extract_command_signature_context(node)
2✔
197
        # Extract additional context like redirections
198
        redirections = []
115✔
199
        node.each do |child|
115✔
200
          if child.type.to_s.include?("redirect")
230!
201
            redirections << child.type.to_s
×
202
          end
203
        end
204
        redirections.empty? ? nil : redirections.sort
115!
205
      end
206

207
      # Extract argument words from a command node.
208
      # Returns the argument text values (everything after the command name).
209
      #
210
      # @param node [Object] A tree-sitter command node
211
      # @return [Array<String>, nil] Argument values, or nil if none
212
      def extract_command_arguments(node)
2✔
213
        args = []
115✔
214
        found_command_name = false
115✔
215
        node.each do |child|
115✔
216
          type_s = child.type.to_s
230✔
217
          # Skip comments and redirections
218
          next if %w[comment file_redirect heredoc_redirect].include?(type_s)
230!
219

220
          if !found_command_name && %w[word command_name].include?(type_s)
230✔
221
            # First word/command_name is the command itself, skip it
115✔
222
            found_command_name = true
115✔
223
            next
115✔
224
          end
225

226
          # Everything after the command name is an argument
227
          if found_command_name
115!
228
            args << node_text(child)
115✔
229
          end
230
        end
231
        args.empty? ? nil : args
115!
232
      end
233

234
      def extract_condition_pattern(node)
2✔
235
        # Try to extract the test/condition from if/while statements
236
        # Look for test_command, compound_statement, etc.
237
        node.each do |child|
2✔
238
          if %w[test_command bracket_command].include?(child.type.to_s)
6✔
239
            return node_text(child).slice(0, 100).strip
1✔
240
          end
241
        end
242
        nil
1✔
243
      end
244

245
      def extract_loop_variable(node)
2✔
246
        # Extract the loop variable from for statements
247
        var_node = node.each.find { |child| child.type.to_s == "variable_name" }
3✔
248
        node_text(var_node) if var_node
1!
249
      end
250

251
      def extract_case_expression(node)
2✔
252
        # Extract the expression being matched in a case statement
253
        node.each do |child|
1✔
254
          return node_text(child).slice(0, 50).strip if child.type.to_s == "word" || child.type.to_s == "variable_name"
5!
255
        end
256
        nil
1✔
257
      end
258

259
      def extract_pipeline_commands(node)
2✔
260
        # Extract command names from a pipeline
261
        commands = []
1✔
262
        node.each do |child|
1✔
263
          if child.type.to_s == "command"
3✔
264
            wrapper = NodeWrapper.new(child, lines: @lines, source: @source)
2✔
265
            cmd_name = wrapper.command_name
2✔
266
            commands << cmd_name if cmd_name
2!
267
          end
268
        end
269
        commands
1✔
270
      end
271
    end
272
  end
273
end
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc