tree_haver 5.0.4 → 7.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (57) hide show
  1. checksums.yaml +4 -4
  2. checksums.yaml.gz.sig +0 -0
  3. data/lib/tree_haver/backend_context.rb +28 -0
  4. data/lib/tree_haver/backend_registry.rb +19 -432
  5. data/lib/tree_haver/contracts.rb +460 -0
  6. data/lib/tree_haver/kaitai_backend.rb +30 -0
  7. data/lib/tree_haver/language_pack.rb +190 -0
  8. data/lib/tree_haver/peg_backends.rb +76 -0
  9. data/lib/tree_haver/version.rb +1 -12
  10. data/lib/tree_haver.rb +7 -1316
  11. data.tar.gz.sig +0 -0
  12. metadata +34 -245
  13. metadata.gz.sig +0 -0
  14. data/CHANGELOG.md +0 -1366
  15. data/CITATION.cff +0 -20
  16. data/CODE_OF_CONDUCT.md +0 -134
  17. data/CONTRIBUTING.md +0 -359
  18. data/FUNDING.md +0 -74
  19. data/LICENSE.txt +0 -21
  20. data/README.md +0 -2347
  21. data/REEK +0 -0
  22. data/RUBOCOP.md +0 -71
  23. data/SECURITY.md +0 -21
  24. data/lib/tree_haver/backend_api.rb +0 -349
  25. data/lib/tree_haver/backends/citrus.rb +0 -487
  26. data/lib/tree_haver/backends/ffi.rb +0 -1009
  27. data/lib/tree_haver/backends/java.rb +0 -893
  28. data/lib/tree_haver/backends/mri.rb +0 -362
  29. data/lib/tree_haver/backends/parslet.rb +0 -560
  30. data/lib/tree_haver/backends/prism.rb +0 -471
  31. data/lib/tree_haver/backends/psych.rb +0 -375
  32. data/lib/tree_haver/backends/rust.rb +0 -239
  33. data/lib/tree_haver/base/language.rb +0 -98
  34. data/lib/tree_haver/base/node.rb +0 -322
  35. data/lib/tree_haver/base/parser.rb +0 -24
  36. data/lib/tree_haver/base/point.rb +0 -48
  37. data/lib/tree_haver/base/tree.rb +0 -128
  38. data/lib/tree_haver/base.rb +0 -12
  39. data/lib/tree_haver/citrus_grammar_finder.rb +0 -218
  40. data/lib/tree_haver/compat.rb +0 -43
  41. data/lib/tree_haver/grammar_finder.rb +0 -374
  42. data/lib/tree_haver/language.rb +0 -295
  43. data/lib/tree_haver/language_registry.rb +0 -190
  44. data/lib/tree_haver/library_path_utils.rb +0 -80
  45. data/lib/tree_haver/node.rb +0 -579
  46. data/lib/tree_haver/parser.rb +0 -438
  47. data/lib/tree_haver/parslet_grammar_finder.rb +0 -224
  48. data/lib/tree_haver/path_validator.rb +0 -353
  49. data/lib/tree_haver/point.rb +0 -27
  50. data/lib/tree_haver/rspec/dependency_tags.rb +0 -1392
  51. data/lib/tree_haver/rspec/testable_node.rb +0 -217
  52. data/lib/tree_haver/rspec.rb +0 -33
  53. data/lib/tree_haver/tree.rb +0 -258
  54. data/sig/tree_haver/backends.rbs +0 -352
  55. data/sig/tree_haver/grammar_finder.rbs +0 -29
  56. data/sig/tree_haver/path_validator.rbs +0 -32
  57. data/sig/tree_haver.rbs +0 -234
@@ -1,893 +0,0 @@
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 = {} # rubocop:disable ThreadSafety/MutableClassInstanceVariable
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
- # :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!
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
167
- def available?
168
- return @loaded if @load_attempted # rubocop:disable ThreadSafety/ClassInstanceVariable
169
- @load_attempted = true # rubocop:disable ThreadSafety/ClassInstanceVariable
170
- @loaded = check_availability # rubocop:disable ThreadSafety/ClassInstanceVariable
171
- end
172
-
173
- # Reset the load state (primarily for testing)
174
- #
175
- # @return [void]
176
- # @api private
177
- def reset!
178
- @load_attempted = false # rubocop:disable ThreadSafety/ClassInstanceVariable
179
- @loaded = false # rubocop:disable ThreadSafety/ClassInstanceVariable
180
- @load_error = nil # rubocop:disable ThreadSafety/ClassInstanceVariable
181
- @loader = nil # rubocop:disable ThreadSafety/ClassInstanceVariable
182
- @java_classes = {} # rubocop:disable ThreadSafety/ClassInstanceVariable
183
- end
184
-
185
- private
186
-
187
- def check_availability
188
- # 1. Check Ruby engine
189
- return false unless RUBY_ENGINE == "jruby"
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
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
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
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
889
- available?
890
- end
891
- end
892
- end
893
- end