tree_haver 1.0.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.
@@ -0,0 +1,568 @@
1
+ # frozen_string_literal: true
2
+
3
+ module TreeHaver
4
+ module Backends
5
+ # Java backend for JRuby using java-tree-sitter (jtreesitter)
6
+ #
7
+ # This backend integrates with java-tree-sitter JARs on JRuby,
8
+ # leveraging JRuby's native Java integration for optimal performance.
9
+ #
10
+ # java-tree-sitter provides Java bindings to Tree-sitter and supports:
11
+ # - Parsing source code into syntax trees
12
+ # - Incremental parsing via Parser.parse(Tree, String)
13
+ # - The Query API for pattern matching
14
+ # - Tree editing for incremental re-parsing
15
+ #
16
+ # == Installation
17
+ #
18
+ # 1. Download the JAR from Maven Central:
19
+ # https://central.sonatype.com/artifact/io.github.tree-sitter/jtreesitter
20
+ #
21
+ # 2. Set the environment variable to point to the JAR directory:
22
+ # export TREE_SITTER_JAVA_JARS_DIR=/path/to/jars
23
+ #
24
+ # 3. Use JRuby to run your code:
25
+ # jruby -e "require 'tree_haver'; puts TreeHaver::Backends::Java.available?"
26
+ #
27
+ # @note Only available on JRuby
28
+ # @see https://tree-sitter.github.io/java-tree-sitter/ java-tree-sitter documentation
29
+ # @see https://central.sonatype.com/artifact/io.github.tree-sitter/jtreesitter Maven Central
30
+ module Java
31
+ # The Java package for java-tree-sitter
32
+ JAVA_PACKAGE = "io.github.treesitter.jtreesitter"
33
+
34
+ @load_attempted = false
35
+ @loaded = false
36
+ @java_classes = {} # rubocop:disable ThreadSafety/MutableClassInstanceVariable
37
+ @runtime_lookup = nil # Cached SymbolLookup for libtree-sitter.so
38
+
39
+ module_function
40
+
41
+ # Get the cached runtime library SymbolLookup
42
+ # @return [Object, nil] the SymbolLookup for libtree-sitter.so
43
+ # @api private
44
+ def runtime_lookup
45
+ @runtime_lookup
46
+ end
47
+
48
+ # Set the cached runtime library SymbolLookup
49
+ # @param lookup [Object] the SymbolLookup
50
+ # @api private
51
+ def runtime_lookup=(lookup)
52
+ @runtime_lookup = lookup
53
+ end
54
+
55
+ # Attempt to append JARs from TREE_SITTER_JAVA_JARS_DIR to JRuby classpath
56
+ # and configure native library path from TREE_SITTER_RUNTIME_LIB
57
+ #
58
+ # If the environment variable is set and points to a directory, all .jar files
59
+ # in that directory (recursively) are added to the JRuby classpath.
60
+ #
61
+ # @return [void]
62
+ # @example
63
+ # ENV["TREE_SITTER_JAVA_JARS_DIR"] = "/path/to/java-tree-sitter/jars"
64
+ # TreeHaver::Backends::Java.add_jars_from_env!
65
+ def add_jars_from_env!
66
+ # :nocov:
67
+ # This method requires JRuby and cannot be tested on MRI/CRuby.
68
+ # JRuby-specific CI jobs would test this code.
69
+ require "java"
70
+
71
+ # Add JARs to classpath
72
+ dir = ENV["TREE_SITTER_JAVA_JARS_DIR"]
73
+ if dir && Dir.exist?(dir)
74
+ Dir[File.join(dir, "**", "*.jar")].each do |jar|
75
+ next if $CLASSPATH.include?(jar)
76
+ $CLASSPATH << jar
77
+ end
78
+ end
79
+
80
+ # Configure native library path for libtree-sitter
81
+ # java-tree-sitter uses JNI and needs to find the native library
82
+ configure_native_library_path!
83
+ # :nocov:
84
+ rescue LoadError
85
+ # ignore; not JRuby or Java bridge not available
86
+ end
87
+
88
+ # Configure java.library.path to include the directory containing libtree-sitter
89
+ #
90
+ # @return [void]
91
+ # @api private
92
+ def configure_native_library_path!
93
+ # :nocov:
94
+ # This method requires JRuby and cannot be tested on MRI/CRuby.
95
+ lib_path = ENV["TREE_SITTER_RUNTIME_LIB"]
96
+ return unless lib_path && File.exist?(lib_path)
97
+
98
+ lib_dir = File.dirname(lib_path)
99
+ current_path = java.lang.System.getProperty("java.library.path") || ""
100
+
101
+ unless current_path.include?(lib_dir)
102
+ new_path = current_path.empty? ? lib_dir : "#{lib_dir}:#{current_path}"
103
+ java.lang.System.setProperty("java.library.path", new_path)
104
+
105
+ # Also set jna.library.path in case it uses JNA
106
+ java.lang.System.setProperty("jna.library.path", new_path)
107
+ end
108
+ # :nocov:
109
+ rescue => _error
110
+ # Ignore errors setting library path
111
+ end
112
+
113
+ # Check if the Java backend is available
114
+ #
115
+ # Returns true if running on JRuby and java-tree-sitter classes can be loaded.
116
+ # Automatically attempts to load JARs from ENV["TREE_SITTER_JAVA_JARS_DIR"] if set.
117
+ #
118
+ # @return [Boolean] true if Java backend is available
119
+ # @example
120
+ # if TreeHaver::Backends::Java.available?
121
+ # puts "Java backend is ready"
122
+ # end
123
+ def available?
124
+ return @loaded if @load_attempted
125
+ @load_attempted = true
126
+ @loaded = false
127
+ @load_error = nil
128
+
129
+ return false unless defined?(RUBY_ENGINE) && RUBY_ENGINE == "jruby"
130
+
131
+ # :nocov:
132
+ # Everything below requires JRuby and cannot be tested on MRI/CRuby.
133
+ # JRuby-specific CI jobs would test this code.
134
+ begin
135
+ require "java"
136
+ rescue LoadError
137
+ @load_error = "JRuby java bridge not available"
138
+ return false
139
+ end
140
+
141
+ # Optionally augment classpath and configure native library path
142
+ add_jars_from_env!
143
+
144
+ # Try to load the java-tree-sitter classes
145
+ # Load Parser first as it doesn't trigger native library loading
146
+ # Language class triggers native lib loading in its static initializer
147
+ begin
148
+ # These classes don't require native library initialization
149
+ @java_classes[:Parser] = ::Java::IoGithubTreesitterJtreesitter::Parser
150
+ @java_classes[:Tree] = ::Java::IoGithubTreesitterJtreesitter::Tree
151
+ @java_classes[:Node] = ::Java::IoGithubTreesitterJtreesitter::Node
152
+ @java_classes[:InputEdit] = ::Java::IoGithubTreesitterJtreesitter::InputEdit
153
+ @java_classes[:Point] = ::Java::IoGithubTreesitterJtreesitter::Point
154
+
155
+ # Language class may fail if native library isn't found - try it last
156
+ # and provide a helpful error message
157
+ begin
158
+ @java_classes[:Language] = ::Java::IoGithubTreesitterJtreesitter::Language
159
+ rescue NameError => e
160
+ # Language failed but other classes loaded - native lib issue
161
+ @load_error = "Language class failed to initialize (native library issue): #{e.message}"
162
+ # Clear loaded classes since we can't fully function without Language
163
+ @java_classes.clear
164
+ return false
165
+ end
166
+
167
+ @loaded = true
168
+ rescue NameError => e
169
+ @load_error = "java-tree-sitter classes not found: #{e.message}"
170
+ @loaded = false
171
+ end
172
+
173
+ @loaded
174
+ # :nocov:
175
+ end
176
+
177
+ # Get the last load error message (for debugging)
178
+ #
179
+ # @return [String, nil] the error message or nil if no error
180
+ def load_error
181
+ @load_error
182
+ end
183
+
184
+ # Reset the load state (primarily for testing)
185
+ #
186
+ # @return [void]
187
+ # @api private
188
+ def reset!
189
+ @load_attempted = false
190
+ @loaded = false
191
+ @load_error = nil
192
+ @java_classes = {}
193
+ end
194
+
195
+ # Get the loaded Java classes
196
+ #
197
+ # @return [Hash] the Java class references
198
+ # @api private
199
+ def java_classes
200
+ @java_classes
201
+ end
202
+
203
+ # Get capabilities supported by this backend
204
+ #
205
+ # @return [Hash{Symbol => Object}] capability map
206
+ # @example
207
+ # TreeHaver::Backends::Java.capabilities
208
+ # # => { backend: :java, parse: true, query: true, bytes_field: true, incremental: true }
209
+ def capabilities
210
+ # :nocov:
211
+ # This method returns meaningful data only on JRuby when java-tree-sitter is available.
212
+ return {} unless available?
213
+ {
214
+ backend: :java,
215
+ parse: true,
216
+ query: true, # java-tree-sitter supports the Query API
217
+ bytes_field: true,
218
+ incremental: true, # java-tree-sitter supports Parser.parse(Tree, String)
219
+ }
220
+ # :nocov:
221
+ end
222
+
223
+ # Wrapper for java-tree-sitter Language
224
+ #
225
+ # @see https://tree-sitter.github.io/java-tree-sitter/io/github/treesitter/jtreesitter/Language.html
226
+ #
227
+ # :nocov:
228
+ # All Java backend implementation classes require JRuby and cannot be tested on MRI/CRuby.
229
+ # JRuby-specific CI jobs would test this code.
230
+ class Language
231
+ attr_reader :impl
232
+
233
+ # @api private
234
+ def initialize(impl)
235
+ @impl = impl
236
+ end
237
+
238
+ # Load a language from a shared library
239
+ #
240
+ # There are three ways java-tree-sitter can load shared libraries:
241
+ #
242
+ # 1. Libraries in OS library search path (LD_LIBRARY_PATH on Linux,
243
+ # DYLD_LIBRARY_PATH on macOS, PATH on Windows) - loaded via
244
+ # SymbolLookup.libraryLookup(String, Arena)
245
+ #
246
+ # 2. Libraries in java.library.path - loaded via SymbolLookup.loaderLookup()
247
+ #
248
+ # 3. Custom NativeLibraryLookup implementation (e.g., for JARs)
249
+ #
250
+ # @param path [String] path to language shared library (.so/.dylib) or library name
251
+ # @param symbol [String, nil] exported symbol name (e.g., "tree_sitter_toml")
252
+ # @param name [String, nil] logical name (used to derive symbol if not provided)
253
+ # @return [Language] the loaded language
254
+ # @raise [TreeHaver::NotAvailable] if Java backend is not available
255
+ # @example Load by path
256
+ # lang = TreeHaver::Backends::Java::Language.from_library(
257
+ # "/usr/lib/libtree-sitter-toml.so",
258
+ # symbol: "tree_sitter_toml"
259
+ # )
260
+ # @example Load by name (searches LD_LIBRARY_PATH)
261
+ # lang = TreeHaver::Backends::Java::Language.from_library(
262
+ # "tree-sitter-toml",
263
+ # symbol: "tree_sitter_toml"
264
+ # )
265
+ class << self
266
+ def from_library(path, symbol: nil, name: nil)
267
+ raise TreeHaver::NotAvailable, "Java backend not available" unless Java.available?
268
+
269
+ # Derive symbol from name or path if not provided
270
+ base_name = File.basename(path, ".*").sub(/^lib/, "")
271
+ sym = symbol || "tree_sitter_#{name || base_name.sub(/^tree-sitter-/, "")}"
272
+
273
+ begin
274
+ arena = ::Java::JavaLangForeign::Arena.global
275
+ symbol_lookup_class = ::Java::JavaLangForeign::SymbolLookup
276
+
277
+ # IMPORTANT: Load libtree-sitter.so FIRST by name so its symbols are available
278
+ # Grammar libraries need symbols like ts_language_version from the runtime
279
+ # We cache this lookup at the module level
280
+ unless Java.runtime_lookup
281
+ # Use libraryLookup(String, Arena) to search LD_LIBRARY_PATH
282
+ Java.runtime_lookup = symbol_lookup_class.libraryLookup("libtree-sitter.so", arena)
283
+ end
284
+
285
+ # Now load the grammar library
286
+ if File.exist?(path)
287
+ # Explicit path provided - use libraryLookup(Path, Arena)
288
+ java_path = ::Java::JavaNioFile::Paths.get(path)
289
+ grammar_lookup = symbol_lookup_class.libraryLookup(java_path, arena)
290
+ else
291
+ # Library name provided - use libraryLookup(String, Arena) to search
292
+ # LD_LIBRARY_PATH / DYLD_LIBRARY_PATH / PATH
293
+ grammar_lookup = symbol_lookup_class.libraryLookup(path, arena)
294
+ end
295
+
296
+ # Chain the lookups: grammar first, then runtime library for ts_* symbols
297
+ # This makes ts_language_version available when Language.load() needs it
298
+ combined_lookup = grammar_lookup.or(Java.runtime_lookup)
299
+
300
+ java_lang = Java.java_classes[:Language].load(combined_lookup, sym)
301
+ new(java_lang)
302
+ rescue ::Java::JavaLang::RuntimeException => e
303
+ cause = e.cause
304
+ root_cause = cause&.cause || cause
305
+
306
+ error_msg = "Failed to load language '#{sym}' from #{path}: #{e.message}"
307
+ if root_cause.is_a?(::Java::JavaLang::UnsatisfiedLinkError)
308
+ unresolved = root_cause.message.to_s
309
+ if unresolved.include?("ts_language_version")
310
+ # This specific symbol was renamed in tree-sitter 0.24
311
+ error_msg += "\n\nVersion mismatch detected: The grammar was built against " \
312
+ "tree-sitter < 0.24 (uses ts_language_version), but your runtime library " \
313
+ "is tree-sitter >= 0.24 (uses ts_language_abi_version).\n\n" \
314
+ "Solutions:\n" \
315
+ "1. Rebuild the grammar against your version of tree-sitter\n" \
316
+ "2. Install a matching version of tree-sitter (< 0.24)\n" \
317
+ "3. Find a pre-built grammar compatible with tree-sitter 0.24+"
318
+ elsif unresolved.include?("ts_language") || unresolved.include?("ts_parser")
319
+ error_msg += "\n\nThe grammar library has unresolved tree-sitter symbols. " \
320
+ "Ensure libtree-sitter.so is in LD_LIBRARY_PATH and version-compatible " \
321
+ "with the grammar."
322
+ end
323
+ end
324
+ raise TreeHaver::NotAvailable, error_msg
325
+ rescue ::Java::JavaLang::UnsatisfiedLinkError => e
326
+ raise TreeHaver::NotAvailable,
327
+ "Native library error loading #{path}: #{e.message}. " \
328
+ "Ensure the library is in LD_LIBRARY_PATH."
329
+ rescue ::Java::JavaLang::IllegalArgumentException => e
330
+ raise TreeHaver::NotAvailable,
331
+ "Could not find library '#{path}': #{e.message}. " \
332
+ "Ensure it's in LD_LIBRARY_PATH or provide an absolute path."
333
+ end
334
+ end
335
+
336
+ # Load a language by name from java-tree-sitter grammar JARs
337
+ #
338
+ # This method loads grammars that are packaged as java-tree-sitter JARs
339
+ # from Maven Central. These JARs include the native grammar library
340
+ # pre-built for Java's Foreign Function API.
341
+ #
342
+ # @param name [String] the language name (e.g., "java", "python", "toml")
343
+ # @return [Language] the loaded language
344
+ # @raise [TreeHaver::NotAvailable] if the language JAR is not available
345
+ #
346
+ # @example
347
+ # # First, add the grammar JAR to TREE_SITTER_JAVA_JARS_DIR:
348
+ # # tree-sitter-toml-0.23.2.jar from Maven Central
349
+ # lang = TreeHaver::Backends::Java::Language.load_by_name("toml")
350
+ def load_by_name(name)
351
+ raise TreeHaver::NotAvailable, "Java backend not available" unless Java.available?
352
+
353
+ begin
354
+ # java-tree-sitter's Language.load(String) searches for the language
355
+ # in the classpath using standard naming conventions
356
+ java_lang = Java.java_classes[:Language].load(name)
357
+ new(java_lang)
358
+ rescue ::Java::JavaLang::RuntimeException => e
359
+ raise TreeHaver::NotAvailable,
360
+ "Failed to load language '#{name}': #{e.message}. " \
361
+ "Ensure the grammar JAR (e.g., tree-sitter-#{name}-X.Y.Z.jar) " \
362
+ "is in TREE_SITTER_JAVA_JARS_DIR."
363
+ end
364
+ end
365
+ end
366
+
367
+ class << self
368
+ alias_method :from_path, :from_library
369
+ end
370
+ end
371
+
372
+ # Wrapper for java-tree-sitter Parser
373
+ #
374
+ # @see https://tree-sitter.github.io/java-tree-sitter/io/github/treesitter/jtreesitter/Parser.html
375
+ class Parser
376
+ # Create a new parser instance
377
+ #
378
+ # @raise [TreeHaver::NotAvailable] if Java backend is not available
379
+ def initialize
380
+ raise TreeHaver::NotAvailable, "Java backend not available" unless Java.available?
381
+ @parser = Java.java_classes[:Parser].new
382
+ end
383
+
384
+ # Set the language for this parser
385
+ #
386
+ # @param lang [Language] the language to use
387
+ # @return [void]
388
+ def language=(lang)
389
+ java_lang = lang.is_a?(Language) ? lang.impl : lang
390
+ @parser.language = java_lang
391
+ end
392
+
393
+ # Parse source code
394
+ #
395
+ # @param source [String] the source code to parse
396
+ # @return [Tree] the parsed syntax tree
397
+ def parse(source)
398
+ java_tree = @parser.parse(source)
399
+ Tree.new(java_tree)
400
+ end
401
+
402
+ # Parse source code with optional incremental parsing
403
+ #
404
+ # When old_tree is provided and has been edited, tree-sitter will reuse
405
+ # unchanged nodes for better performance.
406
+ #
407
+ # @param old_tree [Tree, nil] previous tree for incremental parsing
408
+ # @param source [String] the source code to parse
409
+ # @return [Tree] the parsed syntax tree
410
+ # @see https://tree-sitter.github.io/java-tree-sitter/io/github/treesitter/jtreesitter/Parser.html#parse(io.github.treesitter.jtreesitter.Tree,java.lang.String)
411
+ def parse_string(old_tree, source)
412
+ if old_tree
413
+ java_old_tree = old_tree.is_a?(Tree) ? old_tree.impl : old_tree
414
+ java_tree = @parser.parse(java_old_tree, source)
415
+ else
416
+ java_tree = @parser.parse(source)
417
+ end
418
+ Tree.new(java_tree)
419
+ end
420
+ end
421
+
422
+ # Wrapper for java-tree-sitter Tree
423
+ #
424
+ # @see https://tree-sitter.github.io/java-tree-sitter/io/github/treesitter/jtreesitter/Tree.html
425
+ class Tree
426
+ attr_reader :impl
427
+
428
+ # @api private
429
+ def initialize(impl)
430
+ @impl = impl
431
+ end
432
+
433
+ # Get the root node of the tree
434
+ #
435
+ # @return [Node] the root node
436
+ def root_node
437
+ Node.new(@impl.rootNode)
438
+ end
439
+
440
+ # Mark the tree as edited for incremental re-parsing
441
+ #
442
+ # @param start_byte [Integer] byte offset where the edit starts
443
+ # @param old_end_byte [Integer] byte offset where the old text ended
444
+ # @param new_end_byte [Integer] byte offset where the new text ends
445
+ # @param start_point [Hash] starting position as `{ row:, column: }`
446
+ # @param old_end_point [Hash] old ending position as `{ row:, column: }`
447
+ # @param new_end_point [Hash] new ending position as `{ row:, column: }`
448
+ # @return [void]
449
+ def edit(start_byte:, old_end_byte:, new_end_byte:, start_point:, old_end_point:, new_end_point:)
450
+ point_class = Java.java_classes[:Point]
451
+ input_edit_class = Java.java_classes[:InputEdit]
452
+
453
+ start_pt = point_class.new(start_point[:row], start_point[:column])
454
+ old_end_pt = point_class.new(old_end_point[:row], old_end_point[:column])
455
+ new_end_pt = point_class.new(new_end_point[:row], new_end_point[:column])
456
+
457
+ input_edit = input_edit_class.new(
458
+ start_byte,
459
+ old_end_byte,
460
+ new_end_byte,
461
+ start_pt,
462
+ old_end_pt,
463
+ new_end_pt,
464
+ )
465
+
466
+ @impl.edit(input_edit)
467
+ end
468
+ end
469
+
470
+ # Wrapper for java-tree-sitter Node
471
+ #
472
+ # @see https://tree-sitter.github.io/java-tree-sitter/io/github/treesitter/jtreesitter/Node.html
473
+ class Node
474
+ attr_reader :impl
475
+
476
+ # @api private
477
+ def initialize(impl)
478
+ @impl = impl
479
+ end
480
+
481
+ # Get the type of this node
482
+ #
483
+ # @return [String] the node type
484
+ def type
485
+ @impl.type
486
+ end
487
+
488
+ # Get the number of children
489
+ #
490
+ # @return [Integer] child count
491
+ def child_count
492
+ @impl.childCount
493
+ end
494
+
495
+ # Get a child by index
496
+ #
497
+ # @param index [Integer] the child index
498
+ # @return [Node] the child node
499
+ def child(index)
500
+ Node.new(@impl.child(index))
501
+ end
502
+
503
+ # Iterate over children
504
+ #
505
+ # @yield [Node] each child node
506
+ # @return [void]
507
+ def each
508
+ return enum_for(:each) unless block_given?
509
+ child_count.times do |i|
510
+ yield child(i)
511
+ end
512
+ end
513
+
514
+ # Get the start byte position
515
+ #
516
+ # @return [Integer] start byte
517
+ def start_byte
518
+ @impl.startByte
519
+ end
520
+
521
+ # Get the end byte position
522
+ #
523
+ # @return [Integer] end byte
524
+ def end_byte
525
+ @impl.endByte
526
+ end
527
+
528
+ # Get the start point (row, column)
529
+ #
530
+ # @return [Hash] with :row and :column keys
531
+ def start_point
532
+ pt = @impl.startPoint
533
+ {row: pt.row, column: pt.column}
534
+ end
535
+
536
+ # Get the end point (row, column)
537
+ #
538
+ # @return [Hash] with :row and :column keys
539
+ def end_point
540
+ pt = @impl.endPoint
541
+ {row: pt.row, column: pt.column}
542
+ end
543
+
544
+ # Check if this node has an error
545
+ #
546
+ # @return [Boolean] true if the node or any descendant has an error
547
+ def has_error?
548
+ @impl.hasError
549
+ end
550
+
551
+ # Check if this node is missing
552
+ #
553
+ # @return [Boolean] true if this is a MISSING node
554
+ def missing?
555
+ @impl.isMissing
556
+ end
557
+
558
+ # Get the text of this node
559
+ #
560
+ # @return [String] the source text
561
+ def text
562
+ @impl.text.to_s
563
+ end
564
+ end
565
+ # :nocov:
566
+ end
567
+ end
568
+ end
@@ -0,0 +1,129 @@
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
+ # @note This backend only works on MRI Ruby, not JRuby or TruffleRuby
12
+ # @see https://github.com/Faveod/ruby-tree-sitter ruby_tree_sitter
13
+ module MRI
14
+ @load_attempted = false
15
+ @loaded = false
16
+
17
+ # Check if the MRI backend is available
18
+ #
19
+ # Attempts to require ruby_tree_sitter on first call and caches the result.
20
+ #
21
+ # @return [Boolean] true if ruby_tree_sitter is available
22
+ # @example
23
+ # if TreeHaver::Backends::MRI.available?
24
+ # puts "MRI backend is ready"
25
+ # end
26
+ class << self
27
+ def available?
28
+ return @loaded if @load_attempted # rubocop:disable ThreadSafety/ClassInstanceVariable
29
+ @load_attempted = true # rubocop:disable ThreadSafety/ClassInstanceVariable
30
+ begin
31
+ require "ruby_tree_sitter"
32
+
33
+ @loaded = true # rubocop:disable ThreadSafety/ClassInstanceVariable
34
+ rescue LoadError
35
+ @loaded = false # rubocop:disable ThreadSafety/ClassInstanceVariable
36
+ end
37
+ @loaded # rubocop:disable ThreadSafety/ClassInstanceVariable
38
+ end
39
+
40
+ # Get capabilities supported by this backend
41
+ #
42
+ # @return [Hash{Symbol => Object}] capability map
43
+ # @example
44
+ # TreeHaver::Backends::MRI.capabilities
45
+ # # => { backend: :mri, query: true, bytes_field: true, incremental: true }
46
+ def capabilities
47
+ return {} unless available?
48
+ {
49
+ backend: :mri,
50
+ query: true,
51
+ bytes_field: true,
52
+ incremental: true,
53
+ }
54
+ end
55
+ end
56
+
57
+ # Wrapper for ruby_tree_sitter Language
58
+ #
59
+ # This is a thin pass-through to ::TreeSitter::Language from ruby_tree_sitter.
60
+ class Language
61
+ # Load a language from a shared library path
62
+ #
63
+ # @param path [String] absolute path to the language shared library
64
+ # @return [::TreeSitter::Language] the loaded language handle
65
+ # @raise [TreeHaver::NotAvailable] if ruby_tree_sitter is not available
66
+ # @example
67
+ # lang = TreeHaver::Backends::MRI::Language.from_path("/usr/local/lib/libtree-sitter-toml.so")
68
+ class << self
69
+ def from_path(path)
70
+ raise TreeHaver::NotAvailable, "ruby_tree_sitter not available" unless MRI.available?
71
+ ::TreeSitter::Language.load(path)
72
+ end
73
+ end
74
+ end
75
+
76
+ # Wrapper for ruby_tree_sitter Parser
77
+ #
78
+ # This is a thin pass-through to ::TreeSitter::Parser from ruby_tree_sitter.
79
+ class Parser
80
+ # Create a new parser instance
81
+ #
82
+ # @raise [TreeHaver::NotAvailable] if ruby_tree_sitter is not available
83
+ def initialize
84
+ raise TreeHaver::NotAvailable, "ruby_tree_sitter not available" unless MRI.available?
85
+ @parser = ::TreeSitter::Parser.new
86
+ end
87
+
88
+ # Set the language for this parser
89
+ #
90
+ # @param lang [::TreeSitter::Language] the language to use
91
+ # @return [::TreeSitter::Language] the language that was set
92
+ def language=(lang)
93
+ @parser.language = lang
94
+ end
95
+
96
+ # Parse source code
97
+ #
98
+ # @param source [String] the source code to parse
99
+ # @return [::TreeSitter::Tree] the parsed syntax tree
100
+ def parse(source)
101
+ @parser.parse(source)
102
+ end
103
+
104
+ # Parse source code with optional incremental parsing
105
+ #
106
+ # @param old_tree [::TreeSitter::Tree, nil] previous tree for incremental parsing
107
+ # @param source [String] the source code to parse
108
+ # @return [::TreeSitter::Tree] the parsed syntax tree
109
+ def parse_string(old_tree, source)
110
+ @parser.parse_string(old_tree, source)
111
+ end
112
+ end
113
+
114
+ # Wrapper for ruby_tree_sitter Tree
115
+ #
116
+ # Not used directly; TreeHaver passes through ::TreeSitter::Tree objects.
117
+ class Tree
118
+ # Not used directly; we pass through ruby_tree_sitter::Tree
119
+ end
120
+
121
+ # Wrapper for ruby_tree_sitter Node
122
+ #
123
+ # Not used directly; TreeHaver passes through ::TreeSitter::Node objects.
124
+ class Node
125
+ # Not used directly; we pass through ruby_tree_sitter::Node
126
+ end
127
+ end
128
+ end
129
+ end