tree_haver 3.2.0 → 3.2.2

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.
@@ -1,5 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require "set"
4
+
3
5
  # TreeHaver RSpec Dependency Tags
4
6
  #
5
7
  # This module provides dependency detection helpers for conditional test execution
@@ -89,9 +91,19 @@
89
91
  # unavailable after MRI backend is used (due to libtree-sitter runtime conflicts).
90
92
  # Legacy alias: :ffi
91
93
  #
94
+ # [:ffi_backend_only]
95
+ # ISOLATED FFI tag - use when running FFI tests in isolation (e.g., ffi_specs task).
96
+ # Does NOT trigger mri_backend_available? check, preventing MRI from being loaded.
97
+ # Use this tag for tests that must run before MRI backend is loaded.
98
+ #
92
99
  # [:mri_backend]
93
100
  # ruby_tree_sitter gem is available.
94
101
  #
102
+ # [:mri_backend_only]
103
+ # ISOLATED MRI tag - use when running MRI tests and FFI must not be checked.
104
+ # Does NOT trigger ffi_available? check, preventing FFI availability detection.
105
+ # Use this tag for tests that should run without FFI interference.
106
+ #
95
107
  # [:rust_backend]
96
108
  # tree_stump gem is available.
97
109
  #
@@ -242,7 +254,8 @@ module TreeHaver
242
254
  return @mri_backend_available = false unless mri?
243
255
 
244
256
  @mri_backend_available = begin
245
- require "ruby_tree_sitter"
257
+ # Note: gem is ruby_tree_sitter but requires tree_sitter
258
+ require "tree_sitter"
246
259
  # Record that MRI backend is now loaded - this is critical for
247
260
  # conflict detection with FFI backend
248
261
  TreeHaver.record_backend_usage(:mri)
@@ -252,6 +265,60 @@ module TreeHaver
252
265
  end
253
266
  end
254
267
 
268
+ # Check if FFI backend is available WITHOUT loading MRI first
269
+ #
270
+ # This is used for the :ffi_backend_only tag which runs FFI tests
271
+ # in isolation before MRI can be loaded. Unlike ffi_available?,
272
+ # this method does NOT check mri_backend_available?.
273
+ #
274
+ # @return [Boolean] true if FFI backend is usable in isolation
275
+ def ffi_backend_only_available?
276
+ # TruffleRuby's FFI doesn't support STRUCT_BY_VALUE return types
277
+ return false if truffleruby?
278
+
279
+ # Check if FFI gem is available without loading tree_sitter
280
+ begin
281
+ require "ffi"
282
+ rescue LoadError
283
+ return false
284
+ end
285
+
286
+ # Try to actually use the FFI backend
287
+ path = find_toml_grammar_path
288
+ return false unless path && File.exist?(path)
289
+
290
+ TreeHaver.with_backend(:ffi) do
291
+ TreeHaver::Language.from_library(path, symbol: "tree_sitter_toml")
292
+ end
293
+ true
294
+ rescue TreeHaver::BackendConflict, TreeHaver::NotAvailable, LoadError
295
+ false
296
+ rescue StandardError
297
+ # Catch any other FFI-related errors
298
+ false
299
+ end
300
+
301
+ # Check if MRI backend is available WITHOUT checking FFI availability
302
+ #
303
+ # This is used for the :mri_backend_only tag which runs MRI tests
304
+ # without triggering any FFI availability checks.
305
+ #
306
+ # @return [Boolean] true if MRI backend is usable
307
+ def mri_backend_only_available?
308
+ return @mri_backend_only_available if defined?(@mri_backend_only_available)
309
+
310
+ # ruby_tree_sitter is a C extension that only works on MRI
311
+ return @mri_backend_only_available = false unless mri?
312
+
313
+ @mri_backend_only_available = begin
314
+ require "tree_sitter"
315
+ TreeHaver.record_backend_usage(:mri)
316
+ true
317
+ rescue LoadError
318
+ false
319
+ end
320
+ end
321
+
255
322
  # Check if tree_stump gem is available (Rust backend)
256
323
  #
257
324
  # The Rust backend only works on MRI Ruby (magnus uses MRI's C API).
@@ -311,9 +378,18 @@ module TreeHaver
311
378
  # Grammar paths should be configured via TREE_SITTER_TOML_PATH environment variable.
312
379
  # This keeps configuration explicit and avoids magic path guessing.
313
380
  #
314
- # @return [String, nil] path from environment variable, or nil if not set
381
+ # @return [String, nil] path to TOML grammar library, or nil if not found
315
382
  def find_toml_grammar_path
316
- ENV["TREE_SITTER_TOML_PATH"]
383
+ # First check environment variable
384
+ env_path = ENV["TREE_SITTER_TOML_PATH"]
385
+ return env_path if env_path && File.exist?(env_path)
386
+
387
+ # Use GrammarFinder to search standard paths
388
+ finder = TreeHaver::GrammarFinder.new(:toml, validate: false)
389
+ finder.find_library_path
390
+ rescue StandardError
391
+ # GrammarFinder might not be available or might fail
392
+ nil
317
393
  end
318
394
 
319
395
  # Check if commonmarker gem is available
@@ -587,14 +663,23 @@ RSpec.configure do |config|
587
663
  puts " #{var}: #{value.inspect}"
588
664
  end
589
665
 
590
- puts "\n=== TreeHaver Test Dependencies ==="
591
- deps.summary.each do |dep, available|
592
- status = case available
593
- when true then "✓ available"
594
- when false then "✗ not available"
595
- else available.to_s
666
+ # Only print full dependency summary if we're not running with blocked backends
667
+ # The summary calls grammar availability checks which would load blocked backends
668
+ current_blocked = TreeHaver::RSpec::DependencyTags.instance_variable_get(:@blocked_backends) || Set.new
669
+ if current_blocked.any?
670
+ puts "\n=== TreeHaver Test Dependencies (limited - running isolated tests) ==="
671
+ puts " blocked_backends: #{current_blocked.to_a.inspect}"
672
+ puts " (Skipping full summary to avoid loading blocked backends)"
673
+ else
674
+ puts "\n=== TreeHaver Test Dependencies ==="
675
+ deps.summary.each do |dep, available|
676
+ status = case available
677
+ when true then "✓ available"
678
+ when false then "✗ not available"
679
+ else available.to_s
680
+ end
681
+ puts " #{dep}: #{status}"
596
682
  end
597
- puts " #{dep}: #{status}"
598
683
  end
599
684
  puts "===================================\n"
600
685
  end
@@ -609,21 +694,159 @@ RSpec.configure do |config|
609
694
  # :ffi_backend, :mri_backend, :rust_backend, :java_backend
610
695
  # Pure-Ruby backends:
611
696
  # :prism_backend, :psych_backend, :commonmarker_backend, :markly_backend, :citrus_backend
697
+ #
698
+ # Isolated backend tags (for running tests without loading conflicting backends):
699
+ # :ffi_backend_only - runs FFI tests without loading MRI backend
700
+ # :mri_backend_only - runs MRI tests without checking FFI availability
701
+
702
+ # FFI backend exclusion:
703
+ # If MRI has already been used, FFI is blocked and will never be available.
704
+ # In this case, exclude FFI tests tagged with :ffi_backend entirely rather than
705
+ # showing them as pending.
706
+ #
707
+ # NOTE: We do NOT exclude :ffi_backend_only here because the Rakefile uses
708
+ # `--tag ~ffi_backend_only` for the remaining_specs task. RSpec interprets
709
+ # `--tag ~X` as an include filter with key "~X", which conflicts with
710
+ # filter_run_excluding. Instead, :ffi_backend_only tests will be skipped
711
+ # via the before(:each) hook below when FFI is not available.
712
+ if TreeHaver.backends_used.include?(:mri)
713
+ config.filter_run_excluding(ffi_backend: true)
714
+ end
612
715
 
613
716
  # FFI availability is checked dynamically per-test (not at load time)
614
717
  # because FFI becomes unavailable after MRI backend is used.
615
- config.before(:each, :ffi_backend) do
718
+ # When running with :ffi_backend_only tag, this hook defers to the isolated check.
719
+ config.before(:each, :ffi_backend) do |example|
720
+ # If also tagged with :ffi_backend_only, let that hook handle the check
721
+ next if example.metadata[:ffi_backend_only]
722
+
616
723
  skip "FFI backend not available (MRI backend may have been used)" unless deps.ffi_available?
617
724
  end
618
725
 
619
- config.filter_run_excluding(mri_backend: true) unless deps.mri_backend_available?
620
- config.filter_run_excluding(rust_backend: true) unless deps.rust_backend_available?
621
- config.filter_run_excluding(java_backend: true) unless deps.java_backend_available?
622
- config.filter_run_excluding(prism_backend: true) unless deps.prism_available?
623
- config.filter_run_excluding(psych_backend: true) unless deps.psych_available?
624
- config.filter_run_excluding(commonmarker_backend: true) unless deps.commonmarker_available?
625
- config.filter_run_excluding(markly_backend: true) unless deps.markly_available?
626
- config.filter_run_excluding(citrus_backend: true) unless deps.citrus_available?
726
+ # ISOLATED FFI TAG: Checked dynamically but does NOT trigger mri_backend_available?
727
+ # Use this tag for tests that must run before MRI is loaded (e.g., in ffi_specs task)
728
+ config.before(:each, :ffi_backend_only) do
729
+ skip "FFI backend not available (isolated check)" unless deps.ffi_backend_only_available?
730
+ end
731
+
732
+ # ISOLATED MRI TAG: Checked dynamically but does NOT trigger ffi_available?
733
+ # Use this tag for tests that should run without FFI interference
734
+ config.before(:each, :mri_backend_only) do
735
+ skip "MRI backend not available (isolated check)" unless deps.mri_backend_only_available?
736
+ end
737
+
738
+ # ============================================================
739
+ # Dynamic Backend Exclusions (using BLOCKED_BY)
740
+ # ============================================================
741
+ # When running with *_backend_only tags, we skip availability checks for
742
+ # backends that would block the isolated backend. This prevents loading
743
+ # conflicting backends before isolated tests run.
744
+ #
745
+ # For example, when running with --tag ffi_backend_only:
746
+ # - FFI is blocked by [:mri] (from BLOCKED_BY)
747
+ # - So we skip mri_backend_available? to prevent loading MRI
748
+ #
749
+ # This is dynamic based on TreeHaver::Backends::BLOCKED_BY configuration.
750
+
751
+ # Map of backend symbols to their availability check methods
752
+ backend_availability_methods = {
753
+ mri: :mri_backend_available?,
754
+ rust: :rust_backend_available?,
755
+ ffi: :ffi_available?,
756
+ java: :java_backend_available?,
757
+ prism: :prism_available?,
758
+ psych: :psych_available?,
759
+ commonmarker: :commonmarker_available?,
760
+ markly: :markly_available?,
761
+ citrus: :citrus_available?,
762
+ }
763
+
764
+ # Map of backend symbols to their RSpec tag names
765
+ backend_tags = {
766
+ mri: :mri_backend,
767
+ rust: :rust_backend,
768
+ ffi: :ffi_backend,
769
+ java: :java_backend,
770
+ prism: :prism_backend,
771
+ psych: :psych_backend,
772
+ commonmarker: :commonmarker_backend,
773
+ markly: :markly_backend,
774
+ citrus: :citrus_backend,
775
+ }
776
+
777
+ # Determine which backends should NOT have availability checked
778
+ # based on which *_backend_only tag is being run OR which backend is
779
+ # explicitly selected via TREE_HAVER_BACKEND environment variable.
780
+ blocked_backends = Set.new
781
+
782
+ # Track whether we're in isolated test mode (running *_backend_only tags).
783
+ # This is different from just having TREE_HAVER_BACKEND set.
784
+ # In isolated mode, we skip ALL grammar checks because they might trigger
785
+ # backend loading via TreeHaver.parser_for's auto-detection.
786
+ # When just TREE_HAVER_BACKEND is set, grammar checks are fine because
787
+ # parser_for will use the selected backend, not auto-detect.
788
+ isolated_test_mode = false
789
+
790
+ # First, check if TREE_HAVER_BACKEND explicitly selects a backend.
791
+ # If so, block all backends that would conflict with it.
792
+ # This prevents loading MRI when TREE_HAVER_BACKEND=ffi, for example.
793
+ env_backend = ENV["TREE_HAVER_BACKEND"]
794
+ if env_backend && !env_backend.empty? && env_backend != "auto"
795
+ 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
800
+ end
801
+
802
+ # Check which *_backend_only tags are being run and block their conflicting backends
803
+ # config.inclusion_filter contains tags passed via --tag on command line
804
+ inclusion_rules = config.inclusion_filter.rules
805
+
806
+ # If filter.rules is empty, check ARGV directly for --tag options
807
+ # This handles the case where RSpec hasn't processed filters yet during configuration
808
+ if inclusion_rules.empty?
809
+ ARGV.each_with_index do |arg, i|
810
+ if arg == "--tag" && ARGV[i + 1]
811
+ tag_str = ARGV[i + 1]
812
+ # Skip exclusion tags (prefixed with ~) - they are NOT inclusion filters
813
+ next if tag_str.start_with?("~")
814
+ tag_value = tag_str.to_sym
815
+ inclusion_rules[tag_value] = true
816
+ elsif arg.start_with?("--tag=")
817
+ tag_str = arg.sub("--tag=", "")
818
+ # Skip exclusion tags (prefixed with ~) - they are NOT inclusion filters
819
+ next if tag_str.start_with?("~")
820
+ tag_value = tag_str.to_sym
821
+ inclusion_rules[tag_value] = true
822
+ end
823
+ end
824
+ end
825
+
826
+ TreeHaver::Backends::BLOCKED_BY.each do |backend, blockers|
827
+ # Check if we're running this backend's isolated tests
828
+ isolated_tag = :"#{backend}_backend_only"
829
+ if inclusion_rules[isolated_tag]
830
+ isolated_test_mode = true
831
+ # Add all backends that would block this one
832
+ blockers.each { |blocker| blocked_backends << blocker }
833
+ end
834
+ end
835
+
836
+ # Store blocked_backends in a module variable so before(:suite) can access it
837
+ TreeHaver::RSpec::DependencyTags.instance_variable_set(:@blocked_backends, blocked_backends)
838
+ TreeHaver::RSpec::DependencyTags.instance_variable_set(:@isolated_test_mode, isolated_test_mode)
839
+
840
+ # Now configure exclusions, skipping availability checks for blocked backends
841
+ backend_tags.each do |backend, tag|
842
+ next if blocked_backends.include?(backend)
843
+
844
+ # FFI is handled specially with before(:each) hook above
845
+ next if backend == :ffi
846
+
847
+ availability_method = backend_availability_methods[backend]
848
+ config.filter_run_excluding(tag => true) unless deps.public_send(availability_method)
849
+ end
627
850
 
628
851
  # ============================================================
629
852
  # Ruby Engine Tags
@@ -642,12 +865,21 @@ RSpec.configure do |config|
642
865
  # :bash_grammar, :toml_grammar, :json_grammar, :jsonc_grammar
643
866
  #
644
867
  # Also: :libtree_sitter - requires the libtree-sitter runtime library
645
-
646
- config.filter_run_excluding(libtree_sitter: true) unless deps.libtree_sitter_available?
647
- config.filter_run_excluding(bash_grammar: true) unless deps.tree_sitter_bash_available?
648
- config.filter_run_excluding(toml_grammar: true) unless deps.tree_sitter_toml_available?
649
- config.filter_run_excluding(json_grammar: true) unless deps.tree_sitter_json_available?
650
- config.filter_run_excluding(jsonc_grammar: true) unless deps.tree_sitter_jsonc_available?
868
+ #
869
+ # NOTE: When running with *_backend_only tags, we skip these checks to avoid
870
+ # loading blocked backends. The grammar checks use TreeHaver.parser_for which
871
+ # would load the default backend (MRI) and block FFI.
872
+
873
+ # Skip grammar availability checks only when in isolated test mode.
874
+ # When TREE_HAVER_BACKEND is explicitly set (but not using *_backend_only tags),
875
+ # grammar checks are fine because TreeHaver.parser_for respects the env var.
876
+ unless isolated_test_mode
877
+ config.filter_run_excluding(libtree_sitter: true) unless deps.libtree_sitter_available?
878
+ config.filter_run_excluding(bash_grammar: true) unless deps.tree_sitter_bash_available?
879
+ config.filter_run_excluding(toml_grammar: true) unless deps.tree_sitter_toml_available?
880
+ config.filter_run_excluding(json_grammar: true) unless deps.tree_sitter_json_available?
881
+ config.filter_run_excluding(jsonc_grammar: true) unless deps.tree_sitter_jsonc_available?
882
+ end
651
883
 
652
884
  # ============================================================
653
885
  # Language Parsing Capability Tags
@@ -656,10 +888,15 @@ RSpec.configure do |config|
656
888
  # :toml_parsing - any TOML parser (tree-sitter-toml OR toml-rb/Citrus)
657
889
  # :markdown_parsing - any Markdown parser (commonmarker OR markly)
658
890
  # :native_parsing - any native tree-sitter backend + grammar
891
+ #
892
+ # NOTE: any_toml_backend_available? calls tree_sitter_toml_available? which
893
+ # triggers grammar_works? and loads MRI. Skip when running isolated tests.
659
894
 
660
- config.filter_run_excluding(toml_parsing: true) unless deps.any_toml_backend_available?
661
- config.filter_run_excluding(markdown_parsing: true) unless deps.any_markdown_backend_available?
662
- config.filter_run_excluding(native_parsing: true) unless deps.any_native_grammar_available?
895
+ unless isolated_test_mode
896
+ config.filter_run_excluding(toml_parsing: true) unless deps.any_toml_backend_available?
897
+ config.filter_run_excluding(markdown_parsing: true) unless deps.any_markdown_backend_available?
898
+ config.filter_run_excluding(native_parsing: true) unless deps.any_native_grammar_available?
899
+ end
663
900
 
664
901
  # ============================================================
665
902
  # Specific Library Tags
@@ -676,31 +913,35 @@ RSpec.configure do |config|
676
913
 
677
914
  # NOTE: :not_ffi_backend tag is not provided because FFI availability is dynamic.
678
915
 
679
- # TreeHaver backends
680
- config.filter_run_excluding(not_mri_backend: true) if deps.mri_backend_available?
681
- config.filter_run_excluding(not_rust_backend: true) if deps.rust_backend_available?
682
- config.filter_run_excluding(not_java_backend: true) if deps.java_backend_available?
683
- config.filter_run_excluding(not_prism_backend: true) if deps.prism_available?
684
- config.filter_run_excluding(not_psych_backend: true) if deps.psych_available?
685
- config.filter_run_excluding(not_commonmarker_backend: true) if deps.commonmarker_available?
686
- config.filter_run_excluding(not_markly_backend: true) if deps.markly_available?
687
- config.filter_run_excluding(not_citrus_backend: true) if deps.citrus_available?
916
+ # TreeHaver backends - handled dynamically to respect blocked backends
917
+ backend_tags.each do |backend, tag|
918
+ next if blocked_backends.include?(backend)
919
+
920
+ # FFI is handled specially (availability is always dynamic)
921
+ next if backend == :ffi
922
+
923
+ negated_tag = :"not_#{tag}"
924
+ availability_method = backend_availability_methods[backend]
925
+ config.filter_run_excluding(negated_tag => true) if deps.public_send(availability_method)
926
+ end
688
927
 
689
928
  # Ruby engines
690
929
  config.filter_run_excluding(not_mri_engine: true) if deps.mri?
691
930
  config.filter_run_excluding(not_jruby_engine: true) if deps.jruby?
692
931
  config.filter_run_excluding(not_truffleruby_engine: true) if deps.truffleruby?
693
932
 
694
- # Tree-sitter grammars
695
- config.filter_run_excluding(not_libtree_sitter: true) if deps.libtree_sitter_available?
696
- config.filter_run_excluding(not_bash_grammar: true) if deps.tree_sitter_bash_available?
697
- config.filter_run_excluding(not_toml_grammar: true) if deps.tree_sitter_toml_available?
698
- config.filter_run_excluding(not_json_grammar: true) if deps.tree_sitter_json_available?
699
- config.filter_run_excluding(not_jsonc_grammar: true) if deps.tree_sitter_jsonc_available?
700
-
701
- # Language parsing capabilities
702
- config.filter_run_excluding(not_toml_parsing: true) if deps.any_toml_backend_available?
703
- config.filter_run_excluding(not_markdown_parsing: true) if deps.any_markdown_backend_available?
933
+ # Tree-sitter grammars - skip when running isolated backend tests
934
+ unless isolated_test_mode
935
+ config.filter_run_excluding(not_libtree_sitter: true) if deps.libtree_sitter_available?
936
+ config.filter_run_excluding(not_bash_grammar: true) if deps.tree_sitter_bash_available?
937
+ config.filter_run_excluding(not_toml_grammar: true) if deps.tree_sitter_toml_available?
938
+ config.filter_run_excluding(not_json_grammar: true) if deps.tree_sitter_json_available?
939
+ config.filter_run_excluding(not_jsonc_grammar: true) if deps.tree_sitter_jsonc_available?
940
+
941
+ # Language parsing capabilities
942
+ config.filter_run_excluding(not_toml_parsing: true) if deps.any_toml_backend_available?
943
+ config.filter_run_excluding(not_markdown_parsing: true) if deps.any_markdown_backend_available?
944
+ end
704
945
 
705
946
  # Specific libraries
706
947
  config.filter_run_excluding(not_toml_rb: true) if deps.toml_rb_available?
@@ -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.0"
13
+ VERSION = "3.2.2"
14
14
  end
15
15
 
16
16
  # Traditional location for VERSION constant