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.
- checksums.yaml +4 -4
- checksums.yaml.gz.sig +0 -0
- data/CHANGELOG.md +218 -3
- data/LICENSE.txt +1 -1
- data/README.md +140 -22
- data/lib/tree_haver/backend_api.rb +349 -0
- data/lib/tree_haver/backends/citrus.rb +37 -8
- data/lib/tree_haver/backends/commonmarker.rb +24 -0
- data/lib/tree_haver/backends/java.rb +160 -22
- data/lib/tree_haver/backends/markly.rb +24 -0
- data/lib/tree_haver/backends/prism.rb +21 -8
- data/lib/tree_haver/backends/psych.rb +24 -0
- data/lib/tree_haver/language.rb +25 -10
- data/lib/tree_haver/language_registry.rb +57 -3
- data/lib/tree_haver/node.rb +15 -1
- data/lib/tree_haver/rspec/dependency_tags.rb +249 -23
- data/lib/tree_haver/version.rb +1 -1
- data/lib/tree_haver.rb +370 -108
- data.tar.gz.sig +0 -0
- metadata +5 -4
- metadata.gz.sig +0 -0
|
@@ -2,26 +2,35 @@
|
|
|
2
2
|
|
|
3
3
|
module TreeHaver
|
|
4
4
|
module Backends
|
|
5
|
-
# Java backend for JRuby using java-tree-sitter
|
|
5
|
+
# Java backend for JRuby using jtreesitter (java-tree-sitter)
|
|
6
6
|
#
|
|
7
|
-
# This backend integrates with
|
|
7
|
+
# This backend integrates with jtreesitter JARs on JRuby,
|
|
8
8
|
# leveraging JRuby's native Java integration for optimal performance.
|
|
9
9
|
#
|
|
10
|
-
#
|
|
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 (
|
|
29
|
+
# - TruffleRuby: ✗ Not available (jtreesitter requires JRuby's Java interop)
|
|
21
30
|
#
|
|
22
31
|
# == Installation
|
|
23
32
|
#
|
|
24
|
-
# 1. Download
|
|
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
|
|
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
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
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
|
|
414
|
-
"is in
|
|
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
|
-
|
|
454
|
-
#
|
|
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
|
|
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
|
-
|
|
474
|
-
|
|
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
|
-
|
|
531
|
+
java_result = @parser.parse(source)
|
|
477
532
|
end
|
|
478
|
-
#
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
#
|
|
157
|
+
# Load language from library path (API compatibility)
|
|
157
158
|
#
|
|
158
|
-
# Prism
|
|
159
|
-
# This method exists for API
|
|
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
|
-
# @
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
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
|
data/lib/tree_haver/language.rb
CHANGED
|
@@ -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
|
-
|
|
216
|
-
if
|
|
217
|
-
|
|
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
|
-
|
|
247
|
-
|
|
248
|
-
|
|
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, :
|
|
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 [
|
|
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
|
data/lib/tree_haver/node.rb
CHANGED
|
@@ -227,7 +227,21 @@ module TreeHaver
|
|
|
227
227
|
# @return [String]
|
|
228
228
|
def text
|
|
229
229
|
if @inner_node.respond_to?(:text)
|
|
230
|
-
|
|
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] || ""
|