ruby_tree_sitter 0.20.8.2-x86_64-linux → 1.0.0-x86_64-linux
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/LICENSE +2 -1
- data/README.md +32 -18
- data/ext/tree_sitter/extconf.rb +1 -43
- data/ext/tree_sitter/input.c +1 -0
- data/ext/tree_sitter/language.c +131 -46
- data/ext/tree_sitter/logger.c +28 -12
- data/ext/tree_sitter/node.c +438 -130
- data/ext/tree_sitter/parser.c +232 -37
- data/ext/tree_sitter/query.c +197 -72
- data/ext/tree_sitter/query_cursor.c +140 -28
- data/ext/tree_sitter/repo.rb +121 -0
- data/ext/tree_sitter/tree.c +118 -34
- data/ext/tree_sitter/tree_cursor.c +205 -33
- data/ext/tree_sitter/tree_sitter.c +12 -0
- data/lib/tree_sitter/node.rb +43 -9
- data/lib/tree_sitter/tree_sitter.so +0 -0
- data/lib/tree_sitter/version.rb +4 -2
- data/lib/tree_sitter.rb +1 -0
- data/lib/tree_stand/ast_modifier.rb +30 -0
- data/lib/tree_stand/breadth_first_visitor.rb +54 -0
- data/lib/tree_stand/config.rb +13 -0
- data/lib/tree_stand/node.rb +224 -0
- data/lib/tree_stand/parser.rb +67 -0
- data/lib/tree_stand/range.rb +55 -0
- data/lib/tree_stand/tree.rb +123 -0
- data/lib/tree_stand/utils/printer.rb +73 -0
- data/lib/tree_stand/version.rb +7 -0
- data/lib/tree_stand/visitor.rb +127 -0
- data/lib/tree_stand/visitors/tree_walker.rb +37 -0
- data/lib/tree_stand.rb +48 -0
- data/tree_sitter.gemspec +15 -12
- metadata +37 -107
- data/test/README.md +0 -15
- data/test/test_helper.rb +0 -9
- data/test/tree_sitter/js_test.rb +0 -48
- data/test/tree_sitter/language_test.rb +0 -73
- data/test/tree_sitter/logger_test.rb +0 -70
- data/test/tree_sitter/node_test.rb +0 -355
- data/test/tree_sitter/parser_test.rb +0 -140
- data/test/tree_sitter/query_test.rb +0 -153
- data/test/tree_sitter/tree_cursor_test.rb +0 -83
- data/test/tree_sitter/tree_test.rb +0 -51
@@ -0,0 +1,224 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
# typed: true
|
3
|
+
|
4
|
+
module TreeStand
|
5
|
+
# Wrapper around a TreeSitter node and provides convient
|
6
|
+
# methods that are missing on the original node. This class
|
7
|
+
# overrides the `method_missing` method to delegate to a nodes
|
8
|
+
# named children.
|
9
|
+
class Node
|
10
|
+
extend T::Sig
|
11
|
+
extend Forwardable
|
12
|
+
include Enumerable
|
13
|
+
|
14
|
+
# @!method type
|
15
|
+
# @return [Symbol] the type of the node in the tree-sitter grammar.
|
16
|
+
# @!method error?
|
17
|
+
# @return [bool] true if the node is an error node.
|
18
|
+
def_delegators(
|
19
|
+
:@ts_node,
|
20
|
+
:type,
|
21
|
+
:error?,
|
22
|
+
)
|
23
|
+
|
24
|
+
sig { returns(TreeStand::Tree) }
|
25
|
+
attr_reader :tree
|
26
|
+
|
27
|
+
sig { returns(TreeSitter::Node) }
|
28
|
+
attr_reader :ts_node
|
29
|
+
|
30
|
+
# @api private
|
31
|
+
sig { params(tree: TreeStand::Tree, ts_node: TreeSitter::Node).void }
|
32
|
+
def initialize(tree, ts_node)
|
33
|
+
@tree = tree
|
34
|
+
@ts_node = ts_node
|
35
|
+
@fields = @ts_node.each_field.to_a.map(&:first)
|
36
|
+
end
|
37
|
+
|
38
|
+
# TreeSitter uses a `TreeSitter::Cursor` to iterate over matches by calling
|
39
|
+
# `curser#next_match` repeatedly until it returns `nil`.
|
40
|
+
#
|
41
|
+
# This method does all of that for you and collects all of the matches into
|
42
|
+
# an array and each corresponding capture into a hash.
|
43
|
+
#
|
44
|
+
# @example
|
45
|
+
# # This will return a match for each identifier nodes in the tree.
|
46
|
+
# tree_matches = tree.query(<<~QUERY)
|
47
|
+
# (identifier) @identifier
|
48
|
+
# QUERY
|
49
|
+
#
|
50
|
+
# # It is equivalent to:
|
51
|
+
# tree.root_node.query(<<~QUERY)
|
52
|
+
# (identifier) @identifier
|
53
|
+
# QUERY
|
54
|
+
sig { params(query_string: String).returns(T::Array[T::Hash[String, TreeStand::Node]]) }
|
55
|
+
def query(query_string)
|
56
|
+
ts_query = TreeSitter::Query.new(@tree.parser.ts_language, query_string)
|
57
|
+
ts_cursor = TreeSitter::QueryCursor.exec(ts_query, ts_node)
|
58
|
+
matches = []
|
59
|
+
while ts_match = ts_cursor.next_match
|
60
|
+
captures = {}
|
61
|
+
|
62
|
+
ts_match.captures.each do |ts_capture|
|
63
|
+
capture_name = ts_query.capture_name_for_id(ts_capture.index)
|
64
|
+
captures[capture_name] = TreeStand::Node.new(@tree, ts_capture.node)
|
65
|
+
end
|
66
|
+
|
67
|
+
matches << captures
|
68
|
+
end
|
69
|
+
matches
|
70
|
+
end
|
71
|
+
|
72
|
+
# Returns the first captured node that matches the query string or nil if
|
73
|
+
# there was no captured node.
|
74
|
+
#
|
75
|
+
# @example Find the first identifier node.
|
76
|
+
# identifier_node = tree.root_node.find_node("(identifier) @identifier")
|
77
|
+
#
|
78
|
+
# @see #find_node!
|
79
|
+
# @see #query
|
80
|
+
sig { params(query_string: String).returns(T.nilable(TreeStand::Node)) }
|
81
|
+
def find_node(query_string)
|
82
|
+
query(query_string).first&.values&.first
|
83
|
+
end
|
84
|
+
|
85
|
+
# Like {#find_node}, except that if no node is found, raises an
|
86
|
+
# {TreeStand::NodeNotFound} error.
|
87
|
+
#
|
88
|
+
# @see #find_node
|
89
|
+
# @raise [TreeStand::NodeNotFound]
|
90
|
+
sig { params(query_string: String).returns(TreeStand::Node) }
|
91
|
+
def find_node!(query_string)
|
92
|
+
find_node(query_string) || raise(TreeStand::NodeNotFound)
|
93
|
+
end
|
94
|
+
|
95
|
+
sig { returns(TreeStand::Range) }
|
96
|
+
def range
|
97
|
+
TreeStand::Range.new(
|
98
|
+
start_byte: @ts_node.start_byte,
|
99
|
+
end_byte: @ts_node.end_byte,
|
100
|
+
start_point: @ts_node.start_point,
|
101
|
+
end_point: @ts_node.end_point,
|
102
|
+
)
|
103
|
+
end
|
104
|
+
|
105
|
+
# Node includes enumerable so that you can iterate over the child nodes.
|
106
|
+
# @example
|
107
|
+
# node.text # => "3 * 4"
|
108
|
+
#
|
109
|
+
# @example Iterate over the child nodes
|
110
|
+
# node.each do |child|
|
111
|
+
# print child.text
|
112
|
+
# end
|
113
|
+
# # prints: 3*4
|
114
|
+
#
|
115
|
+
# @example Enumerable methods
|
116
|
+
# node.map(&:text) # => ["3", "*", "4"]
|
117
|
+
#
|
118
|
+
# @yieldparam child [TreeStand::Node]
|
119
|
+
sig do
|
120
|
+
override
|
121
|
+
.params(block: T.nilable(T.proc.params(node: TreeStand::Node).returns(BasicObject)))
|
122
|
+
.returns(T::Enumerator[TreeStand::Node])
|
123
|
+
end
|
124
|
+
def each(&block)
|
125
|
+
enumerator = Enumerator.new do |yielder|
|
126
|
+
@ts_node.each do |child|
|
127
|
+
yielder << TreeStand::Node.new(@tree, child)
|
128
|
+
end
|
129
|
+
end
|
130
|
+
enumerator.each(&block) if block_given?
|
131
|
+
enumerator
|
132
|
+
end
|
133
|
+
|
134
|
+
# (see TreeStand::Visitors::TreeWalker)
|
135
|
+
# Backed by {TreeStand::Visitors::TreeWalker}.
|
136
|
+
#
|
137
|
+
# @example Check the subtree for error nodes
|
138
|
+
# node.walk.any? { |node| node.type == :error }
|
139
|
+
#
|
140
|
+
# @see TreeStand::Visitors::TreeWalker
|
141
|
+
#
|
142
|
+
# @yieldparam node [TreeStand::Node]
|
143
|
+
sig do
|
144
|
+
params(block: T.nilable(T.proc.params(node: TreeStand::Node).returns(BasicObject)))
|
145
|
+
.returns(T::Enumerator[TreeStand::Node])
|
146
|
+
end
|
147
|
+
def walk(&block)
|
148
|
+
enumerator = Enumerator.new do |yielder|
|
149
|
+
Visitors::TreeWalker.new(self) do |child|
|
150
|
+
yielder << child
|
151
|
+
end.visit
|
152
|
+
end
|
153
|
+
enumerator.each(&block) if block_given?
|
154
|
+
enumerator
|
155
|
+
end
|
156
|
+
|
157
|
+
# @example
|
158
|
+
# node.text # => "4"
|
159
|
+
# node.parent.text # => "3 * 4"
|
160
|
+
# node.parent.parent.text # => "1 + 3 * 4"
|
161
|
+
sig { returns(TreeStand::Node) }
|
162
|
+
def parent
|
163
|
+
TreeStand::Node.new(@tree, @ts_node.parent)
|
164
|
+
end
|
165
|
+
|
166
|
+
# @example
|
167
|
+
# node.text # => "3 * 4"
|
168
|
+
# node.to_a.map(&:text) # => ["3", "*", "4"]
|
169
|
+
# node.children.map(&:text) # => ["3", "*", "4"]
|
170
|
+
sig { returns(T::Array[TreeStand::Node]) }
|
171
|
+
def children = to_a
|
172
|
+
|
173
|
+
# A convenience method for getting the text of the node. Each {TreeStand::Node}
|
174
|
+
# wraps the parent {TreeStand::Tree #tree} and has access to the source document.
|
175
|
+
sig { returns(String) }
|
176
|
+
def text
|
177
|
+
T.must(@tree.document[@ts_node.start_byte...@ts_node.end_byte])
|
178
|
+
end
|
179
|
+
|
180
|
+
# This class overrides the `method_missing` method to delegate to the
|
181
|
+
# node's named children.
|
182
|
+
# @example
|
183
|
+
# node.text # => "3 * 4"
|
184
|
+
#
|
185
|
+
# node.left.text # => "3"
|
186
|
+
# node.operator.text # => "*"
|
187
|
+
# node.right.text # => "4"
|
188
|
+
# node.operand # => NoMethodError
|
189
|
+
# @overload method_missing(field_name)
|
190
|
+
# @param name [Symbol, String]
|
191
|
+
# @return [TreeStand::Node] child node for the given field name
|
192
|
+
# @raise [NoMethodError] Raised if the node does not have child with name `field_name`
|
193
|
+
#
|
194
|
+
# @overload method_missing(method_name, *args, &block)
|
195
|
+
# @raise [NoMethodError]
|
196
|
+
def method_missing(method, *args, &block)
|
197
|
+
return super unless @fields.include?(method.to_s)
|
198
|
+
|
199
|
+
TreeStand::Node.new(@tree, T.unsafe(@ts_node).public_send(method, *args, &block))
|
200
|
+
end
|
201
|
+
|
202
|
+
sig { params(other: Object).returns(T::Boolean) }
|
203
|
+
def ==(other)
|
204
|
+
return false unless other.is_a?(TreeStand::Node)
|
205
|
+
|
206
|
+
T.must(range == other.range && type == other.type && text == other.text)
|
207
|
+
end
|
208
|
+
|
209
|
+
# (see TreeStand::Utils::Printer)
|
210
|
+
# Backed by {TreeStand::Utils::Printer}.
|
211
|
+
#
|
212
|
+
# @see TreeStand::Utils::Printer
|
213
|
+
sig { params(pp: PP).void }
|
214
|
+
def pretty_print(pp)
|
215
|
+
Utils::Printer.new(ralign: 80).print(self, io: pp.output)
|
216
|
+
end
|
217
|
+
|
218
|
+
private
|
219
|
+
|
220
|
+
def respond_to_missing?(method, *)
|
221
|
+
@fields.include?(method.to_s) || super
|
222
|
+
end
|
223
|
+
end
|
224
|
+
end
|
@@ -0,0 +1,67 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
# typed: true
|
3
|
+
|
4
|
+
module TreeStand
|
5
|
+
# Wrapper around the TreeSitter parser. It looks up the parser by filename in
|
6
|
+
# the configured parsers directory.
|
7
|
+
# @example
|
8
|
+
# TreeStand.configure do
|
9
|
+
# config.parser_path = "path/to/parser/folder/"
|
10
|
+
# end
|
11
|
+
#
|
12
|
+
# # Looks for a parser in `path/to/parser/folder/sql.{so,dylib}`
|
13
|
+
# sql_parser = TreeStand::Parser.new("sql")
|
14
|
+
#
|
15
|
+
# # Looks for a parser in `path/to/parser/folder/ruby.{so,dylib}`
|
16
|
+
# ruby_parser = TreeStand::Parser.new("ruby")
|
17
|
+
class Parser
|
18
|
+
extend T::Sig
|
19
|
+
|
20
|
+
sig { returns(TreeSitter::Language) }
|
21
|
+
attr_reader :ts_language
|
22
|
+
|
23
|
+
sig { returns(TreeSitter::Parser) }
|
24
|
+
attr_reader :ts_parser
|
25
|
+
|
26
|
+
# @param language [String]
|
27
|
+
sig { params(language: String).void }
|
28
|
+
def initialize(language)
|
29
|
+
@language_string = language
|
30
|
+
@ts_language = TreeSitter::Language.load(
|
31
|
+
language,
|
32
|
+
"#{TreeStand.config.parser_path}/#{language}.so",
|
33
|
+
)
|
34
|
+
@ts_parser = TreeSitter::Parser.new.tap do |parser|
|
35
|
+
parser.language = @ts_language
|
36
|
+
end
|
37
|
+
end
|
38
|
+
|
39
|
+
# Parse the provided document with the TreeSitter parser.
|
40
|
+
# @param tree [TreeStand::Tree, nil] providing the old tree will allow the
|
41
|
+
# parser to take advantage of incremental parsing and improve performance
|
42
|
+
# by re-useing nodes from the old tree.
|
43
|
+
sig { params(document: String, tree: T.nilable(TreeStand::Tree)).returns(TreeStand::Tree) }
|
44
|
+
def parse_string(document, tree: nil)
|
45
|
+
# @todo There's a bug with passing a non-nil tree
|
46
|
+
ts_tree = @ts_parser.parse_string(nil, document)
|
47
|
+
TreeStand::Tree.new(self, ts_tree, document)
|
48
|
+
end
|
49
|
+
|
50
|
+
# (see #parse_string)
|
51
|
+
# @note Like {#parse_string}, except that if the tree contains any parse
|
52
|
+
# errors, raises an {TreeStand::InvalidDocument} error.
|
53
|
+
#
|
54
|
+
# @see #parse_string
|
55
|
+
# @raise [TreeStand::InvalidDocument]
|
56
|
+
sig { params(document: String, tree: T.nilable(TreeStand::Tree)).returns(TreeStand::Tree) }
|
57
|
+
def parse_string!(document, tree: nil)
|
58
|
+
tree = parse_string(document, tree: tree)
|
59
|
+
return tree unless tree.any?(&:error?)
|
60
|
+
|
61
|
+
raise(InvalidDocument, <<~ERROR)
|
62
|
+
Encountered errors in the document. Check the tree for more details.
|
63
|
+
#{tree}
|
64
|
+
ERROR
|
65
|
+
end
|
66
|
+
end
|
67
|
+
end
|
@@ -0,0 +1,55 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
# typed: true
|
3
|
+
|
4
|
+
module TreeStand
|
5
|
+
# Wrapper around a TreeSitter range. This is mainly used to compare ranges.
|
6
|
+
class Range
|
7
|
+
extend T::Sig
|
8
|
+
|
9
|
+
# Point is a Struct containing the row and column from a TreeSitter point.
|
10
|
+
# TreeStand uses this to compare points.
|
11
|
+
# @!attribute [rw] row
|
12
|
+
# @return [Integer]
|
13
|
+
# @!attribute [rw] column
|
14
|
+
# @return [Integer]
|
15
|
+
Point = Struct.new(:row, :column)
|
16
|
+
|
17
|
+
sig { returns(Integer) }
|
18
|
+
attr_reader :start_byte
|
19
|
+
|
20
|
+
sig { returns(Integer) }
|
21
|
+
attr_reader :end_byte
|
22
|
+
|
23
|
+
sig { returns(TreeStand::Range::Point) }
|
24
|
+
attr_reader :start_point
|
25
|
+
|
26
|
+
sig { returns(TreeStand::Range::Point) }
|
27
|
+
attr_reader :end_point
|
28
|
+
|
29
|
+
# @api private
|
30
|
+
sig do
|
31
|
+
params(
|
32
|
+
start_byte: Integer,
|
33
|
+
end_byte: Integer,
|
34
|
+
start_point: T.any(TreeStand::Range::Point, TreeSitter::Point),
|
35
|
+
end_point: T.any(TreeStand::Range::Point, TreeSitter::Point),
|
36
|
+
).void
|
37
|
+
end
|
38
|
+
def initialize(start_byte:, end_byte:, start_point:, end_point:)
|
39
|
+
@start_byte = start_byte
|
40
|
+
@end_byte = end_byte
|
41
|
+
@start_point = Point.new(start_point.row, start_point.column)
|
42
|
+
@end_point = Point.new(end_point.row, end_point.column)
|
43
|
+
end
|
44
|
+
|
45
|
+
sig { params(other: Object).returns(T::Boolean) }
|
46
|
+
def ==(other)
|
47
|
+
return false unless other.is_a?(TreeStand::Range)
|
48
|
+
|
49
|
+
@start_byte == other.start_byte &&
|
50
|
+
@end_byte == other.end_byte &&
|
51
|
+
@start_point == other.start_point &&
|
52
|
+
@end_point == other.end_point
|
53
|
+
end
|
54
|
+
end
|
55
|
+
end
|
@@ -0,0 +1,123 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
# typed: true
|
3
|
+
|
4
|
+
module TreeStand
|
5
|
+
# Wrapper around a TreeSitter tree.
|
6
|
+
#
|
7
|
+
# This class exposes a convient API for working with the tree. There are
|
8
|
+
# dangers in using this class. The tree is mutable and the document can be
|
9
|
+
# changed. This class does not protect against that.
|
10
|
+
#
|
11
|
+
# Some of the moetods on this class edit and re-parse the document updating
|
12
|
+
# the tree. Because the document is re-parsed, the tree will be different. Which
|
13
|
+
# means all outstanding nodes & ranges will be invalid.
|
14
|
+
#
|
15
|
+
# Methods that edit the document are suffixed with `!`, e.g. `#edit!`.
|
16
|
+
#
|
17
|
+
# It's often the case that you will want perfrom multiple edits. One such
|
18
|
+
# pattern is to call #query & #edit on all matches in a loop. It's important
|
19
|
+
# to keep the destructive nature of #edit in mind and re-issue the query
|
20
|
+
# after each edit.
|
21
|
+
#
|
22
|
+
# Another thing to keep in mind is that edits done later in the document will
|
23
|
+
# likely not affect the ranges that occur earlier in the document. This can
|
24
|
+
# be a convient property that could allow you to apply edits in a reverse order.
|
25
|
+
# This is not always possible and depends on the edits you make, beware that
|
26
|
+
# the tree will be different after each edit and this approach may cause bugs.
|
27
|
+
class Tree
|
28
|
+
extend T::Sig
|
29
|
+
extend Forwardable
|
30
|
+
include Enumerable
|
31
|
+
|
32
|
+
sig { returns(String) }
|
33
|
+
attr_reader :document
|
34
|
+
|
35
|
+
sig { returns(TreeSitter::Tree) }
|
36
|
+
attr_reader :ts_tree
|
37
|
+
|
38
|
+
sig { returns(TreeStand::Parser) }
|
39
|
+
attr_reader :parser
|
40
|
+
|
41
|
+
# @!method query(query_string)
|
42
|
+
# (see TreeStand::Node#query)
|
43
|
+
# @note This is a convenience method that calls {TreeStand::Node#query} on
|
44
|
+
# {#root_node}.
|
45
|
+
#
|
46
|
+
# @!method find_node(query_string)
|
47
|
+
# (see TreeStand::Node#find_node)
|
48
|
+
# @note This is a convenience method that calls {TreeStand::Node#find_node} on
|
49
|
+
# {#root_node}.
|
50
|
+
#
|
51
|
+
# @!method find_node!(query_string)
|
52
|
+
# (see TreeStand::Node#find_node!)
|
53
|
+
# @note This is a convenience method that calls {TreeStand::Node#find_node!} on
|
54
|
+
# {#root_node}.
|
55
|
+
#
|
56
|
+
# @!method walk(&block)
|
57
|
+
# (see TreeStand::Node#walk)
|
58
|
+
#
|
59
|
+
# @note This is a convenience method that calls {TreeStand::Node#walk} on
|
60
|
+
# {#root_node}.
|
61
|
+
#
|
62
|
+
# @example Tree includes Enumerable
|
63
|
+
# tree.any? { |node| node.type == :error }
|
64
|
+
#
|
65
|
+
# @!method text
|
66
|
+
# (see TreeStand::Node#text)
|
67
|
+
# @note This is a convenience method that calls {TreeStand::Node#text} on
|
68
|
+
# {#root_node}.
|
69
|
+
def_delegators(
|
70
|
+
:root_node,
|
71
|
+
:query,
|
72
|
+
:find_node,
|
73
|
+
:find_node!,
|
74
|
+
:walk,
|
75
|
+
:text,
|
76
|
+
)
|
77
|
+
|
78
|
+
alias_method :each, :walk
|
79
|
+
|
80
|
+
# @api private
|
81
|
+
sig { params(parser: TreeStand::Parser, tree: TreeSitter::Tree, document: String).void }
|
82
|
+
def initialize(parser, tree, document)
|
83
|
+
@parser = parser
|
84
|
+
@ts_tree = tree
|
85
|
+
@document = document
|
86
|
+
end
|
87
|
+
|
88
|
+
sig { returns(TreeStand::Node) }
|
89
|
+
def root_node
|
90
|
+
TreeStand::Node.new(self, @ts_tree.root_node)
|
91
|
+
end
|
92
|
+
|
93
|
+
# This method replaces the section of the document specified by range and
|
94
|
+
# replaces it with the provided text. Then it will reparse the document and
|
95
|
+
# update the tree!
|
96
|
+
sig { params(range: TreeStand::Range, replacement: String).void }
|
97
|
+
def edit!(range, replacement)
|
98
|
+
new_document = +''
|
99
|
+
new_document << @document[0...range.start_byte]
|
100
|
+
new_document << replacement
|
101
|
+
new_document << @document[range.end_byte..]
|
102
|
+
replace_with_new_doc(new_document)
|
103
|
+
end
|
104
|
+
|
105
|
+
# This method deletes the section of the document specified by range. Then
|
106
|
+
# it will reparse the document and update the tree!
|
107
|
+
sig { params(range: TreeStand::Range).void }
|
108
|
+
def delete!(range)
|
109
|
+
new_document = +''
|
110
|
+
new_document << @document[0...range.start_byte]
|
111
|
+
new_document << @document[range.end_byte..]
|
112
|
+
replace_with_new_doc(new_document)
|
113
|
+
end
|
114
|
+
|
115
|
+
private
|
116
|
+
|
117
|
+
def replace_with_new_doc(new_document)
|
118
|
+
@document = new_document
|
119
|
+
new_tree = @parser.parse_string(@document, tree: self)
|
120
|
+
@ts_tree = new_tree.ts_tree
|
121
|
+
end
|
122
|
+
end
|
123
|
+
end
|
@@ -0,0 +1,73 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
# typed: true
|
3
|
+
|
4
|
+
module TreeStand
|
5
|
+
# A collection of useful methods for working with syntax trees.
|
6
|
+
module Utils
|
7
|
+
# Used to {TreeStand::Node#pretty_print pretty-print} the node.
|
8
|
+
#
|
9
|
+
# @example
|
10
|
+
# pp node
|
11
|
+
# # (expression
|
12
|
+
# # (sum
|
13
|
+
# # left: (number) | 1
|
14
|
+
# # ("+") | +
|
15
|
+
# # right: (variable))) | x
|
16
|
+
class Printer
|
17
|
+
extend T::Sig
|
18
|
+
|
19
|
+
# @param ralign the right alignment for the text column.
|
20
|
+
sig { params(ralign: Integer).void }
|
21
|
+
def initialize(ralign:)
|
22
|
+
@ralign = ralign
|
23
|
+
end
|
24
|
+
|
25
|
+
# (see TreeStand::Utils::Printer)
|
26
|
+
sig { params(node: TreeStand::Node, io: T.any(IO, StringIO, String)).returns(T.any(IO, StringIO, String)) }
|
27
|
+
def print(node, io: StringIO.new)
|
28
|
+
lines = pretty_output_lines(node)
|
29
|
+
|
30
|
+
lines.each do |line|
|
31
|
+
if line.text.empty?
|
32
|
+
io << line.sexpr << "\n"
|
33
|
+
next
|
34
|
+
end
|
35
|
+
|
36
|
+
io << "#{line.sexpr}#{' ' * (@ralign - line.sexpr.size)}| #{line.text}\n"
|
37
|
+
end
|
38
|
+
|
39
|
+
io
|
40
|
+
end
|
41
|
+
|
42
|
+
private
|
43
|
+
|
44
|
+
Line = Struct.new(:sexpr, :text)
|
45
|
+
private_constant :Line
|
46
|
+
|
47
|
+
def pretty_output_lines(node, prefix: '', depth: 0)
|
48
|
+
indent = ' ' * depth
|
49
|
+
ts_node = node.ts_node
|
50
|
+
if indent.size + prefix.size + ts_node.to_s.size < @ralign || ts_node.child_count.zero?
|
51
|
+
return [Line.new("#{indent}#{prefix}#{ts_node}", node.text)]
|
52
|
+
end
|
53
|
+
|
54
|
+
lines = T.let([Line.new("#{indent}#{prefix}(#{ts_node.type}", '')], T::Array[Line])
|
55
|
+
|
56
|
+
node.each.with_index do |child, index|
|
57
|
+
lines += if field_name = ts_node.field_name_for_child(index)
|
58
|
+
pretty_output_lines(
|
59
|
+
child,
|
60
|
+
prefix: "#{field_name}: ",
|
61
|
+
depth: depth + 1,
|
62
|
+
)
|
63
|
+
else
|
64
|
+
pretty_output_lines(child, depth: depth + 1)
|
65
|
+
end
|
66
|
+
end
|
67
|
+
|
68
|
+
T.must(lines.last).sexpr << ')'
|
69
|
+
lines
|
70
|
+
end
|
71
|
+
end
|
72
|
+
end
|
73
|
+
end
|
@@ -0,0 +1,127 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
# typed: true
|
3
|
+
|
4
|
+
module TreeStand
|
5
|
+
# Depth-first traversal through the tree, calling hooks at each stop.
|
6
|
+
#
|
7
|
+
# Hooks are language dependent and are defined by creating methods on the
|
8
|
+
# visitor with the form `on_*` or `around_*`, where `*` is {Node#type}.
|
9
|
+
#
|
10
|
+
# - Hooks prefixed with `on_*` are called *before* visiting a node.
|
11
|
+
# - Hooks prefixed with `around_*` must `yield` to continue visiting child
|
12
|
+
# nodes.
|
13
|
+
#
|
14
|
+
# You can also define default hooks by implementing an {on} or {around}
|
15
|
+
# method to call when visiting each node.
|
16
|
+
#
|
17
|
+
# @example Create a visitor counting certain nodes
|
18
|
+
# class CountingVisitor < TreeStand::Visitor
|
19
|
+
# attr_reader :count
|
20
|
+
#
|
21
|
+
# def initialize(root, type:)
|
22
|
+
# super(root)
|
23
|
+
# @type = type
|
24
|
+
# @count = 0
|
25
|
+
# end
|
26
|
+
#
|
27
|
+
# def on_predicate(node)
|
28
|
+
# # if this node matches our search, increment the counter
|
29
|
+
# @count += 1 if node.type == @type
|
30
|
+
# end
|
31
|
+
# end
|
32
|
+
#
|
33
|
+
# # Initialize a visitor
|
34
|
+
# visitor = CountingVisitor.new(document, :predicate).visit
|
35
|
+
# # Check the result
|
36
|
+
# visitor.count
|
37
|
+
# # => 3
|
38
|
+
#
|
39
|
+
# @example A visitor using around hooks to contruct a tree
|
40
|
+
# class TreeBuilder < TreeStand::Visitor
|
41
|
+
# TreeNode = Struct.new(:name, :children)
|
42
|
+
#
|
43
|
+
# attr_reader :stack
|
44
|
+
#
|
45
|
+
# def initialize(root)
|
46
|
+
# super(root)
|
47
|
+
# @stack = []
|
48
|
+
# end
|
49
|
+
#
|
50
|
+
# def around(node)
|
51
|
+
# @stack << TreeNode.new(node.type, [])
|
52
|
+
#
|
53
|
+
# # visit all children of this node
|
54
|
+
# yield
|
55
|
+
#
|
56
|
+
# # The last node on the stack is the root of the tree.
|
57
|
+
# return if @stack.size == 1
|
58
|
+
#
|
59
|
+
# # Pop the last node off the stack and add it to the parent
|
60
|
+
# @stack[-2].children << @stack.pop
|
61
|
+
# end
|
62
|
+
# end
|
63
|
+
class Visitor
|
64
|
+
extend T::Sig
|
65
|
+
|
66
|
+
sig { params(node: TreeStand::Node).void }
|
67
|
+
def initialize(node)
|
68
|
+
@node = node
|
69
|
+
end
|
70
|
+
|
71
|
+
# Run the visitor on the document and return self. Allows chaining create and visit.
|
72
|
+
# @example
|
73
|
+
# visitor = CountingVisitor.new(node, :predicate).visit
|
74
|
+
sig { returns(T.self_type) }
|
75
|
+
def visit
|
76
|
+
visit_node(@node)
|
77
|
+
self
|
78
|
+
end
|
79
|
+
|
80
|
+
# @abstract The default implementation does nothing.
|
81
|
+
#
|
82
|
+
# @example Create callback to count all nodes in a tree.
|
83
|
+
# def on(node)
|
84
|
+
# @count += 1
|
85
|
+
# end
|
86
|
+
sig { overridable.params(node: TreeStand::Node).void }
|
87
|
+
def on(node) = nil
|
88
|
+
|
89
|
+
# @abstract The default implementation yields to visit all children.
|
90
|
+
#
|
91
|
+
# @example Use around hooks to run logic before & after visiting a node. Pairs will with a stack.
|
92
|
+
# def around(node)
|
93
|
+
# @stack << TreeNode.new(node.type, [])
|
94
|
+
#
|
95
|
+
# # visit all children of this node
|
96
|
+
# yield
|
97
|
+
#
|
98
|
+
# # The last node on the stack is the root of the tree.
|
99
|
+
# return if @stack.size == 1
|
100
|
+
#
|
101
|
+
# # Pop the last node off the stack and add it to the parent
|
102
|
+
# @stack[-2].children << @stack.pop
|
103
|
+
# end
|
104
|
+
sig { overridable.params(node: TreeStand::Node, block: T.proc.void).void }
|
105
|
+
def around(node, &block) = yield
|
106
|
+
|
107
|
+
private
|
108
|
+
|
109
|
+
def visit_node(node)
|
110
|
+
if respond_to?("on_#{node.type}")
|
111
|
+
public_send("on_#{node.type}", node)
|
112
|
+
else
|
113
|
+
on(node)
|
114
|
+
end
|
115
|
+
|
116
|
+
if respond_to?("around_#{node.type}")
|
117
|
+
public_send("around_#{node.type}", node) do
|
118
|
+
node.each { |child| visit_node(child) }
|
119
|
+
end
|
120
|
+
else
|
121
|
+
around(node) do
|
122
|
+
node.each { |child| visit_node(child) }
|
123
|
+
end
|
124
|
+
end
|
125
|
+
end
|
126
|
+
end
|
127
|
+
end
|