ruby_tree_sitter 0.20.8.3-x86_64-linux → 1.1.0-x86_64-linux

Sign up to get free protection for your applications and to get access to all the features.
Files changed (43) hide show
  1. checksums.yaml +4 -4
  2. data/LICENSE +2 -1
  3. data/README.md +32 -18
  4. data/ext/tree_sitter/extconf.rb +1 -1
  5. data/ext/tree_sitter/input.c +1 -0
  6. data/ext/tree_sitter/language.c +131 -46
  7. data/ext/tree_sitter/logger.c +28 -12
  8. data/ext/tree_sitter/node.c +438 -130
  9. data/ext/tree_sitter/parser.c +232 -37
  10. data/ext/tree_sitter/query.c +197 -72
  11. data/ext/tree_sitter/query_cursor.c +140 -28
  12. data/ext/tree_sitter/repo.rb +1 -1
  13. data/ext/tree_sitter/tree.c +118 -34
  14. data/ext/tree_sitter/tree_cursor.c +205 -33
  15. data/ext/tree_sitter/tree_sitter.c +12 -0
  16. data/lib/tree_sitter/node.rb +23 -6
  17. data/lib/tree_sitter/tree_sitter.so +0 -0
  18. data/lib/tree_sitter/version.rb +4 -2
  19. data/lib/tree_sitter.rb +1 -0
  20. data/lib/tree_stand/ast_modifier.rb +30 -0
  21. data/lib/tree_stand/breadth_first_visitor.rb +54 -0
  22. data/lib/tree_stand/config.rb +13 -0
  23. data/lib/tree_stand/node.rb +224 -0
  24. data/lib/tree_stand/parser.rb +67 -0
  25. data/lib/tree_stand/range.rb +55 -0
  26. data/lib/tree_stand/tree.rb +123 -0
  27. data/lib/tree_stand/utils/printer.rb +73 -0
  28. data/lib/tree_stand/version.rb +7 -0
  29. data/lib/tree_stand/visitor.rb +127 -0
  30. data/lib/tree_stand/visitors/tree_walker.rb +37 -0
  31. data/lib/tree_stand.rb +48 -0
  32. data/tree_sitter.gemspec +14 -11
  33. metadata +36 -107
  34. data/test/README.md +0 -15
  35. data/test/test_helper.rb +0 -9
  36. data/test/tree_sitter/js_test.rb +0 -48
  37. data/test/tree_sitter/language_test.rb +0 -73
  38. data/test/tree_sitter/logger_test.rb +0 -70
  39. data/test/tree_sitter/node_test.rb +0 -411
  40. data/test/tree_sitter/parser_test.rb +0 -140
  41. data/test/tree_sitter/query_test.rb +0 -153
  42. data/test/tree_sitter/tree_cursor_test.rb +0 -83
  43. 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), 0].max}| #{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,7 @@
1
+ # frozen_string_literal: true
2
+ # typed: strong
3
+
4
+ module TreeStand
5
+ # The current version of the gem.
6
+ VERSION = '0.2.0'
7
+ 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