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.
- 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
|