decode 0.22.0 → 0.23.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.
Files changed (53) hide show
  1. checksums.yaml +4 -4
  2. checksums.yaml.gz.sig +0 -0
  3. data/bake/decode/index.rb +16 -9
  4. data/context/coverage.md +325 -0
  5. data/context/getting-started.md +242 -0
  6. data/context/ruby-documentation.md +363 -0
  7. data/lib/decode/comment/attribute.rb +9 -3
  8. data/lib/decode/comment/node.rb +4 -2
  9. data/lib/decode/comment/option.rb +1 -1
  10. data/lib/decode/comment/parameter.rb +12 -6
  11. data/lib/decode/comment/pragma.rb +12 -1
  12. data/lib/decode/comment/raises.rb +1 -1
  13. data/lib/decode/comment/returns.rb +3 -4
  14. data/lib/decode/comment/tag.rb +13 -3
  15. data/lib/decode/comment/tags.rb +17 -2
  16. data/lib/decode/comment/text.rb +4 -1
  17. data/lib/decode/comment/throws.rb +1 -1
  18. data/lib/decode/comment/yields.rb +7 -1
  19. data/lib/decode/definition.rb +54 -42
  20. data/lib/decode/documentation.rb +12 -14
  21. data/lib/decode/index.rb +29 -14
  22. data/lib/decode/language/generic.rb +30 -14
  23. data/lib/decode/language/reference.rb +13 -4
  24. data/lib/decode/language/ruby/alias.rb +41 -0
  25. data/lib/decode/language/ruby/attribute.rb +7 -6
  26. data/lib/decode/language/ruby/block.rb +4 -1
  27. data/lib/decode/language/ruby/call.rb +16 -6
  28. data/lib/decode/language/ruby/class.rb +19 -36
  29. data/lib/decode/language/ruby/code.rb +27 -15
  30. data/lib/decode/language/ruby/constant.rb +9 -8
  31. data/lib/decode/language/ruby/definition.rb +31 -20
  32. data/lib/decode/language/ruby/function.rb +2 -1
  33. data/lib/decode/language/ruby/generic.rb +17 -7
  34. data/lib/decode/language/ruby/method.rb +47 -12
  35. data/lib/decode/language/ruby/module.rb +4 -11
  36. data/lib/decode/language/ruby/parser.rb +365 -205
  37. data/lib/decode/language/ruby/reference.rb +26 -17
  38. data/lib/decode/language/ruby/segment.rb +11 -4
  39. data/lib/decode/language/ruby.rb +4 -2
  40. data/lib/decode/language.rb +2 -2
  41. data/lib/decode/languages.rb +25 -6
  42. data/lib/decode/location.rb +2 -0
  43. data/lib/decode/scope.rb +1 -1
  44. data/lib/decode/segment.rb +6 -5
  45. data/lib/decode/source.rb +12 -4
  46. data/lib/decode/syntax/link.rb +9 -1
  47. data/lib/decode/syntax/match.rb +12 -0
  48. data/lib/decode/syntax/rewriter.rb +10 -0
  49. data/lib/decode/trie.rb +27 -22
  50. data/lib/decode/version.rb +1 -1
  51. data.tar.gz.sig +0 -0
  52. metadata +9 -10
  53. metadata.gz.sig +0 -0
@@ -3,26 +3,29 @@
3
3
  # Released under the MIT License.
4
4
  # Copyright, 2020-2024, by Samuel Williams.
5
5
 
6
- require 'parser/current'
6
+ require "prism"
7
7
 
8
- require_relative '../../scope'
8
+ require_relative "../../scope"
9
9
 
10
- require_relative 'attribute'
11
- require_relative 'block'
12
- require_relative 'call'
13
- require_relative 'class'
14
- require_relative 'constant'
15
- require_relative 'function'
16
- require_relative 'method'
17
- require_relative 'module'
10
+ require_relative "alias"
11
+ require_relative "attribute"
12
+ require_relative "block"
13
+ require_relative "call"
14
+ require_relative "class"
15
+ require_relative "constant"
16
+ require_relative "function"
17
+ require_relative "method"
18
+ require_relative "module"
18
19
 
19
- require_relative 'segment'
20
+ require_relative "segment"
20
21
 
21
22
  module Decode
22
23
  module Language
23
24
  module Ruby
24
25
  # The Ruby source code parser.
25
26
  class Parser
27
+ # Initialize a new Ruby parser.
28
+ # @parameter language [Language] The language instance.
26
29
  def initialize(language)
27
30
  @language = language
28
31
 
@@ -30,241 +33,391 @@ module Decode
30
33
  @definitions = Hash.new.compare_by_identity
31
34
  end
32
35
 
33
- private def assign_definition(parent, definition)
34
- (@definitions[parent] ||= {})[definition.name] = definition
35
- end
36
-
37
- private def lookup_definition(parent, name)
38
- (@definitions[parent] ||= {})[name]
39
- end
40
-
41
- # Parse the given source object, can be a string or a Source instance.
42
- # @parameter source [String | Source] The source to parse.
43
- private def parse_source(source)
44
- if source.is_a?(Source)
45
- ::Parser::CurrentRuby.parse_with_comments(source.read, source.relative_path)
46
- else
47
- ::Parser::CurrentRuby.parse_with_comments(source)
48
- end
49
- end
50
-
51
36
  # Extract definitions from the given input file.
52
37
  def definitions_for(source, &block)
53
- top, comments = self.parse_source(source)
38
+ return enum_for(:definitions_for, source) unless block_given?
54
39
 
55
- if top
56
- walk_definitions(top, comments, &block)
57
- end
40
+ result = self.parse_source(source)
41
+ result.attach_comments!
42
+
43
+ # Pass the source to walk_definitions for location tracking
44
+ source = source.is_a?(Source) ? source : nil
45
+ walk_definitions(result.value, nil, source, &block)
58
46
  end
59
47
 
60
- def extract_comments_for(node, comments)
61
- prefix = []
62
-
63
- while comment = comments.first
64
- break if comment.location.line >= node.location.line
65
-
66
- if last_comment = prefix.last
67
- if last_comment.location.line != (comment.location.line - 1)
68
- prefix.clear
69
- end
70
- end
71
-
72
- prefix << comments.shift
48
+ # Walk over the syntax tree and extract relevant definitions with their associated comments.
49
+ def walk_definitions(node, parent = nil, source = nil, &block)
50
+ # Check for scope definitions from comments
51
+ if node.comments.any?
52
+ parent = scope_for(comments_for(node), parent, &block) || parent
73
53
  end
74
54
 
75
- # The last comment must butt up against the node:
76
- if comment = prefix.last
77
- if comment.location.line == (node.location.line - 1)
78
- return prefix.map do |comment|
79
- comment.text.sub(/\A\#\s?/, '')
55
+ case node.type
56
+ when :program_node
57
+ with_visibility do
58
+ node.child_nodes.each do |child|
59
+ walk_definitions(child, parent, source, &block)
80
60
  end
81
61
  end
82
- end
83
- end
84
-
85
- def with_visibility(visibility = :public, &block)
86
- saved_visibility = @visibility
87
- @visibility = visibility
88
- yield
89
- ensure
90
- @visibility = saved_visibility
91
- end
92
-
93
- # Walk over the syntax tree and extract relevant definitions with their associated comments.
94
- def walk_definitions(node, comments, parent = nil, &block)
95
- case node.type
96
- when :module
97
- definition = Module.new(
98
- node, nested_name_for(node.children[0]),
99
- comments: extract_comments_for(node, comments),
62
+ when :statements_node
63
+ node.child_nodes.each do |child|
64
+ walk_definitions(child, parent, source, &block)
65
+ end
66
+ when :block_node
67
+ if node.body
68
+ walk_definitions(node.body, parent, source, &block)
69
+ end
70
+ when :module_node
71
+ path = nested_path_for(node.constant_path)
72
+
73
+ definition = Module.new(path,
74
+ visibility: :public,
75
+ comments: comments_for(node),
100
76
  parent: parent,
101
- language: @language, visibility: :public
77
+ node: node,
78
+ language: @language,
79
+ source: source,
102
80
  )
103
81
 
104
- assign_definition(parent, definition)
105
-
82
+ store_definition(parent, path.last.to_sym, definition)
106
83
  yield definition
107
84
 
108
- if children = node.children[1]
85
+ if body = node.body
109
86
  with_visibility do
110
- walk_definitions(children, comments, definition, &block)
87
+ walk_definitions(body, definition, source, &block)
111
88
  end
112
89
  end
113
- when :class
114
- definition = Class.new(
115
- node, nested_name_for(node.children[0]),
116
- comments: extract_comments_for(node, comments),
117
- parent: parent, language: @language, visibility: :public
118
- )
90
+ when :class_node
91
+ path = nested_path_for(node.constant_path)
92
+ super_class = nested_name_for(node.superclass)
119
93
 
120
- assign_definition(parent, definition)
94
+ definition = Class.new(path,
95
+ super_class: super_class,
96
+ visibility: :public,
97
+ comments: comments_for(node),
98
+ parent: parent,
99
+ node: node,
100
+ language: @language,
101
+ source: source,
102
+ )
121
103
 
104
+ store_definition(parent, path.last.to_sym, definition)
122
105
  yield definition
123
106
 
124
- if children = node.children[2]
107
+ if body = node.body
125
108
  with_visibility do
126
- walk_definitions(children, comments, definition, &block)
109
+ walk_definitions(body, definition, source, &block)
127
110
  end
128
111
  end
129
- when :sclass
130
- if name = singleton_name_for(node.children[0])
131
- definition = Singleton.new(
132
- node, name,
133
- comments: extract_comments_for(node, comments),
134
- parent: parent, language: @language, visibility: :public
112
+ when :singleton_class_node
113
+ if name = singleton_name_for(node)
114
+ definition = Singleton.new(name,
115
+ comments: comments_for(node),
116
+ parent: parent, language: @language, visibility: :public, source: source
135
117
  )
136
118
 
137
119
  yield definition
138
120
 
139
- if children = node.children[1]
140
- walk_definitions(children, comments, definition, &block)
121
+ if body = node.body
122
+ walk_definitions(body, definition, source, &block)
141
123
  end
142
124
  end
143
- when :def
144
- definition = Method.new(
145
- node, node.children[0],
146
- comments: extract_comments_for(node, comments),
147
- parent: parent, language: @language, visibility: @visibility
148
- )
149
-
150
- yield definition
151
- when :defs
152
- extracted_comments = extract_comments_for(node, comments)
125
+ when :def_node
126
+ receiver = receiver_for(node.receiver)
153
127
 
154
- definition = Function.new(
155
- node, node.children[1],
156
- comments: extracted_comments,
157
- parent: scope_for(extracted_comments, parent, &block),
158
- language: @language, visibility: @visibility
128
+ definition = Method.new(node.name,
129
+ visibility: @visibility,
130
+ comments: comments_for(node),
131
+ parent: parent,
132
+ node: node,
133
+ language: @language,
134
+ receiver: receiver,
135
+ source: source,
159
136
  )
160
137
 
161
138
  yield definition
162
- when :casgn
163
- definition = Constant.new(
164
- node, node.children[1],
165
- comments: extract_comments_for(node, comments),
166
- parent: parent, language: @language
139
+ when :constant_write_node
140
+ definition = Constant.new(node.name,
141
+ comments: comments_for(node),
142
+ parent: parent,
143
+ node: node,
144
+ language: @language,
167
145
  )
168
146
 
147
+ store_definition(parent, node.name, definition)
169
148
  yield definition
170
- when :send
171
- name = node.children[1]
149
+ when :call_node
150
+ name = node.name
172
151
 
173
152
  case name
174
153
  when :public, :protected, :private
175
- @visibility = name
154
+ # Handle cases like "private def foo" where method definitions are arguments
155
+ if node.arguments
156
+ has_method_definitions = false
157
+ node.arguments.arguments.each do |arg_node|
158
+ if arg_node.type == :def_node
159
+ has_method_definitions = true
160
+ # Process the method definition with the specified visibility
161
+ receiver = receiver_for(arg_node.receiver)
162
+
163
+ definition = Method.new(arg_node.name,
164
+ visibility: name,
165
+ comments: comments_for(arg_node),
166
+ parent: parent,
167
+ node: arg_node,
168
+ language: @language,
169
+ receiver: receiver,
170
+ )
171
+
172
+ yield definition
173
+ end
174
+ end
175
+
176
+ # Only set visibility state if this is NOT an inline method definition
177
+ unless has_method_definitions
178
+ @visibility = name
179
+ end
180
+ else
181
+ # No arguments, so this is a standalone visibility modifier
182
+ @visibility = name
183
+ end
176
184
  when :private_constant
177
- constant_names_for(node.children[2..]) do |name|
178
- if definition = lookup_definition(parent, name)
179
- definition.visibility = :private
185
+ if node.arguments
186
+ constant_names_for(node.arguments.arguments) do |name|
187
+ if definition = lookup_definition(parent, name)
188
+ definition.visibility = :private
189
+ end
180
190
  end
181
191
  end
182
192
  when :attr, :attr_reader, :attr_writer, :attr_accessor
183
- definition = Attribute.new(
184
- node, name_for(node.children[2]),
185
- comments: extract_comments_for(node, comments),
186
- parent: parent, language: @language
193
+ definition = Attribute.new(attribute_name_for(node),
194
+ comments: comments_for(node),
195
+ parent: parent, language: @language, node: node
187
196
  )
188
197
 
189
198
  yield definition
199
+ when :alias_method
200
+ # Handle alias_method :new_name, :old_name syntax
201
+ if node.arguments && node.arguments.arguments.size >= 2
202
+ new_name_arg = node.arguments.arguments[0]
203
+ old_name_arg = node.arguments.arguments[1]
204
+
205
+ # Extract symbol names from the arguments
206
+ new_name = symbol_name_for(new_name_arg)
207
+ old_name = symbol_name_for(old_name_arg)
208
+
209
+ definition = Alias.new(new_name.to_sym, old_name.to_sym,
210
+ comments: comments_for(node),
211
+ parent: parent,
212
+ node: node,
213
+ language: @language,
214
+ visibility: @visibility,
215
+ source: source,
216
+ )
217
+
218
+ yield definition
219
+ end
190
220
  else
191
- extracted_comments = extract_comments_for(node, comments)
192
- if kind = kind_for(node, extracted_comments)
221
+ # Check if this call should be treated as a definition
222
+ # either because it has a @name comment, @attribute comment, or a block
223
+ has_name_comment = comments_for(node).any? { |comment| comment.match(NAME_ATTRIBUTE) }
224
+ has_attribute_comment = kind_for(node, comments_for(node))
225
+ has_block = node.block
226
+
227
+ if has_name_comment || has_attribute_comment || has_block
193
228
  definition = Call.new(
194
- node, name_for(node, extracted_comments),
195
- comments: extracted_comments,
196
- parent: parent, language: @language
229
+ attribute_name_for(node),
230
+ comments: comments_for(node),
231
+ parent: parent, language: @language, node: node
197
232
  )
198
233
 
199
234
  yield definition
235
+
236
+ # Walk into the block body if it exists
237
+ if node.block
238
+ walk_definitions(node.block, definition, source, &block)
239
+ end
200
240
  end
201
241
  end
202
- when :block
203
- extracted_comments = extract_comments_for(node, comments)
242
+ when :alias_method_node
243
+ # Handle alias new_name old_name syntax
244
+ new_name = node.new_name.unescaped
245
+ old_name = node.old_name.unescaped
204
246
 
205
- if name = name_for(node, extracted_comments)
206
- definition = Block.new(
207
- node, name,
208
- comments: extracted_comments,
209
- parent: scope_for(extracted_comments, parent, &block),
210
- language: @language
211
- )
212
-
213
- if kind = kind_for(node, extracted_comments)
214
- definition = definition.convert(kind)
215
- end
216
-
217
- yield definition
218
-
219
- if children = node.children[2]
220
- walk_definitions(children, comments, definition, &block)
221
- end
222
- end
247
+ definition = Alias.new(new_name.to_sym, old_name.to_sym,
248
+ comments: comments_for(node),
249
+ parent: parent,
250
+ node: node,
251
+ language: @language,
252
+ visibility: @visibility,
253
+ source: source,
254
+ )
255
+
256
+ yield definition
223
257
  else
224
- node.children.each do |child|
225
- if child.is_a?(::Parser::AST::Node)
226
- walk_definitions(child, comments, parent, &block) if child
227
- end
258
+ if node.respond_to?(:statements)
259
+ walk_definitions(node.statements, parent, source, &block)
260
+ else
261
+ # $stderr.puts "Ignoring #{node.type}"
228
262
  end
229
263
  end
230
264
  end
231
265
 
266
+ # Extract segments from the given input file.
267
+ def segments_for(source, &block)
268
+ result = self.parse_source(source)
269
+ comments = result.comments.reject do |comment|
270
+ comment.location.slice.start_with?("#!/") ||
271
+ comment.location.slice.start_with?("# frozen_string_literal:") ||
272
+ comment.location.slice.start_with?("# Released under the MIT License.") ||
273
+ comment.location.slice.start_with?("# Copyright,")
274
+ end
275
+
276
+ # Now we iterate over the syntax tree and generate segments:
277
+ walk_segments(result.value, comments, &block)
278
+ end
279
+
280
+ private
281
+
282
+ # Extract clean comment text from a node by removing leading # symbols and whitespace.
283
+ # Only returns comments that directly precede the node (i.e., are adjacent to it).
284
+ # @parameter node [Node] The AST node with comments.
285
+ # @returns [Array] Array of cleaned comment strings.
286
+ def comments_for(node)
287
+ # Find the node's starting line
288
+ node_start_line = node.location.start_line
289
+
290
+ # Filter comments to only include those that directly precede the node
291
+ # We work backwards from the line before the node to find consecutive comments
292
+ adjacent_comments = []
293
+ expected_line = node_start_line - 1
294
+
295
+ # Process comments in reverse order to work backwards from the node
296
+ node.comments.reverse_each do |comment|
297
+ comment_line = comment.location.start_line
298
+
299
+ # If this comment is on the expected line, it's adjacent
300
+ if comment_line == expected_line
301
+ adjacent_comments.unshift(comment)
302
+ expected_line = comment_line - 1
303
+ elsif comment_line < expected_line
304
+ # If we hit a comment that's too far back, stop
305
+ break
306
+ end
307
+ # If comment_line > expected_line, skip it (it's not adjacent)
308
+ end
309
+
310
+ # Clean and return the adjacent comments
311
+ adjacent_comments.map do |comment|
312
+ text = comment.slice
313
+ # Remove leading # and optional whitespace
314
+ text.sub(/\A\#\s?/, "")
315
+ end
316
+ end
317
+
318
+ def assign_definition(parent, definition)
319
+ (@definitions[parent] ||= {})[definition.name] = definition
320
+ end
321
+
322
+ def lookup_definition(parent, name)
323
+ (@definitions[parent] ||= {})[name]
324
+ end
325
+
326
+ def store_definition(parent, name, definition)
327
+ (@definitions[parent] ||= {})[name] = definition
328
+ end
329
+
330
+ # Parse the given source object, can be a string or a Source instance.
331
+ # @parameter source [String | Source] The source to parse.
332
+ def parse_source(source)
333
+ if source.is_a?(Source)
334
+ Prism.parse(source.read, filepath: source.path)
335
+ else
336
+ Prism.parse(source)
337
+ end
338
+ end
339
+
340
+ def with_visibility(visibility = :public, &block)
341
+ saved_visibility = @visibility
342
+ @visibility = visibility
343
+ yield
344
+ ensure
345
+ @visibility = saved_visibility
346
+ end
347
+
232
348
  NAME_ATTRIBUTE = /\A@name\s+(?<value>.*?)\Z/
233
349
 
234
- def name_for(node, comments = nil)
235
- comments&.each do |comment|
350
+ def attribute_name_for(node)
351
+ comments_for(node).each do |comment|
236
352
  if match = comment.match(NAME_ATTRIBUTE)
237
353
  return match[:value].to_sym
238
354
  end
239
355
  end
240
356
 
357
+ if node.arguments && node.arguments.arguments.any?
358
+ argument = node.arguments.arguments.first
359
+ case argument.type
360
+ when :symbol_node
361
+ return argument.unescaped.to_sym
362
+ when :call_node
363
+ return argument.name
364
+ when :block_node
365
+ return node.name
366
+ end
367
+ end
368
+
369
+ return node.name
370
+ end
371
+
372
+ def nested_path_for(node, path = [])
373
+ return nil if node.nil?
374
+
241
375
  case node.type
242
- when :sym
243
- return node.children[0]
244
- when :send
245
- return node.children[1]
246
- when :block
247
- return node.children[0].children[1]
376
+ when :constant_read_node
377
+ path << node.name
378
+ when :constant_path_node
379
+ nested_path_for(node.parent, path)
380
+ path << node.name
248
381
  end
382
+
383
+ return path.empty? ? nil : path
249
384
  end
250
385
 
251
386
  def nested_name_for(node)
252
- if prefix = node.children[0]
253
- "#{nested_name_for(prefix)}::#{node.children[1]}".to_sym
387
+ nested_path_for(node)&.join("::")
388
+ end
389
+
390
+ def symbol_name_for(node)
391
+ case node.type
392
+ when :symbol_node
393
+ node.unescaped
254
394
  else
255
- node.children[1]
395
+ node.slice
256
396
  end
257
397
  end
258
398
 
259
- def singleton_name_for(node)
399
+ def receiver_for(node)
400
+ return nil unless node
401
+
260
402
  case node.type
261
- when :const
403
+ when :self_node
404
+ "self"
405
+ when :constant_read_node
406
+ node.name.to_s
407
+ when :constant_path_node
262
408
  nested_name_for(node)
263
- when :self
264
- :'self'
265
409
  end
266
410
  end
267
-
411
+
412
+ def singleton_name_for(node)
413
+ case node.expression.type
414
+ when :self_node
415
+ "self"
416
+ when :constant_read_node
417
+ nested_name_for(node.expression)
418
+ end
419
+ end
420
+
268
421
  KIND_ATTRIBUTE = /\A
269
422
  (@(?<kind>attribute)\s+(?<value>.*?))|
270
423
  (@define\s+(?<kind>)\s+(?<value>.*?))
@@ -281,7 +434,7 @@ module Decode
281
434
  end
282
435
 
283
436
  SCOPE_ATTRIBUTE = /\A
284
- (@scope\s+(?<names>.*?))
437
+ @scope\s+(?<names>.*?)
285
438
  \Z/x
286
439
 
287
440
  def scope_for(comments, parent = nil, &block)
@@ -298,59 +451,66 @@ module Decode
298
451
  return parent
299
452
  end
300
453
 
301
- def constant_names_for(children)
302
- children.each do |node|
303
- if node.type == :sym
304
- yield node.children[0]
454
+ def constant_names_for(child_nodes)
455
+ child_nodes.each do |node|
456
+ if node.type == :symbol_node
457
+ yield node.unescaped.to_sym
305
458
  end
306
459
  end
307
460
  end
308
461
 
309
- # Extract segments from the given input file.
310
- def segments_for(source, &block)
311
- top, comments = self.parse_source(source)
312
-
313
- # We delete any leading comments:
314
- line = 0
315
-
316
- while comment = comments.first
317
- if comment.location.line == line
318
- comments.pop
319
- line += 1
320
- else
321
- break
322
- end
323
- end
324
-
325
- # Now we iterate over the syntax tree and generate segments:
326
- walk_segments(top, comments, &block)
327
- end
328
-
329
462
  def walk_segments(node, comments, &block)
330
463
  case node.type
331
- when :begin
332
- segment = nil
464
+ when :program_node
465
+ walk_segments(node.statements, comments, &block)
466
+ when :statements_node
467
+ statements = node.child_nodes
468
+ current_segment = nil
333
469
 
334
- node.children.each do |child|
335
- if segment.nil?
336
- segment = Segment.new(
337
- extract_comments_for(child, comments),
338
- @language, child
470
+ statements.each_with_index do |stmt, stmt_index|
471
+ # Find comments that precede this statement and are not inside previous statements
472
+ preceding_comments = []
473
+ last_stmt_end_line = stmt_index > 0 ? statements[stmt_index - 1].location.end_line : 0
474
+
475
+ comments.each do |comment|
476
+ comment_line = comment.location.start_line
477
+ # Comment must be after the previous statement and before this statement
478
+ if comment_line > last_stmt_end_line && comment_line < stmt.location.start_line
479
+ preceding_comments << comment
480
+ end
481
+ end
482
+
483
+ # Remove consumed comments
484
+ comments -= preceding_comments
485
+
486
+ if preceding_comments.any?
487
+ # Start a new segment with these comments
488
+ yield current_segment if current_segment
489
+ current_segment = Segment.new(
490
+ preceding_comments.map { |c| c.location.slice.sub(/^#[\s\t]?/, "") },
491
+ @language,
492
+ stmt
339
493
  )
340
- elsif next_comments = extract_comments_for(child, comments)
341
- yield segment if segment
342
- segment = Segment.new(next_comments, @language, child)
494
+ elsif current_segment
495
+ # Extend current segment with this statement
496
+ current_segment.expand(stmt)
343
497
  else
344
- segment.expand(child)
498
+ # Start a new segment without comments
499
+ current_segment = Segment.new(
500
+ [],
501
+ @language,
502
+ stmt
503
+ )
345
504
  end
346
505
  end
347
506
 
348
- yield segment if segment
507
+ yield current_segment if current_segment
349
508
  else
350
509
  # One top level segment:
351
510
  segment = Segment.new(
352
- extract_comments_for(node, comments),
353
- @language, node
511
+ [],
512
+ @language,
513
+ node
354
514
  )
355
515
 
356
516
  yield segment