Class: TreeStand::Node

Inherits:
Object
  • Object
show all
Extended by:
Forwardable, T::Sig
Includes:
Enumerable
Defined in:
lib/tree_stand/node.rb

Overview

Wrapper around a TreeSitter node and provides convient methods that are missing on the original node. This class overrides the ‘method_missing` method to delegate to a nodes named children.

Constant Summary collapse

THINLY_REMAPPED_METHODS =

These are methods defined in TreeStand::Node but map to something in TreeSitter::Node, because we want a more idiomatic API.

{
  '[]': :[],
  fetch: :fetch,
  field: :child_by_field_name,
  next: :next_sibling,
  prev: :prev_sibling,
  next_named: :next_named_sibling,
  prev_named: :prev_named_sibling,
  field_names: :fields,
}.freeze
THINLY_WRAPPED_METHODS =

These are methods from TreeSitter that are thinly wrapped to create TreeStand::Node instead.

(
  %i[
    child
    named_child
    parent
  ] + THINLY_REMAPPED_METHODS.keys
).freeze

Instance Attribute Summary collapse

Instance Method Summary collapse

Constructor Details

#initialize(tree, ts_node) ⇒ void

This method is part of a private API. You should avoid using this method if possible, as it may be removed or be changed in the future.

Parameters:



79
80
81
82
# File 'lib/tree_stand/node.rb', line 79

def initialize(tree, ts_node)
  @tree = tree
  @ts_node = ts_node
end

Dynamic Method Handling

This class handles dynamic methods through the method_missing method

#method_missing(field_name) ⇒ TreeStand::Node #method_missing(method_name, *args, &block) ⇒ Object

This class overrides the ‘method_missing` method to delegate to the node’s named children.

Examples:

node.text          # => "3 * 4"

node.left.text     # => "3"
node.operator.text # => "*"
node.right.text    # => "4"
node.operand       # => NoMethodError

Overloads:

  • #method_missing(field_name) ⇒ TreeStand::Node

    Returns child node for the given field name.

    Parameters:

    • name (Symbol, String)

    Returns:

    Raises:

    • (NoMethodError)

      Raised if the node does not have child with name ‘field_name`

  • #method_missing(method_name, *args, &block) ⇒ Object

    Raises:

    • (NoMethodError)


293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
# File 'lib/tree_stand/node.rb', line 293

def method_missing(method, *args, **kwargs, &block)
  if thinly_wrapped?(method)
    from(
      T.unsafe(@ts_node)
        .public_send(
          THINLY_REMAPPED_METHODS[method] || method,
          *args,
          **kwargs,
          &block
        ),
    )
  else
    super
  end
end

Instance Attribute Details

#treeTreeStand::Tree (readonly)

Returns:



72
73
74
# File 'lib/tree_stand/node.rb', line 72

def tree
  @tree
end

#ts_nodeTreeSitter::Node (readonly)

Returns:



75
76
77
# File 'lib/tree_stand/node.rb', line 75

def ts_node
  @ts_node
end

Instance Method Details

#==(other) ⇒ Boolean

Parameters:

  • other (Object)

Returns:

  • (Boolean)


310
311
312
313
314
# File 'lib/tree_stand/node.rb', line 310

def ==(other)
  return false unless other.is_a?(TreeStand::Node)

  T.must(range == other.range && type == other.type && text == other.text)
end

#changed?Boolean

Returns true if a syntax node has been edited.

Returns:

  • (Boolean)

    true if a syntax node has been edited.



34
35
36
37
38
39
40
41
42
43
44
45
46
# File 'lib/tree_stand/node.rb', line 34

def_delegators(
  :@ts_node,
  :changed?,
  :child_count,
  :error?,
  :extra?,
  :has_error?,
  :missing?,
  :named?,
  :named_child_count,
  :sexpr,
  :type,
)

#child_countInteger

Returns the number of child nodes.

Returns:

  • (Integer)

    the number of child nodes.



34
35
36
37
38
39
40
41
42
43
44
45
46
# File 'lib/tree_stand/node.rb', line 34

def_delegators(
  :@ts_node,
  :changed?,
  :child_count,
  :error?,
  :extra?,
  :has_error?,
  :missing?,
  :named?,
  :named_child_count,
  :sexpr,
  :type,
)

#childrenArray<TreeStand::Node>

Examples:

node.text # => "3 * 4"
node.to_a.map(&:text) # => ["3", "*", "4"]
node.children.map(&:text) # => ["3", "*", "4"]

Returns:



268
# File 'lib/tree_stand/node.rb', line 268

def children = to_a

#each(&block) {|child| ... } ⇒ T::Enumerator[TreeStand::Node]

Node includes enumerable so that you can iterate over the child nodes.

Examples:

node.text # => "3 * 4"

Iterate over the child nodes

node.each do |child|
  print child.text
end
# prints: 3*4

Enumerable methods

node.map(&:text) # => ["3", "*", "4"]

Parameters:

Yield Parameters:

Returns:



162
163
164
165
166
167
168
169
170
# File 'lib/tree_stand/node.rb', line 162

def each(&block)
  enumerator = Enumerator.new do |yielder|
    @ts_node.each do |child|
      yielder << TreeStand::Node.new(@tree, child)
    end
  end
  enumerator.each(&block) if block_given?
  enumerator
end

#each_field(&block) {|field, child| ... } ⇒ T::Enumerator[[Symbol, TreeStand::Node]] Also known as: fields

Iterate of (field, child).

Examples:

node.text # => "3 * 4"

Iterate over the child nodes

node.each_field do |field, child|
  puts "#{field}: #{child.text}"
end
# prints:
#  left: 3
#  right: 4

Enumerable methods

node.each_field.map { |f, c| "#{f}: #{c}" } # => ["left: 3", "right: 4"]

Parameters:

Yield Parameters:

Returns:



222
223
224
225
226
227
228
229
230
# File 'lib/tree_stand/node.rb', line 222

def each_field(&block)
  enumerator = Enumerator.new do |yielder|
    @ts_node.each_field do |field, child|
      yielder << [field.to_sym, TreeStand::Node.new(@tree, child)]
    end
  end
  enumerator.each(&block) if block_given?
  enumerator
end

#each_named(&block) {|child| ... } ⇒ T::Enumerator[TreeStand::Node] Also known as: named

Enumerate named children.

Examples:

node.text # => "3 * 4"

Iterate over the child nodes

node.each_named do |child|
  print child.text
end
# prints: 34

Enumerable methods

node.each_named.map(&:text) # => ["3", "4"]

Parameters:

Yield Parameters:

Returns:



190
191
192
193
194
195
196
197
198
# File 'lib/tree_stand/node.rb', line 190

def each_named(&block)
  enumerator = Enumerator.new do |yielder|
    @ts_node.each_named do |child|
      yielder << TreeStand::Node.new(@tree, child)
    end
  end
  enumerator.each(&block) if block_given?
  enumerator
end

#error?bool

Returns true if the node is an error node.

Returns:

  • (bool)

    true if the node is an error node.



34
35
36
37
38
39
40
41
42
43
44
45
46
# File 'lib/tree_stand/node.rb', line 34

def_delegators(
  :@ts_node,
  :changed?,
  :child_count,
  :error?,
  :extra?,
  :has_error?,
  :missing?,
  :named?,
  :named_child_count,
  :sexpr,
  :type,
)

#extra?Boolean

Returns true if the node is extra (e.g. comments).

Returns:

  • (Boolean)

    true if the node is extra (e.g. comments).



34
35
36
37
38
39
40
41
42
43
44
45
46
# File 'lib/tree_stand/node.rb', line 34

def_delegators(
  :@ts_node,
  :changed?,
  :child_count,
  :error?,
  :extra?,
  :has_error?,
  :missing?,
  :named?,
  :named_child_count,
  :sexpr,
  :type,
)

#find_node(query_string) ⇒ TreeStand::Node?

Returns the first captured node that matches the query string or nil if there was no captured node.

Examples:

Find the first identifier node.

identifier_node = tree.root_node.find_node("(identifier) @identifier")

Parameters:

  • query_string (String)

Returns:

See Also:



119
120
121
# File 'lib/tree_stand/node.rb', line 119

def find_node(query_string)
  query(query_string).first&.values&.first
end

#find_node!(query_string) ⇒ TreeStand::Node

Like #find_node, except that if no node is found, raises an TreeStand::NodeNotFound error.

Parameters:

  • query_string (String)

Returns:

Raises:

See Also:



129
130
131
# File 'lib/tree_stand/node.rb', line 129

def find_node!(query_string)
  find_node(query_string) || raise(TreeStand::NodeNotFound)
end

#has_error?Boolean

Returns true if the node is a syntax error or contains any syntax errors.

Returns:

  • (Boolean)

    true if the node is a syntax error or contains any syntax errors.



34
35
36
37
38
39
40
41
42
43
44
45
46
# File 'lib/tree_stand/node.rb', line 34

def_delegators(
  :@ts_node,
  :changed?,
  :child_count,
  :error?,
  :extra?,
  :has_error?,
  :missing?,
  :named?,
  :named_child_count,
  :sexpr,
  :type,
)

#missing?Boolean

Returns true if the parser inserted that node to recover from error.

Returns:

  • (Boolean)

    true if the parser inserted that node to recover from error.



34
35
36
37
38
39
40
41
42
43
44
45
46
# File 'lib/tree_stand/node.rb', line 34

def_delegators(
  :@ts_node,
  :changed?,
  :child_count,
  :error?,
  :extra?,
  :has_error?,
  :missing?,
  :named?,
  :named_child_count,
  :sexpr,
  :type,
)

#named?Boolean

Returns true if the node is not a literal in the grammar.

Returns:

  • (Boolean)

    true if the node is not a literal in the grammar.



34
35
36
37
38
39
40
41
42
43
44
45
46
# File 'lib/tree_stand/node.rb', line 34

def_delegators(
  :@ts_node,
  :changed?,
  :child_count,
  :error?,
  :extra?,
  :has_error?,
  :missing?,
  :named?,
  :named_child_count,
  :sexpr,
  :type,
)

#named_child_countInteger

Returns the number of named children.

Returns:

  • (Integer)

    the number of named children.



34
35
36
37
38
39
40
41
42
43
44
45
46
# File 'lib/tree_stand/node.rb', line 34

def_delegators(
  :@ts_node,
  :changed?,
  :child_count,
  :error?,
  :extra?,
  :has_error?,
  :missing?,
  :named?,
  :named_child_count,
  :sexpr,
  :type,
)

#pretty_print(pp) ⇒ void

This method returns an undefined value.

Parameters:

  • pp (PP)

See Also:

  • TreeSitter:Node::sexpr


318
319
320
# File 'lib/tree_stand/node.rb', line 318

def pretty_print(pp)
  pp.output << sexpr(source: text)
end

#query(query_string) ⇒ Array<Hash{String => TreeStand::Node}>

TreeSitter uses a ‘TreeSitter::Cursor` to iterate over matches by calling `curser#next_match` repeatedly until it returns `nil`.

This method does all of that for you and collects all of the matches into an array and each corresponding capture into a hash.

Examples:

# This will return a match for each identifier nodes in the tree.
tree_matches = tree.query(<<~QUERY)
  (identifier) @identifier
QUERY

# It is equivalent to:
tree.root_node.query(<<~QUERY)
  (identifier) @identifier
QUERY

Parameters:

  • query_string (String)

Returns:



101
102
103
104
105
106
107
108
# File 'lib/tree_stand/node.rb', line 101

def query(query_string)
  ts_query = TreeSitter::Query.new(@tree.parser.ts_language, query_string)
  TreeSitter::QueryCursor
    .new
    .matches(ts_query, @tree.ts_tree.root_node, @tree.document)
    .each_capture_hash
    .map { |h| h.transform_values! { |n| TreeStand::Node.new(@tree, n) } }
end

#rangeTreeStand::Range

Returns:



134
135
136
137
138
139
140
141
# File 'lib/tree_stand/node.rb', line 134

def range
  TreeStand::Range.new(
    start_byte: @ts_node.start_byte,
    end_byte: @ts_node.end_byte,
    start_point: @ts_node.start_point,
    end_point: @ts_node.end_point,
  )
end

#sexprString

Returns a pretty-printed sexpr.

Returns:

  • (String)

    a pretty-printed sexpr.



34
35
36
37
38
39
40
41
42
43
44
45
46
# File 'lib/tree_stand/node.rb', line 34

def_delegators(
  :@ts_node,
  :changed?,
  :child_count,
  :error?,
  :extra?,
  :has_error?,
  :missing?,
  :named?,
  :named_child_count,
  :sexpr,
  :type,
)

#textString

A convenience method for getting the text of the node. Each TreeStand::Node wraps the parent #tree and has access to the source document.

Returns:

  • (String)


273
274
275
# File 'lib/tree_stand/node.rb', line 273

def text
  T.must(@tree.document.byteslice(@ts_node.start_byte...@ts_node.end_byte))
end

#typeSymbol

Returns the type of the node in the tree-sitter grammar.

Returns:

  • (Symbol)

    the type of the node in the tree-sitter grammar.



34
35
36
37
38
39
40
41
42
43
44
45
46
# File 'lib/tree_stand/node.rb', line 34

def_delegators(
  :@ts_node,
  :changed?,
  :child_count,
  :error?,
  :extra?,
  :has_error?,
  :missing?,
  :named?,
  :named_child_count,
  :sexpr,
  :type,
)

#walk(&block) {|node| ... } ⇒ T::Enumerator[TreeStand::Node]

Examples:

Check the subtree for error nodes

node.walk.any? { |node| node.type == :error }

Parameters:

Yield Parameters:

Returns:

See Also:



253
254
255
256
257
258
259
260
261
# File 'lib/tree_stand/node.rb', line 253

def walk(&block)
  enumerator = Enumerator.new do |yielder|
    Visitors::TreeWalker.new(self) do |child|
      yielder << child
    end.visit
  end
  enumerator.each(&block) if block_given?
  enumerator
end