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,267 @@
1
+ # frozen_string_literal: true
2
+
3
+ module TreeHaver
4
+ # Unified Tree wrapper providing a consistent API across all backends
5
+ #
6
+ # This class wraps backend-specific tree objects and provides a unified interface.
7
+ # It stores the source text to enable text extraction from nodes.
8
+ #
9
+ # == Wrapping/Unwrapping Contract
10
+ #
11
+ # TreeHaver follows a consistent pattern for object wrapping:
12
+ #
13
+ # 1. **TreeHaver::Parser** (top level) handles ALL wrapping/unwrapping
14
+ # 2. **Backends** work exclusively with raw backend objects
15
+ # 3. **User-facing API** uses only TreeHaver wrapper classes
16
+ #
17
+ # Specifically for trees:
18
+ # - Backend Parser#parse returns raw backend tree (TreeSitter::Tree, TreeStump::Tree, etc.)
19
+ # - TreeHaver::Parser#parse wraps it in TreeHaver::Tree
20
+ # - TreeHaver::Parser#parse_string unwraps old_tree before passing to backend
21
+ # - Backend Parser#parse_string receives raw backend tree, returns raw backend tree
22
+ # - TreeHaver::Parser#parse_string wraps the returned tree
23
+ #
24
+ # This ensures:
25
+ # - Backends are simple and consistent
26
+ # - All complexity is in one place (TreeHaver top level)
27
+ # - Users always work with TreeHaver wrapper classes
28
+ #
29
+ # @example Basic usage
30
+ # parser = TreeHaver::Parser.new
31
+ # parser.language = TreeHaver::Language.toml
32
+ # tree = parser.parse(source)
33
+ # root = tree.root_node
34
+ # puts root.type
35
+ #
36
+ # @example Incremental parsing (if backend supports it)
37
+ # tree = parser.parse("x = 1")
38
+ # # Edit the source: "x = 1" → "x = 42"
39
+ # tree.edit(
40
+ # start_byte: 4,
41
+ # old_end_byte: 5,
42
+ # new_end_byte: 6,
43
+ # start_point: { row: 0, column: 4 },
44
+ # old_end_point: { row: 0, column: 5 },
45
+ # new_end_point: { row: 0, column: 6 }
46
+ # )
47
+ # new_tree = parser.parse_string(tree, "x = 42")
48
+ #
49
+ # @example Accessing backend-specific features
50
+ # # Via passthrough (method_missing delegates to inner_tree)
51
+ # tree.some_backend_specific_method # Automatically delegated
52
+ #
53
+ # # Or explicitly via inner_tree
54
+ # tree.inner_tree.some_backend_specific_method
55
+ class Tree < Base::Tree
56
+ # The wrapped backend-specific tree object
57
+ #
58
+ # This provides direct access to the underlying backend tree for advanced usage
59
+ # when you need backend-specific features not exposed by the unified API.
60
+ #
61
+ # @return [Object] The underlying tree (TreeSitter::Tree, TreeStump::Tree, etc.)
62
+ # @example Accessing backend-specific methods
63
+ # # Print DOT graph (TreeStump-specific)
64
+ # if tree.inner_tree.respond_to?(:print_dot_graph)
65
+ # File.open("tree.dot", "w") do |f|
66
+ # tree.inner_tree.print_dot_graph(f)
67
+ # end
68
+ # end
69
+ # NOTE: inner_tree is inherited from Base::Tree
70
+
71
+ # The source text
72
+ #
73
+ # Stored to enable text extraction from nodes via byte offsets.
74
+ #
75
+ # @return [String] The original source code
76
+ # NOTE: source is inherited from Base::Tree
77
+
78
+ # @param tree [Object] Backend-specific tree object
79
+ # @param source [String] Source text for node text extraction
80
+ def initialize(tree, source: nil)
81
+ super(tree, source: source)
82
+ end
83
+
84
+ # Get the root node of the tree
85
+ #
86
+ # @return [Node] Wrapped root node
87
+ def root_node
88
+ root = @inner_tree.root_node
89
+ return if root.nil?
90
+
91
+ Node.new(root, source: @source)
92
+ end
93
+
94
+ # Mark the tree as edited for incremental re-parsing
95
+ #
96
+ # Call this method after the source code has been modified but before
97
+ # re-parsing. This tells tree-sitter which parts of the tree are
98
+ # invalidated so it can efficiently re-parse only the affected regions.
99
+ #
100
+ # Not all backends support incremental parsing. Use {#supports_editing?}
101
+ # to check before calling this method.
102
+ #
103
+ # @param start_byte [Integer] byte offset where the edit starts
104
+ # @param old_end_byte [Integer] byte offset where the old text ended
105
+ # @param new_end_byte [Integer] byte offset where the new text ends
106
+ # @param start_point [Hash] starting position as `{ row:, column: }`
107
+ # @param old_end_point [Hash] old ending position as `{ row:, column: }`
108
+ # @param new_end_point [Hash] new ending position as `{ row:, column: }`
109
+ # @return [void]
110
+ # @raise [TreeHaver::NotAvailable] if the backend doesn't support incremental parsing
111
+ # @see https://tree-sitter.github.io/tree-sitter/using-parsers#editing
112
+ #
113
+ # @example Incremental parsing workflow
114
+ # # Original source: "x = 1"
115
+ # tree = parser.parse("x = 1")
116
+ #
117
+ # # Edit the source: replace "1" with "42" at byte offset 4
118
+ # tree.edit(
119
+ # start_byte: 4,
120
+ # old_end_byte: 5, # "1" ends at byte 5
121
+ # new_end_byte: 6, # "42" ends at byte 6
122
+ # start_point: { row: 0, column: 4 },
123
+ # old_end_point: { row: 0, column: 5 },
124
+ # new_end_point: { row: 0, column: 6 }
125
+ # )
126
+ #
127
+ # # Re-parse with the edited tree for incremental parsing
128
+ # new_tree = parser.parse_string(tree, "x = 42")
129
+ def edit(start_byte:, old_end_byte:, new_end_byte:, start_point:, old_end_point:, new_end_point:)
130
+ # MRI backend (ruby_tree_sitter) requires an InputEdit object
131
+ if defined?(::TreeSitter::InputEdit) && @inner_tree.is_a?(::TreeSitter::Tree)
132
+ input_edit = ::TreeSitter::InputEdit.new
133
+ input_edit.start_byte = start_byte
134
+ input_edit.old_end_byte = old_end_byte
135
+ input_edit.new_end_byte = new_end_byte
136
+
137
+ # Convert hash points to Point objects if needed
138
+ input_edit.start_point = make_point(start_point)
139
+ input_edit.old_end_point = make_point(old_end_point)
140
+ input_edit.new_end_point = make_point(new_end_point)
141
+
142
+ @inner_tree.edit(input_edit)
143
+ else
144
+ # Other backends may accept keyword arguments directly
145
+ @inner_tree.edit(
146
+ start_byte: start_byte,
147
+ old_end_byte: old_end_byte,
148
+ new_end_byte: new_end_byte,
149
+ start_point: start_point,
150
+ old_end_point: old_end_point,
151
+ new_end_point: new_end_point
152
+ )
153
+ end
154
+ rescue NoMethodError => e
155
+ # Re-raise as NotAvailable if it's about the edit method
156
+ raise unless e.name == :edit || e.message.include?('edit')
157
+
158
+ raise TreeHaver::NotAvailable,
159
+ 'Incremental parsing not supported by current backend. ' \
160
+ 'Use MRI (ruby_tree_sitter), Rust (tree_stump), or Java (java-tree-sitter / jtreesitter) backend.'
161
+ end
162
+
163
+ private
164
+
165
+ # Convert a point hash to a TreeSitter::Point if available
166
+ # @api private
167
+ def make_point(point_hash)
168
+ if defined?(::TreeSitter::Point)
169
+ pt = ::TreeSitter::Point.new
170
+ pt.row = point_hash[:row]
171
+ pt.column = point_hash[:column]
172
+ pt
173
+ else
174
+ point_hash
175
+ end
176
+ end
177
+
178
+ public
179
+
180
+ # Check if the current backend supports incremental parsing
181
+ #
182
+ # Incremental parsing allows tree-sitter to reuse unchanged nodes when
183
+ # re-parsing edited source code, improving performance for large files
184
+ # with small edits.
185
+ #
186
+ # @return [Boolean] true if {#edit} can be called on this tree
187
+ # @example
188
+ # if tree.supports_editing?
189
+ # tree.edit(
190
+ # start_byte: 4,
191
+ # old_end_byte: 5,
192
+ # new_end_byte: 6,
193
+ # start_point: {row: 0, column: 4},
194
+ # old_end_point: {row: 0, column: 5},
195
+ # new_end_point: {row: 0, column: 6}
196
+ # )
197
+ # new_tree = parser.parse_string(tree, edited_source)
198
+ # else
199
+ # # Fall back to full re-parse
200
+ # new_tree = parser.parse(edited_source)
201
+ # end
202
+ def supports_editing?
203
+ # Try to get the edit method to verify it exists
204
+ # This is more reliable than respond_to? with Delegator wrappers
205
+ @inner_tree.method(:edit)
206
+ true
207
+ rescue NameError
208
+ # NameError is the parent class of NoMethodError, so this catches both
209
+ false
210
+ end
211
+
212
+ # String representation
213
+ # @return [String]
214
+ def inspect
215
+ inner_class = @inner_tree ? @inner_tree.class.name : 'nil'
216
+ "#<#{self.class} source_length=#{@source&.bytesize || 'unknown'} inner_tree=#{inner_class}>"
217
+ end
218
+
219
+ # Check if tree responds to a method (includes delegation to inner_tree)
220
+ #
221
+ # @param method_name [Symbol] method to check
222
+ # @param include_private [Boolean] include private methods
223
+ # @return [Boolean]
224
+ def respond_to_missing?(method_name, include_private = false)
225
+ @inner_tree.respond_to?(method_name, include_private) || super
226
+ end
227
+
228
+ # Delegate unknown methods to the underlying backend-specific tree
229
+ #
230
+ # This provides passthrough access for advanced usage when you need
231
+ # backend-specific features not exposed by TreeHaver's unified API.
232
+ #
233
+ # The delegation is automatic and transparent - you can call backend-specific
234
+ # methods directly on the TreeHaver::Tree and they'll be forwarded to the
235
+ # underlying tree implementation.
236
+ #
237
+ # @param method_name [Symbol] method to call
238
+ # @param args [Array] arguments to pass
239
+ # @param block [Proc] block to pass
240
+ # @return [Object] result from the underlying tree
241
+ #
242
+ # @example Using TreeStump-specific methods
243
+ # # print_dot_graph is TreeStump-specific
244
+ # File.open("tree.dot", "w") do |f|
245
+ # tree.print_dot_graph(f) # Delegated to inner_tree
246
+ # end
247
+ #
248
+ # @example Safe usage with respond_to? check
249
+ # if tree.respond_to?(:print_dot_graph)
250
+ # File.open("tree.dot", "w") { |f| tree.print_dot_graph(f) }
251
+ # end
252
+ #
253
+ # @example Equivalent explicit access
254
+ # tree.print_dot_graph(file) # Via passthrough (method_missing)
255
+ # tree.inner_tree.print_dot_graph(file) # Explicit access (same result)
256
+ #
257
+ # @note This maintains backward compatibility with code written for
258
+ # specific backends while providing the benefits of the unified API
259
+ def method_missing(method_name, *args, **kwargs, &block)
260
+ if @inner_tree.respond_to?(method_name)
261
+ @inner_tree.public_send(method_name, *args, **kwargs, &block)
262
+ else
263
+ super
264
+ end
265
+ end
266
+ end
267
+ end
@@ -1,9 +1,11 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module TreeHaver
4
+ # Version namespace for this gem.
4
5
  module Version
5
- VERSION = "7.0.0"
6
+ # Current gem version.
7
+ VERSION = '7.1.0'
6
8
  end
7
-
8
- VERSION = Version::VERSION
9
+ # Current gem version exposed at the traditional constant location.
10
+ VERSION = Version::VERSION # Traditional Constant Location
9
11
  end