tree_haver 3.0.0 → 3.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.
@@ -165,10 +165,20 @@ module TreeHaver
165
165
  def start_point
166
166
  if @inner_node.respond_to?(:start_point)
167
167
  point = @inner_node.start_point
168
- Point.new(point.row, point.column)
168
+ # Handle both Point objects and hashes
169
+ if point.is_a?(Hash)
170
+ Point.new(point[:row], point[:column])
171
+ else
172
+ Point.new(point.row, point.column)
173
+ end
169
174
  elsif @inner_node.respond_to?(:start_position)
170
175
  point = @inner_node.start_position
171
- Point.new(point.row, point.column)
176
+ # Handle both Point objects and hashes
177
+ if point.is_a?(Hash)
178
+ Point.new(point[:row], point[:column])
179
+ else
180
+ Point.new(point.row, point.column)
181
+ end
172
182
  else
173
183
  raise TreeHaver::Error, "Backend node does not support start_point/start_position"
174
184
  end
@@ -180,15 +190,71 @@ module TreeHaver
180
190
  def end_point
181
191
  if @inner_node.respond_to?(:end_point)
182
192
  point = @inner_node.end_point
183
- Point.new(point.row, point.column)
193
+ # Handle both Point objects and hashes
194
+ if point.is_a?(Hash)
195
+ Point.new(point[:row], point[:column])
196
+ else
197
+ Point.new(point.row, point.column)
198
+ end
184
199
  elsif @inner_node.respond_to?(:end_position)
185
200
  point = @inner_node.end_position
186
- Point.new(point.row, point.column)
201
+ # Handle both Point objects and hashes
202
+ if point.is_a?(Hash)
203
+ Point.new(point[:row], point[:column])
204
+ else
205
+ Point.new(point.row, point.column)
206
+ end
187
207
  else
188
208
  raise TreeHaver::Error, "Backend node does not support end_point/end_position"
189
209
  end
190
210
  end
191
211
 
212
+ # Get the 1-based line number where this node starts
213
+ #
214
+ # Convenience method that converts 0-based row to 1-based line number.
215
+ # This is useful for error messages and matching with editor line numbers.
216
+ #
217
+ # @return [Integer] 1-based line number
218
+ def start_line
219
+ start_point.row + 1
220
+ end
221
+
222
+ # Get the 1-based line number where this node ends
223
+ #
224
+ # Convenience method that converts 0-based row to 1-based line number.
225
+ #
226
+ # @return [Integer] 1-based line number
227
+ def end_line
228
+ end_point.row + 1
229
+ end
230
+
231
+ # Get position information as a hash
232
+ #
233
+ # Returns a hash with 1-based line numbers and 0-based columns.
234
+ # This format is compatible with *-merge gems' FileAnalysisBase.
235
+ #
236
+ # @return [Hash{Symbol => Integer}] Position hash
237
+ # @example
238
+ # node.source_position
239
+ # # => { start_line: 1, end_line: 3, start_column: 0, end_column: 10 }
240
+ def source_position
241
+ {
242
+ start_line: start_line,
243
+ end_line: end_line,
244
+ start_column: start_point.column,
245
+ end_column: end_point.column,
246
+ }
247
+ end
248
+
249
+ # Get the first child node
250
+ #
251
+ # Convenience method for iteration patterns that expect first_child.
252
+ #
253
+ # @return [Node, nil] First child node or nil if no children
254
+ def first_child
255
+ child(0)
256
+ end
257
+
192
258
  # Get the node's text content
193
259
  #
194
260
  # @return [String]
@@ -435,10 +501,10 @@ module TreeHaver
435
501
 
436
502
  # Compare by position first (start_byte, then end_byte)
437
503
  cmp = start_byte <=> other.start_byte
438
- return cmp unless cmp.zero?
504
+ return cmp if cmp.nonzero?
439
505
 
440
506
  cmp = end_byte <=> other.end_byte
441
- return cmp unless cmp.zero?
507
+ return cmp if cmp.nonzero?
442
508
 
443
509
  # For nodes at the same position with same span, compare by type
444
510
  type <=> other.type
@@ -10,7 +10,7 @@ module TreeHaver
10
10
  # Current version of the tree_haver gem
11
11
  #
12
12
  # @return [String] the version string (e.g., "3.0.0")
13
- VERSION = "3.0.0"
13
+ VERSION = "3.1.0"
14
14
  end
15
15
 
16
16
  # Traditional location for VERSION constant
data/lib/tree_haver.rb CHANGED
@@ -10,12 +10,17 @@ require "set"
10
10
  require_relative "tree_haver/version"
11
11
  require_relative "tree_haver/language_registry"
12
12
 
13
- # TreeHaver is a cross-Ruby adapter for the tree-sitter parsing library.
13
+ # TreeHaver is a cross-Ruby adapter for code parsing with 10 backends.
14
14
  #
15
- # It provides a unified API for parsing source code using tree-sitter grammars,
16
- # working seamlessly across MRI Ruby, JRuby, and TruffleRuby.
15
+ # Provides a unified API for parsing source code across MRI Ruby, JRuby, and TruffleRuby
16
+ # using tree-sitter grammars or language-specific native parsers.
17
17
  #
18
- # @example Basic usage with TOML
18
+ # Supports 10 backends:
19
+ # - Tree-sitter: MRI (C), Rust, FFI, Java
20
+ # - Native parsers: Prism (Ruby), Psych (YAML), Commonmarker (Markdown), Markly (GFM)
21
+ # - Pure Ruby: Citrus (portable fallback)
22
+ #
23
+ # @example Basic usage with tree-sitter
19
24
  # # Load a language grammar
20
25
  # language = TreeHaver::Language.from_library(
21
26
  # "/usr/local/lib/libtree-sitter-toml.so",
@@ -30,8 +35,28 @@ require_relative "tree_haver/language_registry"
30
35
  # tree = parser.parse("[package]\nname = \"my-app\"")
31
36
  # root = tree.root_node
32
37
  #
33
- # # Traverse the AST
34
- # root.each { |child| puts child.type }
38
+ # # Use unified Position API (works across all backends)
39
+ # puts root.start_line # => 1 (1-based)
40
+ # puts root.source_position # => {start_line:, end_line:, start_column:, end_column:}
41
+ #
42
+ # @example Using language-specific backends
43
+ # # Parse Ruby with Prism
44
+ # TreeHaver.backend = :prism
45
+ # parser = TreeHaver::Parser.new
46
+ # parser.language = TreeHaver::Backends::Prism::Language.ruby
47
+ # tree = parser.parse("class Example; end")
48
+ #
49
+ # # Parse YAML with Psych
50
+ # TreeHaver.backend = :psych
51
+ # parser = TreeHaver::Parser.new
52
+ # parser.language = TreeHaver::Backends::Psych::Language.yaml
53
+ # tree = parser.parse("key: value")
54
+ #
55
+ # # Parse Markdown with Commonmarker
56
+ # TreeHaver.backend = :commonmarker
57
+ # parser = TreeHaver::Parser.new
58
+ # parser.language = TreeHaver::Backends::Commonmarker::Language.markdown
59
+ # tree = parser.parse("# Heading\nParagraph")
35
60
  #
36
61
  # @example Using language registration
37
62
  # TreeHaver.register_language(:toml, path: "/usr/local/lib/libtree-sitter-toml.so")
@@ -43,22 +68,21 @@ require_relative "tree_haver/language_registry"
43
68
  # finder.register! if finder.available?
44
69
  # language = TreeHaver::Language.toml
45
70
  #
46
- # @example Using GrammarFinder in a *-merge gem
47
- # # Each merge gem (toml-merge, json-merge, bash-merge) uses the same pattern
48
- # finder = TreeHaver::GrammarFinder.new(:toml) # or :json, :bash, etc.
49
- # if finder.available?
50
- # finder.register!
51
- # else
52
- # warn finder.not_found_message
53
- # end
54
- #
55
71
  # @example Selecting a backend
56
- # TreeHaver.backend = :ffi # Force FFI backend
57
- # TreeHaver.backend = :mri # Force MRI backend
58
- # TreeHaver.backend = :auto # Auto-select (default)
72
+ # TreeHaver.backend = :mri # Force MRI (ruby_tree_sitter)
73
+ # TreeHaver.backend = :rust # Force Rust (tree_stump)
74
+ # TreeHaver.backend = :ffi # Force FFI
75
+ # TreeHaver.backend = :java # Force Java (JRuby)
76
+ # TreeHaver.backend = :prism # Force Prism (Ruby)
77
+ # TreeHaver.backend = :psych # Force Psych (YAML)
78
+ # TreeHaver.backend = :commonmarker # Force Commonmarker (Markdown)
79
+ # TreeHaver.backend = :markly # Force Markly (GFM)
80
+ # TreeHaver.backend = :citrus # Force Citrus (pure Ruby)
81
+ # TreeHaver.backend = :auto # Auto-select (default)
59
82
  #
60
83
  # @see https://tree-sitter.github.io/tree-sitter/ tree-sitter documentation
61
84
  # @see GrammarFinder For automatic grammar library discovery
85
+ # @see Backends For available parsing backends
62
86
  module TreeHaver
63
87
  # Base error class for TreeHaver exceptions
64
88
  # @see https://github.com/Faveod/ruby-tree-sitter/pull/83 for inherit from Exception reasoning
@@ -115,12 +139,17 @@ module TreeHaver
115
139
  # - {Backends::FFI} - Uses Ruby FFI to call libtree-sitter directly
116
140
  # - {Backends::Java} - Uses JRuby's Java integration
117
141
  # - {Backends::Citrus} - Uses Citrus PEG parser (pure Ruby, portable)
142
+ # - {Backends::Prism} - Uses Ruby's built-in Prism parser (Ruby-only, stdlib in 3.4+)
118
143
  module Backends
119
144
  autoload :MRI, File.join(__dir__, "tree_haver", "backends", "mri")
120
145
  autoload :Rust, File.join(__dir__, "tree_haver", "backends", "rust")
121
146
  autoload :FFI, File.join(__dir__, "tree_haver", "backends", "ffi")
122
147
  autoload :Java, File.join(__dir__, "tree_haver", "backends", "java")
123
148
  autoload :Citrus, File.join(__dir__, "tree_haver", "backends", "citrus")
149
+ autoload :Prism, File.join(__dir__, "tree_haver", "backends", "prism")
150
+ autoload :Psych, File.join(__dir__, "tree_haver", "backends", "psych")
151
+ autoload :Commonmarker, File.join(__dir__, "tree_haver", "backends", "commonmarker")
152
+ autoload :Markly, File.join(__dir__, "tree_haver", "backends", "markly")
124
153
 
125
154
  # Known backend conflicts
126
155
  #
@@ -135,6 +164,10 @@ module TreeHaver
135
164
  ffi: [:mri], # FFI segfaults if MRI (ruby_tree_sitter) has been loaded
136
165
  java: [],
137
166
  citrus: [],
167
+ prism: [], # Prism has no conflicts with other backends
168
+ psych: [], # Psych has no conflicts with other backends
169
+ commonmarker: [], # Commonmarker has no conflicts with other backends
170
+ markly: [], # Markly has no conflicts with other backends
138
171
  }.freeze
139
172
  end
140
173
 
@@ -201,7 +234,10 @@ module TreeHaver
201
234
  # @return [Boolean]
202
235
  # @example Disable protection for testing
203
236
  # TreeHaver.backend_protect = false
204
- attr_writer :backend_protect
237
+ def backend_protect=(value)
238
+ @backend_protect_mutex ||= Mutex.new
239
+ @backend_protect_mutex.synchronize { @backend_protect = value }
240
+ end
205
241
 
206
242
  # Check if backend conflict protection is enabled
207
243
  #
@@ -267,6 +303,10 @@ module TreeHaver
267
303
  when "ffi" then :ffi
268
304
  when "java" then :java
269
305
  when "citrus" then :citrus
306
+ when "prism" then :prism
307
+ when "psych" then :psych
308
+ when "commonmarker" then :commonmarker
309
+ when "markly" then :markly
270
310
  else :auto
271
311
  end
272
312
  end
@@ -463,6 +503,14 @@ module TreeHaver
463
503
  Backends::Java
464
504
  when :citrus
465
505
  Backends::Citrus
506
+ when :prism
507
+ Backends::Prism
508
+ when :psych
509
+ Backends::Psych
510
+ when :commonmarker
511
+ Backends::Commonmarker
512
+ when :markly
513
+ Backends::Markly
466
514
  when :auto
467
515
  backend_module # Fall back to normal resolution for :auto
468
516
  else
@@ -519,6 +567,14 @@ module TreeHaver
519
567
  Backends::Java
520
568
  when :citrus
521
569
  Backends::Citrus
570
+ when :prism
571
+ Backends::Prism
572
+ when :psych
573
+ Backends::Psych
574
+ when :commonmarker
575
+ Backends::Commonmarker
576
+ when :markly
577
+ Backends::Markly
522
578
  else
523
579
  # auto-select: prefer native/fast backends, fall back to pure Ruby (Citrus)
524
580
  if defined?(RUBY_ENGINE) && RUBY_ENGINE == "jruby" && Backends::Java.available?
@@ -827,7 +883,8 @@ module TreeHaver
827
883
  "Registered backends: #{all_backends.keys.inspect}"
828
884
  end
829
885
 
830
- # For tree-sitter backends, use the path
886
+ # For tree-sitter backends, try to load from path
887
+ # If that fails, fall back to Citrus if available
831
888
  if reg && reg[:path]
832
889
  path = kwargs[:path] || args.first || reg[:path]
833
890
  # Symbol priority: kwargs override > registration > derive from method_name
@@ -842,7 +899,30 @@ module TreeHaver
842
899
  # Using symbol-derived name ensures ruby_tree_sitter gets the correct language name
843
900
  # e.g., "toml" not "toml_both" when symbol is "tree_sitter_toml"
844
901
  name = kwargs[:name] || symbol&.sub(/\Atree_sitter_/, "")
845
- return from_library(path, symbol: symbol, name: name)
902
+
903
+ begin
904
+ return from_library(path, symbol: symbol, name: name)
905
+ rescue NotAvailable, ArgumentError, LoadError, FFI::NotFoundError => _e
906
+ # Tree-sitter failed to load - check for Citrus fallback
907
+ # This handles cases where:
908
+ # - The .so file doesn't exist or can't be loaded (NotAvailable, LoadError)
909
+ # - FFI can't find required symbols like ts_parser_new (FFI::NotFoundError)
910
+ # - Invalid arguments were provided (ArgumentError)
911
+ citrus_reg = all_backends[:citrus]
912
+ if citrus_reg && citrus_reg[:grammar_module]
913
+ return Backends::Citrus::Language.new(citrus_reg[:grammar_module])
914
+ end
915
+ # No Citrus fallback available, re-raise the original error
916
+ raise
917
+ end
918
+ end
919
+
920
+ # No tree-sitter path registered - check for Citrus fallback
921
+ # This enables auto-fallback when tree-sitter grammar is not installed
922
+ # but a Citrus grammar (pure Ruby) is available
923
+ citrus_reg = all_backends[:citrus]
924
+ if citrus_reg && citrus_reg[:grammar_module]
925
+ return Backends::Citrus::Language.new(citrus_reg[:grammar_module])
846
926
  end
847
927
 
848
928
  # No appropriate registration found
@@ -913,8 +993,28 @@ module TreeHaver
913
993
  end
914
994
  end
915
995
 
916
- @impl = mod::Parser.new
917
- @explicit_backend = backend # Remember for introspection (always a Symbol or nil)
996
+ # Try to create the parser, with fallback to Citrus if tree-sitter fails
997
+ # This enables auto-fallback when tree-sitter runtime isn't available
998
+ begin
999
+ @impl = mod::Parser.new
1000
+ @explicit_backend = backend # Remember for introspection (always a Symbol or nil)
1001
+ rescue NoMethodError, FFI::NotFoundError, LoadError => e
1002
+ # Tree-sitter backend failed (likely missing runtime library)
1003
+ # Try Citrus as fallback if we weren't explicitly asked for a specific backend
1004
+ if backend.nil? || backend == :auto
1005
+ if Backends::Citrus.available?
1006
+ @impl = Backends::Citrus::Parser.new
1007
+ @explicit_backend = :citrus
1008
+ else
1009
+ # No fallback available, re-raise original error
1010
+ raise NotAvailable, "Tree-sitter backend failed: #{e.message}. " \
1011
+ "Citrus fallback not available. Install tree-sitter runtime or citrus gem."
1012
+ end
1013
+ else
1014
+ # Explicit backend was requested, don't fallback
1015
+ raise
1016
+ end
1017
+ end
918
1018
  end
919
1019
 
920
1020
  # Get the backend this parser is using (for introspection)
@@ -952,12 +1052,23 @@ module TreeHaver
952
1052
  # @example
953
1053
  # parser.language = TreeHaver::Language.from_library("/path/to/grammar.so")
954
1054
  def language=(lang)
1055
+ # Check if this is a Citrus language - if so, we need a Citrus parser
1056
+ # This enables automatic backend switching when tree-sitter fails and
1057
+ # falls back to Citrus
1058
+ if lang.is_a?(Backends::Citrus::Language)
1059
+ unless @impl.is_a?(Backends::Citrus::Parser)
1060
+ # Switch to Citrus parser to match the Citrus language
1061
+ @impl = Backends::Citrus::Parser.new
1062
+ @explicit_backend = :citrus
1063
+ end
1064
+ end
1065
+
955
1066
  # Unwrap the language before passing to backend
956
1067
  # Backends receive raw language objects, never TreeHaver wrappers
957
1068
  inner_lang = unwrap_language(lang)
958
1069
  @impl.language = inner_lang
959
1070
  # Return the original (possibly wrapped) language for consistency
960
- lang
1071
+ lang # rubocop:disable Lint/Void (intentional return value)
961
1072
  end
962
1073
 
963
1074
  private
@@ -1028,6 +1139,14 @@ module TreeHaver
1028
1139
  return lang.impl if lang.respond_to?(:impl)
1029
1140
  when :citrus
1030
1141
  return lang.grammar_module if lang.respond_to?(:grammar_module)
1142
+ when :prism
1143
+ return lang # Prism backend expects the Language wrapper
1144
+ when :psych
1145
+ return lang # Psych backend expects the Language wrapper
1146
+ when :commonmarker
1147
+ return lang # Commonmarker backend expects the Language wrapper
1148
+ when :markly
1149
+ return lang # Markly backend expects the Language wrapper
1031
1150
  else
1032
1151
  # Unknown backend (e.g., test backend)
1033
1152
  # Try generic unwrapping methods for flexibility in testing
data/sig/tree_haver.rbs CHANGED
@@ -1,6 +1,9 @@
1
1
  # Type definitions for TreeHaver
2
2
  #
3
- # TreeHaver is a cross-Ruby adapter for the tree-sitter parsing library
3
+ # TreeHaver is a cross-Ruby adapter for code parsing with 10 backends:
4
+ # - Tree-sitter: MRI, Rust, FFI, Java
5
+ # - Native parsers: Prism (Ruby), Psych (YAML), Commonmarker (Markdown), Markly (GFM)
6
+ # - Pure Ruby: Citrus
4
7
 
5
8
  module TreeHaver
6
9
  VERSION: String
@@ -134,6 +137,20 @@ module TreeHaver
134
137
  # Get the end point (row, column)
135
138
  def end_point: () -> Point
136
139
 
140
+ # Position API - consistent across all backends
141
+ # Get 1-based line number where node starts
142
+ def start_line: () -> Integer
143
+
144
+ # Get 1-based line number where node ends
145
+ def end_line: () -> Integer
146
+
147
+ # Get complete position information as hash
148
+ # Returns {start_line:, end_line:, start_column:, end_column:}
149
+ def source_position: () -> Hash[Symbol, Integer]
150
+
151
+ # Get first child node (convenience method)
152
+ def first_child: () -> Node?
153
+
137
154
  # Get the number of child nodes
138
155
  def child_count: () -> Integer
139
156
 
data.tar.gz.sig CHANGED
Binary file
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: tree_haver
3
3
  version: !ruby/object:Gem::Version
4
- version: 3.0.0
4
+ version: 3.1.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Peter H. Boling
@@ -241,9 +241,13 @@ files:
241
241
  - SECURITY.md
242
242
  - lib/tree_haver.rb
243
243
  - lib/tree_haver/backends/citrus.rb
244
+ - lib/tree_haver/backends/commonmarker.rb
244
245
  - lib/tree_haver/backends/ffi.rb
245
246
  - lib/tree_haver/backends/java.rb
247
+ - lib/tree_haver/backends/markly.rb
246
248
  - lib/tree_haver/backends/mri.rb
249
+ - lib/tree_haver/backends/prism.rb
250
+ - lib/tree_haver/backends/psych.rb
247
251
  - lib/tree_haver/backends/rust.rb
248
252
  - lib/tree_haver/citrus_grammar_finder.rb
249
253
  - lib/tree_haver/compat.rb
@@ -262,10 +266,10 @@ licenses:
262
266
  - MIT
263
267
  metadata:
264
268
  homepage_uri: https://tree-haver.galtzo.com/
265
- source_code_uri: https://github.com/kettle-rb/tree_haver/tree/v3.0.0
266
- changelog_uri: https://github.com/kettle-rb/tree_haver/blob/v3.0.0/CHANGELOG.md
269
+ source_code_uri: https://github.com/kettle-rb/tree_haver/tree/v3.1.0
270
+ changelog_uri: https://github.com/kettle-rb/tree_haver/blob/v3.1.0/CHANGELOG.md
267
271
  bug_tracker_uri: https://github.com/kettle-rb/tree_haver/issues
268
- documentation_uri: https://www.rubydoc.info/gems/tree_haver/3.0.0
272
+ documentation_uri: https://www.rubydoc.info/gems/tree_haver/3.1.0
269
273
  funding_uri: https://github.com/sponsors/pboling
270
274
  wiki_uri: https://github.com/kettle-rb/tree_haver/wiki
271
275
  news_uri: https://www.railsbling.com/tags/tree_haver
metadata.gz.sig CHANGED
Binary file