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.
- checksums.yaml +4 -4
- data/.github/dependabot.yml +4 -0
- data/.github/workflows/auto-merge.yml +1 -1
- data/.github/workflows/gh-pages.yml +1 -1
- data/.github/workflows/main.yml +5 -2
- data/.gitmodules +9 -0
- data/.rubocop.yml +11 -1
- data/CHANGELOG.md +29 -1
- data/Gemfile.lock +10 -10
- data/README.md +1 -0
- data/Rakefile +7 -0
- data/exe/yarv +63 -0
- data/lib/syntax_tree/cli.rb +3 -2
- data/lib/syntax_tree/formatter.rb +23 -2
- data/lib/syntax_tree/index.rb +374 -0
- data/lib/syntax_tree/node.rb +146 -107
- data/lib/syntax_tree/parser.rb +20 -3
- data/lib/syntax_tree/plugin/disable_ternary.rb +7 -0
- data/lib/syntax_tree/version.rb +1 -1
- data/lib/syntax_tree/yarv/assembler.rb +17 -13
- data/lib/syntax_tree/yarv/bf.rb +13 -16
- data/lib/syntax_tree/yarv/compiler.rb +25 -14
- data/lib/syntax_tree/yarv/decompiler.rb +10 -1
- data/lib/syntax_tree/yarv/disassembler.rb +3 -2
- data/lib/syntax_tree/yarv/instruction_sequence.rb +191 -87
- data/lib/syntax_tree/yarv/instructions.rb +1011 -42
- data/lib/syntax_tree/yarv/legacy.rb +59 -3
- data/lib/syntax_tree/yarv/vm.rb +628 -0
- data/lib/syntax_tree/yarv.rb +0 -269
- data/lib/syntax_tree.rb +16 -0
- metadata +9 -3
@@ -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
|