tree_haver 1.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.
- checksums.yaml +7 -0
- checksums.yaml.gz.sig +0 -0
- data/CHANGELOG.md +48 -0
- data/CITATION.cff +20 -0
- data/CODE_OF_CONDUCT.md +134 -0
- data/CONTRIBUTING.md +227 -0
- data/FUNDING.md +74 -0
- data/LICENSE.txt +21 -0
- data/README.md +1260 -0
- data/REEK +0 -0
- data/RUBOCOP.md +71 -0
- data/SECURITY.md +21 -0
- data/lib/tree_haver/backends/ffi.rb +410 -0
- data/lib/tree_haver/backends/java.rb +568 -0
- data/lib/tree_haver/backends/mri.rb +129 -0
- data/lib/tree_haver/backends/rust.rb +175 -0
- data/lib/tree_haver/compat.rb +43 -0
- data/lib/tree_haver/grammar_finder.rb +245 -0
- data/lib/tree_haver/language_registry.rb +139 -0
- data/lib/tree_haver/path_validator.rb +333 -0
- data/lib/tree_haver/version.rb +20 -0
- data/lib/tree_haver.rb +710 -0
- data/sig/tree_haver/backends.rbs +285 -0
- data/sig/tree_haver/grammar_finder.rbs +29 -0
- data/sig/tree_haver/path_validator.rbs +31 -0
- data/sig/tree_haver.rbs +131 -0
- data.tar.gz.sig +0 -0
- metadata +298 -0
- metadata.gz.sig +0 -0
|
@@ -0,0 +1,568 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module TreeHaver
|
|
4
|
+
module Backends
|
|
5
|
+
# Java backend for JRuby using java-tree-sitter (jtreesitter)
|
|
6
|
+
#
|
|
7
|
+
# This backend integrates with java-tree-sitter JARs on JRuby,
|
|
8
|
+
# leveraging JRuby's native Java integration for optimal performance.
|
|
9
|
+
#
|
|
10
|
+
# java-tree-sitter provides Java bindings to Tree-sitter and supports:
|
|
11
|
+
# - Parsing source code into syntax trees
|
|
12
|
+
# - Incremental parsing via Parser.parse(Tree, String)
|
|
13
|
+
# - The Query API for pattern matching
|
|
14
|
+
# - Tree editing for incremental re-parsing
|
|
15
|
+
#
|
|
16
|
+
# == Installation
|
|
17
|
+
#
|
|
18
|
+
# 1. Download the JAR from Maven Central:
|
|
19
|
+
# https://central.sonatype.com/artifact/io.github.tree-sitter/jtreesitter
|
|
20
|
+
#
|
|
21
|
+
# 2. Set the environment variable to point to the JAR directory:
|
|
22
|
+
# export TREE_SITTER_JAVA_JARS_DIR=/path/to/jars
|
|
23
|
+
#
|
|
24
|
+
# 3. Use JRuby to run your code:
|
|
25
|
+
# jruby -e "require 'tree_haver'; puts TreeHaver::Backends::Java.available?"
|
|
26
|
+
#
|
|
27
|
+
# @note Only available on JRuby
|
|
28
|
+
# @see https://tree-sitter.github.io/java-tree-sitter/ java-tree-sitter documentation
|
|
29
|
+
# @see https://central.sonatype.com/artifact/io.github.tree-sitter/jtreesitter Maven Central
|
|
30
|
+
module Java
|
|
31
|
+
# The Java package for java-tree-sitter
|
|
32
|
+
JAVA_PACKAGE = "io.github.treesitter.jtreesitter"
|
|
33
|
+
|
|
34
|
+
@load_attempted = false
|
|
35
|
+
@loaded = false
|
|
36
|
+
@java_classes = {} # rubocop:disable ThreadSafety/MutableClassInstanceVariable
|
|
37
|
+
@runtime_lookup = nil # Cached SymbolLookup for libtree-sitter.so
|
|
38
|
+
|
|
39
|
+
module_function
|
|
40
|
+
|
|
41
|
+
# Get the cached runtime library SymbolLookup
|
|
42
|
+
# @return [Object, nil] the SymbolLookup for libtree-sitter.so
|
|
43
|
+
# @api private
|
|
44
|
+
def runtime_lookup
|
|
45
|
+
@runtime_lookup
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
# Set the cached runtime library SymbolLookup
|
|
49
|
+
# @param lookup [Object] the SymbolLookup
|
|
50
|
+
# @api private
|
|
51
|
+
def runtime_lookup=(lookup)
|
|
52
|
+
@runtime_lookup = lookup
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
# Attempt to append JARs from TREE_SITTER_JAVA_JARS_DIR to JRuby classpath
|
|
56
|
+
# and configure native library path from TREE_SITTER_RUNTIME_LIB
|
|
57
|
+
#
|
|
58
|
+
# If the environment variable is set and points to a directory, all .jar files
|
|
59
|
+
# in that directory (recursively) are added to the JRuby classpath.
|
|
60
|
+
#
|
|
61
|
+
# @return [void]
|
|
62
|
+
# @example
|
|
63
|
+
# ENV["TREE_SITTER_JAVA_JARS_DIR"] = "/path/to/java-tree-sitter/jars"
|
|
64
|
+
# TreeHaver::Backends::Java.add_jars_from_env!
|
|
65
|
+
def add_jars_from_env!
|
|
66
|
+
# :nocov:
|
|
67
|
+
# This method requires JRuby and cannot be tested on MRI/CRuby.
|
|
68
|
+
# JRuby-specific CI jobs would test this code.
|
|
69
|
+
require "java"
|
|
70
|
+
|
|
71
|
+
# Add JARs to classpath
|
|
72
|
+
dir = ENV["TREE_SITTER_JAVA_JARS_DIR"]
|
|
73
|
+
if dir && Dir.exist?(dir)
|
|
74
|
+
Dir[File.join(dir, "**", "*.jar")].each do |jar|
|
|
75
|
+
next if $CLASSPATH.include?(jar)
|
|
76
|
+
$CLASSPATH << jar
|
|
77
|
+
end
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
# Configure native library path for libtree-sitter
|
|
81
|
+
# java-tree-sitter uses JNI and needs to find the native library
|
|
82
|
+
configure_native_library_path!
|
|
83
|
+
# :nocov:
|
|
84
|
+
rescue LoadError
|
|
85
|
+
# ignore; not JRuby or Java bridge not available
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
# Configure java.library.path to include the directory containing libtree-sitter
|
|
89
|
+
#
|
|
90
|
+
# @return [void]
|
|
91
|
+
# @api private
|
|
92
|
+
def configure_native_library_path!
|
|
93
|
+
# :nocov:
|
|
94
|
+
# This method requires JRuby and cannot be tested on MRI/CRuby.
|
|
95
|
+
lib_path = ENV["TREE_SITTER_RUNTIME_LIB"]
|
|
96
|
+
return unless lib_path && File.exist?(lib_path)
|
|
97
|
+
|
|
98
|
+
lib_dir = File.dirname(lib_path)
|
|
99
|
+
current_path = java.lang.System.getProperty("java.library.path") || ""
|
|
100
|
+
|
|
101
|
+
unless current_path.include?(lib_dir)
|
|
102
|
+
new_path = current_path.empty? ? lib_dir : "#{lib_dir}:#{current_path}"
|
|
103
|
+
java.lang.System.setProperty("java.library.path", new_path)
|
|
104
|
+
|
|
105
|
+
# Also set jna.library.path in case it uses JNA
|
|
106
|
+
java.lang.System.setProperty("jna.library.path", new_path)
|
|
107
|
+
end
|
|
108
|
+
# :nocov:
|
|
109
|
+
rescue => _error
|
|
110
|
+
# Ignore errors setting library path
|
|
111
|
+
end
|
|
112
|
+
|
|
113
|
+
# Check if the Java backend is available
|
|
114
|
+
#
|
|
115
|
+
# Returns true if running on JRuby and java-tree-sitter classes can be loaded.
|
|
116
|
+
# Automatically attempts to load JARs from ENV["TREE_SITTER_JAVA_JARS_DIR"] if set.
|
|
117
|
+
#
|
|
118
|
+
# @return [Boolean] true if Java backend is available
|
|
119
|
+
# @example
|
|
120
|
+
# if TreeHaver::Backends::Java.available?
|
|
121
|
+
# puts "Java backend is ready"
|
|
122
|
+
# end
|
|
123
|
+
def available?
|
|
124
|
+
return @loaded if @load_attempted
|
|
125
|
+
@load_attempted = true
|
|
126
|
+
@loaded = false
|
|
127
|
+
@load_error = nil
|
|
128
|
+
|
|
129
|
+
return false unless defined?(RUBY_ENGINE) && RUBY_ENGINE == "jruby"
|
|
130
|
+
|
|
131
|
+
# :nocov:
|
|
132
|
+
# Everything below requires JRuby and cannot be tested on MRI/CRuby.
|
|
133
|
+
# JRuby-specific CI jobs would test this code.
|
|
134
|
+
begin
|
|
135
|
+
require "java"
|
|
136
|
+
rescue LoadError
|
|
137
|
+
@load_error = "JRuby java bridge not available"
|
|
138
|
+
return false
|
|
139
|
+
end
|
|
140
|
+
|
|
141
|
+
# Optionally augment classpath and configure native library path
|
|
142
|
+
add_jars_from_env!
|
|
143
|
+
|
|
144
|
+
# Try to load the java-tree-sitter classes
|
|
145
|
+
# Load Parser first as it doesn't trigger native library loading
|
|
146
|
+
# Language class triggers native lib loading in its static initializer
|
|
147
|
+
begin
|
|
148
|
+
# These classes don't require native library initialization
|
|
149
|
+
@java_classes[:Parser] = ::Java::IoGithubTreesitterJtreesitter::Parser
|
|
150
|
+
@java_classes[:Tree] = ::Java::IoGithubTreesitterJtreesitter::Tree
|
|
151
|
+
@java_classes[:Node] = ::Java::IoGithubTreesitterJtreesitter::Node
|
|
152
|
+
@java_classes[:InputEdit] = ::Java::IoGithubTreesitterJtreesitter::InputEdit
|
|
153
|
+
@java_classes[:Point] = ::Java::IoGithubTreesitterJtreesitter::Point
|
|
154
|
+
|
|
155
|
+
# Language class may fail if native library isn't found - try it last
|
|
156
|
+
# and provide a helpful error message
|
|
157
|
+
begin
|
|
158
|
+
@java_classes[:Language] = ::Java::IoGithubTreesitterJtreesitter::Language
|
|
159
|
+
rescue NameError => e
|
|
160
|
+
# Language failed but other classes loaded - native lib issue
|
|
161
|
+
@load_error = "Language class failed to initialize (native library issue): #{e.message}"
|
|
162
|
+
# Clear loaded classes since we can't fully function without Language
|
|
163
|
+
@java_classes.clear
|
|
164
|
+
return false
|
|
165
|
+
end
|
|
166
|
+
|
|
167
|
+
@loaded = true
|
|
168
|
+
rescue NameError => e
|
|
169
|
+
@load_error = "java-tree-sitter classes not found: #{e.message}"
|
|
170
|
+
@loaded = false
|
|
171
|
+
end
|
|
172
|
+
|
|
173
|
+
@loaded
|
|
174
|
+
# :nocov:
|
|
175
|
+
end
|
|
176
|
+
|
|
177
|
+
# Get the last load error message (for debugging)
|
|
178
|
+
#
|
|
179
|
+
# @return [String, nil] the error message or nil if no error
|
|
180
|
+
def load_error
|
|
181
|
+
@load_error
|
|
182
|
+
end
|
|
183
|
+
|
|
184
|
+
# Reset the load state (primarily for testing)
|
|
185
|
+
#
|
|
186
|
+
# @return [void]
|
|
187
|
+
# @api private
|
|
188
|
+
def reset!
|
|
189
|
+
@load_attempted = false
|
|
190
|
+
@loaded = false
|
|
191
|
+
@load_error = nil
|
|
192
|
+
@java_classes = {}
|
|
193
|
+
end
|
|
194
|
+
|
|
195
|
+
# Get the loaded Java classes
|
|
196
|
+
#
|
|
197
|
+
# @return [Hash] the Java class references
|
|
198
|
+
# @api private
|
|
199
|
+
def java_classes
|
|
200
|
+
@java_classes
|
|
201
|
+
end
|
|
202
|
+
|
|
203
|
+
# Get capabilities supported by this backend
|
|
204
|
+
#
|
|
205
|
+
# @return [Hash{Symbol => Object}] capability map
|
|
206
|
+
# @example
|
|
207
|
+
# TreeHaver::Backends::Java.capabilities
|
|
208
|
+
# # => { backend: :java, parse: true, query: true, bytes_field: true, incremental: true }
|
|
209
|
+
def capabilities
|
|
210
|
+
# :nocov:
|
|
211
|
+
# This method returns meaningful data only on JRuby when java-tree-sitter is available.
|
|
212
|
+
return {} unless available?
|
|
213
|
+
{
|
|
214
|
+
backend: :java,
|
|
215
|
+
parse: true,
|
|
216
|
+
query: true, # java-tree-sitter supports the Query API
|
|
217
|
+
bytes_field: true,
|
|
218
|
+
incremental: true, # java-tree-sitter supports Parser.parse(Tree, String)
|
|
219
|
+
}
|
|
220
|
+
# :nocov:
|
|
221
|
+
end
|
|
222
|
+
|
|
223
|
+
# Wrapper for java-tree-sitter Language
|
|
224
|
+
#
|
|
225
|
+
# @see https://tree-sitter.github.io/java-tree-sitter/io/github/treesitter/jtreesitter/Language.html
|
|
226
|
+
#
|
|
227
|
+
# :nocov:
|
|
228
|
+
# All Java backend implementation classes require JRuby and cannot be tested on MRI/CRuby.
|
|
229
|
+
# JRuby-specific CI jobs would test this code.
|
|
230
|
+
class Language
|
|
231
|
+
attr_reader :impl
|
|
232
|
+
|
|
233
|
+
# @api private
|
|
234
|
+
def initialize(impl)
|
|
235
|
+
@impl = impl
|
|
236
|
+
end
|
|
237
|
+
|
|
238
|
+
# Load a language from a shared library
|
|
239
|
+
#
|
|
240
|
+
# There are three ways java-tree-sitter can load shared libraries:
|
|
241
|
+
#
|
|
242
|
+
# 1. Libraries in OS library search path (LD_LIBRARY_PATH on Linux,
|
|
243
|
+
# DYLD_LIBRARY_PATH on macOS, PATH on Windows) - loaded via
|
|
244
|
+
# SymbolLookup.libraryLookup(String, Arena)
|
|
245
|
+
#
|
|
246
|
+
# 2. Libraries in java.library.path - loaded via SymbolLookup.loaderLookup()
|
|
247
|
+
#
|
|
248
|
+
# 3. Custom NativeLibraryLookup implementation (e.g., for JARs)
|
|
249
|
+
#
|
|
250
|
+
# @param path [String] path to language shared library (.so/.dylib) or library name
|
|
251
|
+
# @param symbol [String, nil] exported symbol name (e.g., "tree_sitter_toml")
|
|
252
|
+
# @param name [String, nil] logical name (used to derive symbol if not provided)
|
|
253
|
+
# @return [Language] the loaded language
|
|
254
|
+
# @raise [TreeHaver::NotAvailable] if Java backend is not available
|
|
255
|
+
# @example Load by path
|
|
256
|
+
# lang = TreeHaver::Backends::Java::Language.from_library(
|
|
257
|
+
# "/usr/lib/libtree-sitter-toml.so",
|
|
258
|
+
# symbol: "tree_sitter_toml"
|
|
259
|
+
# )
|
|
260
|
+
# @example Load by name (searches LD_LIBRARY_PATH)
|
|
261
|
+
# lang = TreeHaver::Backends::Java::Language.from_library(
|
|
262
|
+
# "tree-sitter-toml",
|
|
263
|
+
# symbol: "tree_sitter_toml"
|
|
264
|
+
# )
|
|
265
|
+
class << self
|
|
266
|
+
def from_library(path, symbol: nil, name: nil)
|
|
267
|
+
raise TreeHaver::NotAvailable, "Java backend not available" unless Java.available?
|
|
268
|
+
|
|
269
|
+
# Derive symbol from name or path if not provided
|
|
270
|
+
base_name = File.basename(path, ".*").sub(/^lib/, "")
|
|
271
|
+
sym = symbol || "tree_sitter_#{name || base_name.sub(/^tree-sitter-/, "")}"
|
|
272
|
+
|
|
273
|
+
begin
|
|
274
|
+
arena = ::Java::JavaLangForeign::Arena.global
|
|
275
|
+
symbol_lookup_class = ::Java::JavaLangForeign::SymbolLookup
|
|
276
|
+
|
|
277
|
+
# IMPORTANT: Load libtree-sitter.so FIRST by name so its symbols are available
|
|
278
|
+
# Grammar libraries need symbols like ts_language_version from the runtime
|
|
279
|
+
# We cache this lookup at the module level
|
|
280
|
+
unless Java.runtime_lookup
|
|
281
|
+
# Use libraryLookup(String, Arena) to search LD_LIBRARY_PATH
|
|
282
|
+
Java.runtime_lookup = symbol_lookup_class.libraryLookup("libtree-sitter.so", arena)
|
|
283
|
+
end
|
|
284
|
+
|
|
285
|
+
# Now load the grammar library
|
|
286
|
+
if File.exist?(path)
|
|
287
|
+
# Explicit path provided - use libraryLookup(Path, Arena)
|
|
288
|
+
java_path = ::Java::JavaNioFile::Paths.get(path)
|
|
289
|
+
grammar_lookup = symbol_lookup_class.libraryLookup(java_path, arena)
|
|
290
|
+
else
|
|
291
|
+
# Library name provided - use libraryLookup(String, Arena) to search
|
|
292
|
+
# LD_LIBRARY_PATH / DYLD_LIBRARY_PATH / PATH
|
|
293
|
+
grammar_lookup = symbol_lookup_class.libraryLookup(path, arena)
|
|
294
|
+
end
|
|
295
|
+
|
|
296
|
+
# Chain the lookups: grammar first, then runtime library for ts_* symbols
|
|
297
|
+
# This makes ts_language_version available when Language.load() needs it
|
|
298
|
+
combined_lookup = grammar_lookup.or(Java.runtime_lookup)
|
|
299
|
+
|
|
300
|
+
java_lang = Java.java_classes[:Language].load(combined_lookup, sym)
|
|
301
|
+
new(java_lang)
|
|
302
|
+
rescue ::Java::JavaLang::RuntimeException => e
|
|
303
|
+
cause = e.cause
|
|
304
|
+
root_cause = cause&.cause || cause
|
|
305
|
+
|
|
306
|
+
error_msg = "Failed to load language '#{sym}' from #{path}: #{e.message}"
|
|
307
|
+
if root_cause.is_a?(::Java::JavaLang::UnsatisfiedLinkError)
|
|
308
|
+
unresolved = root_cause.message.to_s
|
|
309
|
+
if unresolved.include?("ts_language_version")
|
|
310
|
+
# This specific symbol was renamed in tree-sitter 0.24
|
|
311
|
+
error_msg += "\n\nVersion mismatch detected: The grammar was built against " \
|
|
312
|
+
"tree-sitter < 0.24 (uses ts_language_version), but your runtime library " \
|
|
313
|
+
"is tree-sitter >= 0.24 (uses ts_language_abi_version).\n\n" \
|
|
314
|
+
"Solutions:\n" \
|
|
315
|
+
"1. Rebuild the grammar against your version of tree-sitter\n" \
|
|
316
|
+
"2. Install a matching version of tree-sitter (< 0.24)\n" \
|
|
317
|
+
"3. Find a pre-built grammar compatible with tree-sitter 0.24+"
|
|
318
|
+
elsif unresolved.include?("ts_language") || unresolved.include?("ts_parser")
|
|
319
|
+
error_msg += "\n\nThe grammar library has unresolved tree-sitter symbols. " \
|
|
320
|
+
"Ensure libtree-sitter.so is in LD_LIBRARY_PATH and version-compatible " \
|
|
321
|
+
"with the grammar."
|
|
322
|
+
end
|
|
323
|
+
end
|
|
324
|
+
raise TreeHaver::NotAvailable, error_msg
|
|
325
|
+
rescue ::Java::JavaLang::UnsatisfiedLinkError => e
|
|
326
|
+
raise TreeHaver::NotAvailable,
|
|
327
|
+
"Native library error loading #{path}: #{e.message}. " \
|
|
328
|
+
"Ensure the library is in LD_LIBRARY_PATH."
|
|
329
|
+
rescue ::Java::JavaLang::IllegalArgumentException => e
|
|
330
|
+
raise TreeHaver::NotAvailable,
|
|
331
|
+
"Could not find library '#{path}': #{e.message}. " \
|
|
332
|
+
"Ensure it's in LD_LIBRARY_PATH or provide an absolute path."
|
|
333
|
+
end
|
|
334
|
+
end
|
|
335
|
+
|
|
336
|
+
# Load a language by name from java-tree-sitter grammar JARs
|
|
337
|
+
#
|
|
338
|
+
# This method loads grammars that are packaged as java-tree-sitter JARs
|
|
339
|
+
# from Maven Central. These JARs include the native grammar library
|
|
340
|
+
# pre-built for Java's Foreign Function API.
|
|
341
|
+
#
|
|
342
|
+
# @param name [String] the language name (e.g., "java", "python", "toml")
|
|
343
|
+
# @return [Language] the loaded language
|
|
344
|
+
# @raise [TreeHaver::NotAvailable] if the language JAR is not available
|
|
345
|
+
#
|
|
346
|
+
# @example
|
|
347
|
+
# # First, add the grammar JAR to TREE_SITTER_JAVA_JARS_DIR:
|
|
348
|
+
# # tree-sitter-toml-0.23.2.jar from Maven Central
|
|
349
|
+
# lang = TreeHaver::Backends::Java::Language.load_by_name("toml")
|
|
350
|
+
def load_by_name(name)
|
|
351
|
+
raise TreeHaver::NotAvailable, "Java backend not available" unless Java.available?
|
|
352
|
+
|
|
353
|
+
begin
|
|
354
|
+
# java-tree-sitter's Language.load(String) searches for the language
|
|
355
|
+
# in the classpath using standard naming conventions
|
|
356
|
+
java_lang = Java.java_classes[:Language].load(name)
|
|
357
|
+
new(java_lang)
|
|
358
|
+
rescue ::Java::JavaLang::RuntimeException => e
|
|
359
|
+
raise TreeHaver::NotAvailable,
|
|
360
|
+
"Failed to load language '#{name}': #{e.message}. " \
|
|
361
|
+
"Ensure the grammar JAR (e.g., tree-sitter-#{name}-X.Y.Z.jar) " \
|
|
362
|
+
"is in TREE_SITTER_JAVA_JARS_DIR."
|
|
363
|
+
end
|
|
364
|
+
end
|
|
365
|
+
end
|
|
366
|
+
|
|
367
|
+
class << self
|
|
368
|
+
alias_method :from_path, :from_library
|
|
369
|
+
end
|
|
370
|
+
end
|
|
371
|
+
|
|
372
|
+
# Wrapper for java-tree-sitter Parser
|
|
373
|
+
#
|
|
374
|
+
# @see https://tree-sitter.github.io/java-tree-sitter/io/github/treesitter/jtreesitter/Parser.html
|
|
375
|
+
class Parser
|
|
376
|
+
# Create a new parser instance
|
|
377
|
+
#
|
|
378
|
+
# @raise [TreeHaver::NotAvailable] if Java backend is not available
|
|
379
|
+
def initialize
|
|
380
|
+
raise TreeHaver::NotAvailable, "Java backend not available" unless Java.available?
|
|
381
|
+
@parser = Java.java_classes[:Parser].new
|
|
382
|
+
end
|
|
383
|
+
|
|
384
|
+
# Set the language for this parser
|
|
385
|
+
#
|
|
386
|
+
# @param lang [Language] the language to use
|
|
387
|
+
# @return [void]
|
|
388
|
+
def language=(lang)
|
|
389
|
+
java_lang = lang.is_a?(Language) ? lang.impl : lang
|
|
390
|
+
@parser.language = java_lang
|
|
391
|
+
end
|
|
392
|
+
|
|
393
|
+
# Parse source code
|
|
394
|
+
#
|
|
395
|
+
# @param source [String] the source code to parse
|
|
396
|
+
# @return [Tree] the parsed syntax tree
|
|
397
|
+
def parse(source)
|
|
398
|
+
java_tree = @parser.parse(source)
|
|
399
|
+
Tree.new(java_tree)
|
|
400
|
+
end
|
|
401
|
+
|
|
402
|
+
# Parse source code with optional incremental parsing
|
|
403
|
+
#
|
|
404
|
+
# When old_tree is provided and has been edited, tree-sitter will reuse
|
|
405
|
+
# unchanged nodes for better performance.
|
|
406
|
+
#
|
|
407
|
+
# @param old_tree [Tree, nil] previous tree for incremental parsing
|
|
408
|
+
# @param source [String] the source code to parse
|
|
409
|
+
# @return [Tree] the parsed syntax tree
|
|
410
|
+
# @see https://tree-sitter.github.io/java-tree-sitter/io/github/treesitter/jtreesitter/Parser.html#parse(io.github.treesitter.jtreesitter.Tree,java.lang.String)
|
|
411
|
+
def parse_string(old_tree, source)
|
|
412
|
+
if old_tree
|
|
413
|
+
java_old_tree = old_tree.is_a?(Tree) ? old_tree.impl : old_tree
|
|
414
|
+
java_tree = @parser.parse(java_old_tree, source)
|
|
415
|
+
else
|
|
416
|
+
java_tree = @parser.parse(source)
|
|
417
|
+
end
|
|
418
|
+
Tree.new(java_tree)
|
|
419
|
+
end
|
|
420
|
+
end
|
|
421
|
+
|
|
422
|
+
# Wrapper for java-tree-sitter Tree
|
|
423
|
+
#
|
|
424
|
+
# @see https://tree-sitter.github.io/java-tree-sitter/io/github/treesitter/jtreesitter/Tree.html
|
|
425
|
+
class Tree
|
|
426
|
+
attr_reader :impl
|
|
427
|
+
|
|
428
|
+
# @api private
|
|
429
|
+
def initialize(impl)
|
|
430
|
+
@impl = impl
|
|
431
|
+
end
|
|
432
|
+
|
|
433
|
+
# Get the root node of the tree
|
|
434
|
+
#
|
|
435
|
+
# @return [Node] the root node
|
|
436
|
+
def root_node
|
|
437
|
+
Node.new(@impl.rootNode)
|
|
438
|
+
end
|
|
439
|
+
|
|
440
|
+
# Mark the tree as edited for incremental re-parsing
|
|
441
|
+
#
|
|
442
|
+
# @param start_byte [Integer] byte offset where the edit starts
|
|
443
|
+
# @param old_end_byte [Integer] byte offset where the old text ended
|
|
444
|
+
# @param new_end_byte [Integer] byte offset where the new text ends
|
|
445
|
+
# @param start_point [Hash] starting position as `{ row:, column: }`
|
|
446
|
+
# @param old_end_point [Hash] old ending position as `{ row:, column: }`
|
|
447
|
+
# @param new_end_point [Hash] new ending position as `{ row:, column: }`
|
|
448
|
+
# @return [void]
|
|
449
|
+
def edit(start_byte:, old_end_byte:, new_end_byte:, start_point:, old_end_point:, new_end_point:)
|
|
450
|
+
point_class = Java.java_classes[:Point]
|
|
451
|
+
input_edit_class = Java.java_classes[:InputEdit]
|
|
452
|
+
|
|
453
|
+
start_pt = point_class.new(start_point[:row], start_point[:column])
|
|
454
|
+
old_end_pt = point_class.new(old_end_point[:row], old_end_point[:column])
|
|
455
|
+
new_end_pt = point_class.new(new_end_point[:row], new_end_point[:column])
|
|
456
|
+
|
|
457
|
+
input_edit = input_edit_class.new(
|
|
458
|
+
start_byte,
|
|
459
|
+
old_end_byte,
|
|
460
|
+
new_end_byte,
|
|
461
|
+
start_pt,
|
|
462
|
+
old_end_pt,
|
|
463
|
+
new_end_pt,
|
|
464
|
+
)
|
|
465
|
+
|
|
466
|
+
@impl.edit(input_edit)
|
|
467
|
+
end
|
|
468
|
+
end
|
|
469
|
+
|
|
470
|
+
# Wrapper for java-tree-sitter Node
|
|
471
|
+
#
|
|
472
|
+
# @see https://tree-sitter.github.io/java-tree-sitter/io/github/treesitter/jtreesitter/Node.html
|
|
473
|
+
class Node
|
|
474
|
+
attr_reader :impl
|
|
475
|
+
|
|
476
|
+
# @api private
|
|
477
|
+
def initialize(impl)
|
|
478
|
+
@impl = impl
|
|
479
|
+
end
|
|
480
|
+
|
|
481
|
+
# Get the type of this node
|
|
482
|
+
#
|
|
483
|
+
# @return [String] the node type
|
|
484
|
+
def type
|
|
485
|
+
@impl.type
|
|
486
|
+
end
|
|
487
|
+
|
|
488
|
+
# Get the number of children
|
|
489
|
+
#
|
|
490
|
+
# @return [Integer] child count
|
|
491
|
+
def child_count
|
|
492
|
+
@impl.childCount
|
|
493
|
+
end
|
|
494
|
+
|
|
495
|
+
# Get a child by index
|
|
496
|
+
#
|
|
497
|
+
# @param index [Integer] the child index
|
|
498
|
+
# @return [Node] the child node
|
|
499
|
+
def child(index)
|
|
500
|
+
Node.new(@impl.child(index))
|
|
501
|
+
end
|
|
502
|
+
|
|
503
|
+
# Iterate over children
|
|
504
|
+
#
|
|
505
|
+
# @yield [Node] each child node
|
|
506
|
+
# @return [void]
|
|
507
|
+
def each
|
|
508
|
+
return enum_for(:each) unless block_given?
|
|
509
|
+
child_count.times do |i|
|
|
510
|
+
yield child(i)
|
|
511
|
+
end
|
|
512
|
+
end
|
|
513
|
+
|
|
514
|
+
# Get the start byte position
|
|
515
|
+
#
|
|
516
|
+
# @return [Integer] start byte
|
|
517
|
+
def start_byte
|
|
518
|
+
@impl.startByte
|
|
519
|
+
end
|
|
520
|
+
|
|
521
|
+
# Get the end byte position
|
|
522
|
+
#
|
|
523
|
+
# @return [Integer] end byte
|
|
524
|
+
def end_byte
|
|
525
|
+
@impl.endByte
|
|
526
|
+
end
|
|
527
|
+
|
|
528
|
+
# Get the start point (row, column)
|
|
529
|
+
#
|
|
530
|
+
# @return [Hash] with :row and :column keys
|
|
531
|
+
def start_point
|
|
532
|
+
pt = @impl.startPoint
|
|
533
|
+
{row: pt.row, column: pt.column}
|
|
534
|
+
end
|
|
535
|
+
|
|
536
|
+
# Get the end point (row, column)
|
|
537
|
+
#
|
|
538
|
+
# @return [Hash] with :row and :column keys
|
|
539
|
+
def end_point
|
|
540
|
+
pt = @impl.endPoint
|
|
541
|
+
{row: pt.row, column: pt.column}
|
|
542
|
+
end
|
|
543
|
+
|
|
544
|
+
# Check if this node has an error
|
|
545
|
+
#
|
|
546
|
+
# @return [Boolean] true if the node or any descendant has an error
|
|
547
|
+
def has_error?
|
|
548
|
+
@impl.hasError
|
|
549
|
+
end
|
|
550
|
+
|
|
551
|
+
# Check if this node is missing
|
|
552
|
+
#
|
|
553
|
+
# @return [Boolean] true if this is a MISSING node
|
|
554
|
+
def missing?
|
|
555
|
+
@impl.isMissing
|
|
556
|
+
end
|
|
557
|
+
|
|
558
|
+
# Get the text of this node
|
|
559
|
+
#
|
|
560
|
+
# @return [String] the source text
|
|
561
|
+
def text
|
|
562
|
+
@impl.text.to_s
|
|
563
|
+
end
|
|
564
|
+
end
|
|
565
|
+
# :nocov:
|
|
566
|
+
end
|
|
567
|
+
end
|
|
568
|
+
end
|
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module TreeHaver
|
|
4
|
+
module Backends
|
|
5
|
+
# MRI backend using the ruby_tree_sitter gem
|
|
6
|
+
#
|
|
7
|
+
# This backend wraps the ruby_tree_sitter gem, which is a native C extension
|
|
8
|
+
# for MRI Ruby. It provides the most feature-complete Tree-sitter integration
|
|
9
|
+
# on MRI, including support for the Query API.
|
|
10
|
+
#
|
|
11
|
+
# @note This backend only works on MRI Ruby, not JRuby or TruffleRuby
|
|
12
|
+
# @see https://github.com/Faveod/ruby-tree-sitter ruby_tree_sitter
|
|
13
|
+
module MRI
|
|
14
|
+
@load_attempted = false
|
|
15
|
+
@loaded = false
|
|
16
|
+
|
|
17
|
+
# Check if the MRI backend is available
|
|
18
|
+
#
|
|
19
|
+
# Attempts to require ruby_tree_sitter on first call and caches the result.
|
|
20
|
+
#
|
|
21
|
+
# @return [Boolean] true if ruby_tree_sitter is available
|
|
22
|
+
# @example
|
|
23
|
+
# if TreeHaver::Backends::MRI.available?
|
|
24
|
+
# puts "MRI backend is ready"
|
|
25
|
+
# end
|
|
26
|
+
class << self
|
|
27
|
+
def available?
|
|
28
|
+
return @loaded if @load_attempted # rubocop:disable ThreadSafety/ClassInstanceVariable
|
|
29
|
+
@load_attempted = true # rubocop:disable ThreadSafety/ClassInstanceVariable
|
|
30
|
+
begin
|
|
31
|
+
require "ruby_tree_sitter"
|
|
32
|
+
|
|
33
|
+
@loaded = true # rubocop:disable ThreadSafety/ClassInstanceVariable
|
|
34
|
+
rescue LoadError
|
|
35
|
+
@loaded = false # rubocop:disable ThreadSafety/ClassInstanceVariable
|
|
36
|
+
end
|
|
37
|
+
@loaded # rubocop:disable ThreadSafety/ClassInstanceVariable
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
# Get capabilities supported by this backend
|
|
41
|
+
#
|
|
42
|
+
# @return [Hash{Symbol => Object}] capability map
|
|
43
|
+
# @example
|
|
44
|
+
# TreeHaver::Backends::MRI.capabilities
|
|
45
|
+
# # => { backend: :mri, query: true, bytes_field: true, incremental: true }
|
|
46
|
+
def capabilities
|
|
47
|
+
return {} unless available?
|
|
48
|
+
{
|
|
49
|
+
backend: :mri,
|
|
50
|
+
query: true,
|
|
51
|
+
bytes_field: true,
|
|
52
|
+
incremental: true,
|
|
53
|
+
}
|
|
54
|
+
end
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
# Wrapper for ruby_tree_sitter Language
|
|
58
|
+
#
|
|
59
|
+
# This is a thin pass-through to ::TreeSitter::Language from ruby_tree_sitter.
|
|
60
|
+
class Language
|
|
61
|
+
# Load a language from a shared library path
|
|
62
|
+
#
|
|
63
|
+
# @param path [String] absolute path to the language shared library
|
|
64
|
+
# @return [::TreeSitter::Language] the loaded language handle
|
|
65
|
+
# @raise [TreeHaver::NotAvailable] if ruby_tree_sitter is not available
|
|
66
|
+
# @example
|
|
67
|
+
# lang = TreeHaver::Backends::MRI::Language.from_path("/usr/local/lib/libtree-sitter-toml.so")
|
|
68
|
+
class << self
|
|
69
|
+
def from_path(path)
|
|
70
|
+
raise TreeHaver::NotAvailable, "ruby_tree_sitter not available" unless MRI.available?
|
|
71
|
+
::TreeSitter::Language.load(path)
|
|
72
|
+
end
|
|
73
|
+
end
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
# Wrapper for ruby_tree_sitter Parser
|
|
77
|
+
#
|
|
78
|
+
# This is a thin pass-through to ::TreeSitter::Parser from ruby_tree_sitter.
|
|
79
|
+
class Parser
|
|
80
|
+
# Create a new parser instance
|
|
81
|
+
#
|
|
82
|
+
# @raise [TreeHaver::NotAvailable] if ruby_tree_sitter is not available
|
|
83
|
+
def initialize
|
|
84
|
+
raise TreeHaver::NotAvailable, "ruby_tree_sitter not available" unless MRI.available?
|
|
85
|
+
@parser = ::TreeSitter::Parser.new
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
# Set the language for this parser
|
|
89
|
+
#
|
|
90
|
+
# @param lang [::TreeSitter::Language] the language to use
|
|
91
|
+
# @return [::TreeSitter::Language] the language that was set
|
|
92
|
+
def language=(lang)
|
|
93
|
+
@parser.language = lang
|
|
94
|
+
end
|
|
95
|
+
|
|
96
|
+
# Parse source code
|
|
97
|
+
#
|
|
98
|
+
# @param source [String] the source code to parse
|
|
99
|
+
# @return [::TreeSitter::Tree] the parsed syntax tree
|
|
100
|
+
def parse(source)
|
|
101
|
+
@parser.parse(source)
|
|
102
|
+
end
|
|
103
|
+
|
|
104
|
+
# Parse source code with optional incremental parsing
|
|
105
|
+
#
|
|
106
|
+
# @param old_tree [::TreeSitter::Tree, nil] previous tree for incremental parsing
|
|
107
|
+
# @param source [String] the source code to parse
|
|
108
|
+
# @return [::TreeSitter::Tree] the parsed syntax tree
|
|
109
|
+
def parse_string(old_tree, source)
|
|
110
|
+
@parser.parse_string(old_tree, source)
|
|
111
|
+
end
|
|
112
|
+
end
|
|
113
|
+
|
|
114
|
+
# Wrapper for ruby_tree_sitter Tree
|
|
115
|
+
#
|
|
116
|
+
# Not used directly; TreeHaver passes through ::TreeSitter::Tree objects.
|
|
117
|
+
class Tree
|
|
118
|
+
# Not used directly; we pass through ruby_tree_sitter::Tree
|
|
119
|
+
end
|
|
120
|
+
|
|
121
|
+
# Wrapper for ruby_tree_sitter Node
|
|
122
|
+
#
|
|
123
|
+
# Not used directly; TreeHaver passes through ::TreeSitter::Node objects.
|
|
124
|
+
class Node
|
|
125
|
+
# Not used directly; we pass through ruby_tree_sitter::Node
|
|
126
|
+
end
|
|
127
|
+
end
|
|
128
|
+
end
|
|
129
|
+
end
|