Location via proxy:   [ UP ]  
[Report a bug]   [Manage cookies]                
Skip to content

Commit bc9e665

Browse files
committed
Indexing functionality
1 parent d402993 commit bc9e665

File tree

3 files changed

+297
-0
lines changed

3 files changed

+297
-0
lines changed

lib/syntax_tree.rb

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@
2626
require_relative "syntax_tree/parser"
2727
require_relative "syntax_tree/pattern"
2828
require_relative "syntax_tree/search"
29+
require_relative "syntax_tree/index"
2930

3031
require_relative "syntax_tree/yarv"
3132
require_relative "syntax_tree/yarv/bf"
@@ -116,4 +117,18 @@ def self.read(filepath)
116117
def self.search(source, query, &block)
117118
Search.new(Pattern.new(query).compile).scan(parse(source), &block)
118119
end
120+
121+
# Indexes the given source code to return a list of all class, module, and
122+
# method definitions. Used to quickly provide indexing capability for IDEs or
123+
# documentation generation.
124+
def self.index(source)
125+
Index.index(source)
126+
end
127+
128+
# Indexes the given file to return a list of all class, module, and method
129+
# definitions. Used to quickly provide indexing capability for IDEs or
130+
# documentation generation.
131+
def self.index_file(filepath)
132+
Index.index_file(filepath)
133+
end
119134
end

lib/syntax_tree/index.rb

Lines changed: 223 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,223 @@
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
24+
25+
def initialize(nesting, name, location)
26+
@nesting = nesting
27+
@name = name
28+
@location = location
29+
end
30+
end
31+
32+
# This entry represents a module definition using the module keyword.
33+
class ModuleDefinition
34+
attr_reader :nesting, :name, :location
35+
36+
def initialize(nesting, name, location)
37+
@nesting = nesting
38+
@name = name
39+
@location = location
40+
end
41+
end
42+
43+
# This entry represents a method definition using the def keyword.
44+
class MethodDefinition
45+
attr_reader :nesting, :name, :location
46+
47+
def initialize(nesting, name, location)
48+
@nesting = nesting
49+
@name = name
50+
@location = location
51+
end
52+
end
53+
54+
# This entry represents a singleton method definition using the def keyword
55+
# with a specified target.
56+
class SingletonMethodDefinition
57+
attr_reader :nesting, :name, :location
58+
59+
def initialize(nesting, name, location)
60+
@nesting = nesting
61+
@name = name
62+
@location = location
63+
end
64+
end
65+
66+
# This backend creates the index using RubyVM::InstructionSequence, which is
67+
# faster than using the Syntax Tree parser, but is not available on all
68+
# runtimes.
69+
class ISeqBackend
70+
VM_DEFINECLASS_TYPE_CLASS = 0x00
71+
VM_DEFINECLASS_TYPE_SINGLETON_CLASS = 0x01
72+
VM_DEFINECLASS_TYPE_MODULE = 0x02
73+
VM_DEFINECLASS_FLAG_SCOPED = 0x08
74+
VM_DEFINECLASS_FLAG_HAS_SUPERCLASS = 0x10
75+
76+
def index(source)
77+
index_iseq(RubyVM::InstructionSequence.compile(source).to_a)
78+
end
79+
80+
def index_file(filepath)
81+
index_iseq(RubyVM::InstructionSequence.compile_file(filepath).to_a)
82+
end
83+
84+
private
85+
86+
def index_iseq(iseq)
87+
results = []
88+
queue = [[iseq, []]]
89+
90+
while (current_iseq, current_nesting = queue.shift)
91+
current_iseq[13].each_with_index do |insn, index|
92+
next unless insn.is_a?(Array)
93+
94+
case insn[0]
95+
when :defineclass
96+
_, name, class_iseq, flags = insn
97+
98+
if flags == VM_DEFINECLASS_TYPE_SINGLETON_CLASS
99+
# At the moment, we don't support singletons that aren't
100+
# defined on self. We could, but it would require more
101+
# emulation.
102+
if current_iseq[13][index - 2] != [:putself]
103+
raise NotImplementedError,
104+
"singleton class with non-self receiver"
105+
end
106+
elsif flags & VM_DEFINECLASS_TYPE_MODULE > 0
107+
code_location = class_iseq[4][:code_location]
108+
location = Location.new(code_location[0], code_location[1])
109+
results << ModuleDefinition.new(current_nesting, name, location)
110+
else
111+
code_location = class_iseq[4][:code_location]
112+
location = Location.new(code_location[0], code_location[1])
113+
results << ClassDefinition.new(current_nesting, name, location)
114+
end
115+
116+
queue << [class_iseq, current_nesting + [name]]
117+
when :definemethod
118+
_, name, method_iseq = insn
119+
120+
code_location = method_iseq[4][:code_location]
121+
location = Location.new(code_location[0], code_location[1])
122+
results << SingletonMethodDefinition.new(
123+
current_nesting,
124+
name,
125+
location
126+
)
127+
when :definesmethod
128+
_, name, method_iseq = insn
129+
130+
code_location = method_iseq[4][:code_location]
131+
location = Location.new(code_location[0], code_location[1])
132+
results << MethodDefinition.new(current_nesting, name, location)
133+
end
134+
end
135+
end
136+
137+
results
138+
end
139+
end
140+
141+
# This backend creates the index using the Syntax Tree parser and a visitor.
142+
# It is not as fast as using the instruction sequences directly, but is
143+
# supported on all runtimes.
144+
class ParserBackend
145+
class IndexVisitor < Visitor
146+
attr_reader :results, :nesting
147+
148+
def initialize
149+
@results = []
150+
@nesting = []
151+
end
152+
153+
def visit_class(node)
154+
name = visit(node.constant).to_sym
155+
location =
156+
Location.new(node.location.start_line, node.location.start_column)
157+
158+
results << ClassDefinition.new(nesting.dup, name, location)
159+
nesting << name
160+
161+
super
162+
nesting.pop
163+
end
164+
165+
def visit_const_ref(node)
166+
node.constant.value
167+
end
168+
169+
def visit_def(node)
170+
name = node.name.value.to_sym
171+
location =
172+
Location.new(node.location.start_line, node.location.start_column)
173+
174+
results << if node.target.nil?
175+
MethodDefinition.new(nesting.dup, name, location)
176+
else
177+
SingletonMethodDefinition.new(nesting.dup, name, location)
178+
end
179+
end
180+
181+
def visit_module(node)
182+
name = visit(node.constant).to_sym
183+
location =
184+
Location.new(node.location.start_line, node.location.start_column)
185+
186+
results << ModuleDefinition.new(nesting.dup, name, location)
187+
nesting << name
188+
189+
super
190+
nesting.pop
191+
end
192+
193+
def visit_program(node)
194+
super
195+
results
196+
end
197+
end
198+
199+
def index(source)
200+
SyntaxTree.parse(source).accept(IndexVisitor.new)
201+
end
202+
203+
def index_file(filepath)
204+
index(SyntaxTree.read(filepath))
205+
end
206+
end
207+
208+
# The class defined here is used to perform the indexing, depending on what
209+
# functionality is available from the runtime.
210+
INDEX_BACKEND =
211+
defined?(RubyVM::InstructionSequence) ? ISeqBackend : ParserBackend
212+
213+
# This method accepts source code and then indexes it.
214+
def self.index(source)
215+
INDEX_BACKEND.new.index(source)
216+
end
217+
218+
# This method accepts a filepath and then indexes it.
219+
def self.index_file(filepath)
220+
INDEX_BACKEND.new.index_file(filepath)
221+
end
222+
end
223+
end

test/index_test.rb

Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
1+
# frozen_string_literal: true
2+
3+
require_relative "test_helper"
4+
5+
module SyntaxTree
6+
class IndexTest < Minitest::Test
7+
def test_module
8+
index_each("module Foo; end") do |entry|
9+
assert_equal :Foo, entry.name
10+
assert_empty entry.nesting
11+
end
12+
end
13+
14+
def test_module_nested
15+
index_each("module Foo; module Bar; end; end") do |entry|
16+
assert_equal :Bar, entry.name
17+
assert_equal [:Foo], entry.nesting
18+
end
19+
end
20+
21+
def test_class
22+
index_each("class Foo; end") do |entry|
23+
assert_equal :Foo, entry.name
24+
assert_empty entry.nesting
25+
end
26+
end
27+
28+
def test_class_nested
29+
index_each("class Foo; class Bar; end; end") do |entry|
30+
assert_equal :Bar, entry.name
31+
assert_equal [:Foo], entry.nesting
32+
end
33+
end
34+
35+
def test_method
36+
index_each("def foo; end") do |entry|
37+
assert_equal :foo, entry.name
38+
assert_empty entry.nesting
39+
end
40+
end
41+
42+
def test_method_nested
43+
index_each("class Foo; def foo; end; end") do |entry|
44+
assert_equal :foo, entry.name
45+
assert_equal [:Foo], entry.nesting
46+
end
47+
end
48+
49+
private
50+
51+
def index_each(source)
52+
yield SyntaxTree::Index::ParserBackend.new.index(source).last
53+
54+
if defined?(RubyVM::InstructionSequence)
55+
yield SyntaxTree::Index::ISeqBackend.new.index(source).last
56+
end
57+
end
58+
end
59+
end

0 commit comments

Comments
 (0)