tree_haver 7.0.0 → 7.1.0

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 (44) hide show
  1. checksums.yaml +4 -4
  2. checksums.yaml.gz.sig +0 -0
  3. data/LICENSE.md +13 -0
  4. data/README.md +1959 -0
  5. data/lib/tree_haver/backend_api.rb +392 -0
  6. data/lib/tree_haver/backend_registry.rb +153 -3
  7. data/lib/tree_haver/backends/citrus.rb +489 -0
  8. data/lib/tree_haver/backends/ffi.rb +1013 -0
  9. data/lib/tree_haver/backends/java.rb +909 -0
  10. data/lib/tree_haver/backends/mri.rb +367 -0
  11. data/lib/tree_haver/backends/parslet.rb +565 -0
  12. data/lib/tree_haver/backends/prism.rb +568 -0
  13. data/lib/tree_haver/backends/psych.rb +379 -0
  14. data/lib/tree_haver/backends/rust.rb +243 -0
  15. data/lib/tree_haver/backends/tslp.rb +274 -0
  16. data/lib/tree_haver/base/comment.rb +320 -0
  17. data/lib/tree_haver/base/language.rb +98 -0
  18. data/lib/tree_haver/base/node.rb +330 -0
  19. data/lib/tree_haver/base/parser.rb +28 -0
  20. data/lib/tree_haver/base/point.rb +48 -0
  21. data/lib/tree_haver/base/tree.rb +128 -0
  22. data/lib/tree_haver/citrus_grammar_finder.rb +213 -0
  23. data/lib/tree_haver/contracts.rb +661 -96
  24. data/lib/tree_haver/grammar_finder.rb +429 -0
  25. data/lib/tree_haver/kaitai_backend.rb +2 -2
  26. data/lib/tree_haver/language.rb +294 -0
  27. data/lib/tree_haver/language_pack.rb +17 -166
  28. data/lib/tree_haver/language_registry.rb +221 -0
  29. data/lib/tree_haver/library_path_utils.rb +80 -0
  30. data/lib/tree_haver/node.rb +588 -0
  31. data/lib/tree_haver/parser.rb +445 -0
  32. data/lib/tree_haver/parslet_grammar_finder.rb +217 -0
  33. data/lib/tree_haver/path_validator.rb +356 -0
  34. data/lib/tree_haver/peg_backends.rb +7 -7
  35. data/lib/tree_haver/point.rb +27 -0
  36. data/lib/tree_haver/rspec/dependency_tags.rb +52 -0
  37. data/lib/tree_haver/rspec.rb +3 -0
  38. data/lib/tree_haver/tree.rb +267 -0
  39. data/lib/tree_haver/version.rb +5 -3
  40. data/lib/tree_haver.rb +613 -8
  41. data/sig/tree_haver.rbs +6 -0
  42. data.tar.gz.sig +0 -0
  43. metadata +314 -13
  44. metadata.gz.sig +0 -0
@@ -0,0 +1,330 @@
1
+ # frozen_string_literal: true
2
+
3
+ module TreeHaver
4
+ module Base
5
+ # Base class for all backend Node implementations
6
+ #
7
+ # This class defines the API contract for Node objects across all backends.
8
+ # It provides shared implementation for common behaviors and documents
9
+ # required/optional methods that subclasses must implement.
10
+ #
11
+ # == Backend Architecture
12
+ #
13
+ # TreeHaver supports two categories of backends:
14
+ #
15
+ # === Tree-sitter Backends (MRI, Rust, FFI, Java)
16
+ #
17
+ # These backends use the native tree-sitter library (via different bindings).
18
+ # They return raw `::TreeSitter::Node` objects which are wrapped by
19
+ # `TreeHaver::Node` (which inherits from this class).
20
+ #
21
+ # - Backend Tree#root_node returns: `::TreeSitter::Node` (raw)
22
+ # - TreeHaver::Tree#root_node wraps it in: `TreeHaver::Node`
23
+ # - These backends do NOT define their own Tree/Node classes
24
+ #
25
+ # === Pure-Ruby/Plugin Backends (Citrus, Prism, Psych, Commonmarker, Markly)
26
+ #
27
+ # These backends define their own complete implementations:
28
+ # - `Backend::X::Node` - wraps parser-specific node objects
29
+ # - `Backend::X::Tree` - wraps parser-specific tree objects
30
+ #
31
+ # For consistency, these should also inherit from `Base::Node` and `Base::Tree`.
32
+ #
33
+ # @abstract Subclasses must implement #type, #start_byte, #end_byte, and #children
34
+ # @see TreeHaver::Node The main wrapper class that inherits from this
35
+ # @see TreeHaver::Backends::Citrus::Node Example of a backend-specific Node
36
+ class Node
37
+ include Comparable
38
+ include Enumerable
39
+
40
+ # The underlying backend-specific node object
41
+ # @return [Object] Backend node
42
+ attr_reader :inner_node
43
+
44
+ # The source text
45
+ # @return [String] Source code
46
+ attr_reader :source
47
+
48
+ # Source lines for byte offset calculations
49
+ # @return [Array<String>] Lines of source
50
+ attr_reader :lines
51
+
52
+ # Create a new Node wrapper
53
+ #
54
+ # @param node [Object] The backend-specific node object
55
+ # @param source [String, nil] The source code
56
+ # @param lines [Array<String>, nil] Pre-split lines (optional optimization)
57
+ def initialize(node, source: nil, lines: nil)
58
+ @inner_node = node
59
+ @source = source
60
+ @lines = lines || source&.lines || []
61
+ end
62
+
63
+ # -- Required API Methods ------------------------------------------------
64
+
65
+ # Get the node type as a string
66
+ # @return [String] Node type
67
+ def type
68
+ raise NotImplementedError, "#{self.class}#type must be implemented"
69
+ end
70
+
71
+ # The parser's original node type before a backend applies any portable
72
+ # vocabulary aliases. Backends without aliases use their public type.
73
+ #
74
+ # @return [String]
75
+ def native_type
76
+ type
77
+ end
78
+
79
+ # Get byte offset where the node starts
80
+ # @return [Integer] Start byte offset
81
+ def start_byte
82
+ raise NotImplementedError, "#{self.class}#start_byte must be implemented"
83
+ end
84
+
85
+ # Get byte offset where the node ends
86
+ # @return [Integer] End byte offset
87
+ def end_byte
88
+ raise NotImplementedError, "#{self.class}#end_byte must be implemented"
89
+ end
90
+
91
+ # Get all children as an array
92
+ # @return [Array<Node>]
93
+ def children
94
+ raise NotImplementedError, "#{self.class}#children must be implemented"
95
+ end
96
+
97
+ # -- Derived Methods (use #children) -------------------------------------
98
+
99
+ # Get the number of child nodes
100
+ # @return [Integer] Number of children
101
+ def child_count
102
+ children.size
103
+ end
104
+
105
+ # Get a child node by index
106
+ #
107
+ # Returns nil for negative indices or indices out of bounds.
108
+ # This matches tree-sitter behavior where negative indices are invalid.
109
+ #
110
+ # @param index [Integer] Child index (0-based, non-negative)
111
+ # @return [Node, nil] The child node or nil
112
+ def child(index)
113
+ return if index.negative?
114
+ return if index >= child_count
115
+
116
+ children[index]
117
+ end
118
+
119
+ # Iterate over children
120
+ # @yield [Node] Child node
121
+ def each(&block)
122
+ return to_enum(__method__) unless block
123
+
124
+ children.each(&block)
125
+ end
126
+
127
+ # Retrieve the first child
128
+ # @return [Node, nil]
129
+ def first_child
130
+ children.first
131
+ end
132
+
133
+ # Retrieve the last child
134
+ # @return [Node, nil]
135
+ def last_child
136
+ children.last
137
+ end
138
+
139
+ # -- Optional API Methods (with default implementations) -----------------
140
+
141
+ # Get the parent node
142
+ # @return [Node, nil] Parent node or nil
143
+ def parent
144
+ nil
145
+ end
146
+
147
+ # Get the next sibling node
148
+ # @return [Node, nil] Next sibling or nil
149
+ def next_sibling
150
+ nil
151
+ end
152
+
153
+ # Get the previous sibling node
154
+ # @return [Node, nil] Previous sibling or nil
155
+ def prev_sibling
156
+ nil
157
+ end
158
+
159
+ # Check if this node is named (structural)
160
+ # @return [Boolean] true if named
161
+ def named?
162
+ true
163
+ end
164
+
165
+ # Alias for named?
166
+ alias structural? named?
167
+
168
+ # Check if this node represents a syntax error
169
+ # @return [Boolean] true on error
170
+ def has_error?
171
+ false
172
+ end
173
+
174
+ # Check if this node was inserted for error recovery
175
+ # @return [Boolean] true if missing
176
+ def missing?
177
+ false
178
+ end
179
+
180
+ # Get the text content of this node
181
+ # @return [String] Node text
182
+ def text
183
+ return '' unless source
184
+
185
+ source[start_byte...end_byte] || ''
186
+ end
187
+
188
+ # Get a child by field name
189
+ # @param _name [String, Symbol] Field name
190
+ # @return [Node, nil] Child node or nil
191
+ def child_by_field_name(_name)
192
+ nil
193
+ end
194
+
195
+ # Get start position (row/col) - 0-based
196
+ # @return [Hash{Symbol => Integer}] {row: 0, column: 0}
197
+ def start_point
198
+ { row: 0, column: 0 }
199
+ end
200
+
201
+ # Get end position (row/col) - 0-based
202
+ # @return [Hash{Symbol => Integer}] {row: 0, column: 0}
203
+ def end_point
204
+ { row: 0, column: 0 }
205
+ end
206
+
207
+ # -- Shared Implementation -----------------------------------------------
208
+
209
+ # Comparison based on byte range
210
+ # @param other [Object]
211
+ # @return [Integer, nil]
212
+ def <=>(other)
213
+ return unless other.respond_to?(:start_byte) && other.respond_to?(:end_byte)
214
+
215
+ cmp = start_byte <=> other.start_byte
216
+ return cmp unless cmp == 0
217
+
218
+ end_byte <=> other.end_byte
219
+ end
220
+
221
+ # Get 1-based start line
222
+ # @return [Integer]
223
+ def start_line
224
+ sp = start_point
225
+ row = if sp.is_a?(Hash)
226
+ sp[:row]
227
+ else
228
+ (sp.respond_to?(:row) ? sp.row : 0)
229
+ end
230
+ row + 1
231
+ end
232
+
233
+ # Get 1-based end line
234
+ # @return [Integer]
235
+ def end_line
236
+ ep = end_point
237
+ row = if ep.is_a?(Hash)
238
+ ep[:row]
239
+ else
240
+ (ep.respond_to?(:row) ? ep.row : 0)
241
+ end
242
+ row + 1
243
+ end
244
+
245
+ # Get unified source position hash
246
+ # @return [Hash{Symbol => Integer}]
247
+ def source_position
248
+ sp = start_point
249
+ ep = end_point
250
+
251
+ sp_row = if sp.is_a?(Hash)
252
+ sp[:row]
253
+ else
254
+ (sp.respond_to?(:row) ? sp.row : 0)
255
+ end
256
+ sp_col = if sp.is_a?(Hash)
257
+ sp[:column]
258
+ else
259
+ (sp.respond_to?(:column) ? sp.column : 0)
260
+ end
261
+ ep_row = if ep.is_a?(Hash)
262
+ ep[:row]
263
+ else
264
+ (ep.respond_to?(:row) ? ep.row : 0)
265
+ end
266
+ ep_col = if ep.is_a?(Hash)
267
+ ep[:column]
268
+ else
269
+ (ep.respond_to?(:column) ? ep.column : 0)
270
+ end
271
+
272
+ {
273
+ start_line: sp_row + 1,
274
+ end_line: ep_row + 1,
275
+ start_column: sp_col,
276
+ end_column: ep_col
277
+ }
278
+ end
279
+
280
+ # Human-readable representation
281
+ # @return [String]
282
+ def inspect
283
+ class_name = self.class.name || "#{self.class.superclass&.name}(anonymous)"
284
+ node_type = begin
285
+ type
286
+ rescue NotImplementedError
287
+ '(not implemented)'
288
+ end
289
+ "#<#{class_name} type=#{node_type}>"
290
+ end
291
+
292
+ # String conversion returns the text content
293
+ # @return [String]
294
+ def to_s
295
+ text
296
+ end
297
+
298
+ # Equality based on type and byte range
299
+ # @param other [Object]
300
+ # @return [Boolean]
301
+ def ==(other)
302
+ return false unless other.respond_to?(:type) && other.respond_to?(:start_byte) && other.respond_to?(:end_byte)
303
+
304
+ type == other.type && start_byte == other.start_byte && end_byte == other.end_byte
305
+ end
306
+
307
+ protected
308
+
309
+ # Calculate byte offset from line and column
310
+ #
311
+ # @param line [Integer] 0-based line number
312
+ # @param column [Integer] 0-based column number
313
+ # @return [Integer] Byte offset
314
+ def calculate_byte_offset(line, column)
315
+ return 0 if lines.empty?
316
+
317
+ offset = 0
318
+ lines.each_with_index do |line_content, idx|
319
+ if idx < line
320
+ offset += line_content.bytesize
321
+ else
322
+ offset += [column, line_content.bytesize].min
323
+ break
324
+ end
325
+ end
326
+ offset
327
+ end
328
+ end
329
+ end
330
+ end
@@ -0,0 +1,28 @@
1
+ # frozen_string_literal: true
2
+
3
+ module TreeHaver
4
+ module Base
5
+ # Base class for backend Parser implementations
6
+ # Used by wrapper backends (Commonmarker, Markly, etc.)
7
+ # Raw backends (MRI/Rust) do not inherit from this.
8
+ class Parser
9
+ attr_accessor :language
10
+
11
+ def initialize
12
+ @language = nil
13
+ end
14
+
15
+ def parse(source)
16
+ raise NotImplementedError
17
+ end
18
+
19
+ def parse_string(_old_tree, source)
20
+ parse(source)
21
+ end
22
+
23
+ def backend
24
+ language.respond_to?(:backend) ? language.backend : nil
25
+ end
26
+ end
27
+ end
28
+ end
@@ -0,0 +1,48 @@
1
+ # frozen_string_literal: true
2
+
3
+ module TreeHaver
4
+ module Base
5
+ # Point struct for position information (row/column)
6
+ #
7
+ # Provides a consistent interface for 0-based row/column positions.
8
+ # Compatible with both hash-style access and method access.
9
+ #
10
+ # @example
11
+ # point = TreeHaver::Base::Point.new(5, 10)
12
+ # point.row # => 5
13
+ # point.column # => 10
14
+ # point[:row] # => 5
15
+ # point[:column] # => 10
16
+ Point = Struct.new(:row, :column) do
17
+ # Hash-style access for compatibility
18
+ # @param key [Symbol, String] :row or :column
19
+ # @return [Integer, nil]
20
+ def [](key)
21
+ case key
22
+ when :row, 'row', 0
23
+ row
24
+ when :column, 'column', 1
25
+ column
26
+ end
27
+ end
28
+
29
+ # Convert to hash
30
+ # @return [Hash{Symbol => Integer}]
31
+ def to_h
32
+ { row: row, column: column }
33
+ end
34
+
35
+ # String representation
36
+ # @return [String]
37
+ def to_s
38
+ "(#{row}, #{column})"
39
+ end
40
+
41
+ # Human-readable representation
42
+ # @return [String]
43
+ def inspect
44
+ "#<TreeHaver::Base::Point row=#{row} column=#{column}>"
45
+ end
46
+ end
47
+ end
48
+ end
@@ -0,0 +1,128 @@
1
+ # frozen_string_literal: true
2
+
3
+ module TreeHaver
4
+ module Base
5
+ # Base class for all backend Tree implementations
6
+ #
7
+ # This class defines the API contract for Tree objects across all backends.
8
+ # It provides shared implementation and documents required/optional methods.
9
+ #
10
+ # == Backend Architecture
11
+ #
12
+ # TreeHaver supports two categories of backends:
13
+ #
14
+ # === Tree-sitter Backends (MRI, Rust, FFI, Java)
15
+ #
16
+ # These backends use the native tree-sitter library (via different bindings).
17
+ # They return raw `::TreeSitter::Tree` objects which are wrapped by
18
+ # `TreeHaver::Tree` (which inherits from this class).
19
+ #
20
+ # - Backend Parser returns: `::TreeSitter::Tree` (raw)
21
+ # - TreeHaver::Parser wraps it in: `TreeHaver::Tree`
22
+ # - These backends do NOT define their own Tree/Node classes
23
+ #
24
+ # === Pure-Ruby/Plugin Backends (Citrus, Prism, Psych, Commonmarker, Markly)
25
+ #
26
+ # These backends define their own complete implementations:
27
+ # - `Backend::X::Tree` - wraps parser-specific tree objects
28
+ # - `Backend::X::Node` - wraps parser-specific node objects
29
+ #
30
+ # For consistency, these should also inherit from `Base::Tree` and `Base::Node`.
31
+ #
32
+ # @abstract Subclasses must implement #root_node
33
+ # @see TreeHaver::Tree The main wrapper class that inherits from this
34
+ # @see TreeHaver::Backends::Citrus::Tree Example of a backend-specific Tree
35
+ class Tree
36
+ # The underlying backend-specific tree object
37
+ # @return [Object] Backend tree
38
+ attr_reader :inner_tree
39
+
40
+ # The source text
41
+ # @return [String] The original source code
42
+ attr_reader :source
43
+
44
+ # Source lines for byte offset calculations
45
+ # @return [Array<String>] Lines of source
46
+ attr_reader :lines
47
+
48
+ # Create a new Tree
49
+ #
50
+ # @param inner_tree [Object] The backend-specific tree object
51
+ # @param source [String, nil] The source code
52
+ # @param lines [Array<String>, nil] Pre-split lines (optional, derived from source if not provided)
53
+ def initialize(inner_tree = nil, source: nil, lines: nil)
54
+ @inner_tree = inner_tree
55
+ @source = source
56
+ @lines = lines || source&.lines || []
57
+ end
58
+
59
+ # -- Required API Methods ------------------------------------------------
60
+
61
+ # Get the root node of the tree
62
+ # @return [Node] Root node
63
+ def root_node
64
+ raise NotImplementedError, "#{self.class}#root_node must be implemented"
65
+ end
66
+
67
+ # -- Optional API Methods (with defaults) --------------------------------
68
+
69
+ # Get parse errors
70
+ # @return [Array] Errors (empty for most pure-Ruby backends)
71
+ def errors
72
+ []
73
+ end
74
+
75
+ # Get parse warnings
76
+ # @return [Array] Warnings (empty for most pure-Ruby backends)
77
+ def warnings
78
+ []
79
+ end
80
+
81
+ # Get comments from the document
82
+ # @return [Array] Backend comment wrappers (empty for most backends)
83
+ def comments
84
+ []
85
+ end
86
+
87
+ # Mark the tree as edited for incremental re-parsing
88
+ # @return [void]
89
+ def edit(
90
+ start_byte:,
91
+ old_end_byte:,
92
+ new_end_byte:,
93
+ start_point:,
94
+ old_end_point:,
95
+ new_end_point:
96
+ )
97
+ # Default implementation: no-op (incremental parsing not supported)
98
+ # Backends that support it should override this
99
+ end
100
+
101
+ # Check if this tree has syntax errors
102
+ # @return [Boolean]
103
+ def has_error?
104
+ root = root_node
105
+ return false unless root
106
+ return true if root.has_error?
107
+
108
+ # Deep check: traverse tree looking for error nodes
109
+ # Use queue-based traversal to avoid deep recursion
110
+ queue = [root]
111
+ while (node = queue.shift)
112
+ return true if node.has_error? || node.missing?
113
+
114
+ # Add children to queue
115
+ node.each { |child| queue.push(child) }
116
+ end
117
+
118
+ false
119
+ end
120
+
121
+ # Human-readable representation
122
+ # @return [String]
123
+ def inspect
124
+ "#<#{self.class.name}>"
125
+ end
126
+ end
127
+ end
128
+ end