ruby_tree_sitter 0.20.8.2-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.
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 -43
  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 +121 -0
  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 +43 -9
  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 +15 -12
  33. metadata +37 -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 -355
  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)}| #{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