tree_haver 5.0.5 → 7.1.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 (59) hide show
  1. checksums.yaml +4 -4
  2. checksums.yaml.gz.sig +0 -0
  3. data/LICENSE.md +13 -0
  4. data/README.md +299 -660
  5. data/lib/tree_haver/backend_api.rb +78 -35
  6. data/lib/tree_haver/backend_context.rb +28 -0
  7. data/lib/tree_haver/backend_registry.rb +119 -382
  8. data/lib/tree_haver/backends/citrus.rb +55 -53
  9. data/lib/tree_haver/backends/ffi.rb +105 -101
  10. data/lib/tree_haver/backends/java.rb +92 -76
  11. data/lib/tree_haver/backends/mri.rb +44 -39
  12. data/lib/tree_haver/backends/parslet.rb +53 -48
  13. data/lib/tree_haver/backends/prism.rb +140 -43
  14. data/lib/tree_haver/backends/psych.rb +30 -26
  15. data/lib/tree_haver/backends/rust.rb +21 -17
  16. data/lib/tree_haver/backends/tslp.rb +274 -0
  17. data/lib/tree_haver/base/comment.rb +320 -0
  18. data/lib/tree_haver/base/language.rb +2 -2
  19. data/lib/tree_haver/base/node.rb +39 -31
  20. data/lib/tree_haver/base/parser.rb +4 -0
  21. data/lib/tree_haver/base/point.rb +3 -3
  22. data/lib/tree_haver/base/tree.rb +1 -1
  23. data/lib/tree_haver/citrus_grammar_finder.rb +21 -26
  24. data/lib/tree_haver/contracts.rb +1025 -0
  25. data/lib/tree_haver/grammar_finder.rb +125 -70
  26. data/lib/tree_haver/kaitai_backend.rb +30 -0
  27. data/lib/tree_haver/language.rb +36 -37
  28. data/lib/tree_haver/language_pack.rb +41 -0
  29. data/lib/tree_haver/language_registry.rb +34 -3
  30. data/lib/tree_haver/library_path_utils.rb +7 -7
  31. data/lib/tree_haver/node.rb +40 -31
  32. data/lib/tree_haver/parser.rb +44 -37
  33. data/lib/tree_haver/parslet_grammar_finder.rb +19 -26
  34. data/lib/tree_haver/path_validator.rb +39 -36
  35. data/lib/tree_haver/peg_backends.rb +76 -0
  36. data/lib/tree_haver/rspec/dependency_tags.rb +23 -1363
  37. data/lib/tree_haver/rspec.rb +1 -31
  38. data/lib/tree_haver/tree.rb +16 -7
  39. data/lib/tree_haver/version.rb +5 -14
  40. data/lib/tree_haver.rb +536 -1240
  41. data/sig/tree_haver.rbs +1 -229
  42. data.tar.gz.sig +0 -0
  43. metadata +154 -70
  44. metadata.gz.sig +0 -0
  45. data/CHANGELOG.md +0 -1393
  46. data/CITATION.cff +0 -20
  47. data/CODE_OF_CONDUCT.md +0 -134
  48. data/CONTRIBUTING.md +0 -359
  49. data/FUNDING.md +0 -74
  50. data/LICENSE.txt +0 -21
  51. data/REEK +0 -0
  52. data/RUBOCOP.md +0 -71
  53. data/SECURITY.md +0 -21
  54. data/lib/tree_haver/base.rb +0 -12
  55. data/lib/tree_haver/compat.rb +0 -43
  56. data/lib/tree_haver/rspec/testable_node.rb +0 -217
  57. data/sig/tree_haver/backends.rbs +0 -352
  58. data/sig/tree_haver/grammar_finder.rbs +0 -29
  59. data/sig/tree_haver/path_validator.rbs +0 -32
@@ -81,28 +81,29 @@ module TreeHaver
81
81
  # @note Returns false on TruffleRuby because TruffleRuby's FFI doesn't support
82
82
  # STRUCT_BY_VALUE return types (used by ts_tree_root_node, ts_node_child, etc.)
83
83
  def ffi_gem_available?
84
- return @loaded if @load_attempted # rubocop:disable ThreadSafety/ClassInstanceVariable
85
- @load_attempted = true # rubocop:disable ThreadSafety/ClassInstanceVariable
84
+ return @loaded if @load_attempted
86
85
 
87
- @loaded = begin # rubocop:disable ThreadSafety/ClassInstanceVariable
86
+ @load_attempted = true
87
+
88
+ @loaded = begin
88
89
  # TruffleRuby's FFI doesn't support STRUCT_BY_VALUE return types
89
90
  # which tree-sitter uses extensively (ts_tree_root_node, ts_node_child, etc.)
90
- # :nocov: TruffleRuby returns false early - subsequent FFI code paths unreachable on TruffleRuby
91
- if RUBY_ENGINE == "truffleruby"
91
+ # simplecov:disable TruffleRuby returns false early - subsequent FFI code paths unreachable on TruffleRuby
92
+ if RUBY_ENGINE == 'truffleruby'
92
93
  false
93
- # :nocov:
94
+ # simplecov:enable
94
95
  else
95
- require "ffi"
96
+ require 'ffi'
96
97
  true
97
98
  end
98
99
  rescue LoadError
99
100
  false
100
- # :nocov: defensive code - StandardError during require is extremely rare
101
+ # simplecov:disable defensive code - StandardError during require is extremely rare
101
102
  rescue StandardError
102
103
  false
103
- # :nocov:
104
+ # simplecov:enable
104
105
  end
105
- @loaded # rubocop:disable ThreadSafety/ClassInstanceVariable
106
+ @loaded
106
107
  end
107
108
 
108
109
  # Reset the load state (primarily for testing)
@@ -110,8 +111,8 @@ module TreeHaver
110
111
  # @return [void]
111
112
  # @api private
112
113
  def reset!
113
- @load_attempted = false # rubocop:disable ThreadSafety/ClassInstanceVariable
114
- @loaded = false # rubocop:disable ThreadSafety/ClassInstanceVariable
114
+ @load_attempted = false
115
+ @loaded = false
115
116
  end
116
117
 
117
118
  # Get capabilities supported by this backend
@@ -119,15 +120,17 @@ module TreeHaver
119
120
  # @return [Hash{Symbol => Object}] capability map
120
121
  # @example
121
122
  # TreeHaver::Backends::FFI.capabilities
122
- # # => { backend: :ffi, parse: true, query: false, bytes_field: true }
123
+ # # => { backend: :ffi, parse: true, query: false, bytes_field: true, comment_support: :nodes_only }
123
124
  def capabilities
124
125
  return {} unless available?
126
+
125
127
  {
126
128
  backend: :ffi,
127
129
  parse: true,
128
130
  query: false, # Query API not yet implemented in FFI backend
129
131
  bytes_field: true,
130
132
  incremental: false,
133
+ comment_support: :nodes_only
131
134
  }
132
135
  end
133
136
  end
@@ -149,9 +152,7 @@ module TreeHaver
149
152
  def ensure_ffi_extended!
150
153
  return true if @ffi_extended
151
154
 
152
- unless FFI.ffi_gem_available?
153
- raise TreeHaver::NotAvailable, "FFI gem is not available"
154
- end
155
+ raise TreeHaver::NotAvailable, 'FFI gem is not available' unless FFI.ffi_gem_available?
155
156
 
156
157
  extend(::FFI::Library)
157
158
 
@@ -169,9 +170,9 @@ module TreeHaver
169
170
  # Mirrors the C struct layout: struct { uint32_t row; uint32_t column; }
170
171
  ts_point_class = Class.new(::FFI::Struct) do
171
172
  layout :row,
172
- :uint32,
173
- :column,
174
- :uint32
173
+ :uint32,
174
+ :column,
175
+ :uint32
175
176
  end
176
177
  const_set(:TSPoint, ts_point_class)
177
178
  typedef(ts_point_class.by_value, :ts_point)
@@ -186,11 +187,11 @@ module TreeHaver
186
187
  # Mirrors the C struct layout used by tree-sitter
187
188
  ts_node_class = Class.new(::FFI::Struct) do
188
189
  layout :context,
189
- [:uint32, 4],
190
- :id,
191
- :pointer,
192
- :tree,
193
- :pointer
190
+ [:uint32, 4],
191
+ :id,
192
+ :pointer,
193
+ :tree,
194
+ :pointer
194
195
  end
195
196
  const_set(:TSNode, ts_node_class)
196
197
  typedef(ts_node_class.by_value, :ts_node)
@@ -212,12 +213,12 @@ module TreeHaver
212
213
  # @return [Array<String>] list of library names to try
213
214
  def lib_candidates
214
215
  [
215
- ENV["TREE_SITTER_RUNTIME_LIB"],
216
- "tree-sitter",
217
- "libtree-sitter.so.0",
218
- "libtree-sitter.so",
219
- "libtree-sitter.dylib",
220
- "libtree-sitter.dll",
216
+ ENV['TREE_SITTER_RUNTIME_LIB'],
217
+ 'tree-sitter',
218
+ 'libtree-sitter.so.0',
219
+ 'libtree-sitter.so',
220
+ 'libtree-sitter.dylib',
221
+ 'libtree-sitter.dll'
221
222
  ].compact
222
223
  end
223
224
 
@@ -234,11 +235,11 @@ module TreeHaver
234
235
  ensure_ffi_extended!
235
236
 
236
237
  # Warn about potential conflicts with MRI backend
237
- if defined?(::TreeSitter) && defined?(::TreeSitter::Parser)
238
- warn("TreeHaver: FFI backend loading after ruby_tree_sitter (MRI backend). " \
239
- "This may cause symbol conflicts due to different libtree-sitter versions. " \
240
- "Consider using only one backend per process, or set TREE_SITTER_RUNTIME_LIB " \
241
- "to match the version used by ruby_tree_sitter.") if $VERBOSE
238
+ if defined?(::TreeSitter) && defined?(::TreeSitter::Parser) && $VERBOSE
239
+ warn('TreeHaver: FFI backend loading after ruby_tree_sitter (MRI backend). ' \
240
+ 'This may cause symbol conflicts due to different libtree-sitter versions. ' \
241
+ 'Consider using only one backend per process, or set TREE_SITTER_RUNTIME_LIB ' \
242
+ 'to match the version used by ruby_tree_sitter.')
242
243
  end
243
244
 
244
245
  last_error = nil
@@ -249,21 +250,21 @@ module TreeHaver
249
250
  lib_loaded = true
250
251
  break
251
252
  rescue LoadError => e
252
- # Note: FFI::NotFoundError inherits from LoadError, so it's caught here too
253
+ # NOTE: FFI::NotFoundError inherits from LoadError, so it's caught here too
253
254
  last_error = e
254
255
  end
255
256
 
256
257
  unless lib_loaded
257
- # :nocov:
258
- tried = candidates.join(", ")
259
- env_hint = ENV["TREE_SITTER_RUNTIME_LIB"] ? " TREE_SITTER_RUNTIME_LIB=#{ENV["TREE_SITTER_RUNTIME_LIB"]}." : ""
258
+ # simplecov:disable
259
+ tried = candidates.join(', ')
260
+ env_hint = ENV['TREE_SITTER_RUNTIME_LIB'] ? " TREE_SITTER_RUNTIME_LIB=#{ENV['TREE_SITTER_RUNTIME_LIB']}." : ''
260
261
  msg = if last_error
261
- "Could not load libtree-sitter (tried: #{tried}).#{env_hint} #{last_error.class}: #{last_error.message}"
262
- else
263
- "Could not load libtree-sitter (tried: #{tried}).#{env_hint}"
264
- end
262
+ "Could not load libtree-sitter (tried: #{tried}).#{env_hint} #{last_error.class}: #{last_error.message}"
263
+ else
264
+ "Could not load libtree-sitter (tried: #{tried}).#{env_hint}"
265
+ end
265
266
  raise TreeHaver::NotAvailable, msg
266
- # :nocov:
267
+ # simplecov:enable
267
268
  end
268
269
 
269
270
  # Attach functions after lib is selected
@@ -271,16 +272,16 @@ module TreeHaver
271
272
  # so these attach_function calls will fail on TruffleRuby.
272
273
  attach_function(:ts_parser_new, [], :pointer)
273
274
  attach_function(:ts_parser_delete, [:pointer], :void)
274
- attach_function(:ts_parser_set_language, [:pointer, :pointer], :bool)
275
- attach_function(:ts_parser_parse_string, [:pointer, :pointer, :string, :uint32], :pointer)
275
+ attach_function(:ts_parser_set_language, %i[pointer pointer], :bool)
276
+ attach_function(:ts_parser_parse_string, %i[pointer pointer string uint32], :pointer)
276
277
 
277
278
  attach_function(:ts_tree_delete, [:pointer], :void)
278
279
  attach_function(:ts_tree_root_node, [:pointer], :ts_node)
279
280
 
280
281
  attach_function(:ts_node_type, [:ts_node], :string)
281
282
  attach_function(:ts_node_child_count, [:ts_node], :uint32)
282
- attach_function(:ts_node_child, [:ts_node, :uint32], :ts_node)
283
- attach_function(:ts_node_child_by_field_name, [:ts_node, :string, :uint32], :ts_node)
283
+ attach_function(:ts_node_child, %i[ts_node uint32], :ts_node)
284
+ attach_function(:ts_node_child_by_field_name, %i[ts_node string uint32], :ts_node)
284
285
  attach_function(:ts_node_start_byte, [:ts_node], :uint32)
285
286
  attach_function(:ts_node_end_byte, [:ts_node], :uint32)
286
287
  attach_function(:ts_node_start_point, [:ts_node], :ts_point)
@@ -296,14 +297,14 @@ module TreeHaver
296
297
  attach_function(:ts_node_prev_sibling, [:ts_node], :ts_node)
297
298
  attach_function(:ts_node_next_named_sibling, [:ts_node], :ts_node)
298
299
  attach_function(:ts_node_prev_named_sibling, [:ts_node], :ts_node)
299
- attach_function(:ts_node_named_child, [:ts_node, :uint32], :ts_node)
300
+ attach_function(:ts_node_named_child, %i[ts_node uint32], :ts_node)
300
301
  attach_function(:ts_node_named_child_count, [:ts_node], :uint32)
301
302
 
302
303
  # Descendant lookup functions
303
- attach_function(:ts_node_descendant_for_byte_range, [:ts_node, :uint32, :uint32], :ts_node)
304
- attach_function(:ts_node_descendant_for_point_range, [:ts_node, :ts_point, :ts_point], :ts_node)
305
- attach_function(:ts_node_named_descendant_for_byte_range, [:ts_node, :uint32, :uint32], :ts_node)
306
- attach_function(:ts_node_named_descendant_for_point_range, [:ts_node, :ts_point, :ts_point], :ts_node)
304
+ attach_function(:ts_node_descendant_for_byte_range, %i[ts_node uint32 uint32], :ts_node)
305
+ attach_function(:ts_node_descendant_for_point_range, %i[ts_node ts_point ts_point], :ts_node)
306
+ attach_function(:ts_node_named_descendant_for_byte_range, %i[ts_node uint32 uint32], :ts_node)
307
+ attach_function(:ts_node_named_descendant_for_point_range, %i[ts_node ts_point ts_point], :ts_node)
307
308
 
308
309
  # Only mark as fully loaded after all attach_function calls succeed
309
310
  @loaded = true
@@ -368,10 +369,10 @@ module TreeHaver
368
369
  return unless other.backend == @backend
369
370
 
370
371
  # Compare by path first, then symbol
371
- cmp = (@path || "") <=> (other.path || "")
372
+ cmp = (@path || '') <=> (other.path || '')
372
373
  return cmp if cmp.nonzero?
373
374
 
374
- (@symbol || "") <=> (other.symbol || "")
375
+ (@symbol || '') <=> (other.symbol || '')
375
376
  end
376
377
 
377
378
  # Hash value for this language (for use in Sets/Hashes)
@@ -381,7 +382,7 @@ module TreeHaver
381
382
  end
382
383
 
383
384
  # Alias eql? to ==
384
- alias_method :eql?, :==
385
+ alias eql? ==
385
386
 
386
387
  # Get the language name
387
388
  #
@@ -391,7 +392,7 @@ module TreeHaver
391
392
  def language_name
392
393
  # Try to derive from symbol (e.g., "tree_sitter_toml" -> :toml)
393
394
  if @symbol
394
- name = @symbol.to_s.sub(/^tree_sitter_/, "")
395
+ name = @symbol.to_s.sub(/^tree_sitter_/, '')
395
396
  return name.to_sym
396
397
  end
397
398
 
@@ -405,7 +406,7 @@ module TreeHaver
405
406
  end
406
407
 
407
408
  # Alias for language_name (API compatibility)
408
- alias_method :name, :language_name
409
+ alias name language_name
409
410
 
410
411
  # Convert to FFI pointer for passing to native functions
411
412
  #
@@ -434,20 +435,20 @@ module TreeHaver
434
435
  # )
435
436
  class << self
436
437
  def from_library(path, symbol: nil, name: nil)
437
- raise TreeHaver::NotAvailable, "FFI not available" unless Backends::FFI.available?
438
+ raise TreeHaver::NotAvailable, 'FFI not available' unless Backends::FFI.available?
438
439
 
439
440
  # Check for MRI backend conflict BEFORE loading the grammar
440
441
  # If ruby_tree_sitter has already loaded this grammar file, the dynamic
441
442
  # linker will return the cached library with symbols resolved against
442
443
  # MRI's statically-linked tree-sitter, causing segfaults when FFI
443
444
  # tries to use the pointer with its dynamically-linked libtree-sitter.
444
- if defined?(::TreeSitter::Language)
445
- # MRI backend has been loaded - check if it might have loaded this grammar
446
- # We can't reliably detect which grammars MRI loaded, so we warn and
447
- # attempt to proceed. The segfault will occur when setting language on parser.
448
- warn("TreeHaver: FFI backend loading grammar after ruby_tree_sitter (MRI backend). " \
449
- "This may cause segfaults due to tree-sitter symbol conflicts. " \
450
- "For reliable operation, use only one backend per process.") if $VERBOSE
445
+ # MRI backend has been loaded - check if it might have loaded this grammar
446
+ # We can't reliably detect which grammars MRI loaded, so we warn and
447
+ # attempt to proceed. The segfault will occur when setting language on parser.
448
+ if defined?(::TreeSitter::Language) && $VERBOSE
449
+ warn('TreeHaver: FFI backend loading grammar after ruby_tree_sitter (MRI backend). ' \
450
+ 'This may cause segfaults due to tree-sitter symbol conflicts. ' \
451
+ 'For reliable operation, use only one backend per process.')
451
452
  end
452
453
 
453
454
  # Ensure the core libtree-sitter runtime is loaded first so
@@ -462,10 +463,10 @@ module TreeHaver
462
463
  # (RTLD_NOW | RTLD_GLOBAL). If those constants are not present
463
464
  # fall back to RTLD_LAZY for maximum compatibility.
464
465
  flags = if defined?(::FFI::DynamicLibrary::RTLD_NOW) && defined?(::FFI::DynamicLibrary::RTLD_GLOBAL)
465
- ::FFI::DynamicLibrary::RTLD_NOW | ::FFI::DynamicLibrary::RTLD_GLOBAL
466
- else
467
- ::FFI::DynamicLibrary::RTLD_LAZY
468
- end
466
+ ::FFI::DynamicLibrary::RTLD_NOW | ::FFI::DynamicLibrary::RTLD_GLOBAL
467
+ else
468
+ ::FFI::DynamicLibrary::RTLD_LAZY
469
+ end
469
470
 
470
471
  dl = ::FFI::DynamicLibrary.open(path, flags)
471
472
  rescue LoadError, RuntimeError => e
@@ -473,16 +474,16 @@ module TreeHaver
473
474
  raise TreeHaver::NotAvailable, "Could not open language library at #{path}: #{e.message}"
474
475
  end
475
476
 
476
- requested = symbol || ENV["TREE_SITTER_LANG_SYMBOL"]
477
+ requested = symbol || ENV['TREE_SITTER_LANG_SYMBOL']
477
478
  # Use shared utility for consistent symbol derivation across backends
478
479
  guessed_symbol = LibraryPathUtils.derive_symbol_from_path(path)
479
480
  # If an override was provided (arg or ENV), treat it as strict and do not fall back.
480
481
  # Only when no override is provided do we attempt guessed and default candidates.
481
482
  candidates = if requested && !requested.to_s.empty?
482
- [requested]
483
- else
484
- [guessed_symbol, "tree_sitter_toml"].compact.uniq
485
- end
483
+ [requested]
484
+ else
485
+ [guessed_symbol, 'tree_sitter_toml'].compact.uniq
486
+ end
486
487
 
487
488
  func = nil
488
489
  last_err = nil
@@ -495,22 +496,24 @@ module TreeHaver
495
496
  end
496
497
  unless func
497
498
  env_used = []
498
- env_used << "TREE_SITTER_LANG_SYMBOL=#{ENV["TREE_SITTER_LANG_SYMBOL"]}" if ENV["TREE_SITTER_LANG_SYMBOL"]
499
- detail = env_used.empty? ? "" : " Env overrides: #{env_used.join(", ")}."
500
- raise TreeHaver::NotAvailable, "Could not resolve language symbol in #{path} (tried: #{candidates.join(", ")}).#{detail} #{last_err&.message}"
499
+ env_used << "TREE_SITTER_LANG_SYMBOL=#{ENV['TREE_SITTER_LANG_SYMBOL']}" if ENV['TREE_SITTER_LANG_SYMBOL']
500
+ detail = env_used.empty? ? '' : " Env overrides: #{env_used.join(', ')}."
501
+ raise TreeHaver::NotAvailable,
502
+ "Could not resolve language symbol in #{path} (tried: #{candidates.join(', ')}).#{detail} #{last_err&.message}"
501
503
  end
502
504
 
503
505
  # Only ensure the core lib is loaded when we actually need to interact with it
504
506
  # (e.g., during parsing). Creating the Language handle does not require core to be loaded.
505
507
  ptr = func.call
506
508
  raise TreeHaver::NotAvailable, "Language factory returned NULL for #{path}" if ptr.null?
509
+
507
510
  # Pass the opened DynamicLibrary into the Language instance so the
508
511
  # library handle remains alive for the lifetime of the Language.
509
512
  new(ptr, dl, path: path, symbol: symbol)
510
513
  end
511
514
 
512
515
  # Backward-compatible alias
513
- alias_method :from_path, :from_library
516
+ alias from_path from_library
514
517
  end
515
518
  end
516
519
 
@@ -522,13 +525,13 @@ module TreeHaver
522
525
  #
523
526
  # @raise [TreeHaver::NotAvailable] if FFI not available or parser creation fails
524
527
  def initialize
525
- raise TreeHaver::NotAvailable, "FFI not available" unless Backends::FFI.available?
528
+ raise TreeHaver::NotAvailable, 'FFI not available' unless Backends::FFI.available?
526
529
 
527
530
  Native.try_load!
528
531
  @parser = Native.ts_parser_new
529
- raise TreeHaver::NotAvailable, "Failed to create ts_parser" if @parser.null?
532
+ raise TreeHaver::NotAvailable, 'Failed to create ts_parser' if @parser.null?
530
533
 
531
- # Note: We intentionally do NOT register a finalizer here because:
534
+ # NOTE: We intentionally do NOT register a finalizer here because:
532
535
  # 1. ts_parser_delete can segfault if called during certain GC scenarios
533
536
  # 2. The native library may be unloaded before finalizers run
534
537
  # 3. Parser cleanup happens automatically on process exit
@@ -552,17 +555,17 @@ module TreeHaver
552
555
  # Defensive check: ensure we received an FFI Language wrapper
553
556
  unless lang.is_a?(Language)
554
557
  raise TreeHaver::NotAvailable,
555
- "FFI backend expected FFI::Language wrapper, got #{lang.class}. " \
556
- "This usually means TreeHaver::Parser#unwrap_language passed the wrong type. " \
557
- "Check that language caching respects backend boundaries."
558
+ "FFI backend expected FFI::Language wrapper, got #{lang.class}. " \
559
+ 'This usually means TreeHaver::Parser#unwrap_language passed the wrong type. ' \
560
+ 'Check that language caching respects backend boundaries.'
558
561
  end
559
562
 
560
563
  # Additional check: verify the language is actually for FFI backend
561
564
  if lang.respond_to?(:backend) && lang.backend != :ffi
562
565
  raise TreeHaver::NotAvailable,
563
- "FFI backend received Language for wrong backend: #{lang.backend}. " \
564
- "Expected :ffi backend. Class: #{lang.class}. " \
565
- "Path: #{lang.path.inspect}, Symbol: #{lang.symbol.inspect}"
566
+ "FFI backend received Language for wrong backend: #{lang.backend}. " \
567
+ "Expected :ffi backend. Class: #{lang.class}. " \
568
+ "Path: #{lang.path.inspect}, Symbol: #{lang.symbol.inspect}"
566
569
  end
567
570
 
568
571
  # Verify the DynamicLibrary is still valid (not GC'd)
@@ -570,8 +573,8 @@ module TreeHaver
570
573
  lib = lang.instance_variable_get(:@library)
571
574
  if lib.nil?
572
575
  raise TreeHaver::NotAvailable,
573
- "FFI Language has no library reference. The dynamic library may have been unloaded. " \
574
- "Path: #{lang.path.inspect}, Symbol: #{lang.symbol.inspect}"
576
+ 'FFI Language has no library reference. The dynamic library may have been unloaded. ' \
577
+ "Path: #{lang.path.inspect}, Symbol: #{lang.symbol.inspect}"
575
578
  end
576
579
 
577
580
  # Verify the language has a valid pointer
@@ -580,9 +583,9 @@ module TreeHaver
580
583
  # Check ptr is actually an FFI::Pointer
581
584
  unless ptr.is_a?(::FFI::Pointer)
582
585
  raise TreeHaver::NotAvailable,
583
- "FFI Language#to_ptr returned #{ptr.class}, expected FFI::Pointer. " \
584
- "Language class: #{lang.class}. " \
585
- "Path: #{lang.path.inspect}, Symbol: #{lang.symbol.inspect}"
586
+ "FFI Language#to_ptr returned #{ptr.class}, expected FFI::Pointer. " \
587
+ "Language class: #{lang.class}. " \
588
+ "Path: #{lang.path.inspect}, Symbol: #{lang.symbol.inspect}"
586
589
  end
587
590
 
588
591
  ptr_address = ptr.address
@@ -590,25 +593,25 @@ module TreeHaver
590
593
  # Check for NULL (0x0)
591
594
  if ptr.nil? || ptr_address.zero?
592
595
  raise TreeHaver::NotAvailable,
593
- "FFI Language has NULL pointer. Language may not have loaded correctly. " \
594
- "Path: #{lang.path.inspect}, Symbol: #{lang.symbol.inspect}"
596
+ 'FFI Language has NULL pointer. Language may not have loaded correctly. ' \
597
+ "Path: #{lang.path.inspect}, Symbol: #{lang.symbol.inspect}"
595
598
  end
596
599
 
597
600
  # Check for small invalid addresses (< 4KB are typically unmapped memory)
598
601
  # Common invalid addresses like 0x40 (64) indicate corrupted or uninitialized pointers
599
602
  if ptr_address < 4096
600
603
  raise TreeHaver::NotAvailable,
601
- "FFI Language has invalid pointer (address 0x#{ptr_address.to_s(16)}). " \
602
- "This usually indicates the language library was unloaded or never loaded correctly. " \
603
- "Path: #{lang.path.inspect}, Symbol: #{lang.symbol.inspect}"
604
+ "FFI Language has invalid pointer (address 0x#{ptr_address.to_s(16)}). " \
605
+ 'This usually indicates the language library was unloaded or never loaded correctly. ' \
606
+ "Path: #{lang.path.inspect}, Symbol: #{lang.symbol.inspect}"
604
607
  end
605
608
 
606
- # Note: MRI backend conflict is now handled by TreeHaver::BackendConflict
609
+ # NOTE: MRI backend conflict is now handled by TreeHaver::BackendConflict
607
610
  # at a higher level (in TreeHaver.resolve_backend_module)
608
611
 
609
612
  # lang is a wrapped FFI::Language that has to_ptr method
610
613
  ok = Native.ts_parser_set_language(@parser, ptr)
611
- raise TreeHaver::NotAvailable, "Failed to set language on parser" unless ok
614
+ raise TreeHaver::NotAvailable, 'Failed to set language on parser' unless ok
612
615
 
613
616
  lang # rubocop:disable Lint/Void (intentional return value)
614
617
  end
@@ -621,7 +624,7 @@ module TreeHaver
621
624
  def parse(source)
622
625
  src = String(source)
623
626
  tree_ptr = Native.ts_parser_parse_string(@parser, ::FFI::Pointer::NULL, src, src.bytesize)
624
- raise TreeHaver::NotAvailable, "Parse returned NULL" if tree_ptr.null?
627
+ raise TreeHaver::NotAvailable, 'Parse returned NULL' if tree_ptr.null?
625
628
 
626
629
  # Return raw FFI::Tree - TreeHaver::Parser will wrap it
627
630
  Tree.new(tree_ptr)
@@ -739,6 +742,7 @@ module TreeHaver
739
742
  # @return [Node, nil] child node or nil if index out of bounds
740
743
  def child(index)
741
744
  return if index >= child_count || index < 0
745
+
742
746
  child_node = Native.ts_node_child(@val, index)
743
747
  Node.new(child_node)
744
748
  end