tree_haver 3.1.2 → 3.2.1

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
@@ -49,49 +51,59 @@
49
51
  # # This test only runs when Psych is available
50
52
  # end
51
53
  #
52
- # it "requires Commonmarker backend", :commonmarker do
54
+ # it "requires Commonmarker backend", :commonmarker_backend do
53
55
  # # This test only runs when commonmarker gem is available
54
56
  # end
55
57
  #
56
- # it "requires Markly backend", :markly do
58
+ # it "requires Markly backend", :markly_backend do
57
59
  # # This test only runs when markly gem is available
58
60
  # end
59
61
  #
60
- # it "requires Citrus TOML grammar", :citrus_toml do
61
- # # This test only runs when toml-rb with Citrus grammar is available
62
+ # it "requires Citrus backend", :citrus_backend do
63
+ # # This test only runs when Citrus gem is available
62
64
  # end
63
65
  #
64
66
  # @example Language-specific grammar tags (for *-merge gems)
65
- # it "requires tree-sitter-bash", :tree_sitter_bash do
67
+ # it "requires tree-sitter-bash", :bash_grammar do
66
68
  # # This test only runs when bash grammar is available and parsing works
67
69
  # end
68
70
  #
69
- # it "requires tree-sitter-json", :tree_sitter_json do
71
+ # it "requires tree-sitter-json", :json_grammar do
70
72
  # # This test only runs when json grammar is available and parsing works
71
73
  # end
72
74
  #
73
- # @example Inner-merge dependencies (for markdown-merge CodeBlockMerger)
74
- # it "requires toml-merge", :toml_merge do
75
- # # This test only runs when toml-merge is fully functional
76
- # end
75
+ # == Available Tags
77
76
  #
78
- # it "requires prism-merge", :prism_merge do
79
- # # This test only runs when prism-merge is fully functional
80
- # end
77
+ # === Naming Conventions
81
78
  #
82
- # == Available Tags
79
+ # - `*_backend` = TreeHaver backends (mri, rust, ffi, java, prism, psych, commonmarker, markly, citrus)
80
+ # - `*_engine` = Ruby engines (mri, jruby, truffleruby)
81
+ # - `*_grammar` = tree-sitter grammar files (.so)
82
+ # - `*_parsing` = any parsing capability for a language (combines multiple backends/grammars)
83
+ # - `*_merge` = ast-merge family gems (toml-merge, json-merge, etc.)
83
84
  #
84
85
  # === Positive Tags (run when dependency IS available)
85
86
  #
86
- # ==== TreeHaver Backend Tags
87
+ # ==== TreeHaver Backend Tags (*_backend)
87
88
  #
88
- # [:ffi]
89
+ # [:ffi_backend]
89
90
  # FFI backend is available. Checked dynamically per-test because FFI becomes
90
91
  # unavailable after MRI backend is used (due to libtree-sitter runtime conflicts).
92
+ # Legacy alias: :ffi
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.
91
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
  #
@@ -104,83 +116,66 @@
104
116
  # [:psych_backend]
105
117
  # Psych is available (stdlib, should always be true).
106
118
  #
107
- # [:commonmarker]
119
+ # [:commonmarker_backend]
108
120
  # commonmarker gem is available.
109
121
  #
110
- # [:markly]
122
+ # [:markly_backend]
111
123
  # markly gem is available.
112
124
  #
113
- # [:citrus_toml]
114
- # toml-rb gem with Citrus grammar is available.
125
+ # [:citrus_backend]
126
+ # Citrus gem is available.
115
127
  #
116
- # ==== Ruby Engine Tags
128
+ # ==== Ruby Engine Tags (*_engine)
117
129
  #
118
- # [:jruby]
130
+ # [:mri_engine]
131
+ # Running on MRI (CRuby).
132
+ #
133
+ # [:jruby_engine]
119
134
  # Running on JRuby.
120
135
  #
121
- # [:truffleruby]
136
+ # [:truffleruby_engine]
122
137
  # Running on TruffleRuby.
123
138
  #
124
- # [:mri]
125
- # Running on MRI (CRuby).
126
- #
127
- # ==== Grammar/Library Tags
139
+ # ==== Tree-Sitter Grammar Tags (*_grammar)
128
140
  #
129
141
  # [:libtree_sitter]
130
142
  # libtree-sitter.so is loadable via FFI.
131
143
  #
132
- # [:toml_grammar]
133
- # A TOML grammar library is available (via TREE_SITTER_TOML_PATH env var).
134
- #
135
- # [:native_parsing]
136
- # Both libtree_sitter and toml_grammar are available.
137
- #
138
- # ==== Language-Specific Grammar Tags (for *-merge gems)
139
- #
140
- # [:tree_sitter_bash]
144
+ # [:bash_grammar]
141
145
  # tree-sitter-bash grammar is available and parsing works.
142
146
  #
143
- # [:tree_sitter_toml]
147
+ # [:toml_grammar]
144
148
  # tree-sitter-toml grammar is available and parsing works.
145
149
  #
146
- # [:tree_sitter_json]
150
+ # [:json_grammar]
147
151
  # tree-sitter-json grammar is available and parsing works.
148
152
  #
149
- # [:tree_sitter_jsonc]
153
+ # [:jsonc_grammar]
150
154
  # tree-sitter-jsonc grammar is available and parsing works.
151
155
  #
152
- # [:toml_rb]
153
- # toml-rb gem is available (Citrus backend for TOML).
154
- #
155
- # [:toml_backend]
156
- # At least one TOML backend (tree-sitter or toml-rb) is available.
157
- #
158
- # [:markdown_backend]
159
- # At least one markdown backend (markly or commonmarker) is available.
156
+ # ==== Language Parsing Capability Tags (*_parsing)
160
157
  #
161
- # ==== Inner-Merge Dependency Tags (for markdown-merge CodeBlockMerger)
158
+ # [:toml_parsing]
159
+ # At least one TOML parser (tree-sitter-toml OR toml-rb/Citrus) is available.
162
160
  #
163
- # [:toml_merge]
164
- # toml-merge gem is available and functional.
161
+ # [:markdown_parsing]
162
+ # At least one markdown parser (commonmarker OR markly) is available.
165
163
  #
166
- # [:json_merge]
167
- # json-merge gem is available and functional.
164
+ # [:native_parsing]
165
+ # A native tree-sitter backend and grammar are available.
168
166
  #
169
- # [:prism_merge]
170
- # prism-merge gem is available and functional.
167
+ # ==== Specific Library Tags
171
168
  #
172
- # [:psych_merge]
173
- # psych-merge gem is available and functional.
169
+ # [:toml_rb]
170
+ # toml-rb gem is available (Citrus backend for TOML).
174
171
  #
175
172
  # === Negated Tags (run when dependency is NOT available)
176
173
  #
177
174
  # All positive tags have negated versions prefixed with `not_`:
178
- # - :not_mri_backend, :not_rust_backend, :not_java_backend
179
- # - :not_jruby, :not_truffleruby, :not_mri
180
- # - :not_libtree_sitter, :not_toml_grammar
181
- # - :not_tree_sitter_bash, :not_tree_sitter_toml, :not_tree_sitter_json, :not_tree_sitter_jsonc
182
- # - :not_toml_rb, :not_toml_backend, :not_markdown_backend
183
- # - :not_toml_merge, :not_json_merge, :not_prism_merge, :not_psych_merge
175
+ # - :not_mri_backend, :not_rust_backend, :not_java_backend, etc.
176
+ # - :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
184
179
  #
185
180
  # == Backend Conflict Protection
186
181
  #
@@ -225,6 +220,10 @@ module TreeHaver
225
220
  #
226
221
  # @return [Boolean] true if FFI backend is usable
227
222
  def ffi_available?
223
+ # TruffleRuby's FFI doesn't support STRUCT_BY_VALUE return types
224
+ # (used by ts_tree_root_node, ts_node_child, ts_node_start_point, etc.)
225
+ return false if truffleruby?
226
+
228
227
  # Try to actually use the FFI backend
229
228
  path = find_toml_grammar_path
230
229
  return false unless path && File.exist?(path)
@@ -235,10 +234,14 @@ module TreeHaver
235
234
  true
236
235
  rescue TreeHaver::BackendConflict, TreeHaver::NotAvailable, LoadError
237
236
  false
237
+ rescue StandardError
238
+ # Catch any other FFI-related errors (e.g., Polyglot::ForeignException)
239
+ false
238
240
  end
239
241
 
240
242
  # Check if ruby_tree_sitter gem is available (MRI backend)
241
243
  #
244
+ # The MRI backend only works on MRI Ruby (C extension).
242
245
  # When this returns true, it also records MRI backend usage with
243
246
  # TreeHaver.record_backend_usage(:mri). This is critical for conflict
244
247
  # detection - without it, FFI would not know that MRI has been loaded.
@@ -246,8 +249,13 @@ module TreeHaver
246
249
  # @return [Boolean] true if ruby_tree_sitter gem is available
247
250
  def mri_backend_available?
248
251
  return @mri_backend_available if defined?(@mri_backend_available)
252
+
253
+ # ruby_tree_sitter is a C extension that only works on MRI
254
+ return @mri_backend_available = false unless mri?
255
+
249
256
  @mri_backend_available = begin
250
- require "ruby_tree_sitter"
257
+ # Note: gem is ruby_tree_sitter but requires tree_sitter
258
+ require "tree_sitter"
251
259
  # Record that MRI backend is now loaded - this is critical for
252
260
  # conflict detection with FFI backend
253
261
  TreeHaver.record_backend_usage(:mri)
@@ -257,11 +265,71 @@ module TreeHaver
257
265
  end
258
266
  end
259
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
+
260
322
  # Check if tree_stump gem is available (Rust backend)
261
323
  #
324
+ # The Rust backend only works on MRI Ruby (magnus uses MRI's C API).
325
+ #
262
326
  # @return [Boolean] true if tree_stump gem is available
263
327
  def rust_backend_available?
264
328
  return @rust_backend_available if defined?(@rust_backend_available)
329
+
330
+ # tree_stump uses magnus which requires MRI's C API
331
+ return @rust_backend_available = false unless mri?
332
+
265
333
  @rust_backend_available = begin
266
334
  require "tree_stump"
267
335
  true
@@ -289,6 +357,10 @@ module TreeHaver
289
357
  true
290
358
  rescue TreeHaver::NotAvailable, LoadError
291
359
  false
360
+ rescue StandardError
361
+ # TruffleRuby raises Polyglot::ForeignException when FFI
362
+ # encounters unsupported types like STRUCT_BY_VALUE
363
+ false
292
364
  end
293
365
  end
294
366
 
@@ -306,9 +378,18 @@ module TreeHaver
306
378
  # Grammar paths should be configured via TREE_SITTER_TOML_PATH environment variable.
307
379
  # This keeps configuration explicit and avoids magic path guessing.
308
380
  #
309
- # @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
310
382
  def find_toml_grammar_path
311
- 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
312
393
  end
313
394
 
314
395
  # Check if commonmarker gem is available
@@ -343,22 +424,14 @@ module TreeHaver
343
424
  @psych_available = TreeHaver::Backends::Psych.available?
344
425
  end
345
426
 
346
- # Check if toml-rb with Citrus grammar is available
427
+ # Check if Citrus backend is available
347
428
  #
348
- # @return [Boolean] true if toml-rb gem with Citrus grammar is available
349
- def citrus_toml_available?
350
- return @citrus_toml_available if defined?(@citrus_toml_available)
351
- @citrus_toml_available = begin
352
- require "toml-rb"
353
- finder = TreeHaver::CitrusGrammarFinder.new(
354
- language: :toml,
355
- gem_name: "toml-rb",
356
- grammar_const: "TomlRB::Document",
357
- )
358
- finder.available?
359
- rescue LoadError, NameError
360
- false
361
- end
429
+ # This checks if the citrus gem is installed and the backend works.
430
+ #
431
+ # @return [Boolean] true if Citrus backend is available
432
+ def citrus_available?
433
+ return @citrus_available if defined?(@citrus_available)
434
+ @citrus_available = TreeHaver::Backends::Citrus.available?
362
435
  end
363
436
 
364
437
  # ============================================================
@@ -423,16 +496,20 @@ module TreeHaver
423
496
  @tree_sitter_jsonc_available = grammar_works?(:jsonc, '{"key": "value" /* comment */}')
424
497
  end
425
498
 
426
- # Check if toml-rb gem is available (Citrus backend for TOML)
499
+ # Check if toml-rb gem is available and functional (Citrus backend for TOML)
427
500
  #
428
- # @return [Boolean] true if toml-rb gem is available
501
+ # @return [Boolean] true if toml-rb gem is available and can parse TOML
429
502
  def toml_rb_available?
430
503
  return @toml_rb_available if defined?(@toml_rb_available)
431
504
  @toml_rb_available = begin
432
505
  require "toml-rb"
506
+ # Verify it can actually parse - just requiring isn't enough
507
+ TomlRB.parse('key = "value"')
433
508
  true
434
509
  rescue LoadError
435
510
  false
511
+ rescue StandardError
512
+ false
436
513
  end
437
514
  end
438
515
 
@@ -450,41 +527,13 @@ module TreeHaver
450
527
  markly_available? || commonmarker_available?
451
528
  end
452
529
 
453
- # ============================================================
454
- # Inner-Merge Dependencies (for markdown-merge CodeBlockMerger)
455
- # These check both gem availability AND backend functionality
456
- # ============================================================
457
-
458
- # Check if toml-merge is available and functional
459
- #
460
- # @return [Boolean] true if toml-merge works
461
- def toml_merge_available?
462
- return @toml_merge_available if defined?(@toml_merge_available)
463
- @toml_merge_available = inner_merge_works?("toml/merge", "Toml::Merge::SmartMerger", "key = 'test'")
464
- end
465
-
466
- # Check if json-merge is available and functional
467
- #
468
- # @return [Boolean] true if json-merge works
469
- def json_merge_available?
470
- return @json_merge_available if defined?(@json_merge_available)
471
- @json_merge_available = inner_merge_works?("json/merge", "Json::Merge::SmartMerger", '{"a":1}')
472
- end
473
-
474
- # Check if prism-merge is available and functional
475
- #
476
- # @return [Boolean] true if prism-merge works
477
- def prism_merge_available?
478
- return @prism_merge_available if defined?(@prism_merge_available)
479
- @prism_merge_available = inner_merge_works?("prism/merge", "Prism::Merge::SmartMerger", "puts 1")
480
- end
481
-
482
- # Check if psych-merge is available and functional
483
- #
484
- # @return [Boolean] true if psych-merge works
485
- def psych_merge_available?
486
- return @psych_merge_available if defined?(@psych_merge_available)
487
- @psych_merge_available = inner_merge_works?("psych/merge", "Psych::Merge::SmartMerger", "key: value")
530
+ def any_native_grammar_available?
531
+ libtree_sitter_available? && (
532
+ tree_sitter_bash_available? ||
533
+ tree_sitter_toml_available? ||
534
+ tree_sitter_json_available? ||
535
+ tree_sitter_jsonc_available?
536
+ )
488
537
  end
489
538
 
490
539
  # ============================================================
@@ -496,37 +545,33 @@ module TreeHaver
496
545
  # @return [Hash{Symbol => Boolean}] map of dependency name to availability
497
546
  def summary
498
547
  {
499
- # TreeHaver backends
500
- ffi: ffi_available?,
548
+ # TreeHaver backends (*_backend)
549
+ ffi_backend: ffi_available?,
501
550
  mri_backend: mri_backend_available?,
502
551
  rust_backend: rust_backend_available?,
503
552
  java_backend: java_backend_available?,
504
- prism: prism_available?,
505
- psych: psych_available?,
506
- commonmarker: commonmarker_available?,
507
- markly: markly_available?,
508
- citrus_toml: citrus_toml_available?,
509
- # Libraries
510
- libtree_sitter: libtree_sitter_available?,
511
- toml_grammar: toml_grammar_available?,
512
- # Ruby engines
553
+ prism_backend: prism_available?,
554
+ psych_backend: psych_available?,
555
+ commonmarker_backend: commonmarker_available?,
556
+ markly_backend: markly_available?,
557
+ citrus_backend: citrus_available?,
558
+ # Ruby engines (*_engine)
513
559
  ruby_engine: RUBY_ENGINE,
514
- jruby: jruby?,
515
- truffleruby: truffleruby?,
516
- mri: mri?,
517
- # Language grammars
518
- tree_sitter_bash: tree_sitter_bash_available?,
519
- tree_sitter_toml: tree_sitter_toml_available?,
520
- tree_sitter_json: tree_sitter_json_available?,
521
- tree_sitter_jsonc: tree_sitter_jsonc_available?,
560
+ mri_engine: mri?,
561
+ jruby_engine: jruby?,
562
+ truffleruby_engine: truffleruby?,
563
+ # Tree-sitter grammars (*_grammar)
564
+ libtree_sitter: libtree_sitter_available?,
565
+ bash_grammar: tree_sitter_bash_available?,
566
+ toml_grammar: tree_sitter_toml_available?,
567
+ json_grammar: tree_sitter_json_available?,
568
+ jsonc_grammar: tree_sitter_jsonc_available?,
569
+ any_native_grammar: any_native_grammar_available?,
570
+ # Language parsing capabilities (*_parsing)
571
+ toml_parsing: any_toml_backend_available?,
572
+ markdown_parsing: any_markdown_backend_available?,
573
+ # Specific libraries
522
574
  toml_rb: toml_rb_available?,
523
- any_toml_backend: any_toml_backend_available?,
524
- any_markdown_backend: any_markdown_backend_available?,
525
- # Inner-merge dependencies
526
- toml_merge: toml_merge_available?,
527
- json_merge: json_merge_available?,
528
- prism_merge: prism_merge_available?,
529
- psych_merge: psych_merge_available?,
530
575
  }
531
576
  end
532
577
 
@@ -598,21 +643,6 @@ module TreeHaver
598
643
  end
599
644
  false
600
645
  end
601
-
602
- # Generic helper to check if an inner-merge gem is available and functional
603
- #
604
- # @param require_path [String] the require path for the gem
605
- # @param merger_class [String] the full class name of the SmartMerger
606
- # @param test_source [String] sample source code to test merging
607
- # @return [Boolean] true if the merger can be instantiated
608
- def inner_merge_works?(require_path, merger_class, test_source)
609
- require require_path
610
- klass = Object.const_get(merger_class)
611
- klass.new(test_source, test_source)
612
- true
613
- rescue LoadError, NameError, TreeHaver::Error, TreeHaver::NotAvailable
614
- false
615
- end
616
646
  end
617
647
  end
618
648
  end
@@ -633,14 +663,23 @@ RSpec.configure do |config|
633
663
  puts " #{var}: #{value.inspect}"
634
664
  end
635
665
 
636
- puts "\n=== TreeHaver Test Dependencies ==="
637
- deps.summary.each do |dep, available|
638
- status = case available
639
- when true then "✓ available"
640
- when false then "✗ not available"
641
- 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}"
642
682
  end
643
- puts " #{dep}: #{status}"
644
683
  end
645
684
  puts "===================================\n"
646
685
  end
@@ -649,96 +688,237 @@ RSpec.configure do |config|
649
688
  # ============================================================
650
689
  # TreeHaver Backend Tags
651
690
  # ============================================================
691
+ # Tags: *_backend - require a specific TreeHaver backend to be available
692
+ #
693
+ # Native backends (load .so files):
694
+ # :ffi_backend, :mri_backend, :rust_backend, :java_backend
695
+ # Pure-Ruby backends:
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
652
715
 
653
716
  # FFI availability is checked dynamically per-test (not at load time)
654
717
  # because FFI becomes unavailable after MRI backend is used.
655
- config.before(:each, :ffi) 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
+
656
723
  skip "FFI backend not available (MRI backend may have been used)" unless deps.ffi_available?
657
724
  end
658
725
 
659
- config.filter_run_excluding(mri_backend: true) unless deps.mri_backend_available?
660
- config.filter_run_excluding(rust_backend: true) unless deps.rust_backend_available?
661
- config.filter_run_excluding(java_backend: true) unless deps.java_backend_available?
662
- config.filter_run_excluding(prism_backend: true) unless deps.prism_available?
663
- config.filter_run_excluding(psych_backend: true) unless deps.psych_available?
664
- config.filter_run_excluding(commonmarker: true) unless deps.commonmarker_available?
665
- config.filter_run_excluding(markly: true) unless deps.markly_available?
666
- config.filter_run_excluding(citrus_toml: true) unless deps.citrus_toml_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
667
737
 
668
738
  # ============================================================
669
- # Ruby Engine Tags
739
+ # Dynamic Backend Exclusions (using BLOCKED_BY)
670
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
779
+ blocked_backends = Set.new
780
+
781
+ # Check which *_backend_only tags are being run and block their conflicting backends
782
+ # config.inclusion_filter contains tags passed via --tag on command line
783
+ inclusion_rules = config.inclusion_filter.rules
784
+
785
+ # If filter.rules is empty, check ARGV directly for --tag options
786
+ # This handles the case where RSpec hasn't processed filters yet during configuration
787
+ if inclusion_rules.empty?
788
+ ARGV.each_with_index do |arg, i|
789
+ if arg == "--tag" && ARGV[i + 1]
790
+ tag_str = ARGV[i + 1]
791
+ # Skip exclusion tags (prefixed with ~) - they are NOT inclusion filters
792
+ next if tag_str.start_with?("~")
793
+ tag_value = tag_str.to_sym
794
+ inclusion_rules[tag_value] = true
795
+ elsif arg.start_with?("--tag=")
796
+ tag_str = arg.sub("--tag=", "")
797
+ # Skip exclusion tags (prefixed with ~) - they are NOT inclusion filters
798
+ next if tag_str.start_with?("~")
799
+ tag_value = tag_str.to_sym
800
+ inclusion_rules[tag_value] = true
801
+ end
802
+ end
803
+ end
804
+
805
+ TreeHaver::Backends::BLOCKED_BY.each do |backend, blockers|
806
+ # Check if we're running this backend's isolated tests
807
+ isolated_tag = :"#{backend}_backend_only"
808
+ if inclusion_rules[isolated_tag]
809
+ # Add all backends that would block this one
810
+ blockers.each { |blocker| blocked_backends << blocker }
811
+ end
812
+ end
813
+
814
+ # Store blocked_backends in a module variable so before(:suite) can access it
815
+ TreeHaver::RSpec::DependencyTags.instance_variable_set(:@blocked_backends, blocked_backends)
816
+
817
+ # Now configure exclusions, skipping availability checks for blocked backends
818
+ backend_tags.each do |backend, tag|
819
+ next if blocked_backends.include?(backend)
671
820
 
672
- config.filter_run_excluding(jruby: true) unless deps.jruby?
673
- config.filter_run_excluding(truffleruby: true) unless deps.truffleruby?
674
- config.filter_run_excluding(mri: true) unless deps.mri?
821
+ # FFI is handled specially with before(:each) hook above
822
+ next if backend == :ffi
823
+
824
+ availability_method = backend_availability_methods[backend]
825
+ config.filter_run_excluding(tag => true) unless deps.public_send(availability_method)
826
+ end
675
827
 
676
828
  # ============================================================
677
- # Library/Grammar Tags
829
+ # Ruby Engine Tags
678
830
  # ============================================================
831
+ # Tags: *_engine - require a specific Ruby engine
832
+ # :mri_engine, :jruby_engine, :truffleruby_engine
679
833
 
680
- config.filter_run_excluding(libtree_sitter: true) unless deps.libtree_sitter_available?
681
- config.filter_run_excluding(toml_grammar: true) unless deps.toml_grammar_available?
682
- config.filter_run_excluding(native_parsing: true) unless deps.libtree_sitter_available? && deps.toml_grammar_available?
834
+ config.filter_run_excluding(mri_engine: true) unless deps.mri?
835
+ config.filter_run_excluding(jruby_engine: true) unless deps.jruby?
836
+ config.filter_run_excluding(truffleruby_engine: true) unless deps.truffleruby?
683
837
 
684
838
  # ============================================================
685
- # Language-Specific Grammar Tags
839
+ # Tree-Sitter Grammar Tags
686
840
  # ============================================================
841
+ # Tags: *_grammar - require a specific tree-sitter grammar (.so file)
842
+ # :bash_grammar, :toml_grammar, :json_grammar, :jsonc_grammar
843
+ #
844
+ # Also: :libtree_sitter - requires the libtree-sitter runtime library
845
+ #
846
+ # NOTE: When running with *_backend_only tags, we skip these checks to avoid
847
+ # loading blocked backends. The grammar checks use TreeHaver.parser_for which
848
+ # would load the default backend (MRI) and block FFI.
849
+
850
+ # Skip grammar availability checks if any backend is blocked
851
+ # (i.e., we're running isolated backend tests)
852
+ if blocked_backends.none?
853
+ config.filter_run_excluding(libtree_sitter: true) unless deps.libtree_sitter_available?
854
+ config.filter_run_excluding(bash_grammar: true) unless deps.tree_sitter_bash_available?
855
+ config.filter_run_excluding(toml_grammar: true) unless deps.tree_sitter_toml_available?
856
+ config.filter_run_excluding(json_grammar: true) unless deps.tree_sitter_json_available?
857
+ config.filter_run_excluding(jsonc_grammar: true) unless deps.tree_sitter_jsonc_available?
858
+ end
687
859
 
688
- config.filter_run_excluding(tree_sitter_bash: true) unless deps.tree_sitter_bash_available?
689
- config.filter_run_excluding(tree_sitter_toml: true) unless deps.tree_sitter_toml_available?
690
- config.filter_run_excluding(tree_sitter_json: true) unless deps.tree_sitter_json_available?
691
- config.filter_run_excluding(tree_sitter_jsonc: true) unless deps.tree_sitter_jsonc_available?
692
- config.filter_run_excluding(toml_rb: true) unless deps.toml_rb_available?
693
- config.filter_run_excluding(toml_backend: true) unless deps.any_toml_backend_available?
694
- config.filter_run_excluding(markdown_backend: true) unless deps.any_markdown_backend_available?
860
+ # ============================================================
861
+ # Language Parsing Capability Tags
862
+ # ============================================================
863
+ # Tags: *_parsing - require ANY parser for a language (any backend that can parse it)
864
+ # :toml_parsing - any TOML parser (tree-sitter-toml OR toml-rb/Citrus)
865
+ # :markdown_parsing - any Markdown parser (commonmarker OR markly)
866
+ # :native_parsing - any native tree-sitter backend + grammar
867
+ #
868
+ # NOTE: any_toml_backend_available? calls tree_sitter_toml_available? which
869
+ # triggers grammar_works? and loads MRI. Skip when running isolated tests.
870
+
871
+ if blocked_backends.none?
872
+ config.filter_run_excluding(toml_parsing: true) unless deps.any_toml_backend_available?
873
+ config.filter_run_excluding(markdown_parsing: true) unless deps.any_markdown_backend_available?
874
+ config.filter_run_excluding(native_parsing: true) unless deps.any_native_grammar_available?
875
+ end
695
876
 
696
877
  # ============================================================
697
- # Inner-Merge Dependency Tags
878
+ # Specific Library Tags
698
879
  # ============================================================
880
+ # Tags for specific gems/libraries (not backends, but dependencies)
881
+ # :toml_rb - the toml-rb gem (Citrus-based TOML parser)
699
882
 
700
- config.filter_run_excluding(toml_merge: true) unless deps.toml_merge_available?
701
- config.filter_run_excluding(json_merge: true) unless deps.json_merge_available?
702
- config.filter_run_excluding(prism_merge: true) unless deps.prism_merge_available?
703
- config.filter_run_excluding(psych_merge: true) unless deps.psych_merge_available?
883
+ config.filter_run_excluding(toml_rb: true) unless deps.toml_rb_available?
704
884
 
705
885
  # ============================================================
706
886
  # Negated Tags (run when dependency is NOT available)
707
887
  # ============================================================
888
+ # Prefix: not_* - exclude tests when the dependency IS available
708
889
 
709
- # NOTE: :not_ffi tag is not provided because FFI availability is dynamic.
890
+ # NOTE: :not_ffi_backend tag is not provided because FFI availability is dynamic.
710
891
 
711
- # TreeHaver backends
712
- config.filter_run_excluding(not_mri_backend: true) if deps.mri_backend_available?
713
- config.filter_run_excluding(not_rust_backend: true) if deps.rust_backend_available?
714
- config.filter_run_excluding(not_java_backend: true) if deps.java_backend_available?
715
- config.filter_run_excluding(not_prism_backend: true) if deps.prism_available?
716
- config.filter_run_excluding(not_psych_backend: true) if deps.psych_available?
717
- config.filter_run_excluding(not_commonmarker: true) if deps.commonmarker_available?
718
- config.filter_run_excluding(not_markly: true) if deps.markly_available?
719
- config.filter_run_excluding(not_citrus_toml: true) if deps.citrus_toml_available?
892
+ # TreeHaver backends - handled dynamically to respect blocked backends
893
+ backend_tags.each do |backend, tag|
894
+ next if blocked_backends.include?(backend)
895
+
896
+ # FFI is handled specially (availability is always dynamic)
897
+ next if backend == :ffi
898
+
899
+ negated_tag = :"not_#{tag}"
900
+ availability_method = backend_availability_methods[backend]
901
+ config.filter_run_excluding(negated_tag => true) if deps.public_send(availability_method)
902
+ end
720
903
 
721
904
  # Ruby engines
722
- config.filter_run_excluding(not_jruby: true) if deps.jruby?
723
- config.filter_run_excluding(not_truffleruby: true) if deps.truffleruby?
724
- config.filter_run_excluding(not_mri: true) if deps.mri?
725
-
726
- # Libraries/grammars
727
- config.filter_run_excluding(not_libtree_sitter: true) if deps.libtree_sitter_available?
728
- config.filter_run_excluding(not_toml_grammar: true) if deps.toml_grammar_available?
729
-
730
- # Language grammars
731
- config.filter_run_excluding(not_tree_sitter_bash: true) if deps.tree_sitter_bash_available?
732
- config.filter_run_excluding(not_tree_sitter_toml: true) if deps.tree_sitter_toml_available?
733
- config.filter_run_excluding(not_tree_sitter_json: true) if deps.tree_sitter_json_available?
734
- config.filter_run_excluding(not_tree_sitter_jsonc: true) if deps.tree_sitter_jsonc_available?
905
+ config.filter_run_excluding(not_mri_engine: true) if deps.mri?
906
+ config.filter_run_excluding(not_jruby_engine: true) if deps.jruby?
907
+ config.filter_run_excluding(not_truffleruby_engine: true) if deps.truffleruby?
908
+
909
+ # Tree-sitter grammars - skip when running isolated backend tests
910
+ if blocked_backends.none?
911
+ config.filter_run_excluding(not_libtree_sitter: true) if deps.libtree_sitter_available?
912
+ config.filter_run_excluding(not_bash_grammar: true) if deps.tree_sitter_bash_available?
913
+ config.filter_run_excluding(not_toml_grammar: true) if deps.tree_sitter_toml_available?
914
+ config.filter_run_excluding(not_json_grammar: true) if deps.tree_sitter_json_available?
915
+ config.filter_run_excluding(not_jsonc_grammar: true) if deps.tree_sitter_jsonc_available?
916
+
917
+ # Language parsing capabilities
918
+ config.filter_run_excluding(not_toml_parsing: true) if deps.any_toml_backend_available?
919
+ config.filter_run_excluding(not_markdown_parsing: true) if deps.any_markdown_backend_available?
920
+ end
921
+
922
+ # Specific libraries
735
923
  config.filter_run_excluding(not_toml_rb: true) if deps.toml_rb_available?
736
- config.filter_run_excluding(not_toml_backend: true) if deps.any_toml_backend_available?
737
- config.filter_run_excluding(not_markdown_backend: true) if deps.any_markdown_backend_available?
738
-
739
- # Inner-merge dependencies
740
- config.filter_run_excluding(not_toml_merge: true) if deps.toml_merge_available?
741
- config.filter_run_excluding(not_json_merge: true) if deps.json_merge_available?
742
- config.filter_run_excluding(not_prism_merge: true) if deps.prism_merge_available?
743
- config.filter_run_excluding(not_psych_merge: true) if deps.psych_merge_available?
744
924
  end