tree_haver 3.1.2 → 3.2.1

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,87 @@ 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
- return @ffi_gem_available if defined?(@ffi_gem_available)
59
+ return @loaded if @load_attempted
60
+
61
+ @load_attempted = true
62
+ @loaded = begin
63
+ # TruffleRuby's FFI doesn't support STRUCT_BY_VALUE return types
64
+ # which tree-sitter uses extensively (ts_tree_root_node, ts_node_child, etc.)
65
+ return false if RUBY_ENGINE == "truffleruby"
33
66
 
34
- @ffi_gem_available = begin
35
67
  require "ffi"
36
68
  true
37
69
  rescue LoadError
38
70
  false
39
71
  end
40
72
  end
73
+
74
+ # Reset the load state (primarily for testing)
75
+ #
76
+ # @return [void]
77
+ # @api private
78
+ def reset!
79
+ @load_attempted = false
80
+ @loaded = false
81
+ end
82
+
83
+ # Get capabilities supported by this backend
84
+ #
85
+ # @return [Hash{Symbol => Object}] capability map
86
+ # @example
87
+ # TreeHaver::Backends::FFI.capabilities
88
+ # # => { backend: :ffi, parse: true, query: false, bytes_field: true }
89
+ def capabilities
90
+ return {} unless available?
91
+ {
92
+ backend: :ffi,
93
+ parse: true,
94
+ query: false, # Query API not yet implemented in FFI backend
95
+ bytes_field: true,
96
+ incremental: false,
97
+ }
98
+ end
41
99
  end
42
100
 
43
101
  # Native FFI bindings to libtree-sitter
@@ -151,15 +209,16 @@ module TreeHaver
151
209
 
152
210
  last_error = nil
153
211
  candidates = lib_candidates
212
+ lib_loaded = false
154
213
  candidates.each do |name|
155
214
  ffi_lib(name)
156
- @loaded = true
215
+ lib_loaded = true
157
216
  break
158
217
  rescue ::FFI::NotFoundError, LoadError => e
159
218
  last_error = e
160
219
  end
161
220
 
162
- unless @loaded
221
+ unless lib_loaded
163
222
  # :nocov:
164
223
  tried = candidates.join(", ")
165
224
  env_hint = ENV["TREE_SITTER_RUNTIME_LIB"] ? " TREE_SITTER_RUNTIME_LIB=#{ENV["TREE_SITTER_RUNTIME_LIB"]}." : ""
@@ -173,6 +232,8 @@ module TreeHaver
173
232
  end
174
233
 
175
234
  # Attach functions after lib is selected
235
+ # Note: TruffleRuby's FFI doesn't support STRUCT_BY_VALUE return types,
236
+ # so these attach_function calls will fail on TruffleRuby.
176
237
  attach_function(:ts_parser_new, [], :pointer)
177
238
  attach_function(:ts_parser_delete, [:pointer], :void)
178
239
  attach_function(:ts_parser_set_language, [:pointer, :pointer], :bool)
@@ -190,6 +251,9 @@ module TreeHaver
190
251
  attach_function(:ts_node_end_point, [:ts_node], :ts_point)
191
252
  attach_function(:ts_node_is_null, [:ts_node], :bool)
192
253
  attach_function(:ts_node_is_named, [:ts_node], :bool)
254
+
255
+ # Only mark as fully loaded after all attach_function calls succeed
256
+ @loaded = true
193
257
  end
194
258
 
195
259
  def loaded?
@@ -198,57 +262,6 @@ module TreeHaver
198
262
  end
199
263
  end
200
264
 
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
265
  # Represents a tree-sitter language loaded via FFI
253
266
  #
254
267
  # Holds a pointer to a TSLanguage struct from a loaded shared library.
@@ -378,19 +391,20 @@ module TreeHaver
378
391
  end
379
392
 
380
393
  dl = ::FFI::DynamicLibrary.open(path, flags)
381
- rescue LoadError => e
394
+ rescue LoadError, RuntimeError => e
395
+ # TruffleRuby raises RuntimeError instead of LoadError when a shared library cannot be opened
382
396
  raise TreeHaver::NotAvailable, "Could not open language library at #{path}: #{e.message}"
383
397
  end
384
398
 
385
399
  requested = symbol || ENV["TREE_SITTER_LANG_SYMBOL"]
386
- base = File.basename(path)
387
- guessed_lang = base.sub(/^libtree[-_]sitter[-_]/, "").sub(/\.(so(\.\d+)?)|\.dylib|\.dll\z/, "")
400
+ # Use shared utility for consistent symbol derivation across backends
401
+ guessed_symbol = LibraryPathUtils.derive_symbol_from_path(path)
388
402
  # If an override was provided (arg or ENV), treat it as strict and do not fall back.
389
403
  # Only when no override is provided do we attempt guessed and default candidates.
390
404
  candidates = if requested && !requested.to_s.empty?
391
405
  [requested]
392
406
  else
393
- [(guessed_lang.empty? ? nil : "tree_sitter_#{guessed_lang}"), "tree_sitter_toml"].compact
407
+ [guessed_symbol, "tree_sitter_toml"].compact.uniq
394
408
  end
395
409
 
396
410
  func = nil
@@ -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
@@ -311,9 +316,11 @@ module TreeHaver
311
316
  def from_library(path, symbol: nil, name: nil)
312
317
  raise TreeHaver::NotAvailable, "Java backend not available" unless Java.available?
313
318
 
314
- # Derive symbol from name or path if not provided
315
- base_name = File.basename(path, ".*").sub(/^lib/, "")
316
- sym = symbol || "tree_sitter_#{name || base_name.sub(/^tree-sitter-/, "")}"
319
+ # Use shared utility for consistent symbol derivation across backends
320
+ # If symbol not provided, derive from name or path
321
+ sym = symbol || LibraryPathUtils.derive_symbol_from_path(path)
322
+ # If name was provided, use it to override the derived symbol
323
+ sym = "tree_sitter_#{name}" if name && !symbol
317
324
 
318
325
  begin
319
326
  arena = ::Java::JavaLangForeign::Arena.global
@@ -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
 
@@ -153,6 +166,21 @@ module TreeHaver
153
166
  # lang = TreeHaver::Backends::MRI::Language.from_library("/path/to/lib.so", symbol: "tree_sitter_json")
154
167
  class << self
155
168
  def from_library(path, symbol: nil, name: nil)
169
+ # Derive symbol from path if not provided using shared utility
170
+ symbol ||= LibraryPathUtils.derive_symbol_from_path(path)
171
+ from_path(path, symbol: symbol, name: name)
172
+ end
173
+
174
+ private
175
+
176
+ # Load a language from a shared library path (internal implementation)
177
+ #
178
+ # @param path [String] absolute path to the language shared library
179
+ # @param symbol [String] the exported symbol name (e.g., "tree_sitter_json")
180
+ # @param name [String, nil] optional language name
181
+ # @return [Language] wrapped language handle
182
+ # @api private
183
+ def from_path(path, symbol: nil, name: nil)
156
184
  raise TreeHaver::NotAvailable, "ruby_tree_sitter not available" unless MRI.available?
157
185
 
158
186
  # ruby_tree_sitter's TreeSitter::Language.load takes (language_name, path_to_so)
@@ -160,8 +188,8 @@ module TreeHaver
160
188
  # NOT the full symbol name (e.g., NOT "tree_sitter_toml")
161
189
  # and path_to_so is the full path to the .so file
162
190
  #
163
- # If name is not provided, derive it from symbol by stripping "tree_sitter_" prefix
164
- language_name = name || symbol&.sub(/\Atree_sitter_/, "")
191
+ # If name is not provided, derive it from symbol using shared utility
192
+ language_name = name || LibraryPathUtils.derive_language_name_from_symbol(symbol)
165
193
  ts_lang = ::TreeSitter::Language.load(language_name, path)
166
194
  new(ts_lang, path: path, symbol: symbol)
167
195
  rescue NameError => e
@@ -177,16 +205,6 @@ module TreeHaver
177
205
  raise # Re-raise if it's not a TreeSitter error
178
206
  end
179
207
  end
180
-
181
- # Load a language from a shared library path (legacy method)
182
- #
183
- # @param path [String] absolute path to the language shared library
184
- # @param symbol [String] the exported symbol name (e.g., "tree_sitter_json")
185
- # @return [Language] wrapped language handle
186
- # @deprecated Use {from_library} instead
187
- def from_path(path, symbol: nil)
188
- from_library(path, symbol: symbol)
189
- end
190
208
  end
191
209
  end
192
210
 
@@ -216,19 +234,17 @@ module TreeHaver
216
234
 
217
235
  # Set the language for this parser
218
236
  #
219
- # Note: TreeHaver::Parser unwraps language objects before calling this method.
220
- # This backend receives raw ::TreeSitter::Language objects, never wrapped ones.
221
- #
222
- # @param lang [::TreeSitter::Language] the language to use (already unwrapped)
223
- # @return [::TreeSitter::Language] the language that was set
237
+ # @param lang [::TreeSitter::Language, TreeHaver::Backends::MRI::Language] the language to use
238
+ # @return [::TreeSitter::Language, TreeHaver::Backends::MRI::Language] the language that was set
224
239
  # @raise [TreeHaver::NotAvailable] if setting language fails
225
240
  def language=(lang)
226
- # lang is already unwrapped by TreeHaver::Parser, use directly
227
- @parser.language = lang
241
+ # Unwrap if it's a TreeHaver wrapper
242
+ inner_lang = lang.respond_to?(:inner_language) ? lang.inner_language : lang
243
+ @parser.language = inner_lang
228
244
  # Verify it was set
229
245
  raise TreeHaver::NotAvailable, "Language not set correctly" if @parser.language.nil?
230
246
 
231
- # Return the language object
247
+ # Return the original language object (wrapped or unwrapped)
232
248
  lang
233
249
  rescue Exception => e # rubocop:disable Lint/RescueException
234
250
  # TreeSitter errors inherit from Exception (not StandardError) in ruby_tree_sitter v2+
@@ -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
 
@@ -136,16 +149,15 @@ module TreeHaver
136
149
 
137
150
  # tree_stump uses TreeStump.register_lang(name, path) to register languages
138
151
  # The name is used to derive the symbol automatically (tree_sitter_<name>)
139
- lang_name = name || File.basename(path, ".*").sub(/^libtree-sitter-/, "")
152
+ # Use shared utility for consistent path parsing across backends
153
+ lang_name = name || LibraryPathUtils.derive_language_name_from_path(path)
140
154
  ::TreeStump.register_lang(lang_name, path)
141
155
  new(lang_name, path: path)
142
156
  rescue RuntimeError => e
143
157
  raise TreeHaver::NotAvailable, "Failed to load language from #{path}: #{e.message}"
144
158
  end
145
159
 
146
- # Alias for compatibility
147
- #
148
- # @see from_library
160
+ # Backward-compatible alias for from_library
149
161
  alias_method :from_path, :from_library
150
162
  end
151
163
  end
@@ -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
@@ -270,7 +270,10 @@ module TreeHaver
270
270
  # Try to instantiate a parser - this will fail if runtime isn't available
271
271
  mod::Parser.new
272
272
  true
273
- rescue NoMethodError, FFI::NotFoundError, LoadError, NotAvailable => _e
273
+ rescue NoMethodError, LoadError, NotAvailable => _e
274
+ false
275
+ rescue StandardError => _e
276
+ # Catch FFI::NotFoundError and other errors when FFI is loaded
274
277
  false
275
278
  end
276
279
  end