Class: TreeHaver::Base::Node Abstract

Inherits:
Object
  • Object
show all
Includes:
Comparable, Enumerable
Defined in:
lib/tree_haver/base/node.rb

Overview

This class is abstract.

Subclasses must implement #type, #start_byte, #end_byte, and #children

Base class for all backend Node implementations

This class defines the API contract for Node objects across all backends. It provides shared implementation for common behaviors and documents required/optional methods that subclasses must implement.

Backend Architecture

TreeHaver supports two categories of backends:

Tree-sitter Backends (MRI, Rust, FFI, Java)

These backends use the native tree-sitter library (via different bindings). They return raw ::TreeSitter::Node objects which are wrapped by TreeHaver::Node (which inherits from this class).

  • Backend Tree#root_node returns: ::TreeSitter::Node (raw)
  • TreeHaver::Tree#root_node wraps it in: TreeHaver::Node
  • These backends do NOT define their own Tree/Node classes

Pure-Ruby/Plugin Backends (Citrus, Prism, Psych, Commonmarker, Markly)

These backends define their own complete implementations:

  • Backend::X::Node - wraps parser-specific node objects
  • Backend::X::Tree - wraps parser-specific tree objects

For consistency, these should also inherit from Base::Node and Base::Tree.

Instance Attribute Summary collapse

Instance Method Summary collapse

Constructor Details

#initialize(node, source: nil, lines: nil) ⇒ Node

Create a new Node wrapper

Parameters:

  • node (Object)

    The backend-specific node object

  • source (String, nil) (defaults to: nil)

    The source code

  • lines (Array<String>, nil) (defaults to: nil)

    Pre-split lines (optional optimization)



57
58
59
60
61
# File 'lib/tree_haver/base/node.rb', line 57

def initialize(node, source: nil, lines: nil)
  @inner_node = node
  @source = source
  @lines = lines || source&.lines || []
end

Instance Attribute Details

#inner_nodeObject (readonly)

The underlying backend-specific node object

Returns:

  • (Object)

    Backend node



42
43
44
# File 'lib/tree_haver/base/node.rb', line 42

def inner_node
  @inner_node
end

#linesArray<String> (readonly)

Source lines for byte offset calculations

Returns:

  • (Array<String>)

    Lines of source



50
51
52
# File 'lib/tree_haver/base/node.rb', line 50

def lines
  @lines
end

#sourceString (readonly)

The source text

Returns:

  • (String)

    Source code



46
47
48
# File 'lib/tree_haver/base/node.rb', line 46

def source
  @source
end

Instance Method Details

#<=>(other) ⇒ Integer?

Comparison based on byte range

Parameters:

  • other (Object)

Returns:

  • (Integer, nil)


212
213
214
215
216
217
218
219
# File 'lib/tree_haver/base/node.rb', line 212

def <=>(other)
  return unless other.respond_to?(:start_byte) && other.respond_to?(:end_byte)

  cmp = start_byte <=> other.start_byte
  return cmp unless cmp.zero?

  end_byte <=> other.end_byte
end

#==(other) ⇒ Boolean

Equality based on type and byte range

Parameters:

  • other (Object)

Returns:

  • (Boolean)


301
302
303
304
305
# File 'lib/tree_haver/base/node.rb', line 301

def ==(other)
  return false unless other.respond_to?(:type) && other.respond_to?(:start_byte) && other.respond_to?(:end_byte)

  type == other.type && start_byte == other.start_byte && end_byte == other.end_byte
end

#child(index) ⇒ Node?

Get a child node by index

Returns nil for negative indices or indices out of bounds. This matches tree-sitter behavior where negative indices are invalid.

Parameters:

  • index (Integer)

    Child index (0-based, non-negative)

Returns:

  • (Node, nil)

    The child node or nil



112
113
114
115
116
117
# File 'lib/tree_haver/base/node.rb', line 112

def child(index)
  return if index.negative?
  return if index >= child_count

  children[index]
end

#child_by_field_name(_name) ⇒ Node?

Get a child by field name

Parameters:

  • _name (String, Symbol)

    Field name

Returns:

  • (Node, nil)

    Child node or nil



191
192
193
# File 'lib/tree_haver/base/node.rb', line 191

def child_by_field_name(_name)
  nil
end

#child_countInteger

Get the number of child nodes

Returns:

  • (Integer)

    Number of children



101
102
103
# File 'lib/tree_haver/base/node.rb', line 101

def child_count
  children.size
end

#childrenArray<Node>

Get all children as an array

Returns:

Raises:

  • (NotImplementedError)


93
94
95
# File 'lib/tree_haver/base/node.rb', line 93

def children
  raise NotImplementedError, "#{self.class}#children must be implemented"
end

#each {|Node| ... } ⇒ Object

Iterate over children

Yields:

  • (Node)

    Child node



121
122
123
124
125
# File 'lib/tree_haver/base/node.rb', line 121

def each(&block)
  return to_enum(__method__) unless block

  children.each(&block)
end

#end_byteInteger

Get byte offset where the node ends

Returns:

  • (Integer)

    End byte offset

Raises:

  • (NotImplementedError)


87
88
89
# File 'lib/tree_haver/base/node.rb', line 87

def end_byte
  raise NotImplementedError, "#{self.class}#end_byte must be implemented"
end

#end_lineInteger

Get 1-based end line

Returns:

  • (Integer)


235
236
237
238
239
240
241
242
243
# File 'lib/tree_haver/base/node.rb', line 235

def end_line
  ep = end_point
  row = if ep.is_a?(Hash)
          ep[:row]
        else
          (ep.respond_to?(:row) ? ep.row : 0)
        end
  row + 1
end

#end_pointHash{Symbol => Integer}

Get end position (row/col) - 0-based

Returns:

  • (Hash{Symbol => Integer})

    0, column: 0



203
204
205
# File 'lib/tree_haver/base/node.rb', line 203

def end_point
  { row: 0, column: 0 }
end

#first_childNode?

Retrieve the first child

Returns:



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

def first_child
  children.first
end

#has_error?Boolean

Check if this node represents a syntax error

Returns:

  • (Boolean)

    true on error



170
171
172
# File 'lib/tree_haver/base/node.rb', line 170

def has_error?
  false
end

#inspectString

Human-readable representation

Returns:

  • (String)


282
283
284
285
286
287
288
289
290
# File 'lib/tree_haver/base/node.rb', line 282

def inspect
  class_name = self.class.name || "#{self.class.superclass&.name}(anonymous)"
  node_type = begin
    type
  rescue NotImplementedError
    '(not implemented)'
  end
  "#<#{class_name} type=#{node_type}>"
end

#last_childNode?

Retrieve the last child

Returns:



135
136
137
# File 'lib/tree_haver/base/node.rb', line 135

def last_child
  children.last
end

#missing?Boolean

Check if this node was inserted for error recovery

Returns:

  • (Boolean)

    true if missing



176
177
178
# File 'lib/tree_haver/base/node.rb', line 176

def missing?
  false
end

#named?Boolean Also known as: structural?

Check if this node is named (structural)

Returns:

  • (Boolean)

    true if named



161
162
163
# File 'lib/tree_haver/base/node.rb', line 161

def named?
  true
end

#native_typeString

The parser's original node type before a backend applies any portable vocabulary aliases. Backends without aliases use their public type.

Returns:

  • (String)


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

def native_type
  type
end

#next_siblingNode?

Get the next sibling node

Returns:

  • (Node, nil)

    Next sibling or nil



149
150
151
# File 'lib/tree_haver/base/node.rb', line 149

def next_sibling
  nil
end

#parentNode?

Get the parent node

Returns:

  • (Node, nil)

    Parent node or nil



143
144
145
# File 'lib/tree_haver/base/node.rb', line 143

def parent
  nil
end

#prev_siblingNode?

Get the previous sibling node

Returns:

  • (Node, nil)

    Previous sibling or nil



155
156
157
# File 'lib/tree_haver/base/node.rb', line 155

def prev_sibling
  nil
end

#source_positionHash{Symbol => Integer}

Get unified source position hash

Returns:

  • (Hash{Symbol => Integer})


247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
# File 'lib/tree_haver/base/node.rb', line 247

def source_position
  sp = start_point
  ep = end_point

  sp_row = if sp.is_a?(Hash)
             sp[:row]
           else
             (sp.respond_to?(:row) ? sp.row : 0)
           end
  sp_col = if sp.is_a?(Hash)
             sp[:column]
           else
             (sp.respond_to?(:column) ? sp.column : 0)
           end
  ep_row = if ep.is_a?(Hash)
             ep[:row]
           else
             (ep.respond_to?(:row) ? ep.row : 0)
           end
  ep_col = if ep.is_a?(Hash)
             ep[:column]
           else
             (ep.respond_to?(:column) ? ep.column : 0)
           end

  {
    start_line: sp_row + 1,
    end_line: ep_row + 1,
    start_column: sp_col,
    end_column: ep_col
  }
end

#start_byteInteger

Get byte offset where the node starts

Returns:

  • (Integer)

    Start byte offset

Raises:

  • (NotImplementedError)


81
82
83
# File 'lib/tree_haver/base/node.rb', line 81

def start_byte
  raise NotImplementedError, "#{self.class}#start_byte must be implemented"
end

#start_lineInteger

Get 1-based start line

Returns:

  • (Integer)


223
224
225
226
227
228
229
230
231
# File 'lib/tree_haver/base/node.rb', line 223

def start_line
  sp = start_point
  row = if sp.is_a?(Hash)
          sp[:row]
        else
          (sp.respond_to?(:row) ? sp.row : 0)
        end
  row + 1
end

#start_pointHash{Symbol => Integer}

Get start position (row/col) - 0-based

Returns:

  • (Hash{Symbol => Integer})

    0, column: 0



197
198
199
# File 'lib/tree_haver/base/node.rb', line 197

def start_point
  { row: 0, column: 0 }
end

#textString

Get the text content of this node

Returns:

  • (String)

    Node text



182
183
184
185
186
# File 'lib/tree_haver/base/node.rb', line 182

def text
  return '' unless source

  source[start_byte...end_byte] || ''
end

#to_sString

String conversion returns the text content

Returns:

  • (String)


294
295
296
# File 'lib/tree_haver/base/node.rb', line 294

def to_s
  text
end

#typeString

Get the node type as a string

Returns:

  • (String)

    Node type

Raises:

  • (NotImplementedError)


67
68
69
# File 'lib/tree_haver/base/node.rb', line 67

def type
  raise NotImplementedError, "#{self.class}#type must be implemented"
end