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.
@@ -125,6 +125,9 @@ require "set"
125
125
  # [:citrus_backend]
126
126
  # Citrus gem is available.
127
127
  #
128
+ # [:rbs_backend]
129
+ # RBS gem is available (official RBS parser, MRI only).
130
+ #
128
131
  # ==== Ruby Engine Tags (*_engine)
129
132
  #
130
133
  # [:mri_engine]
@@ -153,6 +156,9 @@ require "set"
153
156
  # [:jsonc_grammar]
154
157
  # tree-sitter-jsonc grammar is available and parsing works.
155
158
  #
159
+ # [:rbs_grammar]
160
+ # tree-sitter-rbs grammar is available and parsing works.
161
+ #
156
162
  # ==== Language Parsing Capability Tags (*_parsing)
157
163
  #
158
164
  # [:toml_parsing]
@@ -161,21 +167,29 @@ require "set"
161
167
  # [:markdown_parsing]
162
168
  # At least one markdown parser (commonmarker OR markly) is available.
163
169
  #
170
+ # [:rbs_parsing]
171
+ # At least one RBS parser (rbs gem OR tree-sitter-rbs) is available.
172
+ #
164
173
  # [:native_parsing]
165
174
  # A native tree-sitter backend and grammar are available.
166
175
  #
167
- # ==== Specific Library Tags
176
+ # ==== Specific Library Tags (*_gem)
168
177
  #
169
- # [:toml_rb]
178
+ # [:toml_rb_gem]
170
179
  # toml-rb gem is available (Citrus backend for TOML).
171
180
  #
181
+ # [:rbs_gem]
182
+ # rbs gem is available (official RBS parser, MRI only).
183
+ # Note: Also available as :rbs_backend for consistency with other parser backends.
184
+ #
172
185
  # === Negated Tags (run when dependency is NOT available)
173
186
  #
174
187
  # All positive tags have negated versions prefixed with `not_`:
175
- # - :not_mri_backend, :not_rust_backend, :not_java_backend, etc.
188
+ # - :not_mri_backend, :not_rust_backend, :not_java_backend, :not_rbs_backend, etc.
176
189
  # - :not_mri_engine, :not_jruby_engine, :not_truffleruby_engine
177
- # - :not_libtree_sitter, :not_bash_grammar, :not_toml_grammar, etc.
178
- # - :not_toml_parsing, :not_markdown_parsing
190
+ # - :not_libtree_sitter, :not_bash_grammar, :not_toml_grammar, :not_rbs_grammar, etc.
191
+ # - :not_toml_parsing, :not_markdown_parsing, :not_rbs_parsing
192
+ # - :not_toml_rb_gem, :not_rbs_gem
179
193
  #
180
194
  # == Backend Conflict Protection
181
195
  #
@@ -200,6 +214,63 @@ module TreeHaver
200
214
  # Dependency detection helpers for conditional test execution
201
215
  module DependencyTags
202
216
  class << self
217
+ # ============================================================
218
+ # Backend Selection via Environment Variables
219
+ # ============================================================
220
+ #
221
+ # Three environment variables control backend availability:
222
+ #
223
+ # TREE_HAVER_BACKEND - Single backend selection (the primary one to use)
224
+ # Values: auto, mri, ffi, rust, java, citrus, prism, psych, commonmarker, markly
225
+ # Default: auto
226
+ #
227
+ # TREE_HAVER_NATIVE_BACKEND - Allow list for native backends
228
+ # Values: all, none, or comma-separated list (mri, ffi, rust, java)
229
+ # Default: all (empty or unset)
230
+ # Example: TREE_HAVER_NATIVE_BACKEND=mri,ffi
231
+ #
232
+ # TREE_HAVER_RUBY_BACKEND - Allow list for pure Ruby backends
233
+ # Values: all, none, or comma-separated list (citrus, prism, psych, commonmarker, markly)
234
+ # Default: all (empty or unset)
235
+ # Example: TREE_HAVER_RUBY_BACKEND=citrus
236
+ #
237
+ # This ensures tests tagged with :mri_backend only run when MRI is allowed, etc.
238
+
239
+ # Get the selected backend from TREE_HAVER_BACKEND
240
+ #
241
+ # @return [Symbol] the selected backend (:auto if not set)
242
+ def selected_backend
243
+ return @selected_backend if defined?(@selected_backend)
244
+ @selected_backend = TreeHaver.backend
245
+ end
246
+
247
+ # Get allowed native backends from TREE_HAVER_NATIVE_BACKEND
248
+ #
249
+ # @return [Array<Symbol>] list of allowed native backends, or [:all] or [:none]
250
+ def allowed_native_backends
251
+ return @allowed_native_backends if defined?(@allowed_native_backends)
252
+ @allowed_native_backends = TreeHaver.allowed_native_backends
253
+ end
254
+
255
+ # Get allowed Ruby backends from TREE_HAVER_RUBY_BACKEND
256
+ #
257
+ # @return [Array<Symbol>] list of allowed Ruby backends, or [:all] or [:none]
258
+ def allowed_ruby_backends
259
+ return @allowed_ruby_backends if defined?(@allowed_ruby_backends)
260
+ @allowed_ruby_backends = TreeHaver.allowed_ruby_backends
261
+ end
262
+
263
+ # Check if a specific backend is allowed based on environment variables
264
+ #
265
+ # Delegates to TreeHaver.backend_allowed? which handles both
266
+ # TREE_HAVER_NATIVE_BACKEND and TREE_HAVER_RUBY_BACKEND.
267
+ #
268
+ # @param backend [Symbol] the backend to check (:mri, :ffi, :citrus, etc.)
269
+ # @return [Boolean] true if the backend is allowed
270
+ def backend_allowed?(backend)
271
+ TreeHaver.backend_allowed?(backend)
272
+ end
273
+
203
274
  # ============================================================
204
275
  # TreeHaver Backend Availability
205
276
  # ============================================================
@@ -220,6 +291,10 @@ module TreeHaver
220
291
  #
221
292
  # @return [Boolean] true if FFI backend is usable
222
293
  def ffi_available?
294
+ # If TREE_HAVER_BACKEND explicitly selects a different native backend,
295
+ # FFI is not available for testing
296
+ return false unless backend_allowed?(:ffi)
297
+
223
298
  # TruffleRuby's FFI doesn't support STRUCT_BY_VALUE return types
224
299
  # (used by ts_tree_root_node, ts_node_child, ts_node_start_point, etc.)
225
300
  return false if truffleruby?
@@ -250,6 +325,10 @@ module TreeHaver
250
325
  def mri_backend_available?
251
326
  return @mri_backend_available if defined?(@mri_backend_available)
252
327
 
328
+ # If TREE_HAVER_BACKEND explicitly selects a different native backend,
329
+ # MRI is not available for testing
330
+ return @mri_backend_available = false unless backend_allowed?(:mri)
331
+
253
332
  # ruby_tree_sitter is a C extension that only works on MRI
254
333
  return @mri_backend_available = false unless mri?
255
334
 
@@ -273,6 +352,10 @@ module TreeHaver
273
352
  #
274
353
  # @return [Boolean] true if FFI backend is usable in isolation
275
354
  def ffi_backend_only_available?
355
+ # If TREE_HAVER_BACKEND explicitly selects a different native backend,
356
+ # FFI is not available for testing
357
+ return false unless backend_allowed?(:ffi)
358
+
276
359
  # TruffleRuby's FFI doesn't support STRUCT_BY_VALUE return types
277
360
  return false if truffleruby?
278
361
 
@@ -307,6 +390,10 @@ module TreeHaver
307
390
  def mri_backend_only_available?
308
391
  return @mri_backend_only_available if defined?(@mri_backend_only_available)
309
392
 
393
+ # If TREE_HAVER_BACKEND explicitly selects a different native backend,
394
+ # MRI is not available for testing
395
+ return @mri_backend_only_available = false unless backend_allowed?(:mri)
396
+
310
397
  # ruby_tree_sitter is a C extension that only works on MRI
311
398
  return @mri_backend_only_available = false unless mri?
312
399
 
@@ -327,6 +414,10 @@ module TreeHaver
327
414
  def rust_backend_available?
328
415
  return @rust_backend_available if defined?(@rust_backend_available)
329
416
 
417
+ # If TREE_HAVER_BACKEND explicitly selects a different native backend,
418
+ # Rust is not available for testing
419
+ return @rust_backend_available = false unless backend_allowed?(:rust)
420
+
330
421
  # tree_stump uses magnus which requires MRI's C API
331
422
  return @rust_backend_available = false unless mri?
332
423
 
@@ -338,12 +429,58 @@ module TreeHaver
338
429
  end
339
430
  end
340
431
 
341
- # Check if Java backend is available (requires JRuby + java-tree-sitter / jtreesitter)
432
+ # Check if Java backend is available AND can actually load grammars
433
+ #
434
+ # The Java backend requires:
435
+ # 1. Running on JRuby
436
+ # 2. java-tree-sitter (jtreesitter) JAR available
437
+ # 3. Grammars built for java-tree-sitter's Foreign Function Memory API
342
438
  #
343
- # @return [Boolean] true if Java backend is available
439
+ # Note: Standard `.so` files built for MRI's tree-sitter C bindings are NOT
440
+ # compatible with java-tree-sitter. You need grammar JARs from Maven Central
441
+ # or libraries specifically built for Java FFM API.
442
+ #
443
+ # @return [Boolean] true if Java backend is available and can load grammars
344
444
  def java_backend_available?
345
445
  return @java_backend_available if defined?(@java_backend_available)
346
- @java_backend_available = jruby? && TreeHaver::Backends::Java.available?
446
+
447
+ # If TREE_HAVER_BACKEND explicitly selects a different native backend,
448
+ # Java is not available for testing
449
+ return @java_backend_available = false unless backend_allowed?(:java)
450
+
451
+ # Must be on JRuby and have java-tree-sitter classes available
452
+ return @java_backend_available = false unless jruby?
453
+ return @java_backend_available = false unless TreeHaver::Backends::Java.available?
454
+
455
+ # Try to actually load a grammar to verify the backend works end-to-end
456
+ # This catches the case where Java classes load but grammars fail
457
+ # (e.g., when using MRI-built .so files on JRuby)
458
+ @java_backend_available = java_grammar_loadable?
459
+ end
460
+
461
+ # Check if Java backend can actually load a grammar
462
+ #
463
+ # This does a live test by trying to load a TOML grammar via the Java backend.
464
+ # It catches the common failure case where java-tree-sitter is available but
465
+ # the grammar .so files are incompatible (built for MRI, not java-tree-sitter).
466
+ #
467
+ # @return [Boolean] true if a grammar can be loaded via Java backend
468
+ # @api private
469
+ def java_grammar_loadable?
470
+ return false unless jruby?
471
+
472
+ path = find_toml_grammar_path
473
+ return false unless path && File.exist?(path)
474
+
475
+ TreeHaver.with_backend(:java) do
476
+ TreeHaver::Backends::Java::Language.from_library(path, symbol: "tree_sitter_toml")
477
+ end
478
+ true
479
+ rescue TreeHaver::NotAvailable, TreeHaver::Error, LoadError
480
+ false
481
+ rescue StandardError
482
+ # Catch any other Java-related errors
483
+ false
347
484
  end
348
485
 
349
486
  # Check if libtree-sitter runtime library is loadable
@@ -496,12 +633,53 @@ module TreeHaver
496
633
  @tree_sitter_jsonc_available = grammar_works?(:jsonc, '{"key": "value" /* comment */}')
497
634
  end
498
635
 
636
+ # Check if tree-sitter-rbs grammar is available and working
637
+ #
638
+ # @return [Boolean] true if rbs grammar works
639
+ def tree_sitter_rbs_available?
640
+ return @tree_sitter_rbs_available if defined?(@tree_sitter_rbs_available)
641
+ @tree_sitter_rbs_available = grammar_works?(:rbs, "class Foo end")
642
+ end
643
+
644
+ # Check if the RBS gem is available and functional
645
+ #
646
+ # The RBS gem only works on MRI Ruby (C extension).
647
+ #
648
+ # @return [Boolean] true if rbs gem is available and can parse RBS
649
+ def rbs_gem_available?
650
+ return @rbs_gem_available if defined?(@rbs_gem_available)
651
+ @rbs_gem_available = begin
652
+ require "rbs"
653
+ # Verify it can actually parse - just requiring isn't enough
654
+ buffer = ::RBS::Buffer.new(name: "test.rbs", content: "class Foo end")
655
+ ::RBS::Parser.parse_signature(buffer)
656
+ true
657
+ rescue LoadError
658
+ false
659
+ rescue StandardError
660
+ false
661
+ end
662
+ end
663
+
664
+ # Alias for rbs_gem_available? - for consistency with other backends
665
+ # Use :rbs_backend tag in specs for consistency with :prism_backend, :psych_backend, etc.
666
+ #
667
+ # @return [Boolean] true if rbs gem is available
668
+ alias_method :rbs_backend_available?, :rbs_gem_available?
669
+
670
+ # Check if at least one RBS backend is available
671
+ #
672
+ # @return [Boolean] true if any RBS backend works
673
+ def any_rbs_backend_available?
674
+ rbs_gem_available? || tree_sitter_rbs_available?
675
+ end
676
+
499
677
  # Check if toml-rb gem is available and functional (Citrus backend for TOML)
500
678
  #
501
679
  # @return [Boolean] true if toml-rb gem is available and can parse TOML
502
- def toml_rb_available?
503
- return @toml_rb_available if defined?(@toml_rb_available)
504
- @toml_rb_available = begin
680
+ def toml_rb_gem_available?
681
+ return @toml_rb_gem_available if defined?(@toml_rb_gem_available)
682
+ @toml_rb_gem_available = begin
505
683
  require "toml-rb"
506
684
  # Verify it can actually parse - just requiring isn't enough
507
685
  TomlRB.parse('key = "value"')
@@ -517,7 +695,7 @@ module TreeHaver
517
695
  #
518
696
  # @return [Boolean] true if any TOML backend works
519
697
  def any_toml_backend_available?
520
- tree_sitter_toml_available? || toml_rb_available?
698
+ tree_sitter_toml_available? || toml_rb_gem_available?
521
699
  end
522
700
 
523
701
  # Check if at least one markdown backend is available
@@ -545,6 +723,10 @@ module TreeHaver
545
723
  # @return [Hash{Symbol => Boolean}] map of dependency name to availability
546
724
  def summary
547
725
  {
726
+ # Backend selection from environment variables
727
+ selected_backend: selected_backend,
728
+ allowed_native_backends: allowed_native_backends,
729
+ allowed_ruby_backends: allowed_ruby_backends,
548
730
  # TreeHaver backends (*_backend)
549
731
  ffi_backend: ffi_available?,
550
732
  mri_backend: mri_backend_available?,
@@ -555,6 +737,7 @@ module TreeHaver
555
737
  commonmarker_backend: commonmarker_available?,
556
738
  markly_backend: markly_available?,
557
739
  citrus_backend: citrus_available?,
740
+ rbs_backend: rbs_backend_available?,
558
741
  # Ruby engines (*_engine)
559
742
  ruby_engine: RUBY_ENGINE,
560
743
  mri_engine: mri?,
@@ -566,12 +749,15 @@ module TreeHaver
566
749
  toml_grammar: tree_sitter_toml_available?,
567
750
  json_grammar: tree_sitter_json_available?,
568
751
  jsonc_grammar: tree_sitter_jsonc_available?,
752
+ rbs_grammar: tree_sitter_rbs_available?,
569
753
  any_native_grammar: any_native_grammar_available?,
570
754
  # Language parsing capabilities (*_parsing)
571
755
  toml_parsing: any_toml_backend_available?,
572
756
  markdown_parsing: any_markdown_backend_available?,
573
- # Specific libraries
574
- toml_rb: toml_rb_available?,
757
+ rbs_parsing: any_rbs_backend_available?,
758
+ # Specific libraries (*_gem)
759
+ toml_rb_gem: toml_rb_gem_available?,
760
+ rbs_gem: rbs_gem_available?,
575
761
  }
576
762
  end
577
763
 
@@ -598,10 +784,24 @@ module TreeHaver
598
784
  # @return [void]
599
785
  def reset!
600
786
  instance_variables.each do |ivar|
787
+ # Don't reset ENV-based values
788
+ next if %i[@selected_backend @allowed_native_backends @allowed_ruby_backends].include?(ivar)
601
789
  remove_instance_variable(ivar) if ivar.to_s.end_with?("_available")
602
790
  end
603
791
  end
604
792
 
793
+ # Reset selected backend caches (useful for testing with different ENV values)
794
+ #
795
+ # Also resets TreeHaver's backend caches.
796
+ #
797
+ # @return [void]
798
+ def reset_selected_backend!
799
+ remove_instance_variable(:@selected_backend) if defined?(@selected_backend)
800
+ remove_instance_variable(:@allowed_native_backends) if defined?(@allowed_native_backends)
801
+ remove_instance_variable(:@allowed_ruby_backends) if defined?(@allowed_ruby_backends)
802
+ TreeHaver.reset_backend!
803
+ end
804
+
605
805
  private
606
806
 
607
807
  # Generic helper to check if a grammar works by parsing test source
@@ -655,6 +855,24 @@ RSpec.configure do |config|
655
855
  # Define exclusion filters for optional dependencies
656
856
  # Tests tagged with these will be skipped when the dependency is not available
657
857
 
858
+ # ============================================================
859
+ # Backend Protection for Test Suites
860
+ # ============================================================
861
+ #
862
+ # TreeHaver protects against backend conflicts by default (e.g., FFI cannot
863
+ # be used after MRI has been loaded because it would cause a segfault).
864
+ # This protection remains enabled in test suites to prevent crashes.
865
+ #
866
+ # If you need to test multiple incompatible backends in the same process
867
+ # (accepting the risk of segfaults), you can disable protection:
868
+ # TREE_HAVER_BACKEND_PROTECT=false bundle exec rspec
869
+ #
870
+ # Note: The recommended approach is to run separate test processes for
871
+ # incompatible backends using RSpec tags or separate CI jobs.
872
+ if ENV["TREE_HAVER_BACKEND_PROTECT"] == "false"
873
+ TreeHaver.backend_protect = false
874
+ end
875
+
658
876
  config.before(:suite) do
659
877
  # Print dependency summary if TREE_HAVER_DEBUG is set
660
878
  if ENV["TREE_HAVER_DEBUG"]
@@ -759,6 +977,7 @@ RSpec.configure do |config|
759
977
  commonmarker: :commonmarker_available?,
760
978
  markly: :markly_available?,
761
979
  citrus: :citrus_available?,
980
+ rbs: :rbs_backend_available?,
762
981
  }
763
982
 
764
983
  # Map of backend symbols to their RSpec tag names
@@ -772,6 +991,7 @@ RSpec.configure do |config|
772
991
  commonmarker: :commonmarker_backend,
773
992
  markly: :markly_backend,
774
993
  citrus: :citrus_backend,
994
+ rbs: :rbs_backend,
775
995
  }
776
996
 
777
997
  # Determine which backends should NOT have availability checked
@@ -793,10 +1013,7 @@ RSpec.configure do |config|
793
1013
  env_backend = ENV["TREE_HAVER_BACKEND"]
794
1014
  if env_backend && !env_backend.empty? && env_backend != "auto"
795
1015
  backend_sym = env_backend.to_sym
796
- blockers = TreeHaver::Backends::BLOCKED_BY[backend_sym]
797
- if blockers
798
- blockers.each { |blocker| blocked_backends << blocker }
799
- end
1016
+ TreeHaver::Backends::BLOCKED_BY[backend_sym]&.each { |blocker| blocked_backends << blocker }
800
1017
  end
801
1018
 
802
1019
  # Check which *_backend_only tags are being run and block their conflicting backends
@@ -862,7 +1079,7 @@ RSpec.configure do |config|
862
1079
  # Tree-Sitter Grammar Tags
863
1080
  # ============================================================
864
1081
  # Tags: *_grammar - require a specific tree-sitter grammar (.so file)
865
- # :bash_grammar, :toml_grammar, :json_grammar, :jsonc_grammar
1082
+ # :bash_grammar, :toml_grammar, :json_grammar, :jsonc_grammar, :rbs_grammar
866
1083
  #
867
1084
  # Also: :libtree_sitter - requires the libtree-sitter runtime library
868
1085
  #
@@ -879,6 +1096,7 @@ RSpec.configure do |config|
879
1096
  config.filter_run_excluding(toml_grammar: true) unless deps.tree_sitter_toml_available?
880
1097
  config.filter_run_excluding(json_grammar: true) unless deps.tree_sitter_json_available?
881
1098
  config.filter_run_excluding(jsonc_grammar: true) unless deps.tree_sitter_jsonc_available?
1099
+ config.filter_run_excluding(rbs_grammar: true) unless deps.tree_sitter_rbs_available?
882
1100
  end
883
1101
 
884
1102
  # ============================================================
@@ -887,6 +1105,7 @@ RSpec.configure do |config|
887
1105
  # Tags: *_parsing - require ANY parser for a language (any backend that can parse it)
888
1106
  # :toml_parsing - any TOML parser (tree-sitter-toml OR toml-rb/Citrus)
889
1107
  # :markdown_parsing - any Markdown parser (commonmarker OR markly)
1108
+ # :rbs_parsing - any RBS parser (rbs gem OR tree-sitter-rbs)
890
1109
  # :native_parsing - any native tree-sitter backend + grammar
891
1110
  #
892
1111
  # NOTE: any_toml_backend_available? calls tree_sitter_toml_available? which
@@ -895,16 +1114,20 @@ RSpec.configure do |config|
895
1114
  unless isolated_test_mode
896
1115
  config.filter_run_excluding(toml_parsing: true) unless deps.any_toml_backend_available?
897
1116
  config.filter_run_excluding(markdown_parsing: true) unless deps.any_markdown_backend_available?
1117
+ config.filter_run_excluding(rbs_parsing: true) unless deps.any_rbs_backend_available?
898
1118
  config.filter_run_excluding(native_parsing: true) unless deps.any_native_grammar_available?
899
1119
  end
900
1120
 
901
1121
  # ============================================================
902
1122
  # Specific Library Tags
903
1123
  # ============================================================
904
- # Tags for specific gems/libraries (not backends, but dependencies)
905
- # :toml_rb - the toml-rb gem (Citrus-based TOML parser)
1124
+ # Tags for specific gems/libraries (*_gem suffix)
1125
+ # :toml_rb_gem - the toml-rb gem (Citrus-based TOML parser)
1126
+ # :rbs_gem - the rbs gem (official RBS parser, MRI only)
1127
+ # Note: :rbs_backend is also available as an alias for :rbs_gem
906
1128
 
907
- config.filter_run_excluding(toml_rb: true) unless deps.toml_rb_available?
1129
+ config.filter_run_excluding(toml_rb_gem: true) unless deps.toml_rb_gem_available?
1130
+ config.filter_run_excluding(rbs_gem: true) unless deps.rbs_gem_available?
908
1131
 
909
1132
  # ============================================================
910
1133
  # Negated Tags (run when dependency is NOT available)
@@ -937,12 +1160,15 @@ RSpec.configure do |config|
937
1160
  config.filter_run_excluding(not_toml_grammar: true) if deps.tree_sitter_toml_available?
938
1161
  config.filter_run_excluding(not_json_grammar: true) if deps.tree_sitter_json_available?
939
1162
  config.filter_run_excluding(not_jsonc_grammar: true) if deps.tree_sitter_jsonc_available?
1163
+ config.filter_run_excluding(not_rbs_grammar: true) if deps.tree_sitter_rbs_available?
940
1164
 
941
1165
  # Language parsing capabilities
942
1166
  config.filter_run_excluding(not_toml_parsing: true) if deps.any_toml_backend_available?
943
1167
  config.filter_run_excluding(not_markdown_parsing: true) if deps.any_markdown_backend_available?
1168
+ config.filter_run_excluding(not_rbs_parsing: true) if deps.any_rbs_backend_available?
944
1169
  end
945
1170
 
946
1171
  # Specific libraries
947
- config.filter_run_excluding(not_toml_rb: true) if deps.toml_rb_available?
1172
+ config.filter_run_excluding(not_toml_rb_gem: true) if deps.toml_rb_gem_available?
1173
+ config.filter_run_excluding(not_rbs_gem: true) if deps.rbs_gem_available?
948
1174
  end
@@ -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.2.2"
13
+ VERSION = "3.2.4"
14
14
  end
15
15
 
16
16
  # Traditional location for VERSION constant