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.
- checksums.yaml +4 -4
- checksums.yaml.gz.sig +0 -0
- data/bake/decode/index.rb +16 -9
- data/context/coverage.md +325 -0
- data/context/getting-started.md +242 -0
- data/context/ruby-documentation.md +363 -0
- data/lib/decode/comment/attribute.rb +9 -3
- data/lib/decode/comment/node.rb +4 -2
- data/lib/decode/comment/option.rb +1 -1
- data/lib/decode/comment/parameter.rb +12 -6
- data/lib/decode/comment/pragma.rb +12 -1
- data/lib/decode/comment/raises.rb +1 -1
- data/lib/decode/comment/returns.rb +3 -4
- data/lib/decode/comment/tag.rb +13 -3
- data/lib/decode/comment/tags.rb +17 -2
- data/lib/decode/comment/text.rb +4 -1
- data/lib/decode/comment/throws.rb +1 -1
- data/lib/decode/comment/yields.rb +7 -1
- data/lib/decode/definition.rb +54 -42
- data/lib/decode/documentation.rb +12 -14
- data/lib/decode/index.rb +29 -14
- data/lib/decode/language/generic.rb +30 -14
- data/lib/decode/language/reference.rb +13 -4
- data/lib/decode/language/ruby/alias.rb +41 -0
- data/lib/decode/language/ruby/attribute.rb +7 -6
- data/lib/decode/language/ruby/block.rb +4 -1
- data/lib/decode/language/ruby/call.rb +16 -6
- data/lib/decode/language/ruby/class.rb +19 -36
- data/lib/decode/language/ruby/code.rb +27 -15
- data/lib/decode/language/ruby/constant.rb +9 -8
- data/lib/decode/language/ruby/definition.rb +31 -20
- data/lib/decode/language/ruby/function.rb +2 -1
- data/lib/decode/language/ruby/generic.rb +17 -7
- data/lib/decode/language/ruby/method.rb +47 -12
- data/lib/decode/language/ruby/module.rb +4 -11
- data/lib/decode/language/ruby/parser.rb +365 -205
- data/lib/decode/language/ruby/reference.rb +26 -17
- data/lib/decode/language/ruby/segment.rb +11 -4
- data/lib/decode/language/ruby.rb +4 -2
- data/lib/decode/language.rb +2 -2
- data/lib/decode/languages.rb +25 -6
- data/lib/decode/location.rb +2 -0
- data/lib/decode/scope.rb +1 -1
- data/lib/decode/segment.rb +6 -5
- data/lib/decode/source.rb +12 -4
- data/lib/decode/syntax/link.rb +9 -1
- data/lib/decode/syntax/match.rb +12 -0
- data/lib/decode/syntax/rewriter.rb +10 -0
- data/lib/decode/trie.rb +27 -22
- data/lib/decode/version.rb +1 -1
- data.tar.gz.sig +0 -0
- metadata +9 -10
- 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
|
6
|
+
require "prism"
|
7
7
|
|
8
|
-
require_relative
|
8
|
+
require_relative "../../scope"
|
9
9
|
|
10
|
-
require_relative
|
11
|
-
require_relative
|
12
|
-
require_relative
|
13
|
-
require_relative
|
14
|
-
require_relative
|
15
|
-
require_relative
|
16
|
-
require_relative
|
17
|
-
require_relative
|
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
|
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
|
-
|
38
|
+
return enum_for(:definitions_for, source) unless block_given?
|
54
39
|
|
55
|
-
|
56
|
-
|
57
|
-
|
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
|
-
|
61
|
-
|
62
|
-
|
63
|
-
|
64
|
-
|
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
|
-
|
76
|
-
|
77
|
-
|
78
|
-
|
79
|
-
|
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
|
-
|
83
|
-
|
84
|
-
|
85
|
-
|
86
|
-
|
87
|
-
|
88
|
-
|
89
|
-
|
90
|
-
|
91
|
-
|
92
|
-
|
93
|
-
|
94
|
-
|
95
|
-
|
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
|
-
|
77
|
+
node: node,
|
78
|
+
language: @language,
|
79
|
+
source: source,
|
102
80
|
)
|
103
81
|
|
104
|
-
|
105
|
-
|
82
|
+
store_definition(parent, path.last.to_sym, definition)
|
106
83
|
yield definition
|
107
84
|
|
108
|
-
if
|
85
|
+
if body = node.body
|
109
86
|
with_visibility do
|
110
|
-
walk_definitions(
|
87
|
+
walk_definitions(body, definition, source, &block)
|
111
88
|
end
|
112
89
|
end
|
113
|
-
when :
|
114
|
-
|
115
|
-
|
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
|
-
|
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
|
107
|
+
if body = node.body
|
125
108
|
with_visibility do
|
126
|
-
walk_definitions(
|
109
|
+
walk_definitions(body, definition, source, &block)
|
127
110
|
end
|
128
111
|
end
|
129
|
-
when :
|
130
|
-
if name = singleton_name_for(node
|
131
|
-
definition = Singleton.new(
|
132
|
-
node,
|
133
|
-
|
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
|
140
|
-
walk_definitions(
|
121
|
+
if body = node.body
|
122
|
+
walk_definitions(body, definition, source, &block)
|
141
123
|
end
|
142
124
|
end
|
143
|
-
when :
|
144
|
-
|
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 =
|
155
|
-
|
156
|
-
comments:
|
157
|
-
parent:
|
158
|
-
|
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 :
|
163
|
-
definition = Constant.new(
|
164
|
-
|
165
|
-
|
166
|
-
|
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 :
|
171
|
-
name = node.
|
149
|
+
when :call_node
|
150
|
+
name = node.name
|
172
151
|
|
173
152
|
case name
|
174
153
|
when :public, :protected, :private
|
175
|
-
|
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
|
-
|
178
|
-
|
179
|
-
definition
|
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
|
-
|
185
|
-
|
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
|
-
|
192
|
-
|
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
|
-
|
195
|
-
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 :
|
203
|
-
|
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
|
-
|
206
|
-
|
207
|
-
|
208
|
-
|
209
|
-
|
210
|
-
|
211
|
-
|
212
|
-
|
213
|
-
|
214
|
-
|
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.
|
225
|
-
|
226
|
-
|
227
|
-
|
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
|
235
|
-
|
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 :
|
243
|
-
|
244
|
-
when :
|
245
|
-
|
246
|
-
|
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
|
-
|
253
|
-
|
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.
|
395
|
+
node.slice
|
256
396
|
end
|
257
397
|
end
|
258
398
|
|
259
|
-
def
|
399
|
+
def receiver_for(node)
|
400
|
+
return nil unless node
|
401
|
+
|
260
402
|
case node.type
|
261
|
-
when :
|
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
|
-
|
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(
|
302
|
-
|
303
|
-
if node.type == :
|
304
|
-
yield node.
|
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 :
|
332
|
-
|
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
|
-
|
335
|
-
|
336
|
-
|
337
|
-
|
338
|
-
|
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
|
341
|
-
|
342
|
-
|
494
|
+
elsif current_segment
|
495
|
+
# Extend current segment with this statement
|
496
|
+
current_segment.expand(stmt)
|
343
497
|
else
|
344
|
-
segment
|
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
|
507
|
+
yield current_segment if current_segment
|
349
508
|
else
|
350
509
|
# One top level segment:
|
351
510
|
segment = Segment.new(
|
352
|
-
|
353
|
-
@language,
|
511
|
+
[],
|
512
|
+
@language,
|
513
|
+
node
|
354
514
|
)
|
355
515
|
|
356
516
|
yield segment
|