syntax_tree 5.1.0 → 5.3.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.
@@ -0,0 +1,374 @@
1
+ # frozen_string_literal: true
2
+
3
+ module SyntaxTree
4
+ # This class can be used to build an index of the structure of Ruby files. We
5
+ # define an index as the list of constants and methods defined within a file.
6
+ #
7
+ # This index strives to be as fast as possible to better support tools like
8
+ # IDEs. Because of that, it has different backends depending on what
9
+ # functionality is available.
10
+ module Index
11
+ # This is a location for an index entry.
12
+ class Location
13
+ attr_reader :line, :column
14
+
15
+ def initialize(line, column)
16
+ @line = line
17
+ @column = column
18
+ end
19
+ end
20
+
21
+ # This entry represents a class definition using the class keyword.
22
+ class ClassDefinition
23
+ attr_reader :nesting, :name, :location, :comments
24
+
25
+ def initialize(nesting, name, location, comments)
26
+ @nesting = nesting
27
+ @name = name
28
+ @location = location
29
+ @comments = comments
30
+ end
31
+ end
32
+
33
+ # This entry represents a module definition using the module keyword.
34
+ class ModuleDefinition
35
+ attr_reader :nesting, :name, :location, :comments
36
+
37
+ def initialize(nesting, name, location, comments)
38
+ @nesting = nesting
39
+ @name = name
40
+ @location = location
41
+ @comments = comments
42
+ end
43
+ end
44
+
45
+ # This entry represents a method definition using the def keyword.
46
+ class MethodDefinition
47
+ attr_reader :nesting, :name, :location, :comments
48
+
49
+ def initialize(nesting, name, location, comments)
50
+ @nesting = nesting
51
+ @name = name
52
+ @location = location
53
+ @comments = comments
54
+ end
55
+ end
56
+
57
+ # This entry represents a singleton method definition using the def keyword
58
+ # with a specified target.
59
+ class SingletonMethodDefinition
60
+ attr_reader :nesting, :name, :location, :comments
61
+
62
+ def initialize(nesting, name, location, comments)
63
+ @nesting = nesting
64
+ @name = name
65
+ @location = location
66
+ @comments = comments
67
+ end
68
+ end
69
+
70
+ # When you're using the instruction sequence backend, this class is used to
71
+ # lazily parse comments out of the source code.
72
+ class FileComments
73
+ # We use the ripper library to pull out source comments.
74
+ class Parser < Ripper
75
+ attr_reader :comments
76
+
77
+ def initialize(*)
78
+ super
79
+ @comments = {}
80
+ end
81
+
82
+ def on_comment(value)
83
+ comments[lineno] = value.chomp
84
+ end
85
+ end
86
+
87
+ # This represents the Ruby source in the form of a file. When it needs to
88
+ # be read we'll read the file.
89
+ class FileSource
90
+ attr_reader :filepath
91
+
92
+ def initialize(filepath)
93
+ @filepath = filepath
94
+ end
95
+
96
+ def source
97
+ File.read(filepath)
98
+ end
99
+ end
100
+
101
+ # This represents the Ruby source in the form of a string. When it needs
102
+ # to be read the string is returned.
103
+ class StringSource
104
+ attr_reader :source
105
+
106
+ def initialize(source)
107
+ @source = source
108
+ end
109
+ end
110
+
111
+ attr_reader :source
112
+
113
+ def initialize(source)
114
+ @source = source
115
+ end
116
+
117
+ def comments
118
+ @comments ||= Parser.new(source.source).tap(&:parse).comments
119
+ end
120
+ end
121
+
122
+ # This class handles parsing comments from Ruby source code in the case that
123
+ # we use the instruction sequence backend. Because the instruction sequence
124
+ # backend doesn't provide comments (since they are dropped) we provide this
125
+ # interface to lazily parse them out.
126
+ class EntryComments
127
+ include Enumerable
128
+ attr_reader :file_comments, :location
129
+
130
+ def initialize(file_comments, location)
131
+ @file_comments = file_comments
132
+ @location = location
133
+ end
134
+
135
+ def each(&block)
136
+ line = location.line - 1
137
+ result = []
138
+
139
+ while line >= 0 && (comment = file_comments.comments[line])
140
+ result.unshift(comment)
141
+ line -= 1
142
+ end
143
+
144
+ result.each(&block)
145
+ end
146
+ end
147
+
148
+ # This backend creates the index using RubyVM::InstructionSequence, which is
149
+ # faster than using the Syntax Tree parser, but is not available on all
150
+ # runtimes.
151
+ class ISeqBackend
152
+ VM_DEFINECLASS_TYPE_CLASS = 0x00
153
+ VM_DEFINECLASS_TYPE_SINGLETON_CLASS = 0x01
154
+ VM_DEFINECLASS_TYPE_MODULE = 0x02
155
+ VM_DEFINECLASS_FLAG_SCOPED = 0x08
156
+ VM_DEFINECLASS_FLAG_HAS_SUPERCLASS = 0x10
157
+
158
+ def index(source)
159
+ index_iseq(
160
+ RubyVM::InstructionSequence.compile(source).to_a,
161
+ FileComments.new(FileComments::StringSource.new(source))
162
+ )
163
+ end
164
+
165
+ def index_file(filepath)
166
+ index_iseq(
167
+ RubyVM::InstructionSequence.compile_file(filepath).to_a,
168
+ FileComments.new(FileComments::FileSource.new(filepath))
169
+ )
170
+ end
171
+
172
+ private
173
+
174
+ def location_for(iseq)
175
+ code_location = iseq[4][:code_location]
176
+ Location.new(code_location[0], code_location[1])
177
+ end
178
+
179
+ def index_iseq(iseq, file_comments)
180
+ results = []
181
+ queue = [[iseq, []]]
182
+
183
+ while (current_iseq, current_nesting = queue.shift)
184
+ current_iseq[13].each_with_index do |insn, index|
185
+ next unless insn.is_a?(Array)
186
+
187
+ case insn[0]
188
+ when :defineclass
189
+ _, name, class_iseq, flags = insn
190
+
191
+ if flags == VM_DEFINECLASS_TYPE_SINGLETON_CLASS
192
+ # At the moment, we don't support singletons that aren't
193
+ # defined on self. We could, but it would require more
194
+ # emulation.
195
+ if current_iseq[13][index - 2] != [:putself]
196
+ raise NotImplementedError,
197
+ "singleton class with non-self receiver"
198
+ end
199
+ elsif flags & VM_DEFINECLASS_TYPE_MODULE > 0
200
+ location = location_for(class_iseq)
201
+ results << ModuleDefinition.new(
202
+ current_nesting,
203
+ name,
204
+ location,
205
+ EntryComments.new(file_comments, location)
206
+ )
207
+ else
208
+ location = location_for(class_iseq)
209
+ results << ClassDefinition.new(
210
+ current_nesting,
211
+ name,
212
+ location,
213
+ EntryComments.new(file_comments, location)
214
+ )
215
+ end
216
+
217
+ queue << [class_iseq, current_nesting + [name]]
218
+ when :definemethod
219
+ location = location_for(insn[2])
220
+ results << MethodDefinition.new(
221
+ current_nesting,
222
+ insn[1],
223
+ location,
224
+ EntryComments.new(file_comments, location)
225
+ )
226
+ when :definesmethod
227
+ if current_iseq[13][index - 1] != [:putself]
228
+ raise NotImplementedError,
229
+ "singleton method with non-self receiver"
230
+ end
231
+
232
+ location = location_for(insn[2])
233
+ results << SingletonMethodDefinition.new(
234
+ current_nesting,
235
+ insn[1],
236
+ location,
237
+ EntryComments.new(file_comments, location)
238
+ )
239
+ end
240
+ end
241
+ end
242
+
243
+ results
244
+ end
245
+ end
246
+
247
+ # This backend creates the index using the Syntax Tree parser and a visitor.
248
+ # It is not as fast as using the instruction sequences directly, but is
249
+ # supported on all runtimes.
250
+ class ParserBackend
251
+ class IndexVisitor < Visitor
252
+ attr_reader :results, :nesting, :statements
253
+
254
+ def initialize
255
+ @results = []
256
+ @nesting = []
257
+ @statements = nil
258
+ end
259
+
260
+ def visit_class(node)
261
+ name = visit(node.constant).to_sym
262
+ location =
263
+ Location.new(node.location.start_line, node.location.start_column)
264
+
265
+ results << ClassDefinition.new(
266
+ nesting.dup,
267
+ name,
268
+ location,
269
+ comments_for(node)
270
+ )
271
+
272
+ nesting << name
273
+ super
274
+ nesting.pop
275
+ end
276
+
277
+ def visit_const_ref(node)
278
+ node.constant.value
279
+ end
280
+
281
+ def visit_def(node)
282
+ name = node.name.value.to_sym
283
+ location =
284
+ Location.new(node.location.start_line, node.location.start_column)
285
+
286
+ results << if node.target.nil?
287
+ MethodDefinition.new(
288
+ nesting.dup,
289
+ name,
290
+ location,
291
+ comments_for(node)
292
+ )
293
+ else
294
+ SingletonMethodDefinition.new(
295
+ nesting.dup,
296
+ name,
297
+ location,
298
+ comments_for(node)
299
+ )
300
+ end
301
+ end
302
+
303
+ def visit_module(node)
304
+ name = visit(node.constant).to_sym
305
+ location =
306
+ Location.new(node.location.start_line, node.location.start_column)
307
+
308
+ results << ModuleDefinition.new(
309
+ nesting.dup,
310
+ name,
311
+ location,
312
+ comments_for(node)
313
+ )
314
+
315
+ nesting << name
316
+ super
317
+ nesting.pop
318
+ end
319
+
320
+ def visit_program(node)
321
+ super
322
+ results
323
+ end
324
+
325
+ def visit_statements(node)
326
+ @statements = node
327
+ super
328
+ end
329
+
330
+ private
331
+
332
+ def comments_for(node)
333
+ comments = []
334
+
335
+ body = statements.body
336
+ line = node.location.start_line - 1
337
+ index = body.index(node) - 1
338
+
339
+ while index >= 0 && body[index].is_a?(Comment) &&
340
+ (line - body[index].location.start_line < 2)
341
+ comments.unshift(body[index].value)
342
+ line = body[index].location.start_line
343
+ index -= 1
344
+ end
345
+
346
+ comments
347
+ end
348
+ end
349
+
350
+ def index(source)
351
+ SyntaxTree.parse(source).accept(IndexVisitor.new)
352
+ end
353
+
354
+ def index_file(filepath)
355
+ index(SyntaxTree.read(filepath))
356
+ end
357
+ end
358
+
359
+ # The class defined here is used to perform the indexing, depending on what
360
+ # functionality is available from the runtime.
361
+ INDEX_BACKEND =
362
+ defined?(RubyVM::InstructionSequence) ? ISeqBackend : ParserBackend
363
+
364
+ # This method accepts source code and then indexes it.
365
+ def self.index(source, backend: INDEX_BACKEND.new)
366
+ backend.index(source)
367
+ end
368
+
369
+ # This method accepts a filepath and then indexes it.
370
+ def self.index_file(filepath, backend: INDEX_BACKEND.new)
371
+ backend.index_file(filepath)
372
+ end
373
+ end
374
+ end