ruby_tree_sitter 0.20.8.3-x86_64-linux → 1.0.0-x86_64-linux
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/LICENSE +2 -1
- data/README.md +32 -18
- data/ext/tree_sitter/extconf.rb +1 -1
- 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 +1 -1
- 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 +23 -6
- 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 +14 -11
- metadata +36 -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 -411
- 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
|