Class: TreeHaver::Backends::Java::Language

Inherits:
Object
  • Object
show all
Includes:
Comparable
Defined in:
lib/tree_haver/backends/java.rb

Overview

Wrapper for java-tree-sitter Language

:nocov: All Java backend implementation classes require JRuby and cannot be tested on MRI/CRuby. JRuby-specific CI jobs would test this code.

Instance Attribute Summary collapse

Class Method Summary collapse

Instance Method Summary collapse

Constructor Details

#initialize(impl, path: nil, symbol: nil) ⇒ Language

This method is part of a private API. You should avoid using this method if possible, as it may be removed or be changed in the future.

Returns a new instance of Language.



263
264
265
266
267
268
# File 'lib/tree_haver/backends/java.rb', line 263

def initialize(impl, path: nil, symbol: nil)
  @impl = impl
  @backend = :java
  @path = path
  @symbol = symbol
end

Instance Attribute Details

#backendSymbol (readonly)

The backend this language is for

Returns:

  • (Symbol)


252
253
254
# File 'lib/tree_haver/backends/java.rb', line 252

def backend
  @backend
end

#implObject (readonly)

Returns the value of attribute impl.



248
249
250
# File 'lib/tree_haver/backends/java.rb', line 248

def impl
  @impl
end

#pathString? (readonly)

The path this language was loaded from (if known)

Returns:

  • (String, nil)


256
257
258
# File 'lib/tree_haver/backends/java.rb', line 256

def path
  @path
end

#symbolString? (readonly)

The symbol name (if known)

Returns:

  • (String, nil)


260
261
262
# File 'lib/tree_haver/backends/java.rb', line 260

def symbol
  @symbol
end

Class Method Details

.from_library(path, symbol: nil, name: nil) ⇒ Object Also known as: from_path



325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
# File 'lib/tree_haver/backends/java.rb', line 325

def from_library(path, symbol: nil, name: nil)
  raise TreeHaver::NotAvailable, "Java backend not available" unless Java.available?

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

  begin
    arena = ::Java::JavaLangForeign::Arena.global
    symbol_lookup_class = ::Java::JavaLangForeign::SymbolLookup

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

    # Now load the grammar library
    if File.exist?(path)
      # Explicit path provided - use libraryLookup(Path, Arena)
      java_path = ::Java::JavaNioFile::Paths.get(path)
      grammar_lookup = symbol_lookup_class.libraryLookup(java_path, arena)
    else
      # Library name provided - use libraryLookup(String, Arena) to search
      # LD_LIBRARY_PATH / DYLD_LIBRARY_PATH / PATH
      grammar_lookup = symbol_lookup_class.libraryLookup(path, arena)
    end

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

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

    error_msg = "Failed to load language '#{sym}' from #{path}: #{e.message}"
    if root_cause.is_a?(::Java::JavaLang::UnsatisfiedLinkError)
      unresolved = root_cause.message.to_s
      if unresolved.include?("ts_language_version")
        # This specific symbol was renamed in tree-sitter 0.24
        error_msg += "\n\nVersion mismatch detected: The grammar was built against " \
          "tree-sitter < 0.24 (uses ts_language_version), but your runtime library " \
          "is tree-sitter >= 0.24 (uses ts_language_abi_version).\n\n" \
          "Solutions:\n" \
          "1. Rebuild the grammar against your version of tree-sitter\n" \
          "2. Install a matching version of tree-sitter (< 0.24)\n" \
          "3. Find a pre-built grammar compatible with tree-sitter 0.24+"
      elsif unresolved.include?("ts_language") || unresolved.include?("ts_parser")
        error_msg += "\n\nThe grammar library has unresolved tree-sitter symbols. " \
          "Ensure libtree-sitter.so is in LD_LIBRARY_PATH and version-compatible " \
          "with the grammar."
      end
    end
    raise TreeHaver::NotAvailable, error_msg
  rescue ::Java::JavaLang::UnsatisfiedLinkError => e
    raise TreeHaver::NotAvailable,
      "Native library error loading #{path}: #{e.message}. " \
        "Ensure the library is in LD_LIBRARY_PATH."
  rescue ::Java::JavaLang::IllegalArgumentException => e
    raise TreeHaver::NotAvailable,
      "Could not find library '#{path}': #{e.message}. " \
        "Ensure it's in LD_LIBRARY_PATH or provide an absolute path."
  end
end

.load_by_name(name) ⇒ Language

Load a language by name from java-tree-sitter grammar JARs

This method loads grammars that are packaged as java-tree-sitter JARs from Maven Central. These JARs include the native grammar library pre-built for Java’s Foreign Function API.

Examples:

# First, add the grammar JAR to TREE_SITTER_JAVA_JARS_DIR:
# tree-sitter-toml-0.23.2.jar from Maven Central
lang = TreeHaver::Backends::Java::Language.load_by_name("toml")

Parameters:

  • name (String)

    the language name (e.g., “java”, “python”, “toml”)

Returns:

Raises:



411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
# File 'lib/tree_haver/backends/java.rb', line 411

def load_by_name(name)
  raise TreeHaver::NotAvailable, "Java backend not available" unless Java.available?

  # Try to find the grammar library in standard locations
  # Look for library names like "tree-sitter-toml" or "libtree-sitter-toml"
  lib_names = [
    "tree-sitter-#{name}",
    "libtree-sitter-#{name}",
    "tree_sitter_#{name}",
  ]

  begin
    arena = ::Java::JavaLangForeign::Arena.global
    symbol_lookup_class = ::Java::JavaLangForeign::SymbolLookup

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

    # Try each library name
    grammar_lookup = nil
    lib_names.each do |lib_name|
      grammar_lookup = symbol_lookup_class.libraryLookup(lib_name, arena)
      break
    rescue ::Java::JavaLang::IllegalArgumentException
      # Library not found in search path, try next name
      next
    end

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

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

Instance Method Details

#<=>(other) ⇒ Integer?

Compare languages for equality

Java languages are equal if they have the same backend, path, and symbol. Path and symbol uniquely identify a loaded language.

Parameters:

  • other (Object)

    object to compare with

Returns:

  • (Integer, nil)

    -1, 0, 1, or nil if not comparable



277
278
279
280
281
282
283
284
285
286
# File 'lib/tree_haver/backends/java.rb', line 277

def <=>(other)
  return unless other.is_a?(Language)
  return unless other.backend == @backend

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

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

#hashInteger

Hash value for this language (for use in Sets/Hashes)

Returns:

  • (Integer)


290
291
292
# File 'lib/tree_haver/backends/java.rb', line 290

def hash
  [@backend, @path, @symbol].hash
end