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

kettle-rb / tree_haver / 20813148910

08 Jan 2026 10:08AM UTC coverage: 91.74% (-0.1%) from 91.873%
20813148910

push

github

pboling
🔨 Fix and document bin/rspec-ffi

802 of 956 branches covered (83.89%)

Branch coverage included in aggregate %.

2030 of 2131 relevant lines covered (95.26%)

43.16 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
2✔
161
          @loaded = check_availability # rubocop:disable ThreadSafety/ClassInstanceVariable
2✔
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"
2!
181

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

186
          # 3. Check if we can load the classes
187
          begin
188
            ensure_loader_initialized!
×
189
            true
×
190
          rescue LoadError, NameError
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
      # Get the loaded Java classes
204
      #
205
      # @return [Hash] the Java class references
206
      # @api private
207
      def java_classes
3✔
208
        @java_classes
2✔
209
      end
210

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

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

241
        attr_reader :impl
242

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

532
        private
533

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

671
          Node.new(java_node)
672
        end
673

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

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

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

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

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

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

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

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

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

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

752
          Node.new(java_node)
753
        end
754

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

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

771
          Node.new(java_node)
772
        end
773

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

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

790
          Node.new(java_node)
791
        end
792

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

802
      # Register availability checker for RSpec dependency tags
803
      TreeHaver::BackendRegistry.register_availability_checker(:java) do
3✔
804
        available?
×
805
      end
806
    end
807
  end
808
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