• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

kettle-rb / tree_haver / 20811468724

08 Jan 2026 09:07AM UTC coverage: 91.873% (+5.0%) from 86.828%
20811468724

push

github

pboling
♻️ Major refactor

### Added

- **BackendRegistry**: New `TreeHaver::BackendRegistry` module for registering backend availability checkers
  - Allows external gems (like `commonmarker-merge`, `markly-merge`, `rbs-merge`) to register their availability checkers
  - `register_availability_checker(backend_name, &block)` - Register a callable that returns true if backend is available
  - `available?(backend_name)` - Check if a backend is available (results are cached)
  - `registered?(backend_name)` - Check if a checker is registered
  - `registered_backends` - Get all registered backend names
  - Used by `TreeHaver::RSpec::DependencyTags` for dynamic backend detection
- **Plugin System**: `commonmarker-merge` and `markly-merge` now provide their own backends via `TreeHaver`'s registry system, removing them from `TreeHaver` core.
- **Backend Architecture Documentation**: Added comprehensive documentation to base classes and all tree-sitter backends explaining the two backend categories:
  - Tree-sitter backends (MRI, Rust, FFI, Java): Use `TreeHaver::Tree` and `TreeHaver::Node` wrappers for raw tree-sitter objects
  - Pure-Ruby/Plugin backends (Citrus, Prism, Psych, Commonmarker, Markly): Define own `Backend::X::Tree` and `Backend::X::Node` classes

### Changed

- **Base Class Inheritance**: `TreeHaver::Tree` and `TreeHaver::Node` now properly inherit from their respective `Base::` classes
  - `TreeHaver::Tree < Base::Tree` - inherits `inner_tree`, `source`, `lines` attributes and default implementations
  - `TreeHaver::Node < Base::Node` - inherits `inner_node`, `source`, `lines` attributes and API contract
  - Base classes document the API contract; subclasses document divergence
- **Base::Node#initialize**: Now accepts keyword arguments `source:` and `lines:` instead of positional for consistency with subclasses
- **DependencyTags**: Now uses `BackendRegistry.available?(:backend_name)` instead of hardcoded `TreeHaver::Backends::*` checks
- **TreeHaver*... (continued)

805 of 956 branches covered (84.21%)

Branch coverage included in aggregate %.

385 of 404 new or added lines in 17 files covered. (95.3%)

1 existing line in 1 file now uncovered.

2021 of 2120 relevant lines covered (95.33%)

43.66 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

80.85
/lib/tree_haver/backends/java.rb
1
# frozen_string_literal: true
2

3
module TreeHaver
3✔
4
  module Backends
3✔
5
    # Java backend for JRuby using jtreesitter (java-tree-sitter)
6
    #
7
    # This backend integrates with jtreesitter JARs on JRuby,
8
    # leveraging JRuby's native Java integration for optimal performance.
9
    #
10
    # == Features
11
    #
12
    # jtreesitter (java-tree-sitter) provides Java bindings to tree-sitter and supports:
13
    # - Parsing source code into syntax trees
14
    # - Incremental parsing via Parser.parse(Tree, String)
15
    # - The Query API for pattern matching
16
    # - Tree editing for incremental re-parsing
17
    #
18
    # == Tree/Node Architecture
19
    #
20
    # This backend (like all tree-sitter backends: MRI, Rust, FFI, Java) does NOT
21
    # define its own Tree or Node classes at the Ruby level. Instead:
22
    #
23
    # - Parser#parse returns raw Java tree objects (via JRuby interop)
24
    # - These are wrapped by `TreeHaver::Tree` (inherits from `Base::Tree`)
25
    # - `TreeHaver::Tree#root_node` wraps raw nodes in `TreeHaver::Node`
26
    #
27
    # This differs from pure-Ruby backends (Citrus, Prism, Psych) which define
28
    # their own `Backend::X::Tree` and `Backend::X::Node` classes.
29
    #
30
    # @see TreeHaver::Tree The wrapper class for tree-sitter Tree objects
31
    # @see TreeHaver::Node The wrapper class for tree-sitter Node objects
32
    # @see TreeHaver::Base::Tree Base class documenting the Tree API contract
33
    # @see TreeHaver::Base::Node Base class documenting the Node API contract
34
    #
35
    # == Version Requirements
36
    #
37
    # - jtreesitter >= 0.26.0 (required)
38
    # - tree-sitter runtime library >= 0.26.0 (must match jtreesitter version)
39
    #
40
    # Older versions of jtreesitter are NOT supported due to API changes.
41
    #
42
    # == Platform Compatibility
43
    #
44
    # - MRI Ruby: ✗ Not available (no JVM)
45
    # - JRuby: ✓ Full support (native Java integration)
46
    # - TruffleRuby: ✗ Not available (jtreesitter requires JRuby's Java interop)
47
    #
48
    # == Installation
49
    #
50
    # 1. Download jtreesitter 0.26.0+ JAR from Maven Central:
51
    #    https://central.sonatype.com/artifact/io.github.tree-sitter/jtreesitter
52
    #
53
    # 2. Set the environment variable to point to the JAR directory:
54
    #    export TREE_SITTER_JAVA_JARS_DIR=/path/to/jars
55
    #
56
    # 3. Use JRuby to run your code:
57
    #    jruby -e "require 'tree_haver'; puts TreeHaver::Backends::Java.available?"
58
    #
59
    # @see https://github.com/tree-sitter/java-tree-sitter source
60
    # @see https://tree-sitter.github.io/java-tree-sitter jtreesitter documentation
61
    # @see https://central.sonatype.com/artifact/io.github.tree-sitter/jtreesitter Maven Central
62
    module Java
3✔
63
      # The Java package for java-tree-sitter
64
      JAVA_PACKAGE = "io.github.treesitter.jtreesitter"
3✔
65

66
      @load_attempted = false
3✔
67
      @loaded = false
3✔
68
      @java_classes = {} # rubocop:disable ThreadSafety/MutableClassInstanceVariable
3✔
69
      @runtime_lookup = nil  # Cached SymbolLookup for libtree-sitter.so
3✔
70

71
      module_function
3✔
72

73
      # Get the cached runtime library SymbolLookup
74
      # @return [Object, nil] the SymbolLookup for libtree-sitter.so
75
      # @api private
76
      def runtime_lookup
3✔
77
        @runtime_lookup
34✔
78
      end
79

80
      # Set the cached runtime library SymbolLookup
81
      # @param lookup [Object] the SymbolLookup
82
      # @api private
83
      def runtime_lookup=(lookup)
3✔
84
        @runtime_lookup = lookup
34✔
85
      end
86

87
      # Attempt to append JARs from TREE_SITTER_JAVA_JARS_DIR to JRuby classpath
88
      # and configure native library path from TREE_SITTER_RUNTIME_LIB
89
      #
90
      # If the environment variable is set and points to a directory, all .jar files
91
      # in that directory (recursively) are added to the JRuby classpath.
92
      #
93
      # @return [void]
94
      # @example
95
      #   ENV["TREE_SITTER_JAVA_JARS_DIR"] = "/path/to/java-tree-sitter/jars"
96
      #   TreeHaver::Backends::Java.add_jars_from_env!
97
      def add_jars_from_env!
3✔
98
        # :nocov:
99
        # This method requires JRuby and cannot be tested on MRI/CRuby.
100
        # JRuby-specific CI jobs would test this code.
101
        require "java"
102

103
        # Add JARs to classpath
104
        dir = ENV["TREE_SITTER_JAVA_JARS_DIR"]
105
        if dir && Dir.exist?(dir)
106
          Dir[File.join(dir, "**", "*.jar")].each do |jar|
107
            next if $CLASSPATH.include?(jar)
108
            $CLASSPATH << jar
109
          end
110
        end
111

112
        # Configure native library path for libtree-sitter
113
        # java-tree-sitter uses JNI and needs to find the native library
114
        configure_native_library_path!
115
        # :nocov:
116
      rescue LoadError
117
        # ignore; not JRuby or Java bridge not available
118
      end
119

120
      # Configure java.library.path to include the directory containing libtree-sitter
121
      #
122
      # @return [void]
123
      # @api private
124
      def configure_native_library_path!
3✔
125
        # :nocov:
126
        # This method requires JRuby and cannot be tested on MRI/CRuby.
127
        lib_path = ENV["TREE_SITTER_RUNTIME_LIB"]
128
        return unless lib_path && File.exist?(lib_path)
129

130
        lib_dir = File.dirname(lib_path)
131
        current_path = java.lang.System.getProperty("java.library.path") || ""
132

133
        unless current_path.include?(lib_dir)
134
          new_path = current_path.empty? ? lib_dir : "#{lib_dir}:#{current_path}"
135
          java.lang.System.setProperty("java.library.path", new_path)
136

137
          # Also set jna.library.path in case it uses JNA
138
          java.lang.System.setProperty("jna.library.path", new_path)
139
        end
140
        # :nocov:
141
      rescue => _error
142
        # Ignore errors setting library path
143
      end
144

145
      # Check if the Java backend is available
146
      #
147
      # Checks if:
148
      # 1. We're running on JRuby
149
      # 2. Environment variable TREE_SITTER_JAVA_JARS_DIR is set
150
      # 3. Required JARs (jtreesitter, tree-sitter) are present in that directory
151
      #
152
      # @return [Boolean] true if Java backend is available
153
      # @example
154
      #   if TreeHaver::Backends::Java.available?
155
      #     puts "Java backend ready"
156
      #   end
157
      class << self
3✔
158
        def available?
3✔
159
          return @loaded if @load_attempted # rubocop:disable ThreadSafety/ClassInstanceVariable
7✔
160
          @load_attempted = true # rubocop:disable ThreadSafety/ClassInstanceVariable
4✔
161
          @loaded = check_availability # rubocop:disable ThreadSafety/ClassInstanceVariable
4✔
162
        end
163

164
        # Reset the load state (primarily for testing)
165
        #
166
        # @return [void]
167
        # @api private
168
        def reset!
3✔
169
          @load_attempted = false # rubocop:disable ThreadSafety/ClassInstanceVariable
5✔
170
          @loaded = false # rubocop:disable ThreadSafety/ClassInstanceVariable
5✔
171
          @load_error = nil # rubocop:disable ThreadSafety/ClassInstanceVariable
5✔
172
          @loader = nil # rubocop:disable ThreadSafety/ClassInstanceVariable
5✔
173
          @java_classes = {} # rubocop:disable ThreadSafety/ClassInstanceVariable
5✔
174
        end
175

176
        private
3✔
177

178
        def check_availability
3✔
179
          # 1. Check Ruby engine
180
          return false unless RUBY_ENGINE == "jruby"
4!
181

182
          # 2. Check for required JARs via environment variable
NEW
183
          jars_dir = ENV["TREE_SITTER_JAVA_JARS_DIR"]
×
NEW
184
          return false unless jars_dir && Dir.exist?(jars_dir)
×
185

186
          # 3. Check if we can load the classes
187
          begin
NEW
188
            ensure_loader_initialized!
×
NEW
189
            true
×
190
          rescue LoadError, NameError
NEW
191
            false
×
192
          end
193
        end
194
      end
195

196
      # Get the last load error message (for debugging)
197
      #
198
      # @return [String, nil] the error message or nil if no error
199
      def load_error
3✔
200
        @load_error
2✔
201
      end
202

203

204
      # Get the loaded Java classes
205
      #
206
      # @return [Hash] the Java class references
207
      # @api private
208
      def java_classes
3✔
209
        @java_classes
2✔
210
      end
211

212
      # Get capabilities supported by this backend
213
      #
214
      # @return [Hash{Symbol => Object}] capability map
215
      # @example
216
      #   TreeHaver::Backends::Java.capabilities
217
      #   # => { backend: :java, parse: true, query: true, bytes_field: true, incremental: true }
218
      def capabilities
3✔
219
        # :nocov:
220
        # This method returns meaningful data only on JRuby when java-tree-sitter is available.
221
        return {} unless available?
222
        {
223
          backend: :java,
224
          parse: true,
225
          query: true, # java-tree-sitter supports the Query API
226
          bytes_field: true,
227
          incremental: true, # java-tree-sitter supports Parser.parse(Tree, String)
228
        }
229
        # :nocov:
230
      end
231

232
      # Wrapper for java-tree-sitter Language
233
      #
234
      # @see https://tree-sitter.github.io/java-tree-sitter/io/github/treesitter/jtreesitter/Language.html
235
      #
236
      # :nocov:
237
      # All Java backend implementation classes require JRuby and cannot be tested on MRI/CRuby.
238
      # JRuby-specific CI jobs would test this code.
239
      class Language
240
        include Comparable
241

242
        attr_reader :impl
243

244
        # The backend this language is for
245
        # @return [Symbol]
246
        attr_reader :backend
247

248
        # The path this language was loaded from (if known)
249
        # @return [String, nil]
250
        attr_reader :path
251

252
        # The symbol name (if known)
253
        # @return [String, nil]
254
        attr_reader :symbol
255

256
        # @api private
257
        def initialize(impl, path: nil, symbol: nil)
258
          @impl = impl
259
          @backend = :java
260
          @path = path
261
          @symbol = symbol
262
        end
263

264
        # Compare languages for equality
265
        #
266
        # Java languages are equal if they have the same backend, path, and symbol.
267
        # Path and symbol uniquely identify a loaded language.
268
        #
269
        # @param other [Object] object to compare with
270
        # @return [Integer, nil] -1, 0, 1, or nil if not comparable
271
        def <=>(other)
272
          return unless other.is_a?(Language)
273
          return unless other.backend == @backend
274

275
          # Compare by path first, then symbol
276
          cmp = (@path || "") <=> (other.path || "")
277
          return cmp if cmp.nonzero?
278

279
          (@symbol || "") <=> (other.symbol || "")
280
        end
281

282
        # Hash value for this language (for use in Sets/Hashes)
283
        # @return [Integer]
284
        def hash
285
          [@backend, @path, @symbol].hash
286
        end
287

288
        # Alias eql? to ==
289
        alias_method :eql?, :==
290

291
        # Load a language from a shared library
292
        #
293
        # There are three ways java-tree-sitter can load shared libraries:
294
        #
295
        # 1. Libraries in OS library search path (LD_LIBRARY_PATH on Linux,
296
        #    DYLD_LIBRARY_PATH on macOS, PATH on Windows) - loaded via
297
        #    SymbolLookup.libraryLookup(String, Arena)
298
        #
299
        # 2. Libraries in java.library.path - loaded via SymbolLookup.loaderLookup()
300
        #
301
        # 3. Custom NativeLibraryLookup implementation (e.g., for JARs)
302
        #
303
        # @param path [String] path to language shared library (.so/.dylib) or library name
304
        # @param symbol [String, nil] exported symbol name (e.g., "tree_sitter_toml")
305
        # @param name [String, nil] logical name (used to derive symbol if not provided)
306
        # @return [Language] the loaded language
307
        # @raise [TreeHaver::NotAvailable] if Java backend is not available
308
        # @example Load by path
309
        #   lang = TreeHaver::Backends::Java::Language.from_library(
310
        #     "/usr/lib/libtree-sitter-toml.so",
311
        #     symbol: "tree_sitter_toml"
312
        #   )
313
        # @example Load by name (searches LD_LIBRARY_PATH)
314
        #   lang = TreeHaver::Backends::Java::Language.from_library(
315
        #     "tree-sitter-toml",
316
        #     symbol: "tree_sitter_toml"
317
        #   )
318
        class << self
319
          def from_library(path, symbol: nil, name: nil)
320
            raise TreeHaver::NotAvailable, "Java backend not available" unless Java.available?
321

322
            # Use shared utility for consistent symbol derivation across backends
323
            # If symbol not provided, derive from name or path
324
            sym = symbol || LibraryPathUtils.derive_symbol_from_path(path)
325
            # If name was provided, use it to override the derived symbol
326
            sym = "tree_sitter_#{name}" if name && !symbol
327

328
            begin
329
              arena = ::Java::JavaLangForeign::Arena.global
330
              symbol_lookup_class = ::Java::JavaLangForeign::SymbolLookup
331

332
              # IMPORTANT: Load libtree-sitter.so FIRST by name so its symbols are available
333
              # Grammar libraries need symbols like ts_language_version from the runtime
334
              # We cache this lookup at the module level
335
              unless Java.runtime_lookup
336
                # Use libraryLookup(String, Arena) to search LD_LIBRARY_PATH
337
                Java.runtime_lookup = symbol_lookup_class.libraryLookup("libtree-sitter.so", arena)
338
              end
339

340
              # Now load the grammar library
341
              if File.exist?(path)
342
                # Explicit path provided - use libraryLookup(Path, Arena)
343
                java_path = ::Java::JavaNioFile::Paths.get(path)
344
                grammar_lookup = symbol_lookup_class.libraryLookup(java_path, arena)
345
              else
346
                # Library name provided - use libraryLookup(String, Arena) to search
347
                # LD_LIBRARY_PATH / DYLD_LIBRARY_PATH / PATH
348
                grammar_lookup = symbol_lookup_class.libraryLookup(path, arena)
349
              end
350

351
              # Chain the lookups: grammar first, then runtime library for ts_* symbols
352
              # This makes ts_language_version available when Language.load() needs it
353
              combined_lookup = grammar_lookup.or(Java.runtime_lookup)
354

355
              java_lang = Java.java_classes[:Language].load(combined_lookup, sym)
356
              new(java_lang, path: path, symbol: symbol)
357
            rescue ::Java::JavaLang::RuntimeException => e
358
              cause = e.cause
359
              root_cause = cause&.cause || cause
360

361
              error_msg = "Failed to load language '#{sym}' from #{path}: #{e.message}"
362
              if root_cause.is_a?(::Java::JavaLang::UnsatisfiedLinkError)
363
                unresolved = root_cause.message.to_s
364
                if unresolved.include?("ts_language_version")
365
                  # This specific symbol was renamed in tree-sitter 0.24
366
                  error_msg += "\n\nVersion mismatch detected: The grammar was built against " \
367
                    "tree-sitter < 0.24 (uses ts_language_version), but your runtime library " \
368
                    "is tree-sitter >= 0.24 (uses ts_language_abi_version).\n\n" \
369
                    "Solutions:\n" \
370
                    "1. Rebuild the grammar against your version of tree-sitter\n" \
371
                    "2. Install a matching version of tree-sitter (< 0.24)\n" \
372
                    "3. Find a pre-built grammar compatible with tree-sitter 0.24+"
373
                elsif unresolved.include?("ts_language") || unresolved.include?("ts_parser")
374
                  error_msg += "\n\nThe grammar library has unresolved tree-sitter symbols. " \
375
                    "Ensure libtree-sitter.so is in LD_LIBRARY_PATH and version-compatible " \
376
                    "with the grammar."
377
                end
378
              end
379
              raise TreeHaver::NotAvailable, error_msg
380
            rescue ::Java::JavaLang::UnsatisfiedLinkError => e
381
              raise TreeHaver::NotAvailable,
382
                "Native library error loading #{path}: #{e.message}. " \
383
                  "Ensure the library is in LD_LIBRARY_PATH."
384
            rescue ::Java::JavaLang::IllegalArgumentException => e
385
              raise TreeHaver::NotAvailable,
386
                "Could not find library '#{path}': #{e.message}. " \
387
                  "Ensure it's in LD_LIBRARY_PATH or provide an absolute path."
388
            end
389
          end
390

391
          # Load a language by name from java-tree-sitter grammar JARs
392
          #
393
          # This method loads grammars that are packaged as java-tree-sitter JARs
394
          # from Maven Central. These JARs include the native grammar library
395
          # pre-built for Java's Foreign Function API.
396
          #
397
          # @param name [String] the language name (e.g., "java", "python", "toml")
398
          # @return [Language] the loaded language
399
          # @raise [TreeHaver::NotAvailable] if the language JAR is not available
400
          #
401
          # @example
402
          #   # First, add the grammar JAR to TREE_SITTER_JAVA_JARS_DIR:
403
          #   # tree-sitter-toml-0.23.2.jar from Maven Central
404
          #   lang = TreeHaver::Backends::Java::Language.load_by_name("toml")
405
          def load_by_name(name)
406
            raise TreeHaver::NotAvailable, "Java backend not available" unless Java.available?
407

408
            # Try to find the grammar library in standard locations
409
            # Look for library names like "tree-sitter-toml" or "libtree-sitter-toml"
410
            lib_names = [
411
              "tree-sitter-#{name}",
412
              "libtree-sitter-#{name}",
413
              "tree_sitter_#{name}",
414
            ]
415

416
            begin
417
              arena = ::Java::JavaLangForeign::Arena.global
418
              symbol_lookup_class = ::Java::JavaLangForeign::SymbolLookup
419

420
              # Ensure runtime lookup is available
421
              unless Java.runtime_lookup
422
                Java.runtime_lookup = symbol_lookup_class.libraryLookup("libtree-sitter.so", arena)
423
              end
424

425
              # Try each library name
426
              grammar_lookup = nil
427
              lib_names.each do |lib_name|
428
                grammar_lookup = symbol_lookup_class.libraryLookup(lib_name, arena)
429
                break
430
              rescue ::Java::JavaLang::IllegalArgumentException
431
                # Library not found in search path, try next name
432
                next
433
              end
434

435
              unless grammar_lookup
436
                raise TreeHaver::NotAvailable,
437
                  "Failed to load language '#{name}': Library not found. " \
438
                    "Ensure the grammar library (e.g., libtree-sitter-#{name}.so) " \
439
                    "is in LD_LIBRARY_PATH."
440
              end
441

442
              combined_lookup = grammar_lookup.or(Java.runtime_lookup)
443
              sym = "tree_sitter_#{name}"
444
              java_lang = Java.java_classes[:Language].load(combined_lookup, sym)
445
              new(java_lang, symbol: sym)
446
            rescue ::Java::JavaLang::RuntimeException => e
447
              raise TreeHaver::NotAvailable,
448
                "Failed to load language '#{name}': #{e.message}. " \
449
                  "Ensure the grammar library (e.g., libtree-sitter-#{name}.so) " \
450
                  "is in LD_LIBRARY_PATH."
451
            end
452
          end
453
        end
454

455
        class << self
456
          alias_method :from_path, :from_library
457
        end
458
      end
459

460
      # Wrapper for java-tree-sitter Parser
461
      #
462
      # @see https://tree-sitter.github.io/java-tree-sitter/io/github/treesitter/jtreesitter/Parser.html
463
      class Parser
464
        # Create a new parser instance
465
        #
466
        # @raise [TreeHaver::NotAvailable] if Java backend is not available
467
        def initialize
468
          raise TreeHaver::NotAvailable, "Java backend not available" unless Java.available?
469
          @parser = Java.java_classes[:Parser].new
470
        end
471

472
        # Set the language for this parser
473
        #
474
        # Note: TreeHaver::Parser unwraps language objects before calling this method.
475
        # This backend receives the Language wrapper's inner impl (java Language object).
476
        #
477
        # @param lang [Object] the Java language object (already unwrapped)
478
        # @return [void]
479
        def language=(lang)
480
          # lang is already unwrapped by TreeHaver::Parser
481
          @parser.language = lang
482
        end
483

484
        # Parse source code
485
        #
486
        # @param source [String] the source code to parse
487
        # @return [Tree] raw backend tree (wrapping happens in TreeHaver::Parser)
488
        def parse(source)
489
          java_result = @parser.parse(source)
490
          # jtreesitter 0.26.0 returns Optional<Tree>
491
          java_tree = unwrap_optional(java_result)
492
          raise TreeHaver::Error, "Parser returned no tree" unless java_tree
493
          Tree.new(java_tree)
494
        end
495

496
        # Parse source code with optional incremental parsing
497
        #
498
        # Note: old_tree is already unwrapped by TreeHaver::Parser before reaching this method.
499
        # The backend receives the raw Tree wrapper's impl, not a TreeHaver::Tree.
500
        #
501
        # When old_tree is provided and has been edited, tree-sitter will reuse
502
        # unchanged nodes for better performance.
503
        #
504
        # @param old_tree [Tree, nil] previous backend tree for incremental parsing (already unwrapped)
505
        # @param source [String] the source code to parse
506
        # @return [Tree] raw backend tree (wrapping happens in TreeHaver::Parser)
507
        # @see https://tree-sitter.github.io/java-tree-sitter/io/github/treesitter/jtreesitter/Parser.html#parse(java.lang.String,io.github.treesitter.jtreesitter.Tree)
508
        def parse_string(old_tree, source)
509
          # old_tree is already unwrapped to Tree wrapper's impl by TreeHaver::Parser
510
          if old_tree
511
            # Get the actual Java Tree object
512
            java_old_tree = if old_tree.is_a?(Tree)
513
              old_tree.impl
514
            else
515
              unwrap_optional(old_tree)
516
            end
517

518
            java_result = if java_old_tree
519
              # jtreesitter 0.26.0 API: parse(String source, Tree oldTree)
520
              @parser.parse(source, java_old_tree)
521
            else
522
              @parser.parse(source)
523
            end
524
          else
525
            java_result = @parser.parse(source)
526
          end
527
          # jtreesitter 0.26.0 returns Optional<Tree>
528
          java_tree = unwrap_optional(java_result)
529
          raise TreeHaver::Error, "Parser returned no tree" unless java_tree
530
          Tree.new(java_tree)
531
        end
532

533
        private
534

535
        # Unwrap Java Optional
536
        #
537
        # jtreesitter 0.26.0 returns Optional<T> from many methods.
538
        #
539
        # @param value [Object] an Optional or direct value
540
        # @return [Object, nil] unwrapped value or nil if empty
541
        def unwrap_optional(value)
542
          return value unless value.respond_to?(:isPresent)
543
          value.isPresent ? value.get : nil
544
        end
545
      end
546

547
      # Wrapper for java-tree-sitter Tree
548
      #
549
      # @see https://tree-sitter.github.io/java-tree-sitter/io/github/treesitter/jtreesitter/Tree.html
550
      class Tree
551
        attr_reader :impl
552

553
        # @api private
554
        def initialize(impl)
555
          @impl = impl
556
        end
557

558
        # Get the root node of the tree
559
        #
560
        # @return [Node] the root node
561
        # @raise [TreeHaver::Error] if tree has no root node
562
        def root_node
563
          result = @impl.rootNode
564
          # jtreesitter 0.26.0: rootNode() may return Optional<Node> or Node directly
565
          java_node = if result.respond_to?(:isPresent)
566
            raise TreeHaver::Error, "Tree has no root node" unless result.isPresent
567
            result.get
568
          else
569
            result
570
          end
571
          raise TreeHaver::Error, "Tree has no root node" unless java_node
572
          Node.new(java_node)
573
        end
574

575
        # Mark the tree as edited for incremental re-parsing
576
        #
577
        # @param start_byte [Integer] byte offset where the edit starts
578
        # @param old_end_byte [Integer] byte offset where the old text ended
579
        # @param new_end_byte [Integer] byte offset where the new text ends
580
        # @param start_point [Hash] starting position as `{ row:, column: }`
581
        # @param old_end_point [Hash] old ending position as `{ row:, column: }`
582
        # @param new_end_point [Hash] new ending position as `{ row:, column: }`
583
        # @return [void]
584
        def edit(start_byte:, old_end_byte:, new_end_byte:, start_point:, old_end_point:, new_end_point:)
585
          point_class = Java.java_classes[:Point]
586
          input_edit_class = Java.java_classes[:InputEdit]
587

588
          start_pt = point_class.new(start_point[:row], start_point[:column])
589
          old_end_pt = point_class.new(old_end_point[:row], old_end_point[:column])
590
          new_end_pt = point_class.new(new_end_point[:row], new_end_point[:column])
591

592
          input_edit = input_edit_class.new(
593
            start_byte,
594
            old_end_byte,
595
            new_end_byte,
596
            start_pt,
597
            old_end_pt,
598
            new_end_pt,
599
          )
600

601
          @impl.edit(input_edit)
602
        end
603
      end
604

605
      # Wrapper for java-tree-sitter Node
606
      #
607
      # @see https://tree-sitter.github.io/java-tree-sitter/io/github/treesitter/jtreesitter/Node.html
608
      class Node
609
        attr_reader :impl
610

611
        # @api private
612
        def initialize(impl)
613
          @impl = impl
614
        end
615

616
        # Get the type of this node
617
        #
618
        # @return [String] the node type
619
        def type
620
          @impl.type
621
        end
622

623
        # Get the number of children
624
        #
625
        # @return [Integer] child count
626
        def child_count
627
          @impl.childCount
628
        end
629

630
        # Get a child by index
631
        #
632
        # @param index [Integer] the child index
633
        # @return [Node, nil] the child node or nil if index out of bounds
634
        def child(index)
635
          # jtreesitter 0.26.0: getChild returns Optional<Node> or throws IndexOutOfBoundsException
636
          result = @impl.getChild(index)
637
          return if result.nil?
638

639
          # Handle Java Optional
640
          if result.respond_to?(:isPresent)
641
            return unless result.isPresent
642
            java_node = result.get
643
          else
644
            # Direct Node return (some jtreesitter versions)
645
            java_node = result
646
          end
647

648
          Node.new(java_node)
649
        rescue ::Java::JavaLang::IndexOutOfBoundsException
650
          nil
651
        end
652

653
        # Get a child by field name
654
        #
655
        # @param name [String] the field name
656
        # @return [Node, nil] the child node or nil if not found
657
        def child_by_field_name(name)
658
          # jtreesitter 0.26.0: getChildByFieldName returns Optional<Node>
659
          # However, some versions or scenarios may return null directly
660
          result = @impl.getChildByFieldName(name)
661
          return if result.nil?
662

663
          # Handle Java Optional
664
          if result.respond_to?(:isPresent)
665
            return unless result.isPresent
666
            java_node = result.get
667
          else
668
            # Direct Node return (some jtreesitter versions)
669
            java_node = result
670
          end
671

672
          Node.new(java_node)
673
        end
674

675
        # Iterate over children
676
        #
677
        # @yield [Node] each child node
678
        # @return [void]
679
        def each
680
          return enum_for(:each) unless block_given?
681
          child_count.times do |i|
682
            yield child(i)
683
          end
684
        end
685

686
        # Get the start byte position
687
        #
688
        # @return [Integer] start byte
689
        def start_byte
690
          @impl.startByte
691
        end
692

693
        # Get the end byte position
694
        #
695
        # @return [Integer] end byte
696
        def end_byte
697
          @impl.endByte
698
        end
699

700
        # Get the start point (row, column)
701
        #
702
        # @return [Hash] with :row and :column keys
703
        def start_point
704
          pt = @impl.startPoint
705
          {row: pt.row, column: pt.column}
706
        end
707

708
        # Get the end point (row, column)
709
        #
710
        # @return [Hash] with :row and :column keys
711
        def end_point
712
          pt = @impl.endPoint
713
          {row: pt.row, column: pt.column}
714
        end
715

716
        # Check if this node has an error
717
        #
718
        # @return [Boolean] true if the node or any descendant has an error
719
        def has_error?
720
          @impl.hasError
721
        end
722

723
        # Check if this node is missing
724
        #
725
        # @return [Boolean] true if this is a MISSING node
726
        def missing?
727
          @impl.isMissing
728
        end
729

730
        # Check if this is a named node
731
        #
732
        # @return [Boolean] true if this is a named node
733
        def named?
734
          @impl.isNamed
735
        end
736

737
        # Get the parent node
738
        #
739
        # @return [Node, nil] the parent node or nil if this is the root
740
        def parent
741
          # jtreesitter 0.26.0: getParent returns Optional<Node>
742
          result = @impl.getParent
743
          return if result.nil?
744

745
          # Handle Java Optional
746
          if result.respond_to?(:isPresent)
747
            return unless result.isPresent
748
            java_node = result.get
749
          else
750
            java_node = result
751
          end
752

753
          Node.new(java_node)
754
        end
755

756
        # Get the next sibling node
757
        #
758
        # @return [Node, nil] the next sibling or nil if none
759
        def next_sibling
760
          # jtreesitter 0.26.0: getNextSibling returns Optional<Node>
761
          result = @impl.getNextSibling
762
          return if result.nil?
763

764
          # Handle Java Optional
765
          if result.respond_to?(:isPresent)
766
            return unless result.isPresent
767
            java_node = result.get
768
          else
769
            java_node = result
770
          end
771

772
          Node.new(java_node)
773
        end
774

775
        # Get the previous sibling node
776
        #
777
        # @return [Node, nil] the previous sibling or nil if none
778
        def prev_sibling
779
          # jtreesitter 0.26.0: getPrevSibling returns Optional<Node>
780
          result = @impl.getPrevSibling
781
          return if result.nil?
782

783
          # Handle Java Optional
784
          if result.respond_to?(:isPresent)
785
            return unless result.isPresent
786
            java_node = result.get
787
          else
788
            java_node = result
789
          end
790

791
          Node.new(java_node)
792
        end
793

794
        # Get the text of this node
795
        #
796
        # @return [String] the source text
797
        def text
798
          @impl.text.to_s
799
        end
800
      end
801
      # :nocov:
802

803
      # Register availability checker for RSpec dependency tags
804
      TreeHaver::BackendRegistry.register_availability_checker(:java) do
3✔
NEW
805
        available?
×
806
      end
807
    end
808
  end
809
end
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc