tree_haver 7.0.0 → 7.1.1

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 +490 -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 +8 -8
  35. data/lib/tree_haver/point.rb +27 -0
  36. data/lib/tree_haver/rspec/dependency_tags.rb +56 -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,367 @@
1
+ # frozen_string_literal: true
2
+
3
+ module TreeHaver
4
+ module Backends
5
+ # MRI backend using the ruby_tree_sitter gem
6
+ #
7
+ # This backend wraps the ruby_tree_sitter gem, which is a native C extension
8
+ # for MRI Ruby. It provides the most feature-complete tree-sitter integration
9
+ # on MRI, including support for the Query API.
10
+ #
11
+ # == Tree/Node Architecture
12
+ #
13
+ # This backend (like all tree-sitter backends: MRI, Rust, FFI, Java) does NOT
14
+ # define its own Tree or Node classes. Instead:
15
+ #
16
+ # - Parser#parse returns raw `::TreeSitter::Tree` objects
17
+ # - These are wrapped by `TreeHaver::Tree` (inherits from `Base::Tree`)
18
+ # - `TreeHaver::Tree#root_node` wraps raw nodes in `TreeHaver::Node`
19
+ #
20
+ # This differs from pure-Ruby backends (Citrus, Prism, Psych) which define
21
+ # their own `Backend::X::Tree` and `Backend::X::Node` classes.
22
+ #
23
+ # @see TreeHaver::Tree The wrapper class for tree-sitter Tree objects
24
+ # @see TreeHaver::Node The wrapper class for tree-sitter Node objects
25
+ # @see TreeHaver::Base::Tree Base class documenting the Tree API contract
26
+ # @see TreeHaver::Base::Node Base class documenting the Node API contract
27
+ #
28
+ # == Platform Compatibility
29
+ #
30
+ # - MRI Ruby: ✓ Full support (fastest tree-sitter backend on MRI)
31
+ # - JRuby: ✗ Cannot load native C extensions (runs on JVM)
32
+ # - TruffleRuby: ✗ C extension not compatible with TruffleRuby
33
+ #
34
+ # @see https://github.com/Faveod/ruby-tree-sitter ruby_tree_sitter
35
+ module MRI
36
+ @load_attempted = false
37
+ @loaded = false
38
+
39
+ # Check if the MRI backend is available
40
+ #
41
+ # Attempts to require ruby_tree_sitter on first call and caches the result.
42
+ #
43
+ # @note When this method returns true, the FFI backend becomes permanently
44
+ # unavailable for the remainder of the process. This is because loading
45
+ # ruby_tree_sitter defines `::TreeSitter::Parser`, which the FFI backend
46
+ # checks to detect conflicts. The MRI backend statically links tree-sitter,
47
+ # while FFI dynamically links libtree-sitter.so - when both are loaded,
48
+ # FFI will segfault when trying to set a language on a parser due to
49
+ # incompatible pointer types from different tree-sitter builds.
50
+ #
51
+ # @return [Boolean] true if ruby_tree_sitter is available
52
+ # @see TreeHaver::Backends::FFI.available? FFI availability check
53
+ # @example
54
+ # if TreeHaver::Backends::MRI.available?
55
+ # puts "MRI backend is ready"
56
+ # # Note: FFI backend is now blocked for this process
57
+ # end
58
+ class << self
59
+ def available?
60
+ return @loaded if @load_attempted
61
+
62
+ @load_attempted = true
63
+ begin
64
+ # ruby_tree_sitter is a C extension that only works on MRI
65
+ # It doesn't work on JRuby or TruffleRuby
66
+ if RUBY_ENGINE == 'ruby'
67
+ require 'tree_sitter'
68
+ @loaded = true
69
+ else
70
+ # simplecov:disable only runs on non-MRI engines (JRuby, TruffleRuby)
71
+ @loaded = false
72
+ # simplecov:enable
73
+ end
74
+ rescue LoadError
75
+ # simplecov:disable only runs when ruby_tree_sitter gem is not installed
76
+ @loaded = false
77
+ # simplecov:enable
78
+ rescue StandardError
79
+ # simplecov:disable defensive code - StandardError during require is extremely rare
80
+ @loaded = false
81
+ # simplecov:enable
82
+ end
83
+ @loaded
84
+ end
85
+
86
+ # Reset the load state (primarily for testing)
87
+ #
88
+ # @return [void]
89
+ # @api private
90
+ def reset!
91
+ @load_attempted = false
92
+ @loaded = false
93
+ end
94
+
95
+ # Get capabilities supported by this backend
96
+ #
97
+ # @return [Hash{Symbol => Object}] capability map
98
+ # @example
99
+ # TreeHaver::Backends::MRI.capabilities
100
+ # # => { backend: :mri, query: true, bytes_field: true, incremental: true, comment_support: :nodes_only }
101
+ def capabilities
102
+ return {} unless available?
103
+
104
+ {
105
+ backend: :mri,
106
+ query: true,
107
+ bytes_field: true,
108
+ incremental: true,
109
+ comment_support: :nodes_only
110
+ }
111
+ end
112
+ end
113
+
114
+ # Wrapper for ruby_tree_sitter Language
115
+ #
116
+ # Wraps ::TreeSitter::Language from ruby_tree_sitter to provide a consistent
117
+ # API across all backends.
118
+ class Language
119
+ include Comparable
120
+
121
+ # The wrapped TreeSitter::Language object
122
+ # @return [::TreeSitter::Language]
123
+ attr_reader :inner_language
124
+
125
+ # The backend this language is for
126
+ # @return [Symbol]
127
+ attr_reader :backend
128
+
129
+ # The path this language was loaded from (if known)
130
+ # @return [String, nil]
131
+ attr_reader :path
132
+
133
+ # The symbol name (if known)
134
+ # @return [String, nil]
135
+ attr_reader :symbol
136
+
137
+ # @api private
138
+ # @param lang [::TreeSitter::Language] the language object from ruby_tree_sitter
139
+ # @param path [String, nil] path language was loaded from
140
+ # @param symbol [String, nil] symbol name
141
+ def initialize(lang, path: nil, symbol: nil)
142
+ @inner_language = lang
143
+ @backend = :mri
144
+ @path = path
145
+ @symbol = symbol
146
+ end
147
+
148
+ # Get the language name
149
+ #
150
+ # Derives a name from the symbol or path.
151
+ #
152
+ # @return [Symbol] language name
153
+ def language_name
154
+ # Try to derive from symbol (e.g., "tree_sitter_toml" -> :toml)
155
+ if @symbol
156
+ name = @symbol.to_s.sub(/^tree_sitter_/, '')
157
+ return name.to_sym
158
+ end
159
+
160
+ # Try to derive from path (e.g., "/path/to/libtree-sitter-toml.so" -> :toml)
161
+ if @path
162
+ name = LibraryPathUtils.derive_language_name_from_path(@path)
163
+ return name.to_sym if name
164
+ end
165
+
166
+ :unknown
167
+ end
168
+
169
+ # Alias for language_name (API compatibility)
170
+ alias name language_name
171
+
172
+ # Compare languages for equality
173
+ #
174
+ # MRI languages are equal if they have the same backend, path, and symbol.
175
+ # Path and symbol uniquely identify a loaded language.
176
+ #
177
+ # @param other [Object] object to compare with
178
+ # @return [Integer, nil] -1, 0, 1, or nil if not comparable
179
+ def <=>(other)
180
+ return unless other.is_a?(Language)
181
+ return unless other.backend == @backend
182
+
183
+ # Compare by path first, then symbol
184
+ cmp = (@path || '') <=> (other.path || '')
185
+ return cmp if cmp.nonzero?
186
+
187
+ (@symbol || '') <=> (other.symbol || '')
188
+ end
189
+
190
+ # Hash value for this language (for use in Sets/Hashes)
191
+ # @return [Integer]
192
+ def hash
193
+ [@backend, @path, @symbol].hash
194
+ end
195
+
196
+ # Alias eql? to ==
197
+ alias eql? ==
198
+
199
+ # Convert to the underlying TreeSitter::Language for passing to parser
200
+ #
201
+ # @return [::TreeSitter::Language]
202
+ def to_language
203
+ @inner_language
204
+ end
205
+ alias to_ts_language to_language
206
+
207
+ # Load a language from a shared library (preferred method)
208
+ #
209
+ # @param path [String] absolute path to the language shared library
210
+ # @param symbol [String] the exported symbol name (e.g., "tree_sitter_json")
211
+ # @param name [String, nil] optional language name (unused by MRI backend)
212
+ # @return [Language] wrapped language handle
213
+ # @raise [TreeHaver::NotAvailable] if ruby_tree_sitter is not available
214
+ # @example
215
+ # lang = TreeHaver::Backends::MRI::Language.from_library("/path/to/lib.so", symbol: "tree_sitter_json")
216
+ class << self
217
+ def from_library(path, symbol: nil, name: nil)
218
+ # Derive symbol from path if not provided using shared utility
219
+ symbol ||= LibraryPathUtils.derive_symbol_from_path(path)
220
+ from_path(path, symbol: symbol, name: name)
221
+ end
222
+
223
+ private
224
+
225
+ # Load a language from a shared library path (internal implementation)
226
+ #
227
+ # @param path [String] absolute path to the language shared library
228
+ # @param symbol [String] the exported symbol name (e.g., "tree_sitter_json")
229
+ # @param name [String, nil] optional language name
230
+ # @return [Language] wrapped language handle
231
+ # @api private
232
+ def from_path(path, symbol: nil, name: nil)
233
+ raise TreeHaver::NotAvailable, 'ruby_tree_sitter not available' unless MRI.available?
234
+
235
+ # ruby_tree_sitter's TreeSitter::Language.load takes (language_name, path_to_so)
236
+ # where language_name is the language identifier (e.g., "toml", "json")
237
+ # NOT the full symbol name (e.g., NOT "tree_sitter_toml")
238
+ # and path_to_so is the full path to the .so file
239
+ #
240
+ # If name is not provided, derive it from symbol using shared utility
241
+ language_name = (name || LibraryPathUtils.derive_language_name_from_symbol(symbol)).to_s
242
+ ts_lang = ::TreeSitter::Language.load(language_name, path)
243
+ new(ts_lang, path: path, symbol: symbol)
244
+ rescue NameError => e
245
+ # TreeSitter constant doesn't exist - backend not loaded
246
+ raise TreeHaver::NotAvailable, "ruby_tree_sitter not available: #{e.message}"
247
+ rescue Exception => e # rubocop:disable Lint/RescueException
248
+ # TreeSitter errors inherit from Exception (not StandardError) in ruby_tree_sitter v2+
249
+ # We rescue Exception and check the class name dynamically to avoid NameError
250
+ # at parse time when TreeSitter constant isn't loaded yet
251
+ if defined?(TreeSitter::TreeSitterError) && e.is_a?(TreeSitter::TreeSitterError)
252
+ raise TreeHaver::NotAvailable, "Could not load language: #{e.message}"
253
+ end
254
+
255
+ raise # Re-raise if it's not a TreeSitter error
256
+ end
257
+ end
258
+ end
259
+
260
+ # Wrapper for ruby_tree_sitter Parser
261
+ #
262
+ # This is a thin pass-through to ::TreeSitter::Parser from ruby_tree_sitter.
263
+ class Parser
264
+ # Create a new parser instance
265
+ #
266
+ # @raise [TreeHaver::NotAvailable] if ruby_tree_sitter is not available
267
+ def initialize
268
+ raise TreeHaver::NotAvailable, 'ruby_tree_sitter not available' unless MRI.available?
269
+
270
+ @parser = ::TreeSitter::Parser.new
271
+ rescue NameError => e
272
+ # TreeSitter constant doesn't exist - backend not loaded
273
+ raise TreeHaver::NotAvailable, "ruby_tree_sitter not available: #{e.message}"
274
+ rescue Exception => e # rubocop:disable Lint/RescueException
275
+ # TreeSitter errors inherit from Exception (not StandardError) in ruby_tree_sitter v2+
276
+ # We rescue Exception and check the class name dynamically to avoid NameError
277
+ # at parse time when TreeSitter constant isn't loaded yet
278
+ if defined?(TreeSitter::TreeSitterError) && e.is_a?(TreeSitter::TreeSitterError)
279
+ raise TreeHaver::NotAvailable, "Could not create parser: #{e.message}"
280
+ end
281
+
282
+ raise # Re-raise if it's not a TreeSitter error
283
+ end
284
+
285
+ # Set the language for this parser
286
+ #
287
+ # @param lang [::TreeSitter::Language, TreeHaver::Backends::MRI::Language] the language to use
288
+ # @return [::TreeSitter::Language, TreeHaver::Backends::MRI::Language] the language that was set
289
+ # @raise [TreeHaver::NotAvailable] if setting language fails
290
+ def language=(lang)
291
+ # Unwrap if it's a TreeHaver wrapper
292
+ inner_lang = lang.respond_to?(:inner_language) ? lang.inner_language : lang
293
+ @parser.language = inner_lang
294
+ # Verify it was set
295
+ raise TreeHaver::NotAvailable, 'Language not set correctly' if @parser.language.nil?
296
+
297
+ # Return the original language object (wrapped or unwrapped)
298
+ lang
299
+ rescue Exception => e # rubocop:disable Lint/RescueException
300
+ # TreeSitter errors inherit from Exception (not StandardError) in ruby_tree_sitter v2+
301
+ # We rescue Exception and check the class name dynamically to avoid NameError
302
+ # at parse time when TreeSitter constant isn't loaded yet
303
+ if defined?(TreeSitter::TreeSitterError) && e.is_a?(TreeSitter::TreeSitterError)
304
+ raise TreeHaver::NotAvailable, "Could not set language: #{e.message}"
305
+ end
306
+
307
+ raise # Re-raise if it's not a TreeSitter error
308
+ end
309
+
310
+ # Parse source code
311
+ #
312
+ # ruby_tree_sitter provides parse_string for string input
313
+ #
314
+ # @param source [String] the source code to parse
315
+ # @return [::TreeSitter::Tree] raw tree (NOT wrapped - wrapping happens in TreeHaver::Parser)
316
+ # @raise [TreeHaver::NotAvailable] if parsing returns nil (usually means language not set)
317
+ def parse(source)
318
+ # ruby_tree_sitter's parse_string(old_tree, string) method
319
+ # Pass nil for old_tree (initial parse)
320
+ # Return raw tree - TreeHaver::Parser will wrap it
321
+ tree = @parser.parse_string(nil, source)
322
+ raise TreeHaver::NotAvailable, 'Parse returned nil - is language set?' if tree.nil?
323
+
324
+ tree
325
+ rescue Exception => e # rubocop:disable Lint/RescueException
326
+ # TreeSitter errors inherit from Exception (not StandardError) in ruby_tree_sitter v2+
327
+ # We rescue Exception and check the class name dynamically to avoid NameError
328
+ # at parse time when TreeSitter constant isn't loaded yet
329
+ if defined?(TreeSitter::TreeSitterError) && e.is_a?(TreeSitter::TreeSitterError)
330
+ raise TreeHaver::NotAvailable, "Could not parse source: #{e.message}"
331
+ end
332
+
333
+ raise # Re-raise if it's not a TreeSitter error
334
+ end
335
+
336
+ # Parse source code with optional incremental parsing
337
+ #
338
+ # Note: old_tree should already be unwrapped by TreeHaver::Parser before reaching this method.
339
+ # The backend receives the raw inner tree (::TreeSitter::Tree or nil), not a wrapped TreeHaver::Tree.
340
+ #
341
+ # @param old_tree [::TreeSitter::Tree, nil] previous tree for incremental parsing (already unwrapped)
342
+ # @param source [String] the source code to parse
343
+ # @return [::TreeSitter::Tree] raw tree (NOT wrapped - wrapping happens in TreeHaver::Parser)
344
+ # @raise [TreeHaver::NotAvailable] if parsing fails
345
+ def parse_string(old_tree, source)
346
+ # old_tree is already unwrapped by TreeHaver::Parser, pass it directly
347
+ # Return raw tree - TreeHaver::Parser will wrap it
348
+ @parser.parse_string(old_tree, source)
349
+ rescue Exception => e # rubocop:disable Lint/RescueException
350
+ # TreeSitter errors inherit from Exception (not StandardError) in ruby_tree_sitter v2+
351
+ # We rescue Exception and check the class name dynamically to avoid NameError
352
+ # at parse time when TreeSitter constant isn't loaded yet
353
+ if defined?(TreeSitter::TreeSitterError) && e.is_a?(TreeSitter::TreeSitterError)
354
+ raise TreeHaver::NotAvailable, "Could not parse source: #{e.message}"
355
+ end
356
+
357
+ raise # Re-raise if it's not a TreeSitter error
358
+ end
359
+ end
360
+
361
+ # Register the availability checker for RSpec dependency tags
362
+ TreeHaver::BackendRegistry.register_availability_checker(:mri) do
363
+ available?
364
+ end
365
+ end
366
+ end
367
+ end