tree_haver 7.0.0 → 7.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (44) hide show
  1. checksums.yaml +4 -4
  2. checksums.yaml.gz.sig +0 -0
  3. data/LICENSE.md +13 -0
  4. data/README.md +1959 -0
  5. data/lib/tree_haver/backend_api.rb +392 -0
  6. data/lib/tree_haver/backend_registry.rb +153 -3
  7. data/lib/tree_haver/backends/citrus.rb +489 -0
  8. data/lib/tree_haver/backends/ffi.rb +1013 -0
  9. data/lib/tree_haver/backends/java.rb +909 -0
  10. data/lib/tree_haver/backends/mri.rb +367 -0
  11. data/lib/tree_haver/backends/parslet.rb +565 -0
  12. data/lib/tree_haver/backends/prism.rb +568 -0
  13. data/lib/tree_haver/backends/psych.rb +379 -0
  14. data/lib/tree_haver/backends/rust.rb +243 -0
  15. data/lib/tree_haver/backends/tslp.rb +274 -0
  16. data/lib/tree_haver/base/comment.rb +320 -0
  17. data/lib/tree_haver/base/language.rb +98 -0
  18. data/lib/tree_haver/base/node.rb +330 -0
  19. data/lib/tree_haver/base/parser.rb +28 -0
  20. data/lib/tree_haver/base/point.rb +48 -0
  21. data/lib/tree_haver/base/tree.rb +128 -0
  22. data/lib/tree_haver/citrus_grammar_finder.rb +213 -0
  23. data/lib/tree_haver/contracts.rb +661 -96
  24. data/lib/tree_haver/grammar_finder.rb +429 -0
  25. data/lib/tree_haver/kaitai_backend.rb +2 -2
  26. data/lib/tree_haver/language.rb +294 -0
  27. data/lib/tree_haver/language_pack.rb +17 -166
  28. data/lib/tree_haver/language_registry.rb +221 -0
  29. data/lib/tree_haver/library_path_utils.rb +80 -0
  30. data/lib/tree_haver/node.rb +588 -0
  31. data/lib/tree_haver/parser.rb +445 -0
  32. data/lib/tree_haver/parslet_grammar_finder.rb +217 -0
  33. data/lib/tree_haver/path_validator.rb +356 -0
  34. data/lib/tree_haver/peg_backends.rb +7 -7
  35. data/lib/tree_haver/point.rb +27 -0
  36. data/lib/tree_haver/rspec/dependency_tags.rb +52 -0
  37. data/lib/tree_haver/rspec.rb +3 -0
  38. data/lib/tree_haver/tree.rb +267 -0
  39. data/lib/tree_haver/version.rb +5 -3
  40. data/lib/tree_haver.rb +613 -8
  41. data/sig/tree_haver.rbs +6 -0
  42. data.tar.gz.sig +0 -0
  43. metadata +314 -13
  44. metadata.gz.sig +0 -0
@@ -0,0 +1,909 @@
1
+ # frozen_string_literal: true
2
+
3
+ module TreeHaver
4
+ module Backends
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
72
+ # The Java package for java-tree-sitter
73
+ JAVA_PACKAGE = 'io.github.treesitter.jtreesitter'
74
+
75
+ @load_attempted = false
76
+ @loaded = false
77
+ @java_classes = {}
78
+ @runtime_lookup = nil # Cached SymbolLookup for libtree-sitter.so
79
+
80
+ module_function
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
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)
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!
107
+ # simplecov:disable
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
+
118
+ $CLASSPATH << jar
119
+ end
120
+ end
121
+
122
+ # Configure native library path for libtree-sitter
123
+ # java-tree-sitter uses JNI and needs to find the native library
124
+ configure_native_library_path!
125
+ # simplecov:enable
126
+ rescue LoadError
127
+ # ignore; not JRuby or Java bridge not available
128
+ end
129
+
130
+ # Configure java.library.path to include the directory containing libtree-sitter
131
+ #
132
+ # @return [void]
133
+ # @api private
134
+ def configure_native_library_path!
135
+ # simplecov:disable
136
+ # This method requires JRuby and cannot be tested on MRI/CRuby.
137
+ lib_path = ENV['TREE_SITTER_RUNTIME_LIB']
138
+ return unless lib_path && File.exist?(lib_path)
139
+
140
+ lib_dir = File.dirname(lib_path)
141
+ current_path = java.lang.System.getProperty('java.library.path') || ''
142
+
143
+ unless current_path.include?(lib_dir)
144
+ new_path = current_path.empty? ? lib_dir : "#{lib_dir}:#{current_path}"
145
+ java.lang.System.setProperty('java.library.path', new_path)
146
+
147
+ # Also set jna.library.path in case it uses JNA
148
+ java.lang.System.setProperty('jna.library.path', new_path)
149
+ end
150
+ # simplecov:enable
151
+ rescue StandardError => _e
152
+ # Ignore errors setting library path
153
+ end
154
+
155
+ # Check if the Java backend is available
156
+ #
157
+ # Checks if:
158
+ # 1. We're running on JRuby
159
+ # 2. Environment variable TREE_SITTER_JAVA_JARS_DIR is set
160
+ # 3. Required JARs (jtreesitter, tree-sitter) are present in that directory
161
+ #
162
+ # @return [Boolean] true if Java backend is available
163
+ # @example
164
+ # if TreeHaver::Backends::Java.available?
165
+ # puts "Java backend ready"
166
+ # end
167
+ class << self
168
+ def available?
169
+ return @loaded if @load_attempted
170
+
171
+ @load_attempted = true
172
+ @loaded = check_availability
173
+ end
174
+
175
+ # Reset the load state (primarily for testing)
176
+ #
177
+ # @return [void]
178
+ # @api private
179
+ def reset!
180
+ @load_attempted = false
181
+ @loaded = false
182
+ @load_error = nil
183
+ @loader = nil
184
+ @java_classes = {}
185
+ end
186
+
187
+ private
188
+
189
+ def check_availability
190
+ # 1. Check Ruby engine
191
+ return false unless RUBY_ENGINE == 'jruby'
192
+
193
+ # 2. Check for required JARs via environment variable
194
+ jars_dir = ENV['TREE_SITTER_JAVA_JARS_DIR']
195
+ return false unless jars_dir && Dir.exist?(jars_dir)
196
+
197
+ # 3. Check if we can load the classes
198
+ begin
199
+ ensure_loader_initialized!
200
+ true
201
+ rescue LoadError, NameError
202
+ false
203
+ end
204
+ end
205
+ end
206
+
207
+ # Get the last load error message (for debugging)
208
+ #
209
+ # @return [String, nil] the error message or nil if no error
210
+ def load_error
211
+ @load_error
212
+ end
213
+
214
+ # Get the loaded Java classes
215
+ #
216
+ # @return [Hash] the Java class references
217
+ # @api private
218
+ def java_classes
219
+ @java_classes
220
+ end
221
+
222
+ # Get capabilities supported by this backend
223
+ #
224
+ # @return [Hash{Symbol => Object}] capability map
225
+ # @example
226
+ # TreeHaver::Backends::Java.capabilities
227
+ # # => { backend: :java, parse: true, query: true, bytes_field: true, incremental: true, comment_support: :nodes_only }
228
+ def capabilities
229
+ # simplecov:disable
230
+ # This method returns meaningful data only on JRuby when java-tree-sitter is available.
231
+ return {} unless available?
232
+
233
+ {
234
+ backend: :java,
235
+ parse: true,
236
+ query: true, # java-tree-sitter supports the Query API
237
+ bytes_field: true,
238
+ incremental: true, # java-tree-sitter supports Parser.parse(Tree, String)
239
+ comment_support: :nodes_only
240
+ }
241
+ # simplecov:enable
242
+ end
243
+
244
+ # Java backend language wrapper (raw backend language)
245
+ #
246
+ # This is a **raw backend language** that wraps a jtreesitter Language object
247
+ # via JRuby's Java interop. It is used to configure the parser for a specific
248
+ # grammar (e.g., TOML, JSON, etc.).
249
+ #
250
+ # Unlike `TreeHaver::Language` (which is a module with factory methods), this
251
+ # class holds the actual loaded language data from a grammar shared library.
252
+ #
253
+ # @api private
254
+ # @see TreeHaver::Language The factory module users should interact with
255
+ # @see https://tree-sitter.github.io/java-tree-sitter/io/github/treesitter/jtreesitter/Language.html
256
+ #
257
+ # simplecov:disable
258
+ # All Java backend implementation classes require JRuby and cannot be tested on MRI/CRuby.
259
+ # JRuby-specific CI jobs would test this code.
260
+ class Language
261
+ include Comparable
262
+
263
+ attr_reader :impl
264
+
265
+ # The backend this language is for
266
+ # @return [Symbol]
267
+ attr_reader :backend
268
+
269
+ # The path this language was loaded from (if known)
270
+ # @return [String, nil]
271
+ attr_reader :path
272
+
273
+ # The symbol name (if known)
274
+ # @return [String, nil]
275
+ attr_reader :symbol
276
+
277
+ # @api private
278
+ def initialize(impl, path: nil, symbol: nil)
279
+ @impl = impl
280
+ @backend = :java
281
+ @path = path
282
+ @symbol = symbol
283
+ end
284
+
285
+ # Compare languages for equality
286
+ #
287
+ # Java languages are equal if they have the same backend, path, and symbol.
288
+ # Path and symbol uniquely identify a loaded language.
289
+ #
290
+ # @param other [Object] object to compare with
291
+ # @return [Integer, nil] -1, 0, 1, or nil if not comparable
292
+ def <=>(other)
293
+ return unless other.is_a?(Language)
294
+ return unless other.backend == @backend
295
+
296
+ # Compare by path first, then symbol
297
+ cmp = (@path || '') <=> (other.path || '')
298
+ return cmp if cmp.nonzero?
299
+
300
+ (@symbol || '') <=> (other.symbol || '')
301
+ end
302
+
303
+ # Hash value for this language (for use in Sets/Hashes)
304
+ # @return [Integer]
305
+ def hash
306
+ [@backend, @path, @symbol].hash
307
+ end
308
+
309
+ # Alias eql? to ==
310
+ alias eql? ==
311
+
312
+ # Load a language from a shared library
313
+ #
314
+ # There are three ways java-tree-sitter can load shared libraries:
315
+ #
316
+ # 1. Libraries in OS library search path (LD_LIBRARY_PATH on Linux,
317
+ # DYLD_LIBRARY_PATH on macOS, PATH on Windows) - loaded via
318
+ # SymbolLookup.libraryLookup(String, Arena)
319
+ #
320
+ # 2. Libraries in java.library.path - loaded via SymbolLookup.loaderLookup()
321
+ #
322
+ # 3. Custom NativeLibraryLookup implementation (e.g., for JARs)
323
+ #
324
+ # @param path [String] path to language shared library (.so/.dylib) or library name
325
+ # @param symbol [String, nil] exported symbol name (e.g., "tree_sitter_toml")
326
+ # @param name [String, nil] logical name (used to derive symbol if not provided)
327
+ # @return [Language] the loaded language
328
+ # @raise [TreeHaver::NotAvailable] if Java backend is not available
329
+ # @example Load by path
330
+ # lang = TreeHaver::Backends::Java::Language.from_library(
331
+ # "/usr/lib/libtree-sitter-toml.so",
332
+ # symbol: "tree_sitter_toml"
333
+ # )
334
+ # @example Load by name (searches LD_LIBRARY_PATH)
335
+ # lang = TreeHaver::Backends::Java::Language.from_library(
336
+ # "tree-sitter-toml",
337
+ # symbol: "tree_sitter_toml"
338
+ # )
339
+ class << self
340
+ def from_library(path, symbol: nil, name: nil)
341
+ raise TreeHaver::NotAvailable, 'Java backend not available' unless Java.available?
342
+
343
+ # Use shared utility for consistent symbol derivation across backends
344
+ # If symbol not provided, derive from name or path
345
+ sym = symbol || LibraryPathUtils.derive_symbol_from_path(path)
346
+ # If name was provided, use it to override the derived symbol
347
+ sym = "tree_sitter_#{name}" if name && !symbol
348
+
349
+ begin
350
+ arena = ::Java::JavaLangForeign::Arena.global
351
+ symbol_lookup_class = ::Java::JavaLangForeign::SymbolLookup
352
+
353
+ # IMPORTANT: Load libtree-sitter.so FIRST by name so its symbols are available
354
+ # Grammar libraries need symbols like ts_language_version from the runtime
355
+ # We cache this lookup at the module level
356
+ unless Java.runtime_lookup
357
+ # Use libraryLookup(String, Arena) to search LD_LIBRARY_PATH
358
+ Java.runtime_lookup = symbol_lookup_class.libraryLookup('libtree-sitter.so', arena)
359
+ end
360
+
361
+ # Now load the grammar library
362
+ if File.exist?(path)
363
+ # Explicit path provided - use libraryLookup(Path, Arena)
364
+ java_path = ::Java::JavaNioFile::Paths.get(path)
365
+ grammar_lookup = symbol_lookup_class.libraryLookup(java_path, arena)
366
+ else
367
+ # Library name provided - use libraryLookup(String, Arena) to search
368
+ # LD_LIBRARY_PATH / DYLD_LIBRARY_PATH / PATH
369
+ grammar_lookup = symbol_lookup_class.libraryLookup(path, arena)
370
+ end
371
+
372
+ # Chain the lookups: grammar first, then runtime library for ts_* symbols
373
+ # This makes ts_language_version available when Language.load() needs it
374
+ combined_lookup = grammar_lookup.or(Java.runtime_lookup)
375
+
376
+ java_lang = Java.java_classes[:Language].load(combined_lookup, sym)
377
+ new(java_lang, path: path, symbol: symbol)
378
+ rescue ::Java::JavaLang::RuntimeException => e
379
+ cause = e.cause
380
+ root_cause = cause&.cause || cause
381
+
382
+ error_msg = "Failed to load language '#{sym}' from #{path}: #{e.message}"
383
+ if root_cause.is_a?(::Java::JavaLang::UnsatisfiedLinkError)
384
+ unresolved = root_cause.message.to_s
385
+ if unresolved.include?('ts_language_version')
386
+ # This specific symbol was renamed in tree-sitter 0.24
387
+ error_msg += "\n\nVersion mismatch detected: The grammar was built against " \
388
+ 'tree-sitter < 0.24 (uses ts_language_version), but your runtime library ' \
389
+ "is tree-sitter >= 0.24 (uses ts_language_abi_version).\n\n" \
390
+ "Solutions:\n" \
391
+ "1. Rebuild the grammar against your version of tree-sitter\n" \
392
+ "2. Install a matching version of tree-sitter (< 0.24)\n" \
393
+ '3. Find a pre-built grammar compatible with tree-sitter 0.24+'
394
+ elsif unresolved.include?('ts_language') || unresolved.include?('ts_parser')
395
+ error_msg += "\n\nThe grammar library has unresolved tree-sitter symbols. " \
396
+ 'Ensure libtree-sitter.so is in LD_LIBRARY_PATH and version-compatible ' \
397
+ 'with the grammar.'
398
+ end
399
+ end
400
+ raise TreeHaver::NotAvailable, error_msg
401
+ rescue ::Java::JavaLang::UnsatisfiedLinkError => e
402
+ raise TreeHaver::NotAvailable,
403
+ "Native library error loading #{path}: #{e.message}. " \
404
+ 'Ensure the library is in LD_LIBRARY_PATH.'
405
+ rescue ::Java::JavaLang::IllegalArgumentException => e
406
+ raise TreeHaver::NotAvailable,
407
+ "Could not find library '#{path}': #{e.message}. " \
408
+ "Ensure it's in LD_LIBRARY_PATH or provide an absolute path."
409
+ end
410
+ end
411
+
412
+ # Load a language by name from java-tree-sitter grammar JARs
413
+ #
414
+ # This method loads grammars that are packaged as java-tree-sitter JARs
415
+ # from Maven Central. These JARs include the native grammar library
416
+ # pre-built for Java's Foreign Function API.
417
+ #
418
+ # @param name [String] the language name (e.g., "java", "python", "toml")
419
+ # @return [Language] the loaded language
420
+ # @raise [TreeHaver::NotAvailable] if the language JAR is not available
421
+ #
422
+ # @example
423
+ # # First, add the grammar JAR to TREE_SITTER_JAVA_JARS_DIR:
424
+ # # tree-sitter-toml-0.23.2.jar from Maven Central
425
+ # lang = TreeHaver::Backends::Java::Language.load_by_name("toml")
426
+ def load_by_name(name)
427
+ raise TreeHaver::NotAvailable, 'Java backend not available' unless Java.available?
428
+
429
+ # Try to find the grammar library in standard locations
430
+ # Look for library names like "tree-sitter-toml" or "libtree-sitter-toml"
431
+ lib_names = [
432
+ "tree-sitter-#{name}",
433
+ "libtree-sitter-#{name}",
434
+ "tree_sitter_#{name}"
435
+ ]
436
+
437
+ begin
438
+ arena = ::Java::JavaLangForeign::Arena.global
439
+ symbol_lookup_class = ::Java::JavaLangForeign::SymbolLookup
440
+
441
+ # Ensure runtime lookup is available
442
+ unless Java.runtime_lookup
443
+ Java.runtime_lookup = symbol_lookup_class.libraryLookup('libtree-sitter.so', arena)
444
+ end
445
+
446
+ # Try each library name
447
+ grammar_lookup = nil
448
+ lib_names.each do |lib_name|
449
+ grammar_lookup = symbol_lookup_class.libraryLookup(lib_name, arena)
450
+ break
451
+ rescue ::Java::JavaLang::IllegalArgumentException
452
+ # Library not found in search path, try next name
453
+ next
454
+ end
455
+
456
+ unless grammar_lookup
457
+ raise TreeHaver::NotAvailable,
458
+ "Failed to load language '#{name}': Library not found. " \
459
+ "Ensure the grammar library (e.g., libtree-sitter-#{name}.so) " \
460
+ 'is in LD_LIBRARY_PATH.'
461
+ end
462
+
463
+ combined_lookup = grammar_lookup.or(Java.runtime_lookup)
464
+ sym = "tree_sitter_#{name}"
465
+ java_lang = Java.java_classes[:Language].load(combined_lookup, sym)
466
+ new(java_lang, symbol: sym)
467
+ rescue ::Java::JavaLang::RuntimeException => e
468
+ raise TreeHaver::NotAvailable,
469
+ "Failed to load language '#{name}': #{e.message}. " \
470
+ "Ensure the grammar library (e.g., libtree-sitter-#{name}.so) " \
471
+ 'is in LD_LIBRARY_PATH.'
472
+ end
473
+ end
474
+ end
475
+
476
+ class << self
477
+ alias from_path from_library
478
+ end
479
+ end
480
+
481
+ # Java backend parser wrapper (raw backend parser)
482
+ #
483
+ # This is a **raw backend parser** that wraps a jtreesitter Parser object via
484
+ # JRuby's Java interop. It is NOT intended for direct use by application code.
485
+ #
486
+ # Users should use `TreeHaver::Parser` which wraps this class and provides:
487
+ # - Automatic backend selection
488
+ # - Language wrapper unwrapping
489
+ # - Tree wrapping with source storage
490
+ # - Unified API across all backends
491
+ #
492
+ # @api private
493
+ # @see TreeHaver::Parser The wrapper class users should interact with
494
+ # @see https://tree-sitter.github.io/java-tree-sitter/io/github/treesitter/jtreesitter/Parser.html
495
+ class Parser
496
+ # Create a new parser instance
497
+ #
498
+ # @raise [TreeHaver::NotAvailable] if Java backend is not available
499
+ def initialize
500
+ raise TreeHaver::NotAvailable, 'Java backend not available' unless Java.available?
501
+
502
+ @parser = Java.java_classes[:Parser].new
503
+ end
504
+
505
+ # Set the language for this parser
506
+ #
507
+ # Note: TreeHaver::Parser unwraps language objects before calling this method.
508
+ # This backend receives the Language wrapper's inner impl (java Language object).
509
+ #
510
+ # @param lang [Object] the Java language object (already unwrapped)
511
+ # @return [void]
512
+ def language=(lang)
513
+ # lang is already unwrapped by TreeHaver::Parser
514
+ @parser.language = lang
515
+ end
516
+
517
+ # Parse source code
518
+ #
519
+ # @param source [String] the source code to parse
520
+ # @return [Tree] raw backend tree (wrapping happens in TreeHaver::Parser)
521
+ def parse(source)
522
+ java_result = @parser.parse(source)
523
+ # jtreesitter 0.26.0 returns Optional<Tree>
524
+ java_tree = unwrap_optional(java_result)
525
+ raise TreeHaver::Error, 'Parser returned no tree' unless java_tree
526
+
527
+ Tree.new(java_tree)
528
+ end
529
+
530
+ # Parse source code with optional incremental parsing
531
+ #
532
+ # Note: old_tree is already unwrapped by TreeHaver::Parser before reaching this method.
533
+ # The backend receives the raw Tree wrapper's impl, not a TreeHaver::Tree.
534
+ #
535
+ # When old_tree is provided and has been edited, tree-sitter will reuse
536
+ # unchanged nodes for better performance.
537
+ #
538
+ # @param old_tree [Tree, nil] previous backend tree for incremental parsing (already unwrapped)
539
+ # @param source [String] the source code to parse
540
+ # @return [Tree] raw backend tree (wrapping happens in TreeHaver::Parser)
541
+ # @see https://tree-sitter.github.io/java-tree-sitter/io/github/treesitter/jtreesitter/Parser.html#parse(java.lang.String,io.github.treesitter.jtreesitter.Tree)
542
+ def parse_string(old_tree, source)
543
+ # old_tree is already unwrapped to Tree wrapper's impl by TreeHaver::Parser
544
+ if old_tree
545
+ # Get the actual Java Tree object
546
+ java_old_tree = if old_tree.is_a?(Tree)
547
+ old_tree.impl
548
+ else
549
+ unwrap_optional(old_tree)
550
+ end
551
+
552
+ java_result = if java_old_tree
553
+ # jtreesitter 0.26.0 API: parse(String source, Tree oldTree)
554
+ @parser.parse(source, java_old_tree)
555
+ else
556
+ @parser.parse(source)
557
+ end
558
+ else
559
+ java_result = @parser.parse(source)
560
+ end
561
+ # jtreesitter 0.26.0 returns Optional<Tree>
562
+ java_tree = unwrap_optional(java_result)
563
+ raise TreeHaver::Error, 'Parser returned no tree' unless java_tree
564
+
565
+ Tree.new(java_tree)
566
+ end
567
+
568
+ private
569
+
570
+ # Unwrap Java Optional
571
+ #
572
+ # jtreesitter 0.26.0 returns Optional<T> from many methods.
573
+ #
574
+ # @param value [Object] an Optional or direct value
575
+ # @return [Object, nil] unwrapped value or nil if empty
576
+ def unwrap_optional(value)
577
+ return value unless value.respond_to?(:isPresent)
578
+
579
+ value.isPresent ? value.get : nil
580
+ end
581
+ end
582
+
583
+ # Java backend tree wrapper (raw backend tree)
584
+ #
585
+ # This is a **raw backend tree** that wraps a jtreesitter Tree object via
586
+ # JRuby's Java interop. It is NOT intended for direct use by application code.
587
+ #
588
+ # == Architecture Note
589
+ #
590
+ # Unlike pure-Ruby backends (Citrus, Parslet, Prism, Psych) which define Tree
591
+ # classes that inherit from `TreeHaver::Base::Tree`, tree-sitter backends (MRI,
592
+ # Rust, FFI, Java) define raw wrapper classes that get wrapped by `TreeHaver::Tree`.
593
+ #
594
+ # The wrapping hierarchy is:
595
+ # Java::Tree (this class) → TreeHaver::Tree → Base::Tree
596
+ #
597
+ # When you use `TreeHaver::Parser#parse`, the returned tree is already wrapped
598
+ # in `TreeHaver::Tree`, which provides the full unified API including:
599
+ # - `#source` - The original source text
600
+ # - `#root_node` - Returns a `TreeHaver::Node` (not raw `Java::Node`)
601
+ # - `#errors`, `#warnings`, `#comments` - Parse diagnostics
602
+ # - `#edit` - Mark tree as edited for incremental parsing
603
+ # - `#to_s`, `#inspect` - String representations
604
+ #
605
+ # This raw class only implements methods that require direct calls to jtreesitter.
606
+ # The wrapper adds Ruby-level conveniences and stores the source text needed for
607
+ # `Node#text` extraction.
608
+ #
609
+ # @api private
610
+ # @see TreeHaver::Tree The wrapper class users should interact with
611
+ # @see TreeHaver::Base::Tree The base class documenting the full Tree API
612
+ # @see https://tree-sitter.github.io/java-tree-sitter/io/github/treesitter/jtreesitter/Tree.html
613
+ class Tree
614
+ attr_reader :impl
615
+
616
+ # @api private
617
+ def initialize(impl)
618
+ @impl = impl
619
+ end
620
+
621
+ # Get the root node of the tree
622
+ #
623
+ # @return [Node] the root node
624
+ # @raise [TreeHaver::Error] if tree has no root node
625
+ def root_node
626
+ result = @impl.rootNode
627
+ # jtreesitter 0.26.0: rootNode() may return Optional<Node> or Node directly
628
+ java_node = if result.respond_to?(:isPresent)
629
+ raise TreeHaver::Error, 'Tree has no root node' unless result.isPresent
630
+
631
+ result.get
632
+ else
633
+ result
634
+ end
635
+ raise TreeHaver::Error, 'Tree has no root node' unless java_node
636
+
637
+ Node.new(java_node)
638
+ end
639
+
640
+ # Mark the tree as edited for incremental re-parsing
641
+ #
642
+ # @param start_byte [Integer] byte offset where the edit starts
643
+ # @param old_end_byte [Integer] byte offset where the old text ended
644
+ # @param new_end_byte [Integer] byte offset where the new text ends
645
+ # @param start_point [Hash] starting position as `{ row:, column: }`
646
+ # @param old_end_point [Hash] old ending position as `{ row:, column: }`
647
+ # @param new_end_point [Hash] new ending position as `{ row:, column: }`
648
+ # @return [void]
649
+ def edit(start_byte:, old_end_byte:, new_end_byte:, start_point:, old_end_point:, new_end_point:)
650
+ point_class = Java.java_classes[:Point]
651
+ input_edit_class = Java.java_classes[:InputEdit]
652
+
653
+ start_pt = point_class.new(start_point[:row], start_point[:column])
654
+ old_end_pt = point_class.new(old_end_point[:row], old_end_point[:column])
655
+ new_end_pt = point_class.new(new_end_point[:row], new_end_point[:column])
656
+
657
+ input_edit = input_edit_class.new(
658
+ start_byte,
659
+ old_end_byte,
660
+ new_end_byte,
661
+ start_pt,
662
+ old_end_pt,
663
+ new_end_pt
664
+ )
665
+
666
+ @impl.edit(input_edit)
667
+ end
668
+ end
669
+
670
+ # Java backend node wrapper (raw backend node)
671
+ #
672
+ # This is a **raw backend node** that wraps a jtreesitter Node object via
673
+ # JRuby's Java interop. It provides the minimal interface needed for tree-sitter
674
+ # operations but is NOT intended for direct use by application code.
675
+ #
676
+ # == Architecture Note
677
+ #
678
+ # Unlike pure-Ruby backends (Citrus, Parslet, Prism, Psych) which define Node
679
+ # classes that inherit from `TreeHaver::Base::Node`, tree-sitter backends (MRI,
680
+ # Rust, FFI, Java) define raw wrapper classes that get wrapped by `TreeHaver::Node`.
681
+ #
682
+ # The wrapping hierarchy is:
683
+ # Java::Node (this class) → TreeHaver::Node → Base::Node
684
+ #
685
+ # When you use `TreeHaver::Parser#parse`, the returned tree's nodes are already
686
+ # wrapped in `TreeHaver::Node`, which provides the full unified API including:
687
+ # - `#children` - Array of child nodes
688
+ # - `#text` - Extract text from source
689
+ # - `#first_child`, `#last_child` - Convenience accessors
690
+ # - `#start_line`, `#end_line` - 1-based line numbers
691
+ # - `#source_position` - Hash with position info
692
+ # - `#each`, `#map`, etc. - Enumerable methods
693
+ # - `#to_s`, `#inspect` - String representations
694
+ #
695
+ # This raw class only implements methods that require direct calls to jtreesitter.
696
+ # The wrapper adds Ruby-level conveniences.
697
+ #
698
+ # @api private
699
+ # @see TreeHaver::Node The wrapper class users should interact with
700
+ # @see TreeHaver::Base::Node The base class documenting the full Node API
701
+ # @see https://tree-sitter.github.io/java-tree-sitter/io/github/treesitter/jtreesitter/Node.html
702
+ class Node
703
+ attr_reader :impl
704
+
705
+ # @api private
706
+ def initialize(impl)
707
+ @impl = impl
708
+ end
709
+
710
+ # Get the type of this node
711
+ #
712
+ # @return [String] the node type
713
+ def type
714
+ @impl.type
715
+ end
716
+
717
+ # Get the number of children
718
+ #
719
+ # @return [Integer] child count
720
+ def child_count
721
+ @impl.childCount
722
+ end
723
+
724
+ # Get a child by index
725
+ #
726
+ # @param index [Integer] the child index
727
+ # @return [Node, nil] the child node or nil if index out of bounds
728
+ def child(index)
729
+ # jtreesitter 0.26.0: getChild returns Optional<Node> or throws IndexOutOfBoundsException
730
+ result = @impl.getChild(index)
731
+ return if result.nil?
732
+
733
+ # Handle Java Optional
734
+ if result.respond_to?(:isPresent)
735
+ return unless result.isPresent
736
+
737
+ java_node = result.get
738
+ else
739
+ # Direct Node return (some jtreesitter versions)
740
+ java_node = result
741
+ end
742
+
743
+ Node.new(java_node)
744
+ rescue ::Java::JavaLang::IndexOutOfBoundsException
745
+ nil
746
+ end
747
+
748
+ # Get a child by field name
749
+ #
750
+ # @param name [String] the field name
751
+ # @return [Node, nil] the child node or nil if not found
752
+ def child_by_field_name(name)
753
+ # jtreesitter 0.26.0: getChildByFieldName returns Optional<Node>
754
+ # However, some versions or scenarios may return null directly
755
+ result = @impl.getChildByFieldName(name)
756
+ return if result.nil?
757
+
758
+ # Handle Java Optional
759
+ if result.respond_to?(:isPresent)
760
+ return unless result.isPresent
761
+
762
+ java_node = result.get
763
+ else
764
+ # Direct Node return (some jtreesitter versions)
765
+ java_node = result
766
+ end
767
+
768
+ Node.new(java_node)
769
+ end
770
+
771
+ # Iterate over children
772
+ #
773
+ # @yield [Node] each child node
774
+ # @return [void]
775
+ def each
776
+ return enum_for(:each) unless block_given?
777
+
778
+ child_count.times do |i|
779
+ yield child(i)
780
+ end
781
+ end
782
+
783
+ # Get the start byte position
784
+ #
785
+ # @return [Integer] start byte
786
+ def start_byte
787
+ @impl.startByte
788
+ end
789
+
790
+ # Get the end byte position
791
+ #
792
+ # @return [Integer] end byte
793
+ def end_byte
794
+ @impl.endByte
795
+ end
796
+
797
+ # Get the start point (row, column)
798
+ #
799
+ # @return [Hash] with :row and :column keys
800
+ def start_point
801
+ pt = @impl.startPoint
802
+ { row: pt.row, column: pt.column }
803
+ end
804
+
805
+ # Get the end point (row, column)
806
+ #
807
+ # @return [Hash] with :row and :column keys
808
+ def end_point
809
+ pt = @impl.endPoint
810
+ { row: pt.row, column: pt.column }
811
+ end
812
+
813
+ # Check if this node has an error
814
+ #
815
+ # @return [Boolean] true if the node or any descendant has an error
816
+ def has_error?
817
+ @impl.hasError
818
+ end
819
+
820
+ # Check if this node is missing
821
+ #
822
+ # @return [Boolean] true if this is a MISSING node
823
+ def missing?
824
+ @impl.isMissing
825
+ end
826
+
827
+ # Check if this is a named node
828
+ #
829
+ # @return [Boolean] true if this is a named node
830
+ def named?
831
+ @impl.isNamed
832
+ end
833
+
834
+ # Get the parent node
835
+ #
836
+ # @return [Node, nil] the parent node or nil if this is the root
837
+ def parent
838
+ # jtreesitter 0.26.0: getParent returns Optional<Node>
839
+ result = @impl.getParent
840
+ return if result.nil?
841
+
842
+ # Handle Java Optional
843
+ if result.respond_to?(:isPresent)
844
+ return unless result.isPresent
845
+
846
+ java_node = result.get
847
+ else
848
+ java_node = result
849
+ end
850
+
851
+ Node.new(java_node)
852
+ end
853
+
854
+ # Get the next sibling node
855
+ #
856
+ # @return [Node, nil] the next sibling or nil if none
857
+ def next_sibling
858
+ # jtreesitter 0.26.0: getNextSibling returns Optional<Node>
859
+ result = @impl.getNextSibling
860
+ return if result.nil?
861
+
862
+ # Handle Java Optional
863
+ if result.respond_to?(:isPresent)
864
+ return unless result.isPresent
865
+
866
+ java_node = result.get
867
+ else
868
+ java_node = result
869
+ end
870
+
871
+ Node.new(java_node)
872
+ end
873
+
874
+ # Get the previous sibling node
875
+ #
876
+ # @return [Node, nil] the previous sibling or nil if none
877
+ def prev_sibling
878
+ # jtreesitter 0.26.0: getPrevSibling returns Optional<Node>
879
+ result = @impl.getPrevSibling
880
+ return if result.nil?
881
+
882
+ # Handle Java Optional
883
+ if result.respond_to?(:isPresent)
884
+ return unless result.isPresent
885
+
886
+ java_node = result.get
887
+ else
888
+ java_node = result
889
+ end
890
+
891
+ Node.new(java_node)
892
+ end
893
+
894
+ # Get the text of this node
895
+ #
896
+ # @return [String] the source text
897
+ def text
898
+ @impl.text.to_s
899
+ end
900
+ end
901
+ # simplecov:enable
902
+
903
+ # Register the availability checker for RSpec dependency tags
904
+ TreeHaver::BackendRegistry.register_availability_checker(:java) do
905
+ available?
906
+ end
907
+ end
908
+ end
909
+ end