syntax_tree 5.1.0 → 5.3.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -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