decode 0.22.0 → 0.23.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.
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 +27 -19
  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 +358 -207
  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,244 +33,385 @@ 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(node.comments.map(&:slice), 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: node.comments.map(&:slice),
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: node.comments.map(&:slice),
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: node.comments.map(&:slice),
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
- )
125
+ when :def_node
126
+ receiver = receiver_for(node.receiver)
149
127
 
150
- yield definition
151
- when :defs
152
- extracted_comments = extract_comments_for(node, comments)
153
-
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: node.comments.map(&:slice),
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: node.comments.map(&:slice),
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: arg_node.comments.map(&:slice),
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: node.comments.map(&:slice),
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: node.comments.map(&:slice),
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 = node.comments.any? { |comment| comment.slice.match(NAME_ATTRIBUTE) }
224
+ has_attribute_comment = kind_for(node, node.comments.map(&:slice))
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: node.comments.map(&:slice),
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
247
+ definition = Alias.new(new_name.to_sym, old_name.to_sym,
248
+ comments: node.comments.map(&:slice),
249
+ parent: parent,
250
+ node: node,
251
+ language: @language,
252
+ visibility: @visibility,
253
+ source: source,
254
+ )
255
+
256
+ yield definition
257
+ else
258
+ if node.respond_to?(:statements)
259
+ walk_definitions(node.statements, parent, source, &block)
260
+ else
261
+ # $stderr.puts "Ignoring #{node.type}"
222
262
  end
263
+ end
264
+ end
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
+ def assign_definition(parent, definition)
283
+ (@definitions[parent] ||= {})[definition.name] = definition
284
+ end
285
+
286
+ def lookup_definition(parent, name)
287
+ (@definitions[parent] ||= {})[name]
288
+ end
289
+
290
+ def store_definition(parent, name, definition)
291
+ (@definitions[parent] ||= {})[name] = definition
292
+ end
293
+
294
+ # Parse the given source object, can be a string or a Source instance.
295
+ # @parameter source [String | Source] The source to parse.
296
+ def parse_source(source)
297
+ if source.is_a?(Source)
298
+ Prism.parse(source.read, filepath: source.path)
223
299
  else
224
- node.children.each do |child|
225
- if child.is_a?(::Parser::AST::Node)
226
- walk_definitions(child, comments, parent, &block) if child
300
+ Prism.parse(source)
301
+ end
302
+ end
303
+
304
+ def extract_comments_for(node, comments)
305
+ prefix = []
306
+
307
+ while comment = comments.first
308
+ break if comment.location.line >= node.location.line
309
+
310
+ if last_comment = prefix.last
311
+ if last_comment.location.line != (comment.location.line - 1)
312
+ prefix.clear
313
+ end
314
+ end
315
+
316
+ prefix << comments.shift
317
+ end
318
+
319
+ # The last comment must butt up against the node:
320
+ if comment = prefix.last
321
+ if comment.location.line == (node.location.line - 1)
322
+ return prefix.map do |comment|
323
+ # Remove # and at most one space/tab to preserve indentation
324
+ comment.slice.sub(/\A\#[\s\t]?/, "")
227
325
  end
228
326
  end
229
327
  end
230
328
  end
231
329
 
232
- NAME_ATTRIBUTE = /\A@name\s+(?<value>.*?)\Z/
330
+ def with_visibility(visibility = :public, &block)
331
+ saved_visibility = @visibility
332
+ @visibility = visibility
333
+ yield
334
+ ensure
335
+ @visibility = saved_visibility
336
+ end
233
337
 
234
- def name_for(node, comments = nil)
235
- comments&.each do |comment|
236
- if match = comment.match(NAME_ATTRIBUTE)
338
+ NAME_ATTRIBUTE = /\A\#\s*@name\s+(?<value>.*?)\Z/
339
+
340
+ def attribute_name_for(node)
341
+ node.comments.each do |comment|
342
+ text = comment.slice
343
+ if match = text.match(NAME_ATTRIBUTE)
237
344
  return match[:value].to_sym
238
345
  end
239
346
  end
240
347
 
348
+ if node.arguments && node.arguments.arguments.any?
349
+ argument = node.arguments.arguments.first
350
+ case argument.type
351
+ when :symbol_node
352
+ return argument.unescaped.to_sym
353
+ when :call_node
354
+ return argument.name
355
+ when :block_node
356
+ return node.name
357
+ end
358
+ end
359
+
360
+ return node.name
361
+ end
362
+
363
+ def nested_path_for(node, path = [])
364
+ return nil if node.nil?
365
+
241
366
  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]
367
+ when :constant_read_node
368
+ path << node.name
369
+ when :constant_path_node
370
+ nested_path_for(node.parent, path)
371
+ path << node.name
248
372
  end
373
+
374
+ return path.empty? ? nil : path
249
375
  end
250
376
 
251
377
  def nested_name_for(node)
252
- if prefix = node.children[0]
253
- "#{nested_name_for(prefix)}::#{node.children[1]}".to_sym
378
+ nested_path_for(node)&.join("::")
379
+ end
380
+
381
+ def symbol_name_for(node)
382
+ case node.type
383
+ when :symbol_node
384
+ node.unescaped
254
385
  else
255
- node.children[1]
386
+ node.slice
256
387
  end
257
388
  end
258
389
 
259
- def singleton_name_for(node)
390
+ def receiver_for(node)
391
+ return nil unless node
392
+
260
393
  case node.type
261
- when :const
394
+ when :self_node
395
+ "self"
396
+ when :constant_read_node
397
+ node.name.to_s
398
+ when :constant_path_node
262
399
  nested_name_for(node)
263
- when :self
264
- :'self'
265
400
  end
266
401
  end
267
-
402
+
403
+ def singleton_name_for(node)
404
+ case node.expression.type
405
+ when :self_node
406
+ "self"
407
+ when :constant_read_node
408
+ nested_name_for(node.expression)
409
+ end
410
+ end
411
+
268
412
  KIND_ATTRIBUTE = /\A
269
- (@(?<kind>attribute)\s+(?<value>.*?))|
270
- (@define\s+(?<kind>)\s+(?<value>.*?))
413
+ (\#\s*@(?<kind>attribute)\s+(?<value>.*?))|
414
+ (\#\s*@define\s+(?<kind>)\s+(?<value>.*?))
271
415
  \Z/x
272
416
 
273
417
  def kind_for(node, comments = nil)
@@ -281,7 +425,7 @@ module Decode
281
425
  end
282
426
 
283
427
  SCOPE_ATTRIBUTE = /\A
284
- (@scope\s+(?<names>.*?))
428
+ \#\s*@scope\s+(?<names>.*?)
285
429
  \Z/x
286
430
 
287
431
  def scope_for(comments, parent = nil, &block)
@@ -298,59 +442,66 @@ module Decode
298
442
  return parent
299
443
  end
300
444
 
301
- def constant_names_for(children)
302
- children.each do |node|
303
- if node.type == :sym
304
- yield node.children[0]
445
+ def constant_names_for(child_nodes)
446
+ child_nodes.each do |node|
447
+ if node.type == :symbol_node
448
+ yield node.unescaped.to_sym
305
449
  end
306
450
  end
307
451
  end
308
452
 
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
453
  def walk_segments(node, comments, &block)
330
454
  case node.type
331
- when :begin
332
- segment = nil
455
+ when :program_node
456
+ walk_segments(node.statements, comments, &block)
457
+ when :statements_node
458
+ statements = node.child_nodes
459
+ current_segment = nil
333
460
 
334
- node.children.each do |child|
335
- if segment.nil?
336
- segment = Segment.new(
337
- extract_comments_for(child, comments),
338
- @language, child
461
+ statements.each_with_index do |stmt, stmt_index|
462
+ # Find comments that precede this statement and are not inside previous statements
463
+ preceding_comments = []
464
+ last_stmt_end_line = stmt_index > 0 ? statements[stmt_index - 1].location.end_line : 0
465
+
466
+ comments.each do |comment|
467
+ comment_line = comment.location.start_line
468
+ # Comment must be after the previous statement and before this statement
469
+ if comment_line > last_stmt_end_line && comment_line < stmt.location.start_line
470
+ preceding_comments << comment
471
+ end
472
+ end
473
+
474
+ # Remove consumed comments
475
+ comments -= preceding_comments
476
+
477
+ if preceding_comments.any?
478
+ # Start a new segment with these comments
479
+ yield current_segment if current_segment
480
+ current_segment = Segment.new(
481
+ preceding_comments.map { |c| c.location.slice.sub(/^#[\s\t]?/, "") },
482
+ @language,
483
+ stmt
339
484
  )
340
- elsif next_comments = extract_comments_for(child, comments)
341
- yield segment if segment
342
- segment = Segment.new(next_comments, @language, child)
485
+ elsif current_segment
486
+ # Extend current segment with this statement
487
+ current_segment.expand(stmt)
343
488
  else
344
- segment.expand(child)
489
+ # Start a new segment without comments
490
+ current_segment = Segment.new(
491
+ [],
492
+ @language,
493
+ stmt
494
+ )
345
495
  end
346
496
  end
347
497
 
348
- yield segment if segment
498
+ yield current_segment if current_segment
349
499
  else
350
500
  # One top level segment:
351
501
  segment = Segment.new(
352
- extract_comments_for(node, comments),
353
- @language, node
502
+ [],
503
+ @language,
504
+ node
354
505
  )
355
506
 
356
507
  yield segment