tree_haver 3.2.2 → 3.2.4

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.
@@ -2,26 +2,35 @@
2
2
 
3
3
  module TreeHaver
4
4
  module Backends
5
- # Java backend for JRuby using java-tree-sitter (jtreesitter)
5
+ # Java backend for JRuby using jtreesitter (java-tree-sitter)
6
6
  #
7
- # This backend integrates with java-tree-sitter JARs on JRuby,
7
+ # This backend integrates with jtreesitter JARs on JRuby,
8
8
  # leveraging JRuby's native Java integration for optimal performance.
9
9
  #
10
- # java-tree-sitter provides Java bindings to tree-sitter and supports:
10
+ # == Features
11
+ #
12
+ # jtreesitter (java-tree-sitter) provides Java bindings to tree-sitter and supports:
11
13
  # - Parsing source code into syntax trees
12
14
  # - Incremental parsing via Parser.parse(Tree, String)
13
15
  # - The Query API for pattern matching
14
16
  # - Tree editing for incremental re-parsing
15
17
  #
18
+ # == Version Requirements
19
+ #
20
+ # - jtreesitter >= 0.26.0 (required)
21
+ # - tree-sitter runtime library >= 0.26.0 (must match jtreesitter version)
22
+ #
23
+ # Older versions of jtreesitter are NOT supported due to API changes.
24
+ #
16
25
  # == Platform Compatibility
17
26
  #
18
27
  # - MRI Ruby: ✗ Not available (no JVM)
19
28
  # - JRuby: ✓ Full support (native Java integration)
20
- # - TruffleRuby: ✗ Not available (java-tree-sitter requires JRuby's Java interop)
29
+ # - TruffleRuby: ✗ Not available (jtreesitter requires JRuby's Java interop)
21
30
  #
22
31
  # == Installation
23
32
  #
24
- # 1. Download the JAR from Maven Central:
33
+ # 1. Download jtreesitter 0.26.0+ JAR from Maven Central:
25
34
  # https://central.sonatype.com/artifact/io.github.tree-sitter/jtreesitter
26
35
  #
27
36
  # 2. Set the environment variable to point to the JAR directory:
@@ -31,7 +40,7 @@ module TreeHaver
31
40
  # jruby -e "require 'tree_haver'; puts TreeHaver::Backends::Java.available?"
32
41
  #
33
42
  # @see https://github.com/tree-sitter/java-tree-sitter source
34
- # @see https://tree-sitter.github.io/java-tree-sitter java-tree-sitter documentation
43
+ # @see https://tree-sitter.github.io/java-tree-sitter jtreesitter documentation
35
44
  # @see https://central.sonatype.com/artifact/io.github.tree-sitter/jtreesitter Maven Central
36
45
  module Java
37
46
  # The Java package for java-tree-sitter
@@ -402,16 +411,49 @@ module TreeHaver
402
411
  def load_by_name(name)
403
412
  raise TreeHaver::NotAvailable, "Java backend not available" unless Java.available?
404
413
 
414
+ # Try to find the grammar library in standard locations
415
+ # Look for library names like "tree-sitter-toml" or "libtree-sitter-toml"
416
+ lib_names = [
417
+ "tree-sitter-#{name}",
418
+ "libtree-sitter-#{name}",
419
+ "tree_sitter_#{name}",
420
+ ]
421
+
405
422
  begin
406
- # java-tree-sitter's Language.load(String) searches for the language
407
- # in the classpath using standard naming conventions
408
- java_lang = Java.java_classes[:Language].load(name)
409
- new(java_lang, symbol: "tree_sitter_#{name}")
423
+ arena = ::Java::JavaLangForeign::Arena.global
424
+ symbol_lookup_class = ::Java::JavaLangForeign::SymbolLookup
425
+
426
+ # Ensure runtime lookup is available
427
+ unless Java.runtime_lookup
428
+ Java.runtime_lookup = symbol_lookup_class.libraryLookup("libtree-sitter.so", arena)
429
+ end
430
+
431
+ # Try each library name
432
+ grammar_lookup = nil
433
+ lib_names.each do |lib_name|
434
+ grammar_lookup = symbol_lookup_class.libraryLookup(lib_name, arena)
435
+ break
436
+ rescue ::Java::JavaLang::IllegalArgumentException
437
+ # Library not found in search path, try next name
438
+ next
439
+ end
440
+
441
+ unless grammar_lookup
442
+ raise TreeHaver::NotAvailable,
443
+ "Failed to load language '#{name}': Library not found. " \
444
+ "Ensure the grammar library (e.g., libtree-sitter-#{name}.so) " \
445
+ "is in LD_LIBRARY_PATH."
446
+ end
447
+
448
+ combined_lookup = grammar_lookup.or(Java.runtime_lookup)
449
+ sym = "tree_sitter_#{name}"
450
+ java_lang = Java.java_classes[:Language].load(combined_lookup, sym)
451
+ new(java_lang, symbol: sym)
410
452
  rescue ::Java::JavaLang::RuntimeException => e
411
453
  raise TreeHaver::NotAvailable,
412
454
  "Failed to load language '#{name}': #{e.message}. " \
413
- "Ensure the grammar JAR (e.g., tree-sitter-#{name}-X.Y.Z.jar) " \
414
- "is in TREE_SITTER_JAVA_JARS_DIR."
455
+ "Ensure the grammar library (e.g., libtree-sitter-#{name}.so) " \
456
+ "is in LD_LIBRARY_PATH."
415
457
  end
416
458
  end
417
459
  end
@@ -450,8 +492,10 @@ module TreeHaver
450
492
  # @param source [String] the source code to parse
451
493
  # @return [Tree] raw backend tree (wrapping happens in TreeHaver::Parser)
452
494
  def parse(source)
453
- java_tree = @parser.parse(source)
454
- # Return raw Java::Tree - TreeHaver::Parser will wrap it
495
+ java_result = @parser.parse(source)
496
+ # jtreesitter 0.26.0 returns Optional<Tree>
497
+ java_tree = unwrap_optional(java_result)
498
+ raise TreeHaver::Error, "Parser returned no tree" unless java_tree
455
499
  Tree.new(java_tree)
456
500
  end
457
501
 
@@ -466,18 +510,44 @@ module TreeHaver
466
510
  # @param old_tree [Tree, nil] previous backend tree for incremental parsing (already unwrapped)
467
511
  # @param source [String] the source code to parse
468
512
  # @return [Tree] raw backend tree (wrapping happens in TreeHaver::Parser)
469
- # @see https://tree-sitter.github.io/java-tree-sitter/io/github/treesitter/jtreesitter/Parser.html#parse(io.github.treesitter.jtreesitter.Tree,java.lang.String)
513
+ # @see https://tree-sitter.github.io/java-tree-sitter/io/github/treesitter/jtreesitter/Parser.html#parse(java.lang.String,io.github.treesitter.jtreesitter.Tree)
470
514
  def parse_string(old_tree, source)
471
515
  # old_tree is already unwrapped to Tree wrapper's impl by TreeHaver::Parser
472
516
  if old_tree
473
- java_old_tree = old_tree.is_a?(Tree) ? old_tree.impl : old_tree
474
- java_tree = @parser.parse(java_old_tree, source)
517
+ # Get the actual Java Tree object
518
+ java_old_tree = if old_tree.is_a?(Tree)
519
+ old_tree.impl
520
+ else
521
+ unwrap_optional(old_tree)
522
+ end
523
+
524
+ java_result = if java_old_tree
525
+ # jtreesitter 0.26.0 API: parse(String source, Tree oldTree)
526
+ @parser.parse(source, java_old_tree)
527
+ else
528
+ @parser.parse(source)
529
+ end
475
530
  else
476
- java_tree = @parser.parse(source)
531
+ java_result = @parser.parse(source)
477
532
  end
478
- # Return raw Java::Tree - TreeHaver::Parser will wrap it
533
+ # jtreesitter 0.26.0 returns Optional<Tree>
534
+ java_tree = unwrap_optional(java_result)
535
+ raise TreeHaver::Error, "Parser returned no tree" unless java_tree
479
536
  Tree.new(java_tree)
480
537
  end
538
+
539
+ private
540
+
541
+ # Unwrap Java Optional
542
+ #
543
+ # jtreesitter 0.26.0 returns Optional<T> from many methods.
544
+ #
545
+ # @param value [Object] an Optional or direct value
546
+ # @return [Object, nil] unwrapped value or nil if empty
547
+ def unwrap_optional(value)
548
+ return value unless value.respond_to?(:isPresent)
549
+ value.isPresent ? value.get : nil
550
+ end
481
551
  end
482
552
 
483
553
  # Wrapper for java-tree-sitter Tree
@@ -494,8 +564,18 @@ module TreeHaver
494
564
  # Get the root node of the tree
495
565
  #
496
566
  # @return [Node] the root node
567
+ # @raise [TreeHaver::Error] if tree has no root node
497
568
  def root_node
498
- Node.new(@impl.rootNode)
569
+ result = @impl.rootNode
570
+ # jtreesitter 0.26.0: rootNode() may return Optional<Node> or Node directly
571
+ java_node = if result.respond_to?(:isPresent)
572
+ raise TreeHaver::Error, "Tree has no root node" unless result.isPresent
573
+ result.get
574
+ else
575
+ result
576
+ end
577
+ raise TreeHaver::Error, "Tree has no root node" unless java_node
578
+ Node.new(java_node)
499
579
  end
500
580
 
501
581
  # Mark the tree as edited for incremental re-parsing
@@ -556,9 +636,27 @@ module TreeHaver
556
636
  # Get a child by index
557
637
  #
558
638
  # @param index [Integer] the child index
559
- # @return [Node] the child node
639
+ # @return [Node, nil] the child node or nil if index out of bounds
560
640
  def child(index)
561
- Node.new(@impl.child(index))
641
+ # jtreesitter 0.26.0: getChild returns Optional<Node> or throws IndexOutOfBoundsException
642
+ result = @impl.getChild(index)
643
+ return unless result.respond_to?(:isPresent) ? result.isPresent : result
644
+ java_node = result.respond_to?(:get) ? result.get : result
645
+ Node.new(java_node)
646
+ rescue ::Java::JavaLang::IndexOutOfBoundsException
647
+ nil
648
+ end
649
+
650
+ # Get a child by field name
651
+ #
652
+ # @param name [String] the field name
653
+ # @return [Node, nil] the child node or nil if not found
654
+ def child_by_field_name(name)
655
+ # jtreesitter 0.26.0: getChildByFieldName returns Optional<Node>
656
+ result = @impl.getChildByFieldName(name)
657
+ return unless result.respond_to?(:isPresent) ? result.isPresent : result
658
+ java_node = result.respond_to?(:get) ? result.get : result
659
+ Node.new(java_node)
562
660
  end
563
661
 
564
662
  # Iterate over children
@@ -616,6 +714,46 @@ module TreeHaver
616
714
  @impl.isMissing
617
715
  end
618
716
 
717
+ # Check if this is a named node
718
+ #
719
+ # @return [Boolean] true if this is a named node
720
+ def named?
721
+ @impl.isNamed
722
+ end
723
+
724
+ # Get the parent node
725
+ #
726
+ # @return [Node, nil] the parent node or nil if this is the root
727
+ def parent
728
+ # jtreesitter 0.26.0: getParent returns Optional<Node>
729
+ result = @impl.getParent
730
+ return unless result.respond_to?(:isPresent) ? result.isPresent : result
731
+ java_node = result.respond_to?(:get) ? result.get : result
732
+ Node.new(java_node)
733
+ end
734
+
735
+ # Get the next sibling node
736
+ #
737
+ # @return [Node, nil] the next sibling or nil if none
738
+ def next_sibling
739
+ # jtreesitter 0.26.0: getNextSibling returns Optional<Node>
740
+ result = @impl.getNextSibling
741
+ return unless result.respond_to?(:isPresent) ? result.isPresent : result
742
+ java_node = result.respond_to?(:get) ? result.get : result
743
+ Node.new(java_node)
744
+ end
745
+
746
+ # Get the previous sibling node
747
+ #
748
+ # @return [Node, nil] the previous sibling or nil if none
749
+ def prev_sibling
750
+ # jtreesitter 0.26.0: getPrevSibling returns Optional<Node>
751
+ result = @impl.getPrevSibling
752
+ return unless result.respond_to?(:isPresent) ? result.isPresent : result
753
+ java_node = result.respond_to?(:get) ? result.get : result
754
+ Node.new(java_node)
755
+ end
756
+
619
757
  # Get the text of this node
620
758
  #
621
759
  # @return [String] the source text
@@ -117,6 +117,30 @@ module TreeHaver
117
117
  def markdown(flags: nil, extensions: [:table])
118
118
  new(:markdown, flags: flags, extensions: extensions)
119
119
  end
120
+
121
+ # Load language from library path (API compatibility)
122
+ #
123
+ # Markly only supports Markdown, so path and symbol parameters are ignored.
124
+ # This method exists for API consistency with tree-sitter backends,
125
+ # allowing `TreeHaver.parser_for(:markdown)` to work regardless of backend.
126
+ #
127
+ # @param _path [String] Ignored - Markly doesn't load external grammars
128
+ # @param symbol [String, nil] Ignored
129
+ # @param name [String, nil] Language name hint (defaults to :markdown)
130
+ # @return [Language] Markdown language
131
+ # @raise [TreeHaver::NotAvailable] if requested language is not Markdown
132
+ def from_library(_path = nil, symbol: nil, name: nil)
133
+ # Derive language name from symbol if provided
134
+ lang_name = name || symbol&.to_s&.sub(/^tree_sitter_/, "")&.to_sym || :markdown
135
+
136
+ unless lang_name == :markdown
137
+ raise TreeHaver::NotAvailable,
138
+ "Markly backend only supports Markdown, not #{lang_name}. " \
139
+ "Use a tree-sitter backend for #{lang_name} support."
140
+ end
141
+
142
+ markdown
143
+ end
120
144
  end
121
145
 
122
146
  # Comparison for sorting/equality
@@ -93,6 +93,7 @@ module TreeHaver
93
93
  # The language name (always :ruby for Prism)
94
94
  # @return [Symbol]
95
95
  attr_reader :name
96
+ alias_method :language_name, :name
96
97
 
97
98
  # The backend this language is for
98
99
  # @return [Symbol]
@@ -153,16 +154,28 @@ module TreeHaver
153
154
  new(:ruby, options: options)
154
155
  end
155
156
 
156
- # Not applicable for Prism (tree-sitter-specific)
157
+ # Load language from library path (API compatibility)
157
158
  #
158
- # Prism is Ruby-only and doesn't load external grammar libraries.
159
- # This method exists for API compatibility but will raise an error.
159
+ # Prism only supports Ruby, so path and symbol parameters are ignored.
160
+ # This method exists for API consistency with tree-sitter backends,
161
+ # allowing `TreeHaver.parser_for(:ruby)` to work regardless of backend.
160
162
  #
161
- # @raise [TreeHaver::NotAvailable] always raises
162
- def from_library(path, symbol: nil, name: nil)
163
- raise TreeHaver::NotAvailable,
164
- "Prism backend doesn't use shared libraries. " \
165
- "Use Prism::Language.ruby instead."
163
+ # @param _path [String] Ignored - Prism doesn't load external grammars
164
+ # @param symbol [String, nil] Ignored
165
+ # @param name [String, nil] Language name hint (defaults to :ruby)
166
+ # @return [Language] Ruby language
167
+ # @raise [TreeHaver::NotAvailable] if requested language is not Ruby
168
+ def from_library(_path = nil, symbol: nil, name: nil)
169
+ # Derive language name from symbol if provided
170
+ lang_name = name || symbol&.to_s&.sub(/^tree_sitter_/, "")&.to_sym || :ruby
171
+
172
+ unless lang_name == :ruby
173
+ raise TreeHaver::NotAvailable,
174
+ "Prism backend only supports Ruby, not #{lang_name}. " \
175
+ "Use a tree-sitter backend for #{lang_name} support."
176
+ end
177
+
178
+ ruby
166
179
  end
167
180
 
168
181
  alias_method :from_path, :from_library
@@ -100,6 +100,30 @@ module TreeHaver
100
100
  def yaml
101
101
  new(:yaml)
102
102
  end
103
+
104
+ # Load language from library path (API compatibility)
105
+ #
106
+ # Psych only supports YAML, so path and symbol parameters are ignored.
107
+ # This method exists for API consistency with tree-sitter backends,
108
+ # allowing `TreeHaver.parser_for(:yaml)` to work regardless of backend.
109
+ #
110
+ # @param _path [String] Ignored - Psych doesn't load external grammars
111
+ # @param symbol [String, nil] Ignored
112
+ # @param name [String, nil] Language name hint (defaults to :yaml)
113
+ # @return [Language] YAML language
114
+ # @raise [TreeHaver::NotAvailable] if requested language is not YAML
115
+ def from_library(_path = nil, symbol: nil, name: nil)
116
+ # Derive language name from symbol if provided
117
+ lang_name = name || symbol&.to_s&.sub(/^tree_sitter_/, "")&.to_sym || :yaml
118
+
119
+ unless lang_name == :yaml
120
+ raise TreeHaver::NotAvailable,
121
+ "Psych backend only supports YAML, not #{lang_name}. " \
122
+ "Use a tree-sitter backend for #{lang_name} support."
123
+ end
124
+
125
+ yaml
126
+ end
103
127
  end
104
128
 
105
129
  # Comparison for sorting/equality
@@ -211,10 +211,13 @@ module TreeHaver
211
211
 
212
212
  # No tree-sitter path registered - check for Citrus fallback
213
213
  # This enables auto-fallback when tree-sitter grammar is not installed
214
- # but a Citrus grammar (pure Ruby) is available
215
- citrus_reg = all_backends[:citrus]
216
- if citrus_reg && citrus_reg[:grammar_module]
217
- return Backends::Citrus::Language.new(citrus_reg[:grammar_module])
214
+ # but a Citrus grammar (pure Ruby) is available.
215
+ # Only fall back when backend is :auto - explicit native backend requests should fail.
216
+ if TreeHaver.effective_backend == :auto
217
+ citrus_reg = all_backends[:citrus]
218
+ if citrus_reg && citrus_reg[:grammar_module]
219
+ return Backends::Citrus::Language.new(citrus_reg[:grammar_module])
220
+ end
218
221
  end
219
222
 
220
223
  # No appropriate registration found
@@ -237,17 +240,29 @@ module TreeHaver
237
240
  # - FFI can't find required symbols like ts_parser_new (FFI::NotFoundError)
238
241
  # - Invalid arguments were provided (ArgumentError)
239
242
  #
243
+ # Fallback to Citrus ONLY happens when:
244
+ # - The effective backend is :auto (user didn't explicitly request a native backend)
245
+ # - A Citrus grammar is registered for the language
246
+ #
247
+ # If the user explicitly requested a native backend (:mri, :rust, :ffi, :java),
248
+ # we should NOT silently fall back to Citrus - that would violate the user's intent.
249
+ #
240
250
  # @param error [Exception] the original error
241
251
  # @param all_backends [Hash] all registered backends for the language
242
- # @return [Backends::Citrus::Language] if Citrus fallback available
243
- # @raise [Exception] re-raises original error if no fallback
252
+ # @return [Backends::Citrus::Language] if Citrus fallback available and allowed
253
+ # @raise [Exception] re-raises original error if no fallback or fallback not allowed
244
254
  # @api private
245
255
  def handle_tree_sitter_load_failure(error, all_backends)
246
- citrus_reg = all_backends[:citrus]
247
- if citrus_reg && citrus_reg[:grammar_module]
248
- return Backends::Citrus::Language.new(citrus_reg[:grammar_module])
256
+ # Only fall back to Citrus when backend is :auto
257
+ # If user explicitly requested a native backend, respect that choice
258
+ effective = TreeHaver.effective_backend
259
+ if effective == :auto
260
+ citrus_reg = all_backends[:citrus]
261
+ if citrus_reg && citrus_reg[:grammar_module]
262
+ return Backends::Citrus::Language.new(citrus_reg[:grammar_module])
263
+ end
249
264
  end
250
- # No Citrus fallback available, re-raise the original error
265
+ # No Citrus fallback allowed or available, re-raise the original error
251
266
  raise error
252
267
  end
253
268
  end
@@ -10,12 +10,39 @@ module TreeHaver
10
10
  # The registry supports multiple backends for the same language, allowing runtime
11
11
  # switching, benchmarking, and fallback scenarios.
12
12
  #
13
+ # == Supported Backend Types
14
+ #
15
+ # The registry is extensible and supports any backend type. Common types include:
16
+ #
17
+ # - `:tree_sitter` - Native tree-sitter grammars (.so files)
18
+ # - `:citrus` - Citrus PEG parser grammars (pure Ruby)
19
+ # - `:prism` - Ruby's Prism parser (Ruby source only)
20
+ # - `:psych` - Ruby's Psych parser (YAML only)
21
+ # - `:commonmarker` - Commonmarker gem (Markdown)
22
+ # - `:markly` - Markly gem (Markdown/GFM)
23
+ # - `:rbs` - RBS gem (RBS type signatures) - registered externally by rbs-merge
24
+ #
25
+ # External gems can register their own backend types using the same API.
26
+ #
13
27
  # Registration structure:
14
28
  # ```ruby
15
29
  # @registrations = {
16
30
  # toml: {
17
31
  # tree_sitter: { path: "/path/to/lib.so", symbol: "tree_sitter_toml" },
18
32
  # citrus: { grammar_module: TomlRB::Document, gem_name: "toml-rb" }
33
+ # },
34
+ # ruby: {
35
+ # prism: { backend_module: TreeHaver::Backends::Prism }
36
+ # },
37
+ # yaml: {
38
+ # psych: { backend_module: TreeHaver::Backends::Psych }
39
+ # },
40
+ # markdown: {
41
+ # commonmarker: { backend_module: TreeHaver::Backends::Commonmarker },
42
+ # markly: { backend_module: TreeHaver::Backends::Markly }
43
+ # },
44
+ # rbs: {
45
+ # rbs: { backend_module: Rbs::Merge::Backends::RbsBackend } # External
19
46
  # }
20
47
  # }
21
48
  # ```
@@ -32,6 +59,13 @@ module TreeHaver
32
59
  # grammar_module: TomlRB::Document, gem_name: "toml-rb")
33
60
  # ```
34
61
  #
62
+ # @example Register a pure Ruby backend (internal or external)
63
+ # ```ruby
64
+ # TreeHaver::LanguageRegistry.register(:rbs, :rbs,
65
+ # backend_module: Rbs::Merge::Backends::RbsBackend,
66
+ # gem_name: "rbs")
67
+ # ```
68
+ #
35
69
  # @api private
36
70
  module LanguageRegistry
37
71
  @mutex = Mutex.new
@@ -45,13 +79,14 @@ module TreeHaver
45
79
  # Stores backend-specific configuration for a language. Multiple backends
46
80
  # can be registered for the same language without conflict.
47
81
  #
48
- # @param name [Symbol, String] language identifier (e.g., :toml, :json)
49
- # @param backend_type [Symbol] backend type (:tree_sitter, :citrus, :mri, :rust, :ffi, :java)
82
+ # @param name [Symbol, String] language identifier (e.g., :toml, :json, :ruby, :yaml, :rbs)
83
+ # @param backend_type [Symbol] backend type (:tree_sitter, :citrus, :prism, :psych, :commonmarker, :markly, or custom)
50
84
  # @param config [Hash] backend-specific configuration
51
85
  # @option config [String] :path tree-sitter library path (for tree-sitter backends)
52
86
  # @option config [String] :symbol exported symbol name (for tree-sitter backends)
53
87
  # @option config [Module] :grammar_module Citrus grammar module (for Citrus backend)
54
- # @option config [String] :gem_name gem name for error messages (for Citrus backend)
88
+ # @option config [Module] :backend_module backend module with Language/Parser classes (for pure Ruby backends)
89
+ # @option config [String] :gem_name gem name for error messages and availability checks
55
90
  # @return [void]
56
91
  # @example Register tree-sitter grammar
57
92
  # LanguageRegistry.register(:toml, :tree_sitter,
@@ -59,6 +94,9 @@ module TreeHaver
59
94
  # @example Register Citrus grammar
60
95
  # LanguageRegistry.register(:toml, :citrus,
61
96
  # grammar_module: TomlRB::Document, gem_name: "toml-rb")
97
+ # @example Register pure Ruby backend (external gem)
98
+ # LanguageRegistry.register(:rbs, :rbs,
99
+ # backend_module: Rbs::Merge::Backends::RbsBackend, gem_name: "rbs")
62
100
  def register(name, backend_type, **config)
63
101
  key = name.to_sym
64
102
  backend_key = backend_type.to_sym
@@ -132,5 +170,21 @@ module TreeHaver
132
170
  @mutex.synchronize { @cache.clear }
133
171
  nil
134
172
  end
173
+
174
+ # Clear all registrations and cache
175
+ #
176
+ # Removes all language registrations and cached Language objects.
177
+ # Primarily used in tests to reset state between test cases.
178
+ #
179
+ # @return [void]
180
+ # @example
181
+ # LanguageRegistry.clear
182
+ def clear
183
+ @mutex.synchronize do
184
+ @registrations.clear
185
+ @cache.clear
186
+ end
187
+ nil
188
+ end
135
189
  end
136
190
  end
@@ -227,7 +227,21 @@ module TreeHaver
227
227
  # @return [String]
228
228
  def text
229
229
  if @inner_node.respond_to?(:text)
230
- @inner_node.text
230
+ # Some backends (like TreeStump) require source as argument
231
+ # Check arity to determine how to call
232
+ arity = @inner_node.method(:text).arity
233
+ if arity == 0 || arity == -1
234
+ # No required arguments, or optional arguments only
235
+ @inner_node.text
236
+ elsif arity >= 1 && @source
237
+ # Has required argument(s) - pass source
238
+ @inner_node.text(@source)
239
+ elsif @source
240
+ # Fallback to byte extraction
241
+ @source[start_byte...end_byte] || ""
242
+ else
243
+ raise TreeHaver::Error, "Cannot extract text: backend requires source but none provided"
244
+ end
231
245
  elsif @source
232
246
  # Fallback: extract from source using byte positions
233
247
  @source[start_byte...end_byte] || ""