tree_haver 3.1.2 → 3.2.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.
@@ -451,8 +451,14 @@ module TreeHaver
451
451
  private
452
452
 
453
453
  def calculate_point(offset)
454
+ return {row: 0, column: 0} if offset <= 0
455
+
454
456
  lines_before = @source[0...offset].count("\n")
455
- line_start = @source.rindex("\n", offset - 1) || -1
457
+ # Find the newline before this offset (or -1 if we're on line 0)
458
+ line_start = if offset > 0
459
+ @source.rindex("\n", offset - 1)
460
+ end
461
+ line_start ||= -1
456
462
  column = offset - line_start - 1
457
463
  {row: lines_before, column: column}
458
464
  end
@@ -5,8 +5,7 @@ module TreeHaver
5
5
  # FFI-based backend for calling libtree-sitter directly
6
6
  #
7
7
  # This backend uses Ruby FFI (JNR-FFI on JRuby) to call the native tree-sitter
8
- # C library without requiring MRI C extensions. This makes it compatible with
9
- # JRuby, TruffleRuby, and other Ruby implementations that support FFI.
8
+ # C library without requiring MRI C extensions.
10
9
  #
11
10
  # The FFI backend currently supports:
12
11
  # - Parsing source code
@@ -16,28 +15,85 @@ module TreeHaver
16
15
  # Not yet supported:
17
16
  # - Query API (tree-sitter queries/patterns)
18
17
  #
18
+ # == Platform Compatibility
19
+ #
20
+ # - MRI Ruby: ✓ Full support
21
+ # - JRuby: ✓ Full support (uses JNR-FFI)
22
+ # - TruffleRuby: ✗ TruffleRuby's FFI doesn't support STRUCT_BY_VALUE return types
23
+ # (used by ts_tree_root_node, ts_node_child, ts_node_start_point, etc.)
24
+ #
19
25
  # @note Requires the `ffi` gem and libtree-sitter shared library to be installed
20
26
  # @see https://github.com/ffi/ffi Ruby FFI
21
27
  # @see https://tree-sitter.github.io/tree-sitter/ tree-sitter
22
28
  module FFI
23
- # Check if the FFI gem is available (lazy evaluation)
29
+ # Module-level availability and capability methods
24
30
  #
25
- # This method lazily checks for FFI gem availability to avoid
26
- # polluting the environment at load time.
31
+ # These methods provide API consistency with other backends.
27
32
  class << self
28
- # Check if the FFI gem can be loaded
29
- # @return [Boolean] true if FFI gem can be loaded
33
+ # Check if the FFI backend is available
34
+ #
35
+ # The FFI backend requires:
36
+ # - The ffi gem to be installed
37
+ # - NOT running on TruffleRuby (STRUCT_BY_VALUE limitation)
38
+ # - MRI backend (ruby_tree_sitter) not already loaded (symbol conflicts)
39
+ #
40
+ # @return [Boolean] true if FFI backend can be used
41
+ # @example
42
+ # if TreeHaver::Backends::FFI.available?
43
+ # puts "FFI backend is ready"
44
+ # end
45
+ def available?
46
+ return false unless ffi_gem_available?
47
+
48
+ # Check if MRI backend has been loaded (which blocks FFI)
49
+ !defined?(::TreeSitter::Parser)
50
+ end
51
+
52
+ # Check if the FFI gem can be loaded and is usable for tree-sitter
53
+ #
54
+ # @return [Boolean] true if FFI gem can be loaded and works with tree-sitter
30
55
  # @api private
56
+ # @note Returns false on TruffleRuby because TruffleRuby's FFI doesn't support
57
+ # STRUCT_BY_VALUE return types (used by ts_tree_root_node, ts_node_child, etc.)
31
58
  def ffi_gem_available?
32
59
  return @ffi_gem_available if defined?(@ffi_gem_available)
33
60
 
34
61
  @ffi_gem_available = begin
62
+ # TruffleRuby's FFI doesn't support STRUCT_BY_VALUE return types
63
+ # which tree-sitter uses extensively (ts_tree_root_node, ts_node_child, etc.)
64
+ return false if RUBY_ENGINE == "truffleruby"
65
+
35
66
  require "ffi"
36
67
  true
37
68
  rescue LoadError
38
69
  false
39
70
  end
40
71
  end
72
+
73
+ # Reset the load state (primarily for testing)
74
+ #
75
+ # @return [void]
76
+ # @api private
77
+ def reset!
78
+ @ffi_gem_available = nil
79
+ end
80
+
81
+ # Get capabilities supported by this backend
82
+ #
83
+ # @return [Hash{Symbol => Object}] capability map
84
+ # @example
85
+ # TreeHaver::Backends::FFI.capabilities
86
+ # # => { backend: :ffi, parse: true, query: false, bytes_field: true }
87
+ def capabilities
88
+ return {} unless available?
89
+ {
90
+ backend: :ffi,
91
+ parse: true,
92
+ query: false, # Query API not yet implemented in FFI backend
93
+ bytes_field: true,
94
+ incremental: false,
95
+ }
96
+ end
41
97
  end
42
98
 
43
99
  # Native FFI bindings to libtree-sitter
@@ -151,15 +207,16 @@ module TreeHaver
151
207
 
152
208
  last_error = nil
153
209
  candidates = lib_candidates
210
+ lib_loaded = false
154
211
  candidates.each do |name|
155
212
  ffi_lib(name)
156
- @loaded = true
213
+ lib_loaded = true
157
214
  break
158
215
  rescue ::FFI::NotFoundError, LoadError => e
159
216
  last_error = e
160
217
  end
161
218
 
162
- unless @loaded
219
+ unless lib_loaded
163
220
  # :nocov:
164
221
  tried = candidates.join(", ")
165
222
  env_hint = ENV["TREE_SITTER_RUNTIME_LIB"] ? " TREE_SITTER_RUNTIME_LIB=#{ENV["TREE_SITTER_RUNTIME_LIB"]}." : ""
@@ -173,6 +230,8 @@ module TreeHaver
173
230
  end
174
231
 
175
232
  # Attach functions after lib is selected
233
+ # Note: TruffleRuby's FFI doesn't support STRUCT_BY_VALUE return types,
234
+ # so these attach_function calls will fail on TruffleRuby.
176
235
  attach_function(:ts_parser_new, [], :pointer)
177
236
  attach_function(:ts_parser_delete, [:pointer], :void)
178
237
  attach_function(:ts_parser_set_language, [:pointer, :pointer], :bool)
@@ -190,6 +249,9 @@ module TreeHaver
190
249
  attach_function(:ts_node_end_point, [:ts_node], :ts_point)
191
250
  attach_function(:ts_node_is_null, [:ts_node], :bool)
192
251
  attach_function(:ts_node_is_named, [:ts_node], :bool)
252
+
253
+ # Only mark as fully loaded after all attach_function calls succeed
254
+ @loaded = true
193
255
  end
194
256
 
195
257
  def loaded?
@@ -198,57 +260,6 @@ module TreeHaver
198
260
  end
199
261
  end
200
262
 
201
- class << self
202
- # Check if the FFI backend is available
203
- #
204
- # Returns true if:
205
- # 1. The `ffi` gem is present
206
- # 2. MRI backend (ruby_tree_sitter) has NOT been loaded
207
- #
208
- # FFI and MRI backends conflict at the libtree-sitter level.
209
- # Once MRI loads, using FFI will cause segfaults.
210
- #
211
- # @return [Boolean] true if FFI backend can be used
212
- # @example
213
- # if TreeHaver::Backends::FFI.available?
214
- # puts "FFI backend is ready"
215
- # end
216
- def available?
217
- return false unless TreeHaver::Backends::FFI.ffi_gem_available?
218
-
219
- # Check if MRI backend has been loaded (which blocks FFI)
220
- !defined?(::TreeSitter::Parser)
221
- end
222
-
223
- # Reset the load state (primarily for testing)
224
- #
225
- # Note: FFI backend doesn't maintain load state like other backends,
226
- # but this method is provided for API consistency.
227
- #
228
- # @return [void]
229
- # @api private
230
- def reset!
231
- # FFI backend uses constant-time availability check, no state to reset
232
- nil
233
- end
234
-
235
- # Get capabilities supported by this backend
236
- #
237
- # @return [Hash{Symbol => Object}] capability map
238
- # @example
239
- # TreeHaver::Backends::FFI.capabilities
240
- # # => { backend: :ffi, parse: true, query: false, bytes_field: true }
241
- def capabilities
242
- return {} unless available?
243
- {
244
- backend: :ffi,
245
- parse: true,
246
- query: false,
247
- bytes_field: true,
248
- }
249
- end
250
- end
251
-
252
263
  # Represents a tree-sitter language loaded via FFI
253
264
  #
254
265
  # Holds a pointer to a TSLanguage struct from a loaded shared library.
@@ -378,7 +389,8 @@ module TreeHaver
378
389
  end
379
390
 
380
391
  dl = ::FFI::DynamicLibrary.open(path, flags)
381
- rescue LoadError => e
392
+ rescue LoadError, RuntimeError => e
393
+ # TruffleRuby raises RuntimeError instead of LoadError when a shared library cannot be opened
382
394
  raise TreeHaver::NotAvailable, "Could not open language library at #{path}: #{e.message}"
383
395
  end
384
396
 
@@ -13,6 +13,12 @@ module TreeHaver
13
13
  # - The Query API for pattern matching
14
14
  # - Tree editing for incremental re-parsing
15
15
  #
16
+ # == Platform Compatibility
17
+ #
18
+ # - MRI Ruby: ✗ Not available (no JVM)
19
+ # - JRuby: ✓ Full support (native Java integration)
20
+ # - TruffleRuby: ✗ Not available (java-tree-sitter requires JRuby's Java interop)
21
+ #
16
22
  # == Installation
17
23
  #
18
24
  # 1. Download the JAR from Maven Central:
@@ -24,7 +30,6 @@ module TreeHaver
24
30
  # 3. Use JRuby to run your code:
25
31
  # jruby -e "require 'tree_haver'; puts TreeHaver::Backends::Java.available?"
26
32
  #
27
- # @note Only available on JRuby
28
33
  # @see https://github.com/tree-sitter/java-tree-sitter source
29
34
  # @see https://tree-sitter.github.io/java-tree-sitter java-tree-sitter documentation
30
35
  # @see https://central.sonatype.com/artifact/io.github.tree-sitter/jtreesitter Maven Central
@@ -8,7 +8,12 @@ module TreeHaver
8
8
  # for MRI Ruby. It provides the most feature-complete tree-sitter integration
9
9
  # on MRI, including support for the Query API.
10
10
  #
11
- # @note This backend only works on MRI Ruby, not JRuby or TruffleRuby
11
+ # == Platform Compatibility
12
+ #
13
+ # - MRI Ruby: ✓ Full support (fastest tree-sitter backend on MRI)
14
+ # - JRuby: ✗ Cannot load native C extensions (runs on JVM)
15
+ # - TruffleRuby: ✗ C extension not compatible with TruffleRuby
16
+ #
12
17
  # @see https://github.com/Faveod/ruby-tree-sitter ruby_tree_sitter
13
18
  module MRI
14
19
  @load_attempted = false
@@ -37,6 +42,14 @@ module TreeHaver
37
42
  def available?
38
43
  return @loaded if @load_attempted # rubocop:disable ThreadSafety/ClassInstanceVariable
39
44
  @load_attempted = true # rubocop:disable ThreadSafety/ClassInstanceVariable
45
+
46
+ # ruby_tree_sitter is a C extension that only works on MRI
47
+ # It doesn't work on JRuby or TruffleRuby
48
+ unless RUBY_ENGINE == "ruby"
49
+ @loaded = false # rubocop:disable ThreadSafety/ClassInstanceVariable
50
+ return @loaded
51
+ end
52
+
40
53
  begin
41
54
  require "tree_sitter" # Note: gem is ruby_tree_sitter but requires tree_sitter
42
55
 
@@ -11,7 +11,12 @@ module TreeHaver
11
11
  # tree_stump supports incremental parsing and the Query API, making it
12
12
  # suitable for editor/IDE use cases where performance is critical.
13
13
  #
14
- # @note This backend works on MRI Ruby. JRuby/TruffleRuby support is unknown.
14
+ # == Platform Compatibility
15
+ #
16
+ # - MRI Ruby: ✓ Full support
17
+ # - JRuby: ✗ Cannot load native extensions (runs on JVM)
18
+ # - TruffleRuby: ✗ magnus/rb-sys incompatible with TruffleRuby's C API emulation
19
+ #
15
20
  # @see https://github.com/joker1007/tree_stump tree_stump
16
21
  module Rust
17
22
  @load_attempted = false
@@ -30,6 +35,14 @@ module TreeHaver
30
35
  def available?
31
36
  return @loaded if @load_attempted # rubocop:disable ThreadSafety/ClassInstanceVariable
32
37
  @load_attempted = true # rubocop:disable ThreadSafety/ClassInstanceVariable
38
+
39
+ # tree_stump uses magnus which requires MRI's C API
40
+ # It doesn't work on JRuby or TruffleRuby
41
+ unless RUBY_ENGINE == "ruby"
42
+ @loaded = false # rubocop:disable ThreadSafety/ClassInstanceVariable
43
+ return @loaded
44
+ end
45
+
33
46
  begin
34
47
  require "tree_stump"
35
48
 
@@ -46,12 +46,12 @@ module TreeHaver
46
46
  # @param language [Symbol, String] the language name (e.g., :toml, :json)
47
47
  # @param gem_name [String] the gem name (e.g., "toml-rb")
48
48
  # @param grammar_const [String] constant path to grammar (e.g., "TomlRB::Document")
49
- # @param require_path [String, nil] custom require path (defaults to gem_name with dashes→slashes)
49
+ # @param require_path [String, nil] custom require path (defaults to gem_name as-is)
50
50
  def initialize(language:, gem_name:, grammar_const:, require_path: nil)
51
51
  @language_name = language.to_sym
52
52
  @gem_name = gem_name
53
53
  @grammar_const = grammar_const
54
- @require_path = require_path || gem_name.tr("-", "/")
54
+ @require_path = require_path || gem_name
55
55
  @load_attempted = false
56
56
  @available = false
57
57
  @grammar_module = nil
@@ -67,6 +67,15 @@ module TreeHaver
67
67
  return @available if @load_attempted
68
68
 
69
69
  @load_attempted = true
70
+ debug = ENV["TREE_HAVER_DEBUG"]
71
+
72
+ # Guard against nil require_path (can happen if gem_name was nil)
73
+ if @require_path.nil? || @require_path.empty?
74
+ warn("CitrusGrammarFinder: require_path is nil or empty for #{@language_name}") if debug
75
+ @available = false
76
+ return false
77
+ end
78
+
70
79
  begin
71
80
  # Try to require the gem
72
81
  require @require_path
@@ -76,25 +85,64 @@ module TreeHaver
76
85
 
77
86
  # Verify it responds to parse
78
87
  unless @grammar_module.respond_to?(:parse)
79
- warn("#{@grammar_const} doesn't respond to :parse")
88
+ # :nocov: defensive - requires a gem with malformed grammar module
89
+ # Show what methods ARE available to help diagnose the issue
90
+ if debug
91
+ available_methods = @grammar_module.methods(false).sort.first(20)
92
+ warn("CitrusGrammarFinder: #{@grammar_const} doesn't respond to :parse")
93
+ warn("CitrusGrammarFinder: #{@grammar_const}.class = #{@grammar_module.class}")
94
+ warn("CitrusGrammarFinder: #{@grammar_const} is a #{@grammar_module.is_a?(Module) ? "Module" : "non-Module"}")
95
+ warn("CitrusGrammarFinder: Available singleton methods (first 20): #{available_methods.inspect}")
96
+ if @grammar_module.respond_to?(:instance_methods)
97
+ instance_methods = @grammar_module.instance_methods(false).sort.first(20)
98
+ warn("CitrusGrammarFinder: Available instance methods (first 20): #{instance_methods.inspect}")
99
+ end
100
+ end
80
101
  @available = false
81
102
  return false
103
+ # :nocov:
82
104
  end
83
105
 
84
106
  @available = true
85
107
  rescue LoadError => e
86
- # Always show LoadError for debugging
87
- warn("CitrusGrammarFinder: Failed to load '#{@require_path}': #{e.class}: #{e.message}")
108
+ # :nocov: defensive - requires gem to not be installed
109
+ # Only show LoadError details when debugging
110
+ if debug
111
+ warn("CitrusGrammarFinder: Failed to load '#{@require_path}': #{e.class}: #{e.message}")
112
+ warn("CitrusGrammarFinder: LoadError backtrace:\n #{e.backtrace&.first(10)&.join("\n ")}")
113
+ end
88
114
  @available = false
115
+ # :nocov:
89
116
  rescue NameError => e
90
- # Always show NameError for debugging
91
- warn("CitrusGrammarFinder: Failed to resolve '#{@grammar_const}': #{e.class}: #{e.message}")
117
+ # :nocov: defensive - requires gem with missing constant
118
+ # Only show NameError details when debugging
119
+ if debug
120
+ warn("CitrusGrammarFinder: Failed to resolve '#{@grammar_const}': #{e.class}: #{e.message}")
121
+ warn("CitrusGrammarFinder: NameError backtrace:\n #{e.backtrace&.first(10)&.join("\n ")}")
122
+ end
92
123
  @available = false
124
+ # :nocov:
125
+ rescue TypeError => e
126
+ # :nocov: defensive - TruffleRuby-specific edge case
127
+ # TruffleRuby's bundled_gems.rb can raise TypeError when File.path is called on nil
128
+ # This happens in bundled_gems.rb:124 warning? method when caller locations return nil
129
+ # Always warn about TypeError as it indicates a platform-specific issue
130
+ warn("CitrusGrammarFinder: TypeError during load of '#{@require_path}': #{e.class}: #{e.message}")
131
+ warn("CitrusGrammarFinder: This may be a TruffleRuby bundled_gems.rb issue")
132
+ if debug
133
+ warn("CitrusGrammarFinder: TypeError backtrace:\n #{e.backtrace&.first(10)&.join("\n ")}")
134
+ end
135
+ @available = false
136
+ # :nocov:
93
137
  rescue => e
94
- # Catch any other errors
138
+ # :nocov: defensive - catch-all for unexpected errors
139
+ # Always warn about unexpected errors
95
140
  warn("CitrusGrammarFinder: Unexpected error: #{e.class}: #{e.message}")
96
- warn(e.backtrace.first(3).join("\n")) if ENV["TREE_HAVER_DEBUG"]
141
+ if debug
142
+ warn("CitrusGrammarFinder: backtrace:\n #{e.backtrace&.first(10)&.join("\n ")}")
143
+ end
97
144
  @available = false
145
+ # :nocov:
98
146
  end
99
147
 
100
148
  @available
@@ -290,11 +290,14 @@ module TreeHaver
290
290
  # Get a child by index
291
291
  #
292
292
  # @param index [Integer] Child index
293
- # @return [Node, nil] Wrapped child node
293
+ # @return [Node, nil] Wrapped child node, or nil if index out of bounds
294
294
  def child(index)
295
295
  child_node = @inner_node.child(index)
296
296
  return if child_node.nil?
297
297
  Node.new(child_node, source: @source)
298
+ rescue IndexError
299
+ # Some backends (e.g., MRI w/ ruby_tree_sitter) raise IndexError for out of bounds
300
+ nil
298
301
  end
299
302
 
300
303
  # Get a named child by index