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

kettle-rb / tree_haver / 21504328921

30 Jan 2026 01:56AM UTC coverage: 86.549% (-0.2%) from 86.76%
21504328921

push

github

pboling
✅ Update `spec_helper` and dependency tags for backend-specific tests and improve grammar validation

929 of 1189 branches covered (78.13%)

Branch coverage included in aggregate %.

3 of 3 new or added lines in 1 file covered. (100.0%)

9 existing lines in 1 file now uncovered.

2301 of 2543 relevant lines covered (90.48%)

68.97 hits per line

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

61.7
/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 defines Ruby wrapper classes (`Java::Language`, `Java::Parser`,
21
    # `Java::Tree`, `Java::Node`) that wrap the raw jtreesitter Java objects via
22
    # JRuby's Java interop. These are **raw backend wrappers** not intended for
23
    # direct use by application code.
24
    #
25
    # The wrapping hierarchy is:
26
    #   Java::Tree/Node (this backend) → TreeHaver::Tree/Node → Base::Tree/Node
27
    #
28
    # When you use `TreeHaver::Parser#parse`:
29
    # 1. `Java::Parser#parse` returns a `Java::Tree` (wrapper around jtreesitter Tree)
30
    # 2. `TreeHaver::Parser` wraps it in `TreeHaver::Tree` (adds source storage)
31
    # 3. `TreeHaver::Tree#root_node` wraps `Java::Node` in `TreeHaver::Node`
32
    #
33
    # The `TreeHaver::Tree` and `TreeHaver::Node` wrappers provide the full unified
34
    # API including `#children`, `#text`, `#source`, `#source_position`, etc.
35
    #
36
    # This differs from pure-Ruby backends (Citrus, Parslet, Prism, Psych) which
37
    # define Tree/Node classes that directly inherit from Base::Tree/Base::Node.
38
    #
39
    # @see TreeHaver::Tree The wrapper class users should interact with
40
    # @see TreeHaver::Node The wrapper class users should interact with
41
    # @see TreeHaver::Base::Tree Base class documenting the Tree API contract
42
    # @see TreeHaver::Base::Node Base class documenting the Node API contract
43
    #
44
    # == Version Requirements
45
    #
46
    # - jtreesitter >= 0.26.0 (required)
47
    # - tree-sitter runtime library >= 0.26.0 (must match jtreesitter version)
48
    #
49
    # Older versions of jtreesitter are NOT supported due to API changes.
50
    #
51
    # == Platform Compatibility
52
    #
53
    # - MRI Ruby: ✗ Not available (no JVM)
54
    # - JRuby: ✓ Full support (native Java integration)
55
    # - TruffleRuby: ✗ Not available (jtreesitter requires JRuby's Java interop)
56
    #
57
    # == Installation
58
    #
59
    # 1. Download jtreesitter 0.26.0+ JAR from Maven Central:
60
    #    https://central.sonatype.com/artifact/io.github.tree-sitter/jtreesitter
61
    #
62
    # 2. Set the environment variable to point to the JAR directory:
63
    #    export TREE_SITTER_JAVA_JARS_DIR=/path/to/jars
64
    #
65
    # 3. Use JRuby to run your code:
66
    #    jruby -e "require 'tree_haver'; puts TreeHaver::Backends::Java.available?"
67
    #
68
    # @see https://github.com/tree-sitter/java-tree-sitter source
69
    # @see https://tree-sitter.github.io/java-tree-sitter jtreesitter documentation
70
    # @see https://central.sonatype.com/artifact/io.github.tree-sitter/jtreesitter Maven Central
71
    module Java
3✔
72
      # The Java package for java-tree-sitter
73
      JAVA_PACKAGE = "io.github.treesitter.jtreesitter"
3✔
74

75
      @load_attempted = false
3✔
76
      @loaded = false
3✔
77
      @java_classes = {} # rubocop:disable ThreadSafety/MutableClassInstanceVariable
3✔
78
      @runtime_lookup = nil  # Cached SymbolLookup for libtree-sitter.so
3✔
79

80
      module_function
3✔
81

82
      # Get the cached runtime library SymbolLookup
83
      # @return [Object, nil] the SymbolLookup for libtree-sitter.so
84
      # @api private
85
      def runtime_lookup
3✔
UNCOV
86
        @runtime_lookup
×
87
      end
88

89
      # Set the cached runtime library SymbolLookup
90
      # @param lookup [Object] the SymbolLookup
91
      # @api private
92
      def runtime_lookup=(lookup)
3✔
UNCOV
93
        @runtime_lookup = lookup
×
94
      end
95

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

112
        # Add JARs to classpath
113
        dir = ENV["TREE_SITTER_JAVA_JARS_DIR"]
114
        if dir && Dir.exist?(dir)
115
          Dir[File.join(dir, "**", "*.jar")].each do |jar|
116
            next if $CLASSPATH.include?(jar)
117
            $CLASSPATH << jar
118
          end
119
        end
120

121
        # Configure native library path for libtree-sitter
122
        # java-tree-sitter uses JNI and needs to find the native library
123
        configure_native_library_path!
124
        # :nocov:
125
      rescue LoadError
126
        # ignore; not JRuby or Java bridge not available
127
      end
128

129
      # Configure java.library.path to include the directory containing libtree-sitter
130
      #
131
      # @return [void]
132
      # @api private
133
      def configure_native_library_path!
3✔
134
        # :nocov:
135
        # This method requires JRuby and cannot be tested on MRI/CRuby.
136
        lib_path = ENV["TREE_SITTER_RUNTIME_LIB"]
137
        return unless lib_path && File.exist?(lib_path)
138

139
        lib_dir = File.dirname(lib_path)
140
        current_path = java.lang.System.getProperty("java.library.path") || ""
141

142
        unless current_path.include?(lib_dir)
143
          new_path = current_path.empty? ? lib_dir : "#{lib_dir}:#{current_path}"
144
          java.lang.System.setProperty("java.library.path", new_path)
145

146
          # Also set jna.library.path in case it uses JNA
147
          java.lang.System.setProperty("jna.library.path", new_path)
148
        end
149
        # :nocov:
150
      rescue => _error
151
        # Ignore errors setting library path
152
      end
153

154
      # Check if the Java backend is available
155
      #
156
      # Checks if:
157
      # 1. We're running on JRuby
158
      # 2. Environment variable TREE_SITTER_JAVA_JARS_DIR is set
159
      # 3. Required JARs (jtreesitter, tree-sitter) are present in that directory
160
      #
161
      # @return [Boolean] true if Java backend is available
162
      # @example
163
      #   if TreeHaver::Backends::Java.available?
164
      #     puts "Java backend ready"
165
      #   end
166
      class << self
3✔
167
        def available?
3✔
168
          return @loaded if @load_attempted # rubocop:disable ThreadSafety/ClassInstanceVariable
3✔
169
          @load_attempted = true # rubocop:disable ThreadSafety/ClassInstanceVariable
1✔
170
          @loaded = check_availability # rubocop:disable ThreadSafety/ClassInstanceVariable
1✔
171
        end
172

173
        # Reset the load state (primarily for testing)
174
        #
175
        # @return [void]
176
        # @api private
177
        def reset!
3✔
UNCOV
178
          @load_attempted = false # rubocop:disable ThreadSafety/ClassInstanceVariable
×
UNCOV
179
          @loaded = false # rubocop:disable ThreadSafety/ClassInstanceVariable
×
UNCOV
180
          @load_error = nil # rubocop:disable ThreadSafety/ClassInstanceVariable
×
UNCOV
181
          @loader = nil # rubocop:disable ThreadSafety/ClassInstanceVariable
×
UNCOV
182
          @java_classes = {} # rubocop:disable ThreadSafety/ClassInstanceVariable
×
183
        end
184

185
        private
3✔
186

187
        def check_availability
3✔
188
          # 1. Check Ruby engine
189
          return false unless RUBY_ENGINE == "jruby"
1!
190

191
          # 2. Check for required JARs via environment variable
192
          jars_dir = ENV["TREE_SITTER_JAVA_JARS_DIR"]
×
193
          return false unless jars_dir && Dir.exist?(jars_dir)
×
194

195
          # 3. Check if we can load the classes
196
          begin
197
            ensure_loader_initialized!
×
198
            true
×
199
          rescue LoadError, NameError
200
            false
×
201
          end
202
        end
203
      end
204

205
      # Get the last load error message (for debugging)
206
      #
207
      # @return [String, nil] the error message or nil if no error
208
      def load_error
3✔
UNCOV
209
        @load_error
×
210
      end
211

212
      # Get the loaded Java classes
213
      #
214
      # @return [Hash] the Java class references
215
      # @api private
216
      def java_classes
3✔
UNCOV
217
        @java_classes
×
218
      end
219

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

240
      # Java backend language wrapper (raw backend language)
241
      #
242
      # This is a **raw backend language** that wraps a jtreesitter Language object
243
      # via JRuby's Java interop. It is used to configure the parser for a specific
244
      # grammar (e.g., TOML, JSON, etc.).
245
      #
246
      # Unlike `TreeHaver::Language` (which is a module with factory methods), this
247
      # class holds the actual loaded language data from a grammar shared library.
248
      #
249
      # @api private
250
      # @see TreeHaver::Language The factory module users should interact with
251
      # @see https://tree-sitter.github.io/java-tree-sitter/io/github/treesitter/jtreesitter/Language.html
252
      #
253
      # :nocov:
254
      # All Java backend implementation classes require JRuby and cannot be tested on MRI/CRuby.
255
      # JRuby-specific CI jobs would test this code.
256
      class Language
257
        include Comparable
258

259
        attr_reader :impl
260

261
        # The backend this language is for
262
        # @return [Symbol]
263
        attr_reader :backend
264

265
        # The path this language was loaded from (if known)
266
        # @return [String, nil]
267
        attr_reader :path
268

269
        # The symbol name (if known)
270
        # @return [String, nil]
271
        attr_reader :symbol
272

273
        # @api private
274
        def initialize(impl, path: nil, symbol: nil)
275
          @impl = impl
276
          @backend = :java
277
          @path = path
278
          @symbol = symbol
279
        end
280

281
        # Compare languages for equality
282
        #
283
        # Java languages are equal if they have the same backend, path, and symbol.
284
        # Path and symbol uniquely identify a loaded language.
285
        #
286
        # @param other [Object] object to compare with
287
        # @return [Integer, nil] -1, 0, 1, or nil if not comparable
288
        def <=>(other)
289
          return unless other.is_a?(Language)
290
          return unless other.backend == @backend
291

292
          # Compare by path first, then symbol
293
          cmp = (@path || "") <=> (other.path || "")
294
          return cmp if cmp.nonzero?
295

296
          (@symbol || "") <=> (other.symbol || "")
297
        end
298

299
        # Hash value for this language (for use in Sets/Hashes)
300
        # @return [Integer]
301
        def hash
302
          [@backend, @path, @symbol].hash
303
        end
304

305
        # Alias eql? to ==
306
        alias_method :eql?, :==
307

308
        # Load a language from a shared library
309
        #
310
        # There are three ways java-tree-sitter can load shared libraries:
311
        #
312
        # 1. Libraries in OS library search path (LD_LIBRARY_PATH on Linux,
313
        #    DYLD_LIBRARY_PATH on macOS, PATH on Windows) - loaded via
314
        #    SymbolLookup.libraryLookup(String, Arena)
315
        #
316
        # 2. Libraries in java.library.path - loaded via SymbolLookup.loaderLookup()
317
        #
318
        # 3. Custom NativeLibraryLookup implementation (e.g., for JARs)
319
        #
320
        # @param path [String] path to language shared library (.so/.dylib) or library name
321
        # @param symbol [String, nil] exported symbol name (e.g., "tree_sitter_toml")
322
        # @param name [String, nil] logical name (used to derive symbol if not provided)
323
        # @return [Language] the loaded language
324
        # @raise [TreeHaver::NotAvailable] if Java backend is not available
325
        # @example Load by path
326
        #   lang = TreeHaver::Backends::Java::Language.from_library(
327
        #     "/usr/lib/libtree-sitter-toml.so",
328
        #     symbol: "tree_sitter_toml"
329
        #   )
330
        # @example Load by name (searches LD_LIBRARY_PATH)
331
        #   lang = TreeHaver::Backends::Java::Language.from_library(
332
        #     "tree-sitter-toml",
333
        #     symbol: "tree_sitter_toml"
334
        #   )
335
        class << self
336
          def from_library(path, symbol: nil, name: nil)
337
            raise TreeHaver::NotAvailable, "Java backend not available" unless Java.available?
338

339
            # Use shared utility for consistent symbol derivation across backends
340
            # If symbol not provided, derive from name or path
341
            sym = symbol || LibraryPathUtils.derive_symbol_from_path(path)
342
            # If name was provided, use it to override the derived symbol
343
            sym = "tree_sitter_#{name}" if name && !symbol
344

345
            begin
346
              arena = ::Java::JavaLangForeign::Arena.global
347
              symbol_lookup_class = ::Java::JavaLangForeign::SymbolLookup
348

349
              # IMPORTANT: Load libtree-sitter.so FIRST by name so its symbols are available
350
              # Grammar libraries need symbols like ts_language_version from the runtime
351
              # We cache this lookup at the module level
352
              unless Java.runtime_lookup
353
                # Use libraryLookup(String, Arena) to search LD_LIBRARY_PATH
354
                Java.runtime_lookup = symbol_lookup_class.libraryLookup("libtree-sitter.so", arena)
355
              end
356

357
              # Now load the grammar library
358
              if File.exist?(path)
359
                # Explicit path provided - use libraryLookup(Path, Arena)
360
                java_path = ::Java::JavaNioFile::Paths.get(path)
361
                grammar_lookup = symbol_lookup_class.libraryLookup(java_path, arena)
362
              else
363
                # Library name provided - use libraryLookup(String, Arena) to search
364
                # LD_LIBRARY_PATH / DYLD_LIBRARY_PATH / PATH
365
                grammar_lookup = symbol_lookup_class.libraryLookup(path, arena)
366
              end
367

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

372
              java_lang = Java.java_classes[:Language].load(combined_lookup, sym)
373
              new(java_lang, path: path, symbol: symbol)
374
            rescue ::Java::JavaLang::RuntimeException => e
375
              cause = e.cause
376
              root_cause = cause&.cause || cause
377

378
              error_msg = "Failed to load language '#{sym}' from #{path}: #{e.message}"
379
              if root_cause.is_a?(::Java::JavaLang::UnsatisfiedLinkError)
380
                unresolved = root_cause.message.to_s
381
                if unresolved.include?("ts_language_version")
382
                  # This specific symbol was renamed in tree-sitter 0.24
383
                  error_msg += "\n\nVersion mismatch detected: The grammar was built against " \
384
                    "tree-sitter < 0.24 (uses ts_language_version), but your runtime library " \
385
                    "is tree-sitter >= 0.24 (uses ts_language_abi_version).\n\n" \
386
                    "Solutions:\n" \
387
                    "1. Rebuild the grammar against your version of tree-sitter\n" \
388
                    "2. Install a matching version of tree-sitter (< 0.24)\n" \
389
                    "3. Find a pre-built grammar compatible with tree-sitter 0.24+"
390
                elsif unresolved.include?("ts_language") || unresolved.include?("ts_parser")
391
                  error_msg += "\n\nThe grammar library has unresolved tree-sitter symbols. " \
392
                    "Ensure libtree-sitter.so is in LD_LIBRARY_PATH and version-compatible " \
393
                    "with the grammar."
394
                end
395
              end
396
              raise TreeHaver::NotAvailable, error_msg
397
            rescue ::Java::JavaLang::UnsatisfiedLinkError => e
398
              raise TreeHaver::NotAvailable,
399
                "Native library error loading #{path}: #{e.message}. " \
400
                  "Ensure the library is in LD_LIBRARY_PATH."
401
            rescue ::Java::JavaLang::IllegalArgumentException => e
402
              raise TreeHaver::NotAvailable,
403
                "Could not find library '#{path}': #{e.message}. " \
404
                  "Ensure it's in LD_LIBRARY_PATH or provide an absolute path."
405
            end
406
          end
407

408
          # Load a language by name from java-tree-sitter grammar JARs
409
          #
410
          # This method loads grammars that are packaged as java-tree-sitter JARs
411
          # from Maven Central. These JARs include the native grammar library
412
          # pre-built for Java's Foreign Function API.
413
          #
414
          # @param name [String] the language name (e.g., "java", "python", "toml")
415
          # @return [Language] the loaded language
416
          # @raise [TreeHaver::NotAvailable] if the language JAR is not available
417
          #
418
          # @example
419
          #   # First, add the grammar JAR to TREE_SITTER_JAVA_JARS_DIR:
420
          #   # tree-sitter-toml-0.23.2.jar from Maven Central
421
          #   lang = TreeHaver::Backends::Java::Language.load_by_name("toml")
422
          def load_by_name(name)
423
            raise TreeHaver::NotAvailable, "Java backend not available" unless Java.available?
424

425
            # Try to find the grammar library in standard locations
426
            # Look for library names like "tree-sitter-toml" or "libtree-sitter-toml"
427
            lib_names = [
428
              "tree-sitter-#{name}",
429
              "libtree-sitter-#{name}",
430
              "tree_sitter_#{name}",
431
            ]
432

433
            begin
434
              arena = ::Java::JavaLangForeign::Arena.global
435
              symbol_lookup_class = ::Java::JavaLangForeign::SymbolLookup
436

437
              # Ensure runtime lookup is available
438
              unless Java.runtime_lookup
439
                Java.runtime_lookup = symbol_lookup_class.libraryLookup("libtree-sitter.so", arena)
440
              end
441

442
              # Try each library name
443
              grammar_lookup = nil
444
              lib_names.each do |lib_name|
445
                grammar_lookup = symbol_lookup_class.libraryLookup(lib_name, arena)
446
                break
447
              rescue ::Java::JavaLang::IllegalArgumentException
448
                # Library not found in search path, try next name
449
                next
450
              end
451

452
              unless grammar_lookup
453
                raise TreeHaver::NotAvailable,
454
                  "Failed to load language '#{name}': Library not found. " \
455
                    "Ensure the grammar library (e.g., libtree-sitter-#{name}.so) " \
456
                    "is in LD_LIBRARY_PATH."
457
              end
458

459
              combined_lookup = grammar_lookup.or(Java.runtime_lookup)
460
              sym = "tree_sitter_#{name}"
461
              java_lang = Java.java_classes[:Language].load(combined_lookup, sym)
462
              new(java_lang, symbol: sym)
463
            rescue ::Java::JavaLang::RuntimeException => e
464
              raise TreeHaver::NotAvailable,
465
                "Failed to load language '#{name}': #{e.message}. " \
466
                  "Ensure the grammar library (e.g., libtree-sitter-#{name}.so) " \
467
                  "is in LD_LIBRARY_PATH."
468
            end
469
          end
470
        end
471

472
        class << self
473
          alias_method :from_path, :from_library
474
        end
475
      end
476

477
      # Java backend parser wrapper (raw backend parser)
478
      #
479
      # This is a **raw backend parser** that wraps a jtreesitter Parser object via
480
      # JRuby's Java interop. It is NOT intended for direct use by application code.
481
      #
482
      # Users should use `TreeHaver::Parser` which wraps this class and provides:
483
      # - Automatic backend selection
484
      # - Language wrapper unwrapping
485
      # - Tree wrapping with source storage
486
      # - Unified API across all backends
487
      #
488
      # @api private
489
      # @see TreeHaver::Parser The wrapper class users should interact with
490
      # @see https://tree-sitter.github.io/java-tree-sitter/io/github/treesitter/jtreesitter/Parser.html
491
      class Parser
492
        # Create a new parser instance
493
        #
494
        # @raise [TreeHaver::NotAvailable] if Java backend is not available
495
        def initialize
496
          raise TreeHaver::NotAvailable, "Java backend not available" unless Java.available?
497
          @parser = Java.java_classes[:Parser].new
498
        end
499

500
        # Set the language for this parser
501
        #
502
        # Note: TreeHaver::Parser unwraps language objects before calling this method.
503
        # This backend receives the Language wrapper's inner impl (java Language object).
504
        #
505
        # @param lang [Object] the Java language object (already unwrapped)
506
        # @return [void]
507
        def language=(lang)
508
          # lang is already unwrapped by TreeHaver::Parser
509
          @parser.language = lang
510
        end
511

512
        # Parse source code
513
        #
514
        # @param source [String] the source code to parse
515
        # @return [Tree] raw backend tree (wrapping happens in TreeHaver::Parser)
516
        def parse(source)
517
          java_result = @parser.parse(source)
518
          # jtreesitter 0.26.0 returns Optional<Tree>
519
          java_tree = unwrap_optional(java_result)
520
          raise TreeHaver::Error, "Parser returned no tree" unless java_tree
521
          Tree.new(java_tree)
522
        end
523

524
        # Parse source code with optional incremental parsing
525
        #
526
        # Note: old_tree is already unwrapped by TreeHaver::Parser before reaching this method.
527
        # The backend receives the raw Tree wrapper's impl, not a TreeHaver::Tree.
528
        #
529
        # When old_tree is provided and has been edited, tree-sitter will reuse
530
        # unchanged nodes for better performance.
531
        #
532
        # @param old_tree [Tree, nil] previous backend tree for incremental parsing (already unwrapped)
533
        # @param source [String] the source code to parse
534
        # @return [Tree] raw backend tree (wrapping happens in TreeHaver::Parser)
535
        # @see https://tree-sitter.github.io/java-tree-sitter/io/github/treesitter/jtreesitter/Parser.html#parse(java.lang.String,io.github.treesitter.jtreesitter.Tree)
536
        def parse_string(old_tree, source)
537
          # old_tree is already unwrapped to Tree wrapper's impl by TreeHaver::Parser
538
          if old_tree
539
            # Get the actual Java Tree object
540
            java_old_tree = if old_tree.is_a?(Tree)
541
              old_tree.impl
542
            else
543
              unwrap_optional(old_tree)
544
            end
545

546
            java_result = if java_old_tree
547
              # jtreesitter 0.26.0 API: parse(String source, Tree oldTree)
548
              @parser.parse(source, java_old_tree)
549
            else
550
              @parser.parse(source)
551
            end
552
          else
553
            java_result = @parser.parse(source)
554
          end
555
          # jtreesitter 0.26.0 returns Optional<Tree>
556
          java_tree = unwrap_optional(java_result)
557
          raise TreeHaver::Error, "Parser returned no tree" unless java_tree
558
          Tree.new(java_tree)
559
        end
560

561
        private
562

563
        # Unwrap Java Optional
564
        #
565
        # jtreesitter 0.26.0 returns Optional<T> from many methods.
566
        #
567
        # @param value [Object] an Optional or direct value
568
        # @return [Object, nil] unwrapped value or nil if empty
569
        def unwrap_optional(value)
570
          return value unless value.respond_to?(:isPresent)
571
          value.isPresent ? value.get : nil
572
        end
573
      end
574

575
      # Java backend tree wrapper (raw backend tree)
576
      #
577
      # This is a **raw backend tree** that wraps a jtreesitter Tree object via
578
      # JRuby's Java interop. It is NOT intended for direct use by application code.
579
      #
580
      # == Architecture Note
581
      #
582
      # Unlike pure-Ruby backends (Citrus, Parslet, Prism, Psych) which define Tree
583
      # classes that inherit from `TreeHaver::Base::Tree`, tree-sitter backends (MRI,
584
      # Rust, FFI, Java) define raw wrapper classes that get wrapped by `TreeHaver::Tree`.
585
      #
586
      # The wrapping hierarchy is:
587
      #   Java::Tree (this class) → TreeHaver::Tree → Base::Tree
588
      #
589
      # When you use `TreeHaver::Parser#parse`, the returned tree is already wrapped
590
      # in `TreeHaver::Tree`, which provides the full unified API including:
591
      # - `#source` - The original source text
592
      # - `#root_node` - Returns a `TreeHaver::Node` (not raw `Java::Node`)
593
      # - `#errors`, `#warnings`, `#comments` - Parse diagnostics
594
      # - `#edit` - Mark tree as edited for incremental parsing
595
      # - `#to_s`, `#inspect` - String representations
596
      #
597
      # This raw class only implements methods that require direct calls to jtreesitter.
598
      # The wrapper adds Ruby-level conveniences and stores the source text needed for
599
      # `Node#text` extraction.
600
      #
601
      # @api private
602
      # @see TreeHaver::Tree The wrapper class users should interact with
603
      # @see TreeHaver::Base::Tree The base class documenting the full Tree API
604
      # @see https://tree-sitter.github.io/java-tree-sitter/io/github/treesitter/jtreesitter/Tree.html
605
      class Tree
606
        attr_reader :impl
607

608
        # @api private
609
        def initialize(impl)
610
          @impl = impl
611
        end
612

613
        # Get the root node of the tree
614
        #
615
        # @return [Node] the root node
616
        # @raise [TreeHaver::Error] if tree has no root node
617
        def root_node
618
          result = @impl.rootNode
619
          # jtreesitter 0.26.0: rootNode() may return Optional<Node> or Node directly
620
          java_node = if result.respond_to?(:isPresent)
621
            raise TreeHaver::Error, "Tree has no root node" unless result.isPresent
622
            result.get
623
          else
624
            result
625
          end
626
          raise TreeHaver::Error, "Tree has no root node" unless java_node
627
          Node.new(java_node)
628
        end
629

630
        # Mark the tree as edited for incremental re-parsing
631
        #
632
        # @param start_byte [Integer] byte offset where the edit starts
633
        # @param old_end_byte [Integer] byte offset where the old text ended
634
        # @param new_end_byte [Integer] byte offset where the new text ends
635
        # @param start_point [Hash] starting position as `{ row:, column: }`
636
        # @param old_end_point [Hash] old ending position as `{ row:, column: }`
637
        # @param new_end_point [Hash] new ending position as `{ row:, column: }`
638
        # @return [void]
639
        def edit(start_byte:, old_end_byte:, new_end_byte:, start_point:, old_end_point:, new_end_point:)
640
          point_class = Java.java_classes[:Point]
641
          input_edit_class = Java.java_classes[:InputEdit]
642

643
          start_pt = point_class.new(start_point[:row], start_point[:column])
644
          old_end_pt = point_class.new(old_end_point[:row], old_end_point[:column])
645
          new_end_pt = point_class.new(new_end_point[:row], new_end_point[:column])
646

647
          input_edit = input_edit_class.new(
648
            start_byte,
649
            old_end_byte,
650
            new_end_byte,
651
            start_pt,
652
            old_end_pt,
653
            new_end_pt,
654
          )
655

656
          @impl.edit(input_edit)
657
        end
658
      end
659

660
      # Java backend node wrapper (raw backend node)
661
      #
662
      # This is a **raw backend node** that wraps a jtreesitter Node object via
663
      # JRuby's Java interop. It provides the minimal interface needed for tree-sitter
664
      # operations but is NOT intended for direct use by application code.
665
      #
666
      # == Architecture Note
667
      #
668
      # Unlike pure-Ruby backends (Citrus, Parslet, Prism, Psych) which define Node
669
      # classes that inherit from `TreeHaver::Base::Node`, tree-sitter backends (MRI,
670
      # Rust, FFI, Java) define raw wrapper classes that get wrapped by `TreeHaver::Node`.
671
      #
672
      # The wrapping hierarchy is:
673
      #   Java::Node (this class) → TreeHaver::Node → Base::Node
674
      #
675
      # When you use `TreeHaver::Parser#parse`, the returned tree's nodes are already
676
      # wrapped in `TreeHaver::Node`, which provides the full unified API including:
677
      # - `#children` - Array of child nodes
678
      # - `#text` - Extract text from source
679
      # - `#first_child`, `#last_child` - Convenience accessors
680
      # - `#start_line`, `#end_line` - 1-based line numbers
681
      # - `#source_position` - Hash with position info
682
      # - `#each`, `#map`, etc. - Enumerable methods
683
      # - `#to_s`, `#inspect` - String representations
684
      #
685
      # This raw class only implements methods that require direct calls to jtreesitter.
686
      # The wrapper adds Ruby-level conveniences.
687
      #
688
      # @api private
689
      # @see TreeHaver::Node The wrapper class users should interact with
690
      # @see TreeHaver::Base::Node The base class documenting the full Node API
691
      # @see https://tree-sitter.github.io/java-tree-sitter/io/github/treesitter/jtreesitter/Node.html
692
      class Node
693
        attr_reader :impl
694

695
        # @api private
696
        def initialize(impl)
697
          @impl = impl
698
        end
699

700
        # Get the type of this node
701
        #
702
        # @return [String] the node type
703
        def type
704
          @impl.type
705
        end
706

707
        # Get the number of children
708
        #
709
        # @return [Integer] child count
710
        def child_count
711
          @impl.childCount
712
        end
713

714
        # Get a child by index
715
        #
716
        # @param index [Integer] the child index
717
        # @return [Node, nil] the child node or nil if index out of bounds
718
        def child(index)
719
          # jtreesitter 0.26.0: getChild returns Optional<Node> or throws IndexOutOfBoundsException
720
          result = @impl.getChild(index)
721
          return if result.nil?
722

723
          # Handle Java Optional
724
          if result.respond_to?(:isPresent)
725
            return unless result.isPresent
726
            java_node = result.get
727
          else
728
            # Direct Node return (some jtreesitter versions)
729
            java_node = result
730
          end
731

732
          Node.new(java_node)
733
        rescue ::Java::JavaLang::IndexOutOfBoundsException
734
          nil
735
        end
736

737
        # Get a child by field name
738
        #
739
        # @param name [String] the field name
740
        # @return [Node, nil] the child node or nil if not found
741
        def child_by_field_name(name)
742
          # jtreesitter 0.26.0: getChildByFieldName returns Optional<Node>
743
          # However, some versions or scenarios may return null directly
744
          result = @impl.getChildByFieldName(name)
745
          return if result.nil?
746

747
          # Handle Java Optional
748
          if result.respond_to?(:isPresent)
749
            return unless result.isPresent
750
            java_node = result.get
751
          else
752
            # Direct Node return (some jtreesitter versions)
753
            java_node = result
754
          end
755

756
          Node.new(java_node)
757
        end
758

759
        # Iterate over children
760
        #
761
        # @yield [Node] each child node
762
        # @return [void]
763
        def each
764
          return enum_for(:each) unless block_given?
765
          child_count.times do |i|
766
            yield child(i)
767
          end
768
        end
769

770
        # Get the start byte position
771
        #
772
        # @return [Integer] start byte
773
        def start_byte
774
          @impl.startByte
775
        end
776

777
        # Get the end byte position
778
        #
779
        # @return [Integer] end byte
780
        def end_byte
781
          @impl.endByte
782
        end
783

784
        # Get the start point (row, column)
785
        #
786
        # @return [Hash] with :row and :column keys
787
        def start_point
788
          pt = @impl.startPoint
789
          {row: pt.row, column: pt.column}
790
        end
791

792
        # Get the end point (row, column)
793
        #
794
        # @return [Hash] with :row and :column keys
795
        def end_point
796
          pt = @impl.endPoint
797
          {row: pt.row, column: pt.column}
798
        end
799

800
        # Check if this node has an error
801
        #
802
        # @return [Boolean] true if the node or any descendant has an error
803
        def has_error?
804
          @impl.hasError
805
        end
806

807
        # Check if this node is missing
808
        #
809
        # @return [Boolean] true if this is a MISSING node
810
        def missing?
811
          @impl.isMissing
812
        end
813

814
        # Check if this is a named node
815
        #
816
        # @return [Boolean] true if this is a named node
817
        def named?
818
          @impl.isNamed
819
        end
820

821
        # Get the parent node
822
        #
823
        # @return [Node, nil] the parent node or nil if this is the root
824
        def parent
825
          # jtreesitter 0.26.0: getParent returns Optional<Node>
826
          result = @impl.getParent
827
          return if result.nil?
828

829
          # Handle Java Optional
830
          if result.respond_to?(:isPresent)
831
            return unless result.isPresent
832
            java_node = result.get
833
          else
834
            java_node = result
835
          end
836

837
          Node.new(java_node)
838
        end
839

840
        # Get the next sibling node
841
        #
842
        # @return [Node, nil] the next sibling or nil if none
843
        def next_sibling
844
          # jtreesitter 0.26.0: getNextSibling returns Optional<Node>
845
          result = @impl.getNextSibling
846
          return if result.nil?
847

848
          # Handle Java Optional
849
          if result.respond_to?(:isPresent)
850
            return unless result.isPresent
851
            java_node = result.get
852
          else
853
            java_node = result
854
          end
855

856
          Node.new(java_node)
857
        end
858

859
        # Get the previous sibling node
860
        #
861
        # @return [Node, nil] the previous sibling or nil if none
862
        def prev_sibling
863
          # jtreesitter 0.26.0: getPrevSibling returns Optional<Node>
864
          result = @impl.getPrevSibling
865
          return if result.nil?
866

867
          # Handle Java Optional
868
          if result.respond_to?(:isPresent)
869
            return unless result.isPresent
870
            java_node = result.get
871
          else
872
            java_node = result
873
          end
874

875
          Node.new(java_node)
876
        end
877

878
        # Get the text of this node
879
        #
880
        # @return [String] the source text
881
        def text
882
          @impl.text.to_s
883
        end
884
      end
885
      # :nocov:
886

887
      # Register the availability checker for RSpec dependency tags
888
      TreeHaver::BackendRegistry.register_availability_checker(:java) do
3✔
889
        available?
×
890
      end
891
    end
892
  end
893
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