tree_haver 3.1.0 → 3.1.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.
@@ -0,0 +1,744 @@
1
+ # frozen_string_literal: true
2
+
3
+ # TreeHaver RSpec Dependency Tags
4
+ #
5
+ # This module provides dependency detection helpers for conditional test execution
6
+ # across all gems in the TreeHaver/ast-merge family. It detects which optional
7
+ # dependencies are available and configures RSpec to skip tests that require
8
+ # unavailable dependencies.
9
+ #
10
+ # @example Loading in spec_helper.rb
11
+ # require "tree_haver/rspec/dependency_tags"
12
+ #
13
+ # @example Usage in specs
14
+ # it "requires FFI", :ffi do
15
+ # # This test only runs when FFI is available
16
+ # end
17
+ #
18
+ # it "requires ruby_tree_sitter", :mri_backend do
19
+ # # This test only runs when ruby_tree_sitter gem is available
20
+ # end
21
+ #
22
+ # it "requires tree_stump", :rust_backend do
23
+ # # This test only runs when tree_stump gem is available
24
+ # end
25
+ #
26
+ # it "requires JRuby", :jruby do
27
+ # # This test only runs on JRuby
28
+ # end
29
+ #
30
+ # it "requires libtree-sitter", :libtree_sitter do
31
+ # # This test only runs when libtree-sitter.so is loadable
32
+ # end
33
+ #
34
+ # it "requires a TOML grammar", :toml_grammar do
35
+ # # This test only runs when a TOML grammar library is available
36
+ # end
37
+ #
38
+ # @example Negated tags (for testing behavior when dependencies are NOT available)
39
+ # it "only runs when ruby_tree_sitter is NOT available", :not_mri_backend do
40
+ # # This test only runs when ruby_tree_sitter gem is NOT available
41
+ # end
42
+ #
43
+ # @example Backend-specific tags
44
+ # it "requires Prism backend", :prism_backend do
45
+ # # This test only runs when Prism is available
46
+ # end
47
+ #
48
+ # it "requires Psych backend", :psych_backend do
49
+ # # This test only runs when Psych is available
50
+ # end
51
+ #
52
+ # it "requires Commonmarker backend", :commonmarker do
53
+ # # This test only runs when commonmarker gem is available
54
+ # end
55
+ #
56
+ # it "requires Markly backend", :markly do
57
+ # # This test only runs when markly gem is available
58
+ # end
59
+ #
60
+ # it "requires Citrus TOML grammar", :citrus_toml do
61
+ # # This test only runs when toml-rb with Citrus grammar is available
62
+ # end
63
+ #
64
+ # @example Language-specific grammar tags (for *-merge gems)
65
+ # it "requires tree-sitter-bash", :tree_sitter_bash do
66
+ # # This test only runs when bash grammar is available and parsing works
67
+ # end
68
+ #
69
+ # it "requires tree-sitter-json", :tree_sitter_json do
70
+ # # This test only runs when json grammar is available and parsing works
71
+ # end
72
+ #
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
77
+ #
78
+ # it "requires prism-merge", :prism_merge do
79
+ # # This test only runs when prism-merge is fully functional
80
+ # end
81
+ #
82
+ # == Available Tags
83
+ #
84
+ # === Positive Tags (run when dependency IS available)
85
+ #
86
+ # ==== TreeHaver Backend Tags
87
+ #
88
+ # [:ffi]
89
+ # FFI backend is available. Checked dynamically per-test because FFI becomes
90
+ # unavailable after MRI backend is used (due to libtree-sitter runtime conflicts).
91
+ #
92
+ # [:mri_backend]
93
+ # ruby_tree_sitter gem is available.
94
+ #
95
+ # [:rust_backend]
96
+ # tree_stump gem is available.
97
+ #
98
+ # [:java_backend]
99
+ # Java backend is available (requires JRuby + java-tree-sitter/jtreesitter).
100
+ #
101
+ # [:prism_backend]
102
+ # Prism gem is available.
103
+ #
104
+ # [:psych_backend]
105
+ # Psych is available (stdlib, should always be true).
106
+ #
107
+ # [:commonmarker]
108
+ # commonmarker gem is available.
109
+ #
110
+ # [:markly]
111
+ # markly gem is available.
112
+ #
113
+ # [:citrus_toml]
114
+ # toml-rb gem with Citrus grammar is available.
115
+ #
116
+ # ==== Ruby Engine Tags
117
+ #
118
+ # [:jruby]
119
+ # Running on JRuby.
120
+ #
121
+ # [:truffleruby]
122
+ # Running on TruffleRuby.
123
+ #
124
+ # [:mri]
125
+ # Running on MRI (CRuby).
126
+ #
127
+ # ==== Grammar/Library Tags
128
+ #
129
+ # [:libtree_sitter]
130
+ # libtree-sitter.so is loadable via FFI.
131
+ #
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]
141
+ # tree-sitter-bash grammar is available and parsing works.
142
+ #
143
+ # [:tree_sitter_toml]
144
+ # tree-sitter-toml grammar is available and parsing works.
145
+ #
146
+ # [:tree_sitter_json]
147
+ # tree-sitter-json grammar is available and parsing works.
148
+ #
149
+ # [:tree_sitter_jsonc]
150
+ # tree-sitter-jsonc grammar is available and parsing works.
151
+ #
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.
160
+ #
161
+ # ==== Inner-Merge Dependency Tags (for markdown-merge CodeBlockMerger)
162
+ #
163
+ # [:toml_merge]
164
+ # toml-merge gem is available and functional.
165
+ #
166
+ # [:json_merge]
167
+ # json-merge gem is available and functional.
168
+ #
169
+ # [:prism_merge]
170
+ # prism-merge gem is available and functional.
171
+ #
172
+ # [:psych_merge]
173
+ # psych-merge gem is available and functional.
174
+ #
175
+ # === Negated Tags (run when dependency is NOT available)
176
+ #
177
+ # 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
184
+ #
185
+ # == Backend Conflict Protection
186
+ #
187
+ # The MRI backend (ruby_tree_sitter) and FFI backend cannot coexist in the same
188
+ # process. Once MRI loads its native extension, FFI will segfault when trying
189
+ # to set a language on a parser.
190
+ #
191
+ # This module records backend usage when checking availability. When
192
+ # `mri_backend_available?` successfully loads ruby_tree_sitter, it calls
193
+ # `TreeHaver.record_backend_usage(:mri)`. This allows TreeHaver's conflict
194
+ # detection (`TreeHaver.conflicting_backends_for`) to properly identify when
195
+ # FFI would conflict with already-loaded backends.
196
+ #
197
+ # @see TreeHaver.record_backend_usage
198
+ # @see TreeHaver.conflicting_backends_for
199
+ # @see TreeHaver::Backends::BLOCKED_BY
200
+
201
+ require "tree_haver"
202
+
203
+ module TreeHaver
204
+ module RSpec
205
+ # Dependency detection helpers for conditional test execution
206
+ module DependencyTags
207
+ class << self
208
+ # ============================================================
209
+ # TreeHaver Backend Availability
210
+ # ============================================================
211
+
212
+ # Check if FFI backend is actually usable (live check, not memoized)
213
+ #
214
+ # This method attempts to actually use the FFI backend by loading a language.
215
+ # This provides "live" validation of backend availability because:
216
+ # - If FFI gem is missing, it will fail
217
+ # - If MRI backend was used first, BackendConflict will be raised
218
+ # - If libtree-sitter is missing, it will fail
219
+ #
220
+ # NOT MEMOIZED: Each call re-checks availability. This validates that
221
+ # backend protection works correctly as tests run. FFI tests should run
222
+ # first (via `rake spec` which runs ffi_specs then remaining_specs).
223
+ #
224
+ # For isolated FFI testing, use bin/rspec-ffi
225
+ #
226
+ # @return [Boolean] true if FFI backend is usable
227
+ def ffi_available?
228
+ # Try to actually use the FFI backend
229
+ path = find_toml_grammar_path
230
+ return false unless path && File.exist?(path)
231
+
232
+ TreeHaver.with_backend(:ffi) do
233
+ TreeHaver::Language.from_library(path, symbol: "tree_sitter_toml")
234
+ end
235
+ true
236
+ rescue TreeHaver::BackendConflict, TreeHaver::NotAvailable, LoadError
237
+ false
238
+ end
239
+
240
+ # Check if ruby_tree_sitter gem is available (MRI backend)
241
+ #
242
+ # When this returns true, it also records MRI backend usage with
243
+ # TreeHaver.record_backend_usage(:mri). This is critical for conflict
244
+ # detection - without it, FFI would not know that MRI has been loaded.
245
+ #
246
+ # @return [Boolean] true if ruby_tree_sitter gem is available
247
+ def mri_backend_available?
248
+ return @mri_backend_available if defined?(@mri_backend_available)
249
+ @mri_backend_available = begin
250
+ require "ruby_tree_sitter"
251
+ # Record that MRI backend is now loaded - this is critical for
252
+ # conflict detection with FFI backend
253
+ TreeHaver.record_backend_usage(:mri)
254
+ true
255
+ rescue LoadError
256
+ false
257
+ end
258
+ end
259
+
260
+ # Check if tree_stump gem is available (Rust backend)
261
+ #
262
+ # @return [Boolean] true if tree_stump gem is available
263
+ def rust_backend_available?
264
+ return @rust_backend_available if defined?(@rust_backend_available)
265
+ @rust_backend_available = begin
266
+ require "tree_stump"
267
+ true
268
+ rescue LoadError
269
+ false
270
+ end
271
+ end
272
+
273
+ # Check if Java backend is available (requires JRuby + java-tree-sitter / jtreesitter)
274
+ #
275
+ # @return [Boolean] true if Java backend is available
276
+ def java_backend_available?
277
+ return @java_backend_available if defined?(@java_backend_available)
278
+ @java_backend_available = jruby? && TreeHaver::Backends::Java.available?
279
+ end
280
+
281
+ # Check if libtree-sitter runtime library is loadable
282
+ #
283
+ # @return [Boolean] true if libtree-sitter.so is loadable via FFI
284
+ def libtree_sitter_available?
285
+ return @libtree_sitter_available if defined?(@libtree_sitter_available)
286
+ @libtree_sitter_available = begin
287
+ return false unless ffi_available?
288
+ TreeHaver::Backends::FFI::Native.try_load!
289
+ true
290
+ rescue TreeHaver::NotAvailable, LoadError
291
+ false
292
+ end
293
+ end
294
+
295
+ # Check if a TOML grammar library is available via environment variable
296
+ #
297
+ # @return [Boolean] true if TREE_SITTER_TOML_PATH points to an existing file
298
+ def toml_grammar_available?
299
+ return @toml_grammar_available if defined?(@toml_grammar_available)
300
+ path = find_toml_grammar_path
301
+ @toml_grammar_available = path && File.exist?(path)
302
+ end
303
+
304
+ # Find the path to a TOML grammar library from environment variable
305
+ #
306
+ # Grammar paths should be configured via TREE_SITTER_TOML_PATH environment variable.
307
+ # This keeps configuration explicit and avoids magic path guessing.
308
+ #
309
+ # @return [String, nil] path from environment variable, or nil if not set
310
+ def find_toml_grammar_path
311
+ ENV["TREE_SITTER_TOML_PATH"]
312
+ end
313
+
314
+ # Check if commonmarker gem is available
315
+ #
316
+ # @return [Boolean] true if commonmarker gem is available
317
+ def commonmarker_available?
318
+ return @commonmarker_available if defined?(@commonmarker_available)
319
+ @commonmarker_available = TreeHaver::Backends::Commonmarker.available?
320
+ end
321
+
322
+ # Check if markly gem is available
323
+ #
324
+ # @return [Boolean] true if markly gem is available
325
+ def markly_available?
326
+ return @markly_available if defined?(@markly_available)
327
+ @markly_available = TreeHaver::Backends::Markly.available?
328
+ end
329
+
330
+ # Check if prism gem is available
331
+ #
332
+ # @return [Boolean] true if Prism is available
333
+ def prism_available?
334
+ return @prism_available if defined?(@prism_available)
335
+ @prism_available = TreeHaver::Backends::Prism.available?
336
+ end
337
+
338
+ # Check if psych is available (stdlib, should always be true)
339
+ #
340
+ # @return [Boolean] true if Psych is available
341
+ def psych_available?
342
+ return @psych_available if defined?(@psych_available)
343
+ @psych_available = TreeHaver::Backends::Psych.available?
344
+ end
345
+
346
+ # Check if toml-rb with Citrus grammar is available
347
+ #
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
362
+ end
363
+
364
+ # ============================================================
365
+ # Ruby Engine Detection
366
+ # ============================================================
367
+
368
+ # Check if running on JRuby
369
+ #
370
+ # @return [Boolean] true if running on JRuby
371
+ def jruby?
372
+ defined?(RUBY_ENGINE) && RUBY_ENGINE == "jruby"
373
+ end
374
+
375
+ # Check if running on TruffleRuby
376
+ #
377
+ # @return [Boolean] true if running on TruffleRuby
378
+ def truffleruby?
379
+ defined?(RUBY_ENGINE) && RUBY_ENGINE == "truffleruby"
380
+ end
381
+
382
+ # Check if running on MRI (CRuby)
383
+ #
384
+ # @return [Boolean] true if running on MRI
385
+ def mri?
386
+ defined?(RUBY_ENGINE) && RUBY_ENGINE == "ruby"
387
+ end
388
+
389
+ # ============================================================
390
+ # Language-Specific Grammar Availability
391
+ # These check that parsing actually works, not just that a grammar exists
392
+ # ============================================================
393
+
394
+ # Check if tree-sitter-bash grammar is available and working
395
+ #
396
+ # @return [Boolean] true if bash grammar works
397
+ def tree_sitter_bash_available?
398
+ return @tree_sitter_bash_available if defined?(@tree_sitter_bash_available)
399
+ @tree_sitter_bash_available = grammar_works?(:bash, "echo hello")
400
+ end
401
+
402
+ # Check if tree-sitter-toml grammar is available and working via TreeHaver
403
+ #
404
+ # @return [Boolean] true if toml grammar works
405
+ def tree_sitter_toml_available?
406
+ return @tree_sitter_toml_available if defined?(@tree_sitter_toml_available)
407
+ @tree_sitter_toml_available = grammar_works?(:toml, 'key = "value"')
408
+ end
409
+
410
+ # Check if tree-sitter-json grammar is available and working
411
+ #
412
+ # @return [Boolean] true if json grammar works
413
+ def tree_sitter_json_available?
414
+ return @tree_sitter_json_available if defined?(@tree_sitter_json_available)
415
+ @tree_sitter_json_available = grammar_works?(:json, '{"key": "value"}')
416
+ end
417
+
418
+ # Check if tree-sitter-jsonc grammar is available and working
419
+ #
420
+ # @return [Boolean] true if jsonc grammar works
421
+ def tree_sitter_jsonc_available?
422
+ return @tree_sitter_jsonc_available if defined?(@tree_sitter_jsonc_available)
423
+ @tree_sitter_jsonc_available = grammar_works?(:jsonc, '{"key": "value" /* comment */}')
424
+ end
425
+
426
+ # Check if toml-rb gem is available (Citrus backend for TOML)
427
+ #
428
+ # @return [Boolean] true if toml-rb gem is available
429
+ def toml_rb_available?
430
+ return @toml_rb_available if defined?(@toml_rb_available)
431
+ @toml_rb_available = begin
432
+ require "toml-rb"
433
+ true
434
+ rescue LoadError
435
+ false
436
+ end
437
+ end
438
+
439
+ # Check if at least one TOML backend is available
440
+ #
441
+ # @return [Boolean] true if any TOML backend works
442
+ def any_toml_backend_available?
443
+ tree_sitter_toml_available? || toml_rb_available?
444
+ end
445
+
446
+ # Check if at least one markdown backend is available
447
+ #
448
+ # @return [Boolean] true if any markdown backend works
449
+ def any_markdown_backend_available?
450
+ markly_available? || commonmarker_available?
451
+ end
452
+
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")
488
+ end
489
+
490
+ # ============================================================
491
+ # Summary and Reset
492
+ # ============================================================
493
+
494
+ # Get a summary of available dependencies (for debugging)
495
+ #
496
+ # @return [Hash{Symbol => Boolean}] map of dependency name to availability
497
+ def summary
498
+ {
499
+ # TreeHaver backends
500
+ ffi: ffi_available?,
501
+ mri_backend: mri_backend_available?,
502
+ rust_backend: rust_backend_available?,
503
+ 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
513
+ 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?,
522
+ 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
+ }
531
+ end
532
+
533
+ # Get environment variable summary for debugging
534
+ #
535
+ # @return [Hash{String => String}] relevant environment variables
536
+ def env_summary
537
+ {
538
+ "TREE_SITTER_BASH_PATH" => ENV["TREE_SITTER_BASH_PATH"],
539
+ "TREE_SITTER_TOML_PATH" => ENV["TREE_SITTER_TOML_PATH"],
540
+ "TREE_SITTER_JSON_PATH" => ENV["TREE_SITTER_JSON_PATH"],
541
+ "TREE_SITTER_JSONC_PATH" => ENV["TREE_SITTER_JSONC_PATH"],
542
+ "TREE_SITTER_RUNTIME_LIB" => ENV["TREE_SITTER_RUNTIME_LIB"],
543
+ "TREE_HAVER_BACKEND" => ENV["TREE_HAVER_BACKEND"],
544
+ "TREE_HAVER_DEBUG" => ENV["TREE_HAVER_DEBUG"],
545
+ }
546
+ end
547
+
548
+ # Reset all memoized availability checks
549
+ #
550
+ # Useful in tests that need to re-check availability after mocking.
551
+ # Note: This does NOT undo backend usage recording.
552
+ #
553
+ # @return [void]
554
+ def reset!
555
+ instance_variables.each do |ivar|
556
+ remove_instance_variable(ivar) if ivar.to_s.end_with?("_available")
557
+ end
558
+ end
559
+
560
+ private
561
+
562
+ # Generic helper to check if a grammar works by parsing test source
563
+ #
564
+ # @param language [Symbol] the language to test
565
+ # @param test_source [String] sample source code to parse
566
+ # @return [Boolean] true if parsing works without errors
567
+ def grammar_works?(language, test_source)
568
+ debug = ENV["TREE_HAVER_DEBUG"]
569
+ env_var = "TREE_SITTER_#{language.to_s.upcase}_PATH"
570
+ env_value = ENV[env_var]
571
+
572
+ if debug
573
+ puts " [grammar_works? #{language}] ENV[#{env_var}] = #{env_value.inspect}"
574
+ puts " [grammar_works? #{language}] Attempting TreeHaver.parser_for(#{language.inspect})..."
575
+ end
576
+
577
+ parser = TreeHaver.parser_for(language)
578
+ if debug
579
+ puts " [grammar_works? #{language}] Parser created: #{parser.class}"
580
+ puts " [grammar_works? #{language}] Parser backend: #{parser.respond_to?(:backend) ? parser.backend : "unknown"}"
581
+ end
582
+
583
+ result = parser.parse(test_source)
584
+ success = !result.nil? && result.root_node && !result.root_node.has_error?
585
+
586
+ if debug
587
+ puts " [grammar_works? #{language}] Parse result nil?: #{result.nil?}"
588
+ puts " [grammar_works? #{language}] Root node: #{result&.root_node&.class}"
589
+ puts " [grammar_works? #{language}] Has error?: #{result&.root_node&.has_error?}"
590
+ puts " [grammar_works? #{language}] Success: #{success}"
591
+ end
592
+
593
+ success
594
+ rescue TreeHaver::NotAvailable, TreeHaver::Error, StandardError => e
595
+ if debug
596
+ puts " [grammar_works? #{language}] Exception: #{e.class}: #{e.message}"
597
+ puts " [grammar_works? #{language}] Returning false"
598
+ end
599
+ false
600
+ 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
+ end
617
+ end
618
+ end
619
+ end
620
+
621
+ # Configure RSpec with dependency-based exclusion filters
622
+ RSpec.configure do |config|
623
+ deps = TreeHaver::RSpec::DependencyTags
624
+
625
+ # Define exclusion filters for optional dependencies
626
+ # Tests tagged with these will be skipped when the dependency is not available
627
+
628
+ config.before(:suite) do
629
+ # Print dependency summary if TREE_HAVER_DEBUG is set
630
+ if ENV["TREE_HAVER_DEBUG"]
631
+ puts "\n=== TreeHaver Environment Variables ==="
632
+ deps.env_summary.each do |var, value|
633
+ puts " #{var}: #{value.inspect}"
634
+ end
635
+
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
642
+ end
643
+ puts " #{dep}: #{status}"
644
+ end
645
+ puts "===================================\n"
646
+ end
647
+ end
648
+
649
+ # ============================================================
650
+ # TreeHaver Backend Tags
651
+ # ============================================================
652
+
653
+ # FFI availability is checked dynamically per-test (not at load time)
654
+ # because FFI becomes unavailable after MRI backend is used.
655
+ config.before(:each, :ffi) do
656
+ skip "FFI backend not available (MRI backend may have been used)" unless deps.ffi_available?
657
+ end
658
+
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?
667
+
668
+ # ============================================================
669
+ # Ruby Engine Tags
670
+ # ============================================================
671
+
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?
675
+
676
+ # ============================================================
677
+ # Library/Grammar Tags
678
+ # ============================================================
679
+
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?
683
+
684
+ # ============================================================
685
+ # Language-Specific Grammar Tags
686
+ # ============================================================
687
+
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?
695
+
696
+ # ============================================================
697
+ # Inner-Merge Dependency Tags
698
+ # ============================================================
699
+
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?
704
+
705
+ # ============================================================
706
+ # Negated Tags (run when dependency is NOT available)
707
+ # ============================================================
708
+
709
+ # NOTE: :not_ffi tag is not provided because FFI availability is dynamic.
710
+
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?
720
+
721
+ # 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?
735
+ 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
+ end